mcp-server-implementation
Write the MCP server code: SDK setup, primitive registration, transport wiring,
error semantics, and protocol behavior. Fixes the failures agents actually hit:
stale/deprecated SDK APIs, stdout-corrupted stdio streams, exceptions where
isError results belong, hand-rolled framing, and transport wiring that breaks
in real clients.
Targets spec revision 2025-11-25 and SDKs verified current on 2026-07-20:
TypeScript @modelcontextprotocol/sdk v1.29 and Python mcp v1.28
(pins below). Re-verify at https://modelcontextprotocol.io/docs/sdk if months
have passed — v2 SDKs targeting the 2026-07-28 spec revision are in beta.
When NOT to use
- Deciding what tools/resources/prompts to expose →
mcp-server-design(do it first; coding an undesigned surface produces API-wrapper servers). - OAuth/token validation →
mcp-server-auth. Threats/hardening →mcp-server-security. Evals/Inspector deep-dives →mcp-server-testing. Hosting/packaging/registry →mcp-server-deployment. - Building MCP clients/hosts, or generic non-MCP servers.
Workflow
1. Preconditions
Surface designed (tool list, schemas, descriptions — else apply
mcp-server-design). Pick the language: TypeScript or Python cover most cases
(Tier 1 SDKs, best docs); C#/Go also Tier 1; Java/Rust Tier 2. Pick the
transport by deployment target: stdio for a local subprocess of one client,
Streamable HTTP for anything remote/multi-client. Wrong transport choice is
a rewrite, not a config flag.
2. Project setup — pinned
Python (official SDK; its high-level API is the bundled FastMCP class — NOT
the third-party fastmcp/jlowin package):
uv add "mcp[cli]>=1.27,<2" # or: pip install "mcp[cli]>=1.27,<2"
TypeScript (v1 is the production line; required peer dep zod):
npm i @modelcontextprotocol/sdk@^1.29 zod && npm i -D typescript @types/node
Never invoke servers with unpinned npx package@latest — a dependency that
starts printing a banner to stdout kills every stdio install downstream.
3. Build shared infrastructure first
One HTTP/API client with timeouts, one centralized error mapper (404/403/429/timeout → specific, recovery-oriented strings), shared pagination/formatting helpers — then register tools against them. Copy-pasted per-tool fetch/error code is the main source of inconsistent agent-facing behavior. All I/O async (httpx / native fetch or axios); validate env config at startup and exit non-zero with a clear message when missing.
4. Register primitives with current APIs
Per-language walkthroughs with complete code: references/python-server.md,
references/typescript-server.md. The load-bearing rules:
- Python:
FastMCP(name);@mcp.tool()(schema inferred from type hints + docstring, or explicit Pydantic models withextra="forbid"),@mcp.resource("scheme://{param}"),@mcp.prompt().Contextinjection givesctx.report_progress,ctx.info/debug/error(protocol logging),ctx.elicit, lifespan state. - TypeScript:
new McpServer({name, version})+server.registerTool(name, config, handler)/registerResource/registerPromptwith zod schemas. The olderserver.tool()and rawsetRequestHandlerstyles work but are legacy; useregister*in new code. - Declare only capabilities you implement (
tools.listChangedonly if you actually emitnotifications/tools/list_changed, etc.). Invoking an un-negotiated feature is a protocol violation. - Tool names/schemas/descriptions come from the design doc; schema dialect
defaults to JSON Schema 2020-12;
inputSchemamust never be null.
5. Wire the transport
stdio — the client spawns you; stdin/stdout carry newline-delimited
JSON-RPC. THE RULE: nothing but MCP messages on stdout, ever. One stray
print() / console.log() / dependency banner corrupts framing and the client
drops the connection — works-in-terminal-fails-in-client is the signature.
Route all logging to stderr (spec-legal for any log level) or files. Run
scripts/check_stdio_hygiene.py over the source tree to gate this. Credentials
come from the environment, not an OAuth flow.
Streamable HTTP — one endpoint (e.g. /mcp): every client message is a new
POST with Accept: application/json, text/event-stream; the server answers
with a single JSON body or an SSE stream (client must support both); GET opens
the optional server-push stream. Mechanics you must get right (details +
wiring code in references/streamable-http.md):
MCP-Protocol-Versionheader on post-initialize requests; absent → assume 2025-03-26; unsupported → 400.- Sessions are optional:
Mcp-Session-Idissued on the InitializeResult; treat it as a lookup key (externalizable to Redis/DB), never as authentication; missing → 400, expired → 404 (client re-initializes). - Resumability via SSE event IDs +
Last-Event-IDon GET; IDs unique per session; never replay across streams. - Security floor even without auth: validate
Origin(invalid → 403, DNS-rebinding defense), bind127.0.0.1for local servers. - Old HTTP+SSE (two-endpoint, 2024-11-05) is deprecated since revision 2025-03-26 — migrate; optionally serve both during a window.
6. Error semantics — two distinct layers
- Protocol errors (JSON-RPC error responses, e.g.
-32602): unknown tool, malformed request — the tool never ran. - Execution errors: return a normal result with
isError: trueand a recovery-oriented message incontent. The model reads this and self-corrects. Since 2025-11-25 (SEP-1303) input-validation failures belong here too, not in protocol errors. Never leak stack traces; never let an unhandled exception escape a handler.
7. Structured output
Declare outputSchema; return structuredContent (SDKs validate against the
schema) plus the same data serialized in a text content block for
backward compatibility. Keep them consistent — divergence is a common bug.
Known sharp edge: some SDK versions validate structuredContent against
outputSchema even on isError results — test your error path.
8. Long-running work
Requests have client timeouts; serverless platforms add ~30s ceilings. For
anything slow: emit notifications/progress against the request's
progressToken (rate-limited; stop after completion), honor
notifications/cancelled (clean up; the response/cancel race is normal). The
tasks feature (SEP-1686) is experimental in 2025-11-25 and moves to an
extension in the next revision — gate any use behind its capability and say so.
9. Server→client features — capability-gate everything
ctx.elicit / elicitInput (ask the user mid-tool), sampling (ask the
client's LLM), roots — all require the client to declare the capability, and
client support is thin (verify your target clients). Secrets/credentials MUST
NOT be collected via form elicitation — 2025-11-25 adds URL mode for that.
RC horizon: the 2026-07-28 revision deprecates roots/sampling/logging and
replaces server-initiated requests with a retry pattern (MRTR) — prefer tool
parameters and stderr/OTel logging over deep investment in these.
10. Verify
Build gates: npm run build / python -m py_compile + server starts. Then
launch MCP Inspector against it (npx @modelcontextprotocol/inspector <cmd>,
or uv run mcp dev server.py) and exercise every tool, including one error
path each. Full test/eval methodology: mcp-server-testing.
Output spec
A running server: pinned deps; shared client/error/pagination infra; primitives
registered with current APIs; transport wired per the rules above (stdio
hygiene script passes, or HTTP endpoint validates Origin/version headers);
errors split protocol-vs-isError; structured output consistent; logs on
stderr or protocol logging; env-validated config; verified in Inspector.
Gotchas
- The stdout rule is absolute under stdio — including dependencies' banners and deprecation warnings. Grep them all; the failure is silent.
- JSON-RPC batching was removed in 2025-06-18 — never send/expect batches; batch inside tool semantics instead.
- Version negotiation is strict: respond to
initializewith the client's version or your latest supported; mismatched clients disconnect. Wait fornotifications/initializedbefore server-initiated traffic. - The two FastMCPs: official SDK's bundled
FastMCP≠ third-partyfastmcp(jlowin) /punkpeye/fastmcp(TS). This skill uses official only. - v2 SDK betas (TS split packages
@modelcontextprotocol/server+/client; Python renamesFastMCP→MCPServer) target the 2026-07-28 spec — migration material, not production defaults; av1-to-v2codemod exists for TS. - stdio servers inherit the client's spawn environment — a config with a bare
pythonor missingPATHfails only inside GUI clients; use absolute paths. - Empty result sets must still return a well-formed result (clients hang on nothing); return "no results found" text, not null.
Pointers
references/python-server.md— complete FastMCP server (tools/resources/ prompts, Context, structured output, lifespan, both transports).references/typescript-server.md— completeMcpServerserver (registerTool- zod, structured output, both transports, express wiring).
references/streamable-http.md— wire-level transport + lifecycle contract (headers, sessions, resumability, SSE polling, migration, RC horizon).scripts/check_stdio_hygiene.py— greps a source tree for stdout writes that corrupt stdio framing; non-zero exit on findings.