-
Notifications
You must be signed in to change notification settings - Fork 0
Home
The batteries-included starter for production Model Context Protocol servers.
Built by Sarma Linux. MIT licence.
MCP became the default integration layer for agents, and most reference servers are still toys: a single tool, no auth, no observability. This toolkit is the opinionated alternative that ships an MCP 1.0 compliant server with the plumbing already wired in. Write a tool once as an async function and reach it over stdio for a local agent and over streamable HTTP for a remote one, with schema validation, auth, rate limiting, and tracing on every call.
- Engineers wiring tools into desktop agents and IDEs over stdio.
- Teams building remote MCP servers that need auth, validation, and observability from day one.
- Anyone who does not want to hand-write the plumbing for transport, auth, schema generation, and tracing.
-
MCP 1.0 compliant.
initializewith protocol version negotiation (2025-06-18,2025-03-26,2024-11-05),notifications/initialized,ping,tools/list,tools/callwith content blocks and structured results. -
Two transports, one code path. stdio (JSON-RPC 2.0) and streamable HTTP (
POST /mcp) share one protocol layer. Both accept JSON-RPC 2.0 batches: send an array of messages, get an array of responses back, dispatched concurrently. -
Auth. API key (constant-time) or OAuth 2.1 bearer tokens validated against the issuer JWKS, selected by
MCP_AUTH.mcp-toolkit loginruns the OAuth 2.1 PKCE flow. -
Schema validation. Input schema generated from handler type hints, arguments validated per call, optional output schema validated on return and surfaced as
structuredContent. -
Per-client rate limiting via a token bucket, enabled with
MCP_RATE_LIMIT_RPS. -
Span export. Every tool call runs inside an OpenTelemetry span exported over OTLP when
MCP_OTEL_ENDPOINTis set.
Python 3.12+, uv, FastAPI, MCP SDK, OpenTelemetry, jsonschema, python-jose.
graph TD
Client[MCP client]
Client -->|stdio JSON-RPC| Stdio[stdio transport]
Client -->|streamable HTTP POST /mcp| HTTP[FastAPI HTTP transport]
HTTP --> Auth[auth + rate limit]
Stdio --> Proto[protocol.dispatch]
Auth --> Proto
Proto --> Reg[registry: validate + span]
Reg --> P1[plugin: filesystem]
Reg --> P2[plugin: sarmalink]
Reg -->|OTLP| OTEL[collector]
A single serve() call selects the transport from settings, configures telemetry, and imports the plugin packages. Importing a plugin runs its @registry.tool decorators, which is the only registration step. Both transports parse the request body and hand it to protocol.dispatch_batch, which routes a single message to protocol.dispatch and a JSON-RPC 2.0 batch array to a concurrent fan-out, so a tool written once behaves identically over stdio and HTTP.
The registry derives a JSON Schema from each handler's type hints: str becomes string, int becomes integer, list[str] becomes an array of strings, parameters without a default become required, and the schema sets additionalProperties: false. Arguments are validated on every call; declare an output_schema and the return value is validated too. Each call runs inside an OpenTelemetry span recording the tool name, argument count, duration, and any error.
The bundled filesystem plugin, taken from src/mcp_toolkit/plugins/filesystem/handlers.py:
@registry.tool("write_file", description="Write a file in the sandboxed root")
async def write_file(path: str, content: str) -> str:
p = _resolve(path)
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(content)
return f"wrote {len(content)} bytes to {p}"list_files declares an output schema, so its return value is validated and surfaced as structured content:
@registry.tool(
"list_files",
description="List files in a sandboxed directory",
output_schema={"type": "array", "items": {"type": "string"}},
)
async def list_files(path: str = ".") -> list[str]:
...A complete initialize then tools/list exchange over stdio, the same sequence a desktop client uses:
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
| uv run mcp-toolkit run --transport stdioThe runnable examples/mcp_client.py performs this handshake as a subprocess client and calls a tool.
uv sync fails to resolve or build. The project declares its wheel package explicitly, so a clean uv sync --dev resolves everything. A VIRTUAL_ENV does not match warning is harmless: uv targets the project .venv. Pass --active only if you deliberately want the active environment.
No tools show up in tools/list. Tools register as a side effect of importing the plugin package. Confirm the plugin lives under src/mcp_toolkit/plugins/<name>/handlers.py, that its __init__.py imports handlers, and that the package is imported in server.py. Run uv run mcp-toolkit doctor to print the registered tool count.
tools/call returns error code -32602. That is an argument validation failure: the arguments do not match the tool's input schema. Codes follow JSON-RPC, so -32601 is an unknown tool or method and -32602 is invalid parameters. Check argument names and types against tools/list.
HTTP transport returns 401, 429, or 500. 401 means auth is on (MCP_AUTH=api_key or oauth) and the credential is missing or invalid; send X-API-Key or a bearer token. 429 means the per-client rate limit fired; raise MCP_RATE_LIMIT_RPS or back off. Every setting uses the MCP_ prefix, so it is MCP_AUTH, not AUTH.
Garbage on stdout breaks my stdio client. The server writes logs to stderr precisely so stdout stays a clean JSON-RPC channel. If you see non-JSON on stdout, you are likely printing from inside a handler; log through structlog instead.
No traces in my collector. Span export is opt-in. Set MCP_OTEL_ENDPOINT to your OTLP collector; if unset, the server still emits JSON logs to stderr but exports no spans. Verify the endpoint is reachable from the container, not just the host.
The sarmalink plugin reports it is not configured. Set MCP_SARMALINK_API_KEY. uv run mcp-toolkit doctor reports whether it is configured.
- Architecture: protocol layer, registry, transports, validation, tracing
- Quick-Start: install, stdio and HTTP modes, desktop and IDE integration
- Plugin-Authoring: minimum plugin, type hints to schema, output schemas, errors
- Auth-Modes: none, API key, OAuth 2.1, and the PKCE login flow
- Observability: span export, structlog, health, what to alert on
- Deployment: container image, Fly.io, Kubernetes, behind a reverse proxy
- Roadmap: shipped and planned