diff --git a/.env.example b/.env.example index affc21b..ce120d4 100644 --- a/.env.example +++ b/.env.example @@ -1,25 +1,37 @@ # ══════════════════════════════════════════════════════════════════════════════ -# DEPLOYMENT MODE +# THREE SUPPORTED MODES # ══════════════════════════════════════════════════════════════════════════════ # -# SSE / VPS mode (hosted server — recommended): -# Frappe credentials are NOT stored here. Each user passes their own -# X-Frappe-URL, X-Frappe-API-Key, X-Frappe-API-Secret headers when connecting. -# Only set MCP_HOST, MCP_PORT, MCP_BEARER_TOKEN below. +# MODE 1 — Local / stdio (single user, your machine) +# Run: frappe-mcp +# Config: Set FRAPPE_URL + credentials below. +# Claude Desktop spawns the process directly via stdio. # -# Local / stdio mode: -# Credentials are read from this file. Run: frappe-mcp (no --sse flag). -# Uncomment the Frappe Connection block below. +# MODE 2 — VPS / SSE, multi-tenant (public hosted server) +# Run: frappe-mcp --sse +# Config: Set only MCP_HOST, MCP_PORT, MCP_BEARER_TOKEN here. +# Each connecting user supplies their own Frappe credentials as +# request headers: X-Frappe-URL, X-Frappe-API-Key, X-Frappe-API-Secret +# No Frappe credentials are stored on the server. # -# ── SSE Server Settings (VPS hosting) ──────────────────────────────────────── +# MODE 3 — VPS / SSE, single-tenant (you host it for yourself) +# Run: frappe-mcp --sse +# Config: Set BOTH the MCP server settings AND Frappe credentials here. +# Users connect without sending credential headers — the server's +# .env credentials are used for everyone. +# +# ── SSE Server Settings (modes 2 & 3) ──────────────────────────────────────── MCP_HOST=0.0.0.0 MCP_PORT=8001 -# Shared access token — all users must send this as: Authorization: Bearer -# Generate with: python -c "import secrets; print(secrets.token_hex(32))" +# Shared access token — clients must send: Authorization: Bearer +# Generate: python -c "import secrets; print(secrets.token_hex(32))" MCP_BEARER_TOKEN=change_this_to_a_strong_random_token -# ── Frappe Connection (local/stdio mode only) ───────────────────────────────── +# ── Frappe Connection (modes 1 & 3) ────────────────────────────────────────── +# Required for local stdio mode (mode 1). +# Optional for SSE mode — set these only if you want a single fixed Frappe +# instance for all connections (mode 3). Leave commented for multi-tenant (mode 2). # FRAPPE_URL=http://YOUR_VPS_IP:8000 # FRAPPE_API_KEY=your_api_key_here # FRAPPE_API_SECRET=your_api_secret_here diff --git a/frappe_mcp/server.py b/frappe_mcp/server.py index b79e204..7447067 100644 --- a/frappe_mcp/server.py +++ b/frappe_mcp/server.py @@ -83,25 +83,30 @@ def _run_sse(): if auth != f"Bearer {bearer_token}": return Response("Unauthorized", status_code=401) - # Extract per-user Frappe credentials from request headers + # 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 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, - )) + 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