""" 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 mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent 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()