133 lines
4.5 KiB
Python
133 lines
4.5 KiB
Python
"""
|
|
Frappe MCP Server — entry point.
|
|
Modules can be individually enabled/disabled via ENABLED_MODULES in .env.
|
|
|
|
Transports:
|
|
(default) stdio — for local Claude Desktop use
|
|
--sse HTTP/SSE — for VPS hosting (requires starlette + uvicorn)
|
|
"""
|
|
|
|
import asyncio
|
|
import os
|
|
import sys
|
|
from dotenv import load_dotenv
|
|
from mcp.server import Server
|
|
from mcp.server.stdio import stdio_server
|
|
from mcp.types import Tool, TextContent
|
|
|
|
load_dotenv()
|
|
|
|
from frappe_mcp.module_registry import get_enabled_tools, get_enabled_handlers
|
|
|
|
app = Server("frappe-mcp")
|
|
|
|
|
|
@app.list_tools()
|
|
async def list_tools() -> list[Tool]:
|
|
return get_enabled_tools()
|
|
|
|
|
|
@app.call_tool()
|
|
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
|
|
handlers = get_enabled_handlers()
|
|
handler = handlers.get(name)
|
|
if not handler:
|
|
return [TextContent(type="text", text=f"Unknown tool: {name}")]
|
|
try:
|
|
result = await handler(arguments)
|
|
return [TextContent(type="text", text=result)]
|
|
except Exception as e:
|
|
return [TextContent(type="text", text=f"Error: {e}")]
|
|
|
|
|
|
def main():
|
|
if "--test-connection" in sys.argv:
|
|
from frappe_mcp.healthcheck import run_health_check
|
|
ok = asyncio.run(run_health_check())
|
|
sys.exit(0 if ok else 1)
|
|
if "--list-modules" in sys.argv:
|
|
from frappe_mcp.module_registry import print_module_status
|
|
print_module_status()
|
|
sys.exit(0)
|
|
if "--sse" in sys.argv:
|
|
_run_sse()
|
|
return
|
|
asyncio.run(_run_stdio())
|
|
|
|
|
|
async def _run_stdio():
|
|
async with stdio_server() as (read_stream, write_stream):
|
|
await app.run(read_stream, write_stream, app.create_initialization_options())
|
|
|
|
|
|
def _run_sse():
|
|
try:
|
|
import uvicorn
|
|
from starlette.applications import Starlette
|
|
from starlette.middleware import Middleware
|
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
from starlette.requests import Request
|
|
from starlette.responses import Response
|
|
from starlette.routing import Mount, Route
|
|
from mcp.server.sse import SseServerTransport
|
|
except ImportError:
|
|
print("SSE dependencies missing. Run: pip install 'frappe-mcp[sse]'", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
host = os.environ.get("MCP_HOST", "0.0.0.0")
|
|
port = int(os.environ.get("MCP_PORT", "8001"))
|
|
bearer_token = os.environ.get("MCP_BEARER_TOKEN", "")
|
|
|
|
sse = SseServerTransport("/messages/")
|
|
|
|
async def handle_sse(request: Request) -> Response:
|
|
if bearer_token:
|
|
auth = request.headers.get("Authorization", "")
|
|
if auth != f"Bearer {bearer_token}":
|
|
return Response("Unauthorized", status_code=401)
|
|
|
|
# Per-user Frappe credentials via request headers (optional).
|
|
# If omitted, FrappeClient falls back to .env values — useful when the
|
|
# server owner is the only user, or for a single-tenant VPS deployment.
|
|
frappe_url = request.headers.get("X-Frappe-URL", "")
|
|
api_key = request.headers.get("X-Frappe-API-Key", "")
|
|
api_secret = request.headers.get("X-Frappe-API-Secret", "")
|
|
site_name = request.headers.get("X-Frappe-Site-Name", "")
|
|
|
|
if frappe_url or api_key or api_secret:
|
|
# At least one header present — require all three to be explicit
|
|
if not (frappe_url and api_key and api_secret):
|
|
return Response(
|
|
"Partial credentials: supply all three headers together — "
|
|
"X-Frappe-URL, X-Frappe-API-Key, X-Frappe-API-Secret",
|
|
status_code=400,
|
|
)
|
|
from frappe_mcp.session import SessionConfig, set_session
|
|
set_session(SessionConfig(
|
|
frappe_url=frappe_url,
|
|
api_key=api_key,
|
|
api_secret=api_secret,
|
|
site_name=site_name,
|
|
))
|
|
# else: no headers — FrappeClient will use .env credentials
|
|
|
|
async with sse.connect_sse(
|
|
request.scope, request.receive, request._send
|
|
) as streams:
|
|
await app.run(streams[0], streams[1], app.create_initialization_options())
|
|
return Response()
|
|
|
|
starlette_app = Starlette(
|
|
routes=[
|
|
Route("/sse", endpoint=handle_sse),
|
|
Mount("/messages/", app=sse.handle_post_message),
|
|
]
|
|
)
|
|
|
|
print(f"Frappe MCP SSE server starting on {host}:{port}")
|
|
uvicorn.run(starlette_app, host=host, port=port)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|