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()