Thanks to visit codestin.com
Credit goes to github.com

Skip to content
sarmakska edited this page Jun 7, 2026 · 6 revisions

mcp-server-toolkit

The batteries-included starter for production Model Context Protocol servers.

Built by Sarma Linux. MIT licence.


What this is

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.

Who this is for

  • 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.

Key features

  • MCP 1.0 compliant. initialize with protocol version negotiation (2025-06-18, 2025-03-26, 2024-11-05), notifications/initialized, ping, tools/list, tools/call with 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 login runs 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_ENDPOINT is set.

Stack

Python 3.12+, uv, FastAPI, MCP SDK, OpenTelemetry, jsonschema, python-jose.


How it fits together

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]
Loading

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.

Real examples from the codebase

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 stdio

The runnable examples/mcp_client.py performs this handshake as a subprocess client and calls a tool.


Troubleshooting

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.

Wiki pages

  • 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

Repository

github.com/sarmakska/mcp-server-toolkit

Clone this wiki locally