125 lines
4.0 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 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)
# Extract per-user Frappe credentials from request headers
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 not frappe_url or not api_key or not api_secret:
return Response(
"Missing required headers: 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,
))
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()