Anatomy of an MCP Server: Handlers, Schemas, and Transport
An MCP server is a process that speaks JSON-RPC 2.0 over a transport and advertises capabilities (tools, resources, prompts) to an MCP host or client.
The host connects, negotiates protocol version and features, then lists and invokes those capabilities on behalf of a model-driven agent.
Summary
- Every MCP server has three structural layers - identity and capabilities, typed handlers (list + call/read/get), and a transport that carries newline-delimited or HTTP JSON-RPC messages.
- Insight: Agents only see what you declare. Clear schemas, tight handlers, and the right transport decide reliability, security, and how hosts wire your server into production.
- Key Concepts: initialize, capabilities, tools / resources / prompts, JSON Schema inputs, content blocks, stdio vs Streamable HTTP, session id.
- When to Use: Building any custom connector (APIs, databases, internal tools) that multiple agent hosts should reuse without rewriting function-calling glue.
- Limitations/Trade-offs: You own process lifecycle, auth for remote transport, and output size. MCP standardizes the wire format, not your business logic or SLAs.
- Related Topics: first server in TypeScript or Python, resources and prompts, remote HTTP deploy, auth for remote servers.
Foundations
MCP (Model Context Protocol) is an open standard for connecting hosts (Claude Desktop, VS Code, agent SDKs) to external systems through a shared protocol rather than one-off plugins.
A server does not embed the LLM. It exposes data and actions. The client (inside a host) discovers them and the host decides when the model may call them.
At startup the client sends initialize. The server replies with protocol version, server info (name, version), and a capabilities object.
After notifications/initialized, the client may list tools, resources, and prompts, then invoke them with JSON-RPC methods such as tools/call, resources/read, and prompts/get (exact method set is defined by the current MCP revision - verify at build).
Conceptually the server is:
| Piece | Role |
|---|---|
| Identity | Name, version, optional instructions for the host |
| Capability flags | Which primitives you support and list-change notifications |
| Handlers | Code that lists definitions and executes each primitive |
| Schemas | Machine-readable argument shapes (usually JSON Schema) |
| Transport | stdio subprocess pipe, or Streamable HTTP endpoint |
Without a clean separation between schema (what the model sees) and handler (what runs), hosts cannot safely present tools, and agents call the wrong parameters.
Mechanics & Interactions
Handlers
Handlers are the functions (or methods) that answer list and invoke requests.
Typical pairs:
- Tools: list tools → call tool with arguments → return content blocks (text, image, resource links, structured data depending on SDK).
- Resources: list resources (or templates) → read a URI → return contents.
- Prompts: list prompts → get a named prompt with arguments → return a message template.
High-level SDKs hide the JSON-RPC method names. In Python, FastMCP turns decorated functions into tools. In TypeScript, McpServer.registerTool binds name, description, input schema, and handler.
Handlers should:
- Validate inputs (SDK schema + server-side checks).
- Perform the side effect or fetch.
- Return content the host can feed back into the model, not raw exceptions that crash the process.
- Stay free of stdout pollution on stdio (log to stderr only).
Schemas
Tool (and often prompt) arguments are described with JSON Schema so the host can show forms, and models can produce structured arguments.
Python FastMCP often derives schema from type hints and docstrings. TypeScript commonly uses Zod objects mapped into the SDK input schema.
Good schemas are narrow:
- Prefer enums and bounds over free text when values are finite.
- Describe every field the model must fill.
- Keep parameter names stable across versions; breaking renames break agent prompts and evals.
The description on the tool and each field is part of the schema surface. Models follow descriptions more than they invent APIs.
Transport
MCP messages are JSON-RPC. The transport defines how those messages move.
stdio
- Client spawns the server as a subprocess.
- JSON-RPC lines on stdin/stdout; logs on stderr only.
- Best for local desktop hosts and single-user developer machines.
- Process isolation is natural: kill the child, connection ends.
Streamable HTTP (current remote standard; replaces older dedicated HTTP+SSE split - verify at build)
- Server is a long-running HTTP process with an MCP endpoint (POST for client messages; optional GET for SSE streams).
- Optional
Mcp-Session-Idafter initialize for stateful sessions. - Clients send
MCP-Protocol-Versionon subsequent requests when using HTTP. - Required for multi-user remote services; needs TLS, origin checks, and auth in production.
Older HTTP+SSE layouts still appear in the wild. New servers should target Streamable HTTP and document any compatibility shim.
# Pattern only: identity + one tool + stdio transport (FastMCP)
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("demo")
@mcp.tool()
def ping() -> str:
"""Health check tool for the host."""
return "pong"
if __name__ == "__main__":
mcp.run(transport="stdio")Lifecycle sketch
- Transport opens (spawn or HTTP connect).
initialize/ capability negotiation.- Client lists primitives.
- Host injects tool definitions into the model loop.
- Model selects a tool → client
tools/call→ handler runs → content returns → model continues. - Session ends (stdio process exit, HTTP DELETE/session expiry, or client close).
Advanced Considerations & Applications
| Approach | Strength | Weakness | Best fit |
|---|---|---|---|
| Tools only | Simple agent actions | No first-class context files or templates | CRUD APIs, actions |
| Tools + resources | Agents can pull large or static context by URI | Host must support resource reads | Codebases, configs, docs |
| Tools + prompts | Reusable task templates | Overlap with host-side prompts | Shared org playbooks |
| stdio | Easy local install | Not multi-tenant | IDE and desktop |
| Streamable HTTP | Shared remote service | Auth, ops, session design | Team and product agents |
Design handlers as small, composable units. One tool that "does the whole product" is hard to describe, hard to authorize, and hard to evaluate.
Version the server identity string when schemas change. Hosts and agents cache tool lists; silent renames look like random model failures.
Common Misconceptions
- "MCP is another agent framework." No. It is a connection protocol. LangGraph, CrewAI, OpenAI Agents SDK, and others remain the loop owners.
- "Stdout is fine for logs on stdio." It is not. Any non-JSON-RPC byte on stdout corrupts the session.
- "Schemas are optional fluff." Without them hosts cannot reliably drive tools, and models invent argument shapes.
- "One transport fits all." Local desktop and multi-tenant SaaS need different transports and security models.
- "Returning a huge dump is fine." Context windows and latency disagree. Prefer resources or file spill patterns for large payloads.
FAQs
What must every MCP server implement?
At minimum: respond to initialize with identity and capabilities, speak valid JSON-RPC over a supported transport, and implement handlers for every capability you advertise. An empty tools list is legal but useless for agents.
How do handlers differ from ordinary REST handlers?
They are invoked through MCP methods with structured arguments chosen by a model (or host UI), and they return content blocks for the conversation, not arbitrary HTTP response codes alone. Side effects still need the same care as REST.
Where do JSON Schemas live?
In tool (and related) definitions exchanged on list operations. SDKs generate them from types or Zod; the wire format is JSON Schema shaped per the MCP revision in use.
Can one process expose multiple servers?
Hosts usually map one config entry to one process or URL. You can modularize code, but capability sets are negotiated per connection. Prefer clear server names per domain.
What is a content block?
A typed unit in a tool result (commonly text, sometimes images or resource references). Hosts flatten these into model-visible observations.
Do I need resources and prompts on day one?
No. Tools alone ship most integrations. Add resources when agents need readable context by URI; add prompts when you want shared templates across hosts.
How does transport choice affect testing?
stdio is easiest with a subprocess test client. HTTP needs a live port, session headers, and often auth. Test both if you ship both entrypoints.
What happens if initialize fails?
The client will not list or call tools. Check protocol version mismatches, crashed process, broken stdout, or HTTP 4xx/5xx before debugging handlers.
Are capabilities fixed for the life of a process?
You declare them at initialize. Some servers support list-changed notifications when tools/resources change; hosts that care will re-list. Verify notification support for your SDK and host.
How does this relate to function calling?
MCP tools are the portable cousin of provider function-calling schemas. The host translates MCP tool definitions into whatever the model API expects.
Should business logic live inside handlers?
Thin handlers that call a service layer are easier to unit test without spinning MCP. Keep transport code at the edge.
What about cancellation and timeouts?
Long tools should respect host cancel notifications where supported, and set their own upstream timeouts. A hung handler freezes an agent turn.
Related
Related: Building & Deploying MCP Basics
Related: Building Your First MCP Server in TypeScript or Python
Related: Exposing Resources and Prompts, Not Just Tools, from an MCP Server
Related: Deploying an MCP Server as a Remote HTTP Service
Related: MCP's Three Primitives: Tools, Resources, and Prompts
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.