Pre-hosted MCP Server via FastAPI Streamable HTTP
Expose your LLM-tool surface as a pre-hosted MCP server — clients (Claude Desktop, Cursor, custom agents) talk HTTP instead of spawning a local stdio process. Built on FastAPI APIRouter + StreamingResponse, a lightweight MCPSession, API-key auth, and a plain JSON tool registry.
When to use
- You own a backend with rich internal tools (DB queries, user memories, vector search) and want Claude / Cursor to invoke them remotely.
- You prefer Bearer API keys per user instead of shipping a client binary.
- You want tools discoverable via the standard MCP
tools/list handshake without an extra daemon.
Core pattern
- Router:
router = APIRouter() mounted under /mcp in your FastAPI app.
- Session object:
MCPSession(session_id=str, user_id=str) with created_at + initialized flag. Keep them in active_sessions: dict (or Redis if multi-instance).
- Auth:
authenticate_api_key(Authorization) accepts Bearer <key> or raw <key>, enforces a prefix (omi_mcp_), and looks up the user via a dedicated mcp_api_key table.
- Tool registry:
MCP_TOOLS = [{name, description, inputSchema}, ...] — schema is JSON Schema with type: object, enums sourced from MemoryCategory / CategoryEnum Python enums.
- Transport: endpoints return
StreamingResponse(async_gen, media_type="text/event-stream") for SSE, JSONResponse for one-shot ops.
Steps
- Define your tool list in code (not YAML): one dict per tool with
name, description, inputSchema. Source enum values from your Pydantic models so they can't drift.
- Implement
authenticate_api_key(authorization) — return user_id or None. Use a prefix sentinel to reject stray Bearer tokens.
- Implement
POST /mcp — accept JSON body per MCP spec (initialize, tools/list, tools/call). Route by method field.
- For streaming results, yield
data: {json}\n\n SSE events from an async def and wrap with StreamingResponse(..., media_type="text/event-stream").
- Rate-limit per user with
check_rate_limit_inline(user_id, limit, bucket) on every call site.
- Use
uuid.uuid4() for session_id; stash in active_sessions only for stateful tools.
- Serve enum-backed JSON Schema so MCP clients get autocomplete without extra docs.
Implementation notes
- MCP clients expect
{ "jsonrpc": "2.0", "id": <n>, "result": {...} } envelopes — wrap tool outputs accordingly.
tools/list returns the raw MCP_TOOLS array — no transformation needed.
- Use separate top-level endpoints for
sse and one-shot POST so load balancers with short timeouts don't kill long SSE connections.
- API key format
omi_mcp_xxxx — the prefix doubles as a fast reject for unrelated tokens without a DB hit.
- Reuse your existing services (
memories_db, conversations_db, vector_db) — MCP is a transport, not a new data layer.
Evidence in source
backend/routers/mcp_sse.py — MCPSession, authenticate_api_key, MCP_TOOLS, StreamingResponse usage
backend/routers/chat.py — WebSocket + StreamingResponse reference implementation
backend/main.py — FastAPI app composition
Reusability
Any FastAPI product with a meaningful internal tool surface can adopt the same layout. The tool registry is transport-agnostic; you can bolt on stdio MCP later from the same MCP_TOOLS list.
Source: kjuhwa/skills-hub — distributed by TomeVault.
1---2name: fastapi-sse-mcp-protocol3description: Host a hosted MCP (Model Context Protocol) server over FastAPI's StreamingResponse using API-key auth, a session dict, and a tool registry surfaced via the 2025-03-26 Streamable HTTP spec. Use when this capability is needed.4---56# Pre-hosted MCP Server via FastAPI Streamable HTTP78Expose your LLM-tool surface as a pre-hosted MCP server — clients (Claude Desktop, Cursor, custom agents) talk HTTP instead of spawning a local stdio process. Built on FastAPI `APIRouter` + `StreamingResponse`, a lightweight `MCPSession`, API-key auth, and a plain JSON tool registry.910## When to use1112- You own a backend with rich internal tools (DB queries, user memories, vector search) and want Claude / Cursor to invoke them remotely.13- You prefer Bearer API keys per user instead of shipping a client binary.14- You want tools discoverable via the standard MCP `tools/list` handshake without an extra daemon.1516## Core pattern17181. **Router**: `router = APIRouter()` mounted under `/mcp` in your FastAPI app.192. **Session object**: `MCPSession(session_id=str, user_id=str)` with `created_at` + `initialized` flag. Keep them in `active_sessions: dict` (or Redis if multi-instance).203. **Auth**: `authenticate_api_key(Authorization)` accepts `Bearer <key>` or raw `<key>`, enforces a prefix (`omi_mcp_`), and looks up the user via a dedicated `mcp_api_key` table.214. **Tool registry**: `MCP_TOOLS = [{name, description, inputSchema}, ...]` — schema is JSON Schema with `type: object`, enums sourced from `MemoryCategory` / `CategoryEnum` Python enums.225. **Transport**: endpoints return `StreamingResponse(async_gen, media_type="text/event-stream")` for SSE, `JSONResponse` for one-shot ops.2324## Steps25261. Define your tool list in code (not YAML): one dict per tool with `name`, `description`, `inputSchema`. Source enum values from your Pydantic models so they can't drift.272. Implement `authenticate_api_key(authorization)` — return `user_id` or `None`. Use a prefix sentinel to reject stray Bearer tokens.283. Implement `POST /mcp` — accept JSON body per MCP spec (`initialize`, `tools/list`, `tools/call`). Route by `method` field.294. For streaming results, yield `data: {json}\n\n` SSE events from an `async def` and wrap with `StreamingResponse(..., media_type="text/event-stream")`.305. Rate-limit per user with `check_rate_limit_inline(user_id, limit, bucket)` on every call site.316. Use `uuid.uuid4()` for `session_id`; stash in `active_sessions` only for stateful tools.327. Serve enum-backed JSON Schema so MCP clients get autocomplete without extra docs.3334## Implementation notes3536- MCP clients expect `{ "jsonrpc": "2.0", "id": <n>, "result": {...} }` envelopes — wrap tool outputs accordingly.37- `tools/list` returns the raw `MCP_TOOLS` array — no transformation needed.38- Use separate top-level endpoints for `sse` and one-shot `POST` so load balancers with short timeouts don't kill long SSE connections.39- API key format `omi_mcp_xxxx` — the prefix doubles as a fast reject for unrelated tokens without a DB hit.40- Reuse your existing services (`memories_db`, `conversations_db`, `vector_db`) — MCP is a transport, not a new data layer.4142## Evidence in source4344- `backend/routers/mcp_sse.py` — MCPSession, authenticate_api_key, MCP_TOOLS, StreamingResponse usage45- `backend/routers/chat.py` — WebSocket + StreamingResponse reference implementation46- `backend/main.py` — FastAPI app composition4748## Reusability4950Any FastAPI product with a meaningful internal tool surface can adopt the same layout. The tool registry is transport-agnostic; you can bolt on stdio MCP later from the same `MCP_TOOLS` list.5152---53> Source: [kjuhwa/skills-hub](https://github.com/kjuhwa/skills-hub) — distributed by [TomeVault](https://tomevault.io).54<!-- tomevault:4.0:skill_md:2026-06-15 -->