feat(mcp): serve stateless streamable-http with stdio dispatch

This commit is contained in:
CREDO23 2026-07-07 16:52:30 +02:00
parent f3c06b1fbf
commit 730face126
2 changed files with 40 additions and 4 deletions

View file

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

View file

@ -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 "