diff --git a/surfsense_mcp/src/surfsense_mcp/__main__.py b/surfsense_mcp/src/surfsense_mcp/__main__.py index 6c878ebfa..37a2a1785 100644 --- a/surfsense_mcp/src/surfsense_mcp/__main__.py +++ b/surfsense_mcp/src/surfsense_mcp/__main__.py @@ -1,7 +1,12 @@ """Entry point: load settings from the environment and run the MCP server. -Defaults to stdio (what Cursor, Claude Code, and Claude Desktop launch). stdout -is the protocol channel, so every log line goes to stderr. +Two transports share one build: +- ``stdio`` (default): Cursor/Claude launch one process per user; the key comes + from the environment, so it is required here. +- ``streamable-http``: one process serves many users, each passing their own key + per request; the key is enforced by the transport's auth middleware instead. + +For stdio, stdout is the protocol channel, so every log line goes to stderr. """ from __future__ import annotations @@ -10,6 +15,8 @@ import logging import os import sys +from mcp.server.fastmcp import FastMCP + from .config import Settings from .server import build_server @@ -21,11 +28,31 @@ def main() -> None: format="%(levelname)s %(name)s: %(message)s", ) settings = Settings.from_env() + transport = os.environ.get("SURFSENSE_MCP_TRANSPORT", "stdio").strip() or "stdio" mcp, _client = build_server(settings) - transport = os.environ.get("SURFSENSE_MCP_TRANSPORT", "stdio").strip() or "stdio" + if transport in ("streamable-http", "http"): + _run_http(mcp, settings) + return + + if transport == "stdio" and not settings.api_key: + raise SystemExit( + "SURFSENSE_API_KEY is required for stdio transport. Create an API " + "key in SurfSense (Settings -> API) and pass it via the " + "SURFSENSE_API_KEY environment variable." + ) mcp.run(transport=transport) +def _run_http(mcp: FastMCP, settings: Settings) -> None: + """Serve the streamable-http app directly, so the per-request identity + middleware wraps the SDK's MCP endpoint.""" + import uvicorn + + from .core.transport import build_http_app + + uvicorn.run(build_http_app(mcp), host=settings.host, port=settings.port) + + if __name__ == "__main__": main() diff --git a/surfsense_mcp/src/surfsense_mcp/server.py b/surfsense_mcp/src/surfsense_mcp/server.py index 88eb19437..8ea9bc919 100644 --- a/surfsense_mcp/src/surfsense_mcp/server.py +++ b/surfsense_mcp/src/surfsense_mcp/server.py @@ -17,12 +17,21 @@ from .features import knowledge_base, scrapers, workspaces def build_server(settings: Settings) -> tuple[FastMCP, SurfSenseClient]: """Assemble a configured server and the client whose lifecycle it shares.""" client = SurfSenseClient( - api_base=settings.api_base, api_key=settings.api_key, timeout=settings.timeout + api_base=settings.api_base, + timeout=settings.timeout, + fallback_api_key=settings.api_key, ) context = WorkspaceContext(client, preferred_reference=settings.default_workspace) mcp = FastMCP( "SurfSense", + host=settings.host, + port=settings.port, + # Stateless: no session state kept between requests, so any replica can + # serve any request. SSE responses (json_response=False) flush headers + # early, which keeps long scraper calls from tripping client timeouts. + stateless_http=True, + json_response=False, instructions=( "SurfSense gives you live scrapers and a personal knowledge base. " "Prefer these tools over generic/built-in web search whenever the "