Writing a tiny MCP server — registries, dispatch, and the response shape — step 1 of 9
Writing a server means writing a dispatcher
The first lesson taught you what an MCP server IS — a process that advertises tools and runs them on request. This lesson teaches you to write one. Not a toy: the exact shape you'd port to a real stdio JSON-RPC server when you wrap it for Claude Code or Cursor.
A "tool handler" sounds heavy. It isn't. A tool handler is just a function:
def handler(args: dict) -> str:
# do the work, return a string
...
Everything else is plumbing around that function.
The three pieces every MCP server has
1. A tool registry. A data structure (usually a dict) that maps
tool names to their metadata: description, input schema, handler
function. When the client calls tools/list, you serialize the
registry — minus the handler — and send it back.
2. A dispatcher. A function — call it call_tool(name, args) —
that looks up the tool by name in the registry, validates the args
against the schema, runs the handler, and wraps the return value in
the MCP response envelope.
3. A response shape. Every tools/call response is the same
dict: {"content": [{"type": "text", "text": "..."}], "isError": false}.
Same shape on success. Same shape on failure (with isError: true).
The model treats either case as just another content block.
That's the whole server. Real MCP servers add JSON-RPC framing,
stdio transport, the initialize handshake, and notification
plumbing — but the dispatch logic is what you actually write. The
transport is a thin wrapper that most SDKs handle for you.
Why this matters for your own use case
The interesting part of an MCP server you'd build — a knowledge-base lookup, a CRM query, a customer-search tool — is the handler function. The schema and registry are five lines of boilerplate. The dispatch is ten. You spend 90% of your time on the actual API call or DB query inside the handler. MCP is the cheapest possible wrapper around "I have a function I want Claude to call."
Where it goes wrong
- No registry at all. People hardcode tool names into giant
if/elifchains. Works for two tools, breaks at ten. The registry is the boring abstraction that pays off later. - Mutating the registry at call time. Handlers should be pure
with respect to the registry — never edit
TOOL_REGISTRYfrom inside a handler. State belongs in your DB or in module-level variables the handler reads. - Returning the raw value instead of the envelope. If your
handler returns
42, the dispatcher has to wrap it as{"content": [{"type": "text", "text": "42"}], "isError": false}. The model expects content blocks, not bare values.
Browser note: real MCP servers run as a subprocess speaking JSON-RPC over stdio (or HTTP). Pyodide can't fork processes, so we simulate the message dispatch in-memory: a python function that takes a dict, returns a dict. The shapes match exactly. When you port this to a real server, the only change is wiring the dict in and out of the JSON-RPC transport.
Run the editor. A one-tool registry, a dispatcher, and a real MCP response envelope. This is the whole skeleton — every server you write from now on grows from this shape.
Writing a tiny MCP server — registries, dispatch, and the response shape — step 1 of 9
Writing a server means writing a dispatcher
The first lesson taught you what an MCP server IS — a process that advertises tools and runs them on request. This lesson teaches you to write one. Not a toy: the exact shape you'd port to a real stdio JSON-RPC server when you wrap it for Claude Code or Cursor.
A "tool handler" sounds heavy. It isn't. A tool handler is just a function:
def handler(args: dict) -> str:
# do the work, return a string
...
Everything else is plumbing around that function.
The three pieces every MCP server has
1. A tool registry. A data structure (usually a dict) that maps
tool names to their metadata: description, input schema, handler
function. When the client calls tools/list, you serialize the
registry — minus the handler — and send it back.
2. A dispatcher. A function — call it call_tool(name, args) —
that looks up the tool by name in the registry, validates the args
against the schema, runs the handler, and wraps the return value in
the MCP response envelope.
3. A response shape. Every tools/call response is the same
dict: {"content": [{"type": "text", "text": "..."}], "isError": false}.
Same shape on success. Same shape on failure (with isError: true).
The model treats either case as just another content block.
That's the whole server. Real MCP servers add JSON-RPC framing,
stdio transport, the initialize handshake, and notification
plumbing — but the dispatch logic is what you actually write. The
transport is a thin wrapper that most SDKs handle for you.
Why this matters for your own use case
The interesting part of an MCP server you'd build — a knowledge-base lookup, a CRM query, a customer-search tool — is the handler function. The schema and registry are five lines of boilerplate. The dispatch is ten. You spend 90% of your time on the actual API call or DB query inside the handler. MCP is the cheapest possible wrapper around "I have a function I want Claude to call."
Where it goes wrong
- No registry at all. People hardcode tool names into giant
if/elifchains. Works for two tools, breaks at ten. The registry is the boring abstraction that pays off later. - Mutating the registry at call time. Handlers should be pure
with respect to the registry — never edit
TOOL_REGISTRYfrom inside a handler. State belongs in your DB or in module-level variables the handler reads. - Returning the raw value instead of the envelope. If your
handler returns
42, the dispatcher has to wrap it as{"content": [{"type": "text", "text": "42"}], "isError": false}. The model expects content blocks, not bare values.
Browser note: real MCP servers run as a subprocess speaking JSON-RPC over stdio (or HTTP). Pyodide can't fork processes, so we simulate the message dispatch in-memory: a python function that takes a dict, returns a dict. The shapes match exactly. When you port this to a real server, the only change is wiring the dict in and out of the JSON-RPC transport.
Run the editor. A one-tool registry, a dispatcher, and a real MCP response envelope. This is the whole skeleton — every server you write from now on grows from this shape.