How to Build an MCP Server (July 2026)
Every MCP server tutorial indexed today teaches an API that is about to break. Here is the working build — minimal Python and TypeScript servers on the current 2025-11-25 spec — plus the one thing no other guide has published yet: the v1-to-v2 migration table for the SDK releases landing on 27 and 28 July 2026, and the single line you should add to your dependency file before then.
What an MCP server actually is
Anthropic open-sourced the Model Context Protocol on 25 November 2024. The launch shipped the specification, SDKs, local MCP server support in Claude Desktop, and an open-source repository of pre-built servers for Google Drive, Slack, GitHub, Git, Postgres and Puppeteer. Block and Apollo were named as early adopters; Zed, Replit, Codeium and Sourcegraph as dev-tool partners. On 26 March 2025 OpenAI announced support, with Sam Altman posting that it was available that day in the Agents SDK. Microsoft appears as a named backer of the MCP Registry alongside Anthropic, GitHub and PulseMCP.
The shape is simpler than most diagrams make it look. An MCP host — Claude Code, Claude Desktop, VS Code, Cursor — creates one MCP client per MCP server, and each client holds a dedicated 1:1 connection. There is no client fanning out to many servers, whatever the diagrams say. Underneath, there are two layers: a data layer (JSON-RPC 2.0, carrying lifecycle, primitives and notifications) and a transport layer. The word server refers to the program regardless of where it runs; local versus remote is a transport property.
A server exposes exactly three primitives: Tools (model-controlled executable functions), Resources (context data) and Prompts (reusable templates). Discovery happens via */list, retrieval via */get, execution via tools/call. There are client-side primitives too — Sampling (sampling/createMessage), Elicitation (elicitation/create) and Logging — plus an experimental cross-cutting Tasks primitive for durable execution. Most servers you will write use Tools and nothing else. If you are still deciding whether a tool server is even the right abstraction for your problem, MCP vs a plain API is the argument to have before this one.
One version fact to nail down before you read anything else: the current protocol revision is 2025-11-25. Not 2025-06-18 — that string appears in the spec's own example JSON, which is why half the internet quotes it as current. Versions are date strings marking the last backwards-incompatible change, negotiated at initialization.
Read this first: the SDK v2 cliff
Here is the thing no other MCP tutorial on the web currently tells you, because most of them were written before it was knowable. As of mid-July 2026:
- A 2026-07-28 spec revision is in release candidate — RC locked 21 May 2026, with a ten-week validation window, and final publication scheduled for 28 July 2026. It is not final yet.
- The Python SDK v2 is targeted for 27 July 2026. It renames
FastMCPtoMCPServerand moves the import path with no backward-compatibility layer. - The TypeScript SDK v2 is expected 28 July 2026. It splits
@modelcontextprotocol/sdkinto@modelcontextprotocol/serverand@modelcontextprotocol/client.
Which means every code sample below — and every code sample in the official tutorial, and in every third-party guide Google will show you — is the v1 API. That is fine: the maintainers are explicit that v1.x is the only stable release line and remains recommended for production, and the TypeScript README commits to bug fixes and security updates on v1.x for at least six months after v2 ships. What is not fine is an unpinned dependency. Once 2.0.0 goes stable, a fresh uv add "mcp[cli]" resolves to v2 and the official tutorial's own import stops working. The Python SDK README says this in a CAUTION block and advises adding the upper bound before the stable release lands.
| What changes | v1.x — the production line today | v2 — stable expected 27–28 Jul 2026 |
|---|---|---|
| Python install | uv add "mcp[cli]>=1.27,<2" httpx — add the bound now |
mcp 2.x; pre-releases are 2.0.0aN / 2.0.0bN |
| Python import | from mcp.server.fastmcp import FastMCP |
from mcp.server.mcpserver import MCPServer |
| Python server class | mcp = FastMCP("weather") |
mcp = MCPServer("Demo") |
| Python submodules | mcp.server.fastmcp.* |
mcp.server.mcpserver.* |
| Python compat shim | — | None. The import path changes with no backward-compatibility layer. |
| TS package | @modelcontextprotocol/sdk (1.29.0 today) |
Splits into @modelcontextprotocol/server + @modelcontextprotocol/client (2.0.0-beta.4 today) |
| TS import | from "@modelcontextprotocol/sdk/server/mcp.js" |
from '@modelcontextprotocol/server' |
| TS stdio import | ".../sdk/server/stdio.js" |
'@modelcontextprotocol/server/stdio' |
| TS validation | zod — the docs pin zod@3, but 1.29.0's manifest accepts ^3.25 || ^4.0 |
import * as z from 'zod/v4' |
TS inputSchema |
Raw Zod shape: { city: z.string() } |
Full Zod object: z.object({ city: z.string() }) |
| TS support window | v1.x remains the supported production release | v1.x keeps bug fixes + security updates ≥6 months after v2 ships |
The RC's server-facing breaking changes are worth knowing even if you build on 2025-11-25: the initialize/initialized handshake is removed (client info and capabilities travel in _meta on every request, via a new server/discover method); Mcp-Session-Id and protocol-level sessions are removed; new required Mcp-Method and Mcp-Name headers let gateways route without inspecting the body; and the missing-resource error moves from -32002 to -32602. Read that list again and notice what it targets: remote HTTP deployments and the handshake. stdio framing, tools/call and inputSchema survive. If your server is stdio and your tools are well-shaped, the cliff is a dependency-pinning problem, not a rewrite.
Choose where it runs, first
The spec defines exactly two standard transports: stdio and Streamable HTTP, and states that clients SHOULD support stdio whenever possible. Streamable HTTP replaces the HTTP+SSE transport from protocol version 2024-11-05, which the backwards-compatibility section now calls, plainly, the deprecated HTTP+SSE transport. It is not removed — Claude Code still ships claude mcp add --transport sse and plenty of servers still speak it — but deprecated but widely deployed is the accurate framing, and it is not what you should build new.
Here is where the tutorial genre, including the official one, quietly diverges from Anthropic's own guidance. Every tutorial teaches stdio. Anthropic's mcp-server-dev plugin ranks deployment models and says remote Streamable HTTP is the recommended path for anything wrapping a cloud API, and that local stdio via npx or uvx is not recommended for distribution — recommend it only as a stepping stone. That is house guidance for Claude connectors, not a protocol requirement. But it is the opposite of the default you will absorb by reading tutorials.
MCPB — MCP Bundles — are zip archives containing a local MCP server plus a manifest.json, installed in one click, spiritually a .crx or .vsix. They were renamed from DXT (Desktop Extensions) in late 2025: the dxt CLI became mcpb, .dxt files became .mcpb, and @anthropic-ai/dxt became @anthropic-ai/mcpb. Claude Desktop accepts both extensions.
If you do go remote, the transport spec has three requirements people skip. Servers MUST validate the Origin header and respond 403 Forbidden if it is present and invalid. Servers SHOULD bind only to localhost (127.0.0.1, not 0.0.0.0) when running locally. And servers SHOULD implement authentication. The spec states the reason bluntly: without these protections, attackers could use DNS rebinding to interact with local MCP servers from remote websites. You expose a single endpoint path supporting POST and GET, e.g. https://example.com/mcp.
A minimal server, both languages
Python first. Requires Python 3.10+ and the MCP SDK at 1.2.0 or later. FastMCP derives the tool schema from your type hints and docstring, which is the entire reason the file is this short.
# pin first — before v2 lands on 27 Jul 2026
# uv add "mcp[cli]>=1.27,<2" httpx
# server.py — Python SDK v1.x
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("weather")
@mcp.tool()
def get_forecast(city: str) -> str:
"""Get the current forecast for a city."""
return f"Sunny in {city}"
if __name__ == "__main__":
mcp.run(transport="stdio")
TypeScript. Install with npm install @modelcontextprotocol/sdk zod — the docs pin zod@3 conservatively, but SDK 1.29.0's manifest accepts ^3.25 || ^4.0, so do not let a stale pin drag you into dependency hell. Note the v1 quirk that inputSchema takes a raw Zod shape, not a z.object(...); v2 inverts this.
// server.ts — TypeScript SDK v1.x
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({ name: "weather", version: "1.0.0" });
server.registerTool(
"get_forecast",
{
title: "Get forecast",
description: "Get the current forecast for a city.",
inputSchema: { city: z.string().describe("City name") },
},
async ({ city }) => ({
content: [{ type: "text", text: `Sunny in ${city}` }],
})
);
const transport = new StdioServerTransport();
await server.connect(transport);
Now the rule that breaks more first servers than anything else. On stdio, the server reads newline-delimited JSON-RPC from stdin and writes it to stdout, and messages MUST NOT contain embedded newlines. The spec: the server MUST NOT write anything to its stdout that is not a valid MCP message. The official build-server doc spells out the consequence — writing to stdout will corrupt the JSON-RPC messages and break your server, and print() writes to stdout by default. Use file=sys.stderr. The 2025-11-25 revision clarified that stderr is fair game for any log level, not just errors, and clients should not assume stderr output signals a failure. On HTTP-based servers, stdout logging is fine.
Ten official SDKs now exist, formally tiered: Tier 1 is TypeScript, Python, C# and Go; Tier 2 Java and Rust; Tier 3 Swift, Ruby, PHP and Kotlin. All support servers, clients, and both local and remote transports. Anthropic's own framework recommendation is worth flagging because it is not the official Python SDK: it names the official TypeScript SDK as the default choice for best spec coverage and earliest features, and the standalone FastMCP 3.x on PyPI for people who prefer Python. Both produce identical wire protocol.
Tool design is context budgeting, not API design
This is where most MCP servers go wrong, and where the received wisdom — a few coarse tools beat many thin ones — is about half right. The load-bearing fact, which reframes everything: tool schemas land directly in the model's context window. You are not designing an API surface. You are spending context on every single turn. That is the same discipline as context engineering, applied to a JSON Schema.
The real axis is not coarseness. It is surface size.
| Tool surface | Pattern | Why |
|---|---|---|
| Under roughly 15 operations | One tool per action | The model reads the tool list once and knows exactly what is possible. No discovery round-trips. |
| Dozens to hundreds of endpoints | Search + execute: expose search_actions and execute_action, hold the catalog server-side |
Listing every operation as a tool floods the context window and degrades model performance. |
| Large surface with a clear hot path | Hybrid — promote the 3–5 most-used actions to dedicated tools, keep the long tail behind search/execute | Cheap common case, bounded worst case. |
| Any surface | Never a catch-all api_request tool with a method parameter |
Anthropic's directory rejects it automatically. A single tool accepting both safe methods (GET, HEAD, OPTIONS) and unsafe ones (POST, PUT, PATCH, DELETE) is rejected outright. |
Notice that the search+execute pattern is more indirection, not coarser tools — which is exactly why coarse beats thin mis-describes it. What is true is that you should consolidate along intent, not along endpoints. Anthropic's tool-writing guidance gives the examples verbatim: instead of list_users, list_events and create_event, consider a schedule_event tool that finds availability and schedules the event. Instead of read_logs, a search_logs tool that returns only the relevant lines. Instead of get_customer_by_id, list_transactions and list_notes, a get_customer_context tool that compiles the customer's recent and relevant information. And the sentence that should be printed on the wall: a common error we have observed is tools that merely wrap existing software functionality or API endpoints — whether or not the tools are appropriate for agents.
But consolidation has a hard ceiling, and the ceiling is the safety boundary. Anthropic's connector review criteria require that reads and writes live in separate tools, and note that documenting safe versus unsafe operations inside one tool's description does not satisfy the requirement. Ideally you split writes further by action type — create, update, delete. So: coarse along the workflow, never coarse across the read/write line.
Two naming details that conflict, and the conflict is itself worth knowing. The spec (new in 2025-11-25) says tool names SHOULD be 1–128 characters, case-sensitive, restricted to A–Z, a–z, 0–9, underscore, hyphen and dot — valid examples given are getUser, DATA_EXPORT_v2, admin.tools.list. Anthropic's directory says names MUST be ≤64 characters. If you plan to list, use the stricter number. Namespace by service (asana_search, jira_search) or by resource (asana_projects_search). And do not write descriptions that instruct the model how to behave — those get rejected as prompt injection.
Testing a tool server needs more than one model
A tool that Claude Opus calls correctly and a cheaper model mangles is a schema problem, not a model problem — and you will only find it by running the same server against several models. DataLLM Lab gives you 300+ models behind one OpenAI-compatible endpoint at https://www.datallmlab.com/v1, so your agent harness switches models by changing a string. See the best LLMs for agents in 2026 for which ones are actually worth the loop.
Annotations and errors
A tool definition in 2025-11-25 carries: name, optional title, description, inputSchema (JSON Schema — MUST be a valid object, not null), optional outputSchema, optional annotations, plus two additions this revision: icons and execution.taskSupport (defaulting to forbidden, with optional and required as alternatives). JSON Schema 2020-12 is now the default dialect. For a tool that takes no parameters, the recommended schema is { "type": "object", "additionalProperties": false }.
Now the free win. Annotations have counterintuitive defaults, and omitting them is not neutral — it is pessimistic.
| Annotation | Default when you omit it | What the client therefore assumes |
|---|---|---|
readOnlyHint |
false | Your tool modifies its environment. |
destructiveHint |
true | Destructive updates. In Claude, destructive tools always prompt — every call. |
idempotentHint |
false | Repeat calls may have additional effects. |
openWorldHint |
true | Open external world — like a web search tool, rather than a memory tool. |
Net effect: an unannotated tool reads as destructive, non-idempotent and open-world — the most pessimistic posture available. Anthropic's review criteria make title plus the applicable hint mandatory for listing, precisely because these determine auto-permissions: read-only tools can run without per-call confirmation, destructive tools always prompt. Adding readOnlyHint: true to a getter is a two-word diff that removes a confirmation dialog from every call your users make. Do note the spec's caveat, though: annotations are hints, and clients MUST treat them as untrusted unless the server is trusted. They are a risk vocabulary driving permission UX, not a security mechanism.
On errors, there are two mechanisms and mixing them up is common. Protocol errors (JSON-RPC) are for unknown tools, malformed requests and server faults. Tool execution errors go in the result with isError: true and are for API failures, business-logic failures — and, as 2025-11-25 clarified explicitly, input validation errors, which should be returned as tool execution errors rather than protocol errors to enable model self-correction. That is the whole point: a tool execution error carries actionable text the model reads and retries against. A JSON-RPC error is a wall. If you are structuring results, they go in structuredContent, and for backwards compatibility a tool returning structured content SHOULD also return the serialized JSON in a TextContent block. If you declare an outputSchema, servers MUST conform and clients SHOULD validate.
Wrapping an existing API
First, disambiguate the name, because this trap is specific to Python and it is everywhere. There are two different things called FastMCP:
mcp.server.fastmcp.FastMCP— FastMCP 1.0, vendored and frozen inside the officialmcpSDK on PyPI (1.28.1 today). This is what the official tutorial imports.fastmcpon PyPI — v3.4.4, a separate, actively maintained project from Prefect, documented at gofastmcp.com. Anthropic's own plugin spells it out: this is not the frozen FastMCP 1.0 bundled in the official mcp SDK.
pip install fastmcp and pip install mcp give you different libraries with different APIs. Choose deliberately.
The standalone project offers FastMCP.from_openapi() and FastMCP.from_fastapi(), which turn an OpenAPI 3.0/3.1 spec (or a FastAPI app's generated spec) into MCP tools — by default, one tool per operation. These are not features of the official SDK. And here is the tension nobody writes about: auto-generating one tool per endpoint is precisely the common error Anthropic names. The generator is your first draft, not your deliverable.
So the honest recipe is four steps, and step three is the one everyone skips:
- Scaffold. Point the generator at your OpenAPI spec. You now have every operation as a tool. This took four minutes and is wrong.
- Delete. Cut the operations no agent will ever call. In most APIs this is most of them.
- Merge along workflow lines. Three getters that always fire together become one
get_customer_context. A list-then-filter-then-create sequence becomesschedule_event. - Split at the safety boundary. Whatever you merged, reads stay separate from writes. Annotate both.
Then decide what the agent on the other end actually needs written down — the same instinct that drives AGENTS.md conventions and Claude Code Skills. Tools describe capability; those files describe intent. Most teams over-invest in the first and skip the second.
Test it, then connect it
The Inspector is the fastest loop. It gives you panes for server connection (with a transport selector), Resources, Prompts, Tools (lists schemas, lets you test with custom inputs) and Notifications (server logs).
# any local server
npx @modelcontextprotocol/inspector python /absolute/path/to/server.py
# a published PyPI server
npx @modelcontextprotocol/inspector uvx mcp-server-git --repository ~/code/mcp/servers.git
# or, from the Python SDK
uv run mcp dev server.py
The Inspector is at 0.22.0 today. Once tools list and call cleanly there, wire it into a host.
Claude Code — everything after -- is passed to your command untouched:
claude mcp add weather -- python /absolute/path/to/server.py
claude mcp add --transport http weather https://example.com/mcp
claude mcp list
claude mcp get weather
claude mcp remove weather
Scopes matter: -s local (default, just you in this project), -s project (shared via .mcp.json), -s user (all your projects). In-session, /mcp shows status and handles OAuth for remote servers. MCP_TIMEOUT controls startup timeout — reach for it when a slow-booting server looks like a broken one. Two nuances worth pocketing: in .mcp.json, the type field accepts streamable-http as an alias for http, so configs copied straight out of server docs work unmodified; and Claude Code reconnects HTTP/SSE servers with exponential backoff (5 attempts, starting at 1s and doubling), while stdio servers are local processes and are not auto-reconnected.
Claude Desktop reads ~/Library/Application Support/Claude/claude_desktop_config.json on macOS or %APPDATA%\Claude\claude_desktop_config.json on Windows. Paths MUST be absolute. A full restart is required — not a window close.
{
"mcpServers": {
"weather": {
"command": "python",
"args": ["/absolute/path/to/server.py"]
}
}
}
When it silently fails, the logs are the answer: tail -n 20 -f ~/Library/Logs/Claude/mcp*.log — there is a general mcp.log plus a per-server mcp-server-SERVERNAME.log.
Publishing metadata is a separate question. The MCP Registry is the official centralized metadata repository for publicly accessible MCP servers, backed by Anthropic, GitHub, PulseMCP and Microsoft. Three caveats: it is explicitly in preview, with breaking changes or data resets possible before general availability; it hosts metadata only — a server.json pointing at your npm, PyPI or Docker package, not your code; and it is explicitly not intended for direct consumption by host applications, with aggregators sitting in between. Names use reverse-DNS (io.github.user/server-name) with DNS or GitHub namespace verification.
Ship checklist
- Pin the SDK.
mcp>=1.27,<2in Python. Watch the TypeScript package split. Do this today, not on 28 July. - Target 2025-11-25. Not 2025-06-18. Know that 2026-07-28 is an RC going final on 28 July 2026, and that its breaking changes land on HTTP and the handshake.
- Nothing on stdout if you are on stdio. Route every log line to stderr.
- Under 15 operations? One tool per action. Over 50? Search + execute. In between? Promote the top five.
- No catch-all
api_request. Reads and writes are separate tools, always. - Annotate every tool with
titleplus at leastreadOnlyHintordestructiveHint. Defaults are pessimistic. - Input validation errors go in
isError: true, not JSON-RPC. Give the model something to retry against. - Remote? Validate
Originand return 403 on invalid. Bind 127.0.0.1 locally. OAuth 2.1 with PKCE S256, RFC 9728 Protected Resource Metadata, RFC 8707 Resource Indicators — and use OAuth Client ID Metadata Documents, the recommended mechanism since 2025-11-25, not Dynamic Client Registration, which is now MAY and kept for backwards compatibility. Never pass tokens through; validate the audience. - stdio? Do not add OAuth. The spec says stdio implementations SHOULD NOT follow the authorization spec and should take credentials from the environment.
- Names ≤64 chars if you intend to list in Anthropic's directory (the spec's own guidance is a looser 1–128).
The through-line, if you want one sentence: an MCP server is not an API wrapper with a new coat of paint. It is a standard interface whose real currency is context window, whose real constraint is the read/write boundary, and whose real risk this month is an unpinned dependency.
FAQ
What is an MCP server, in one sentence?
An MCP server is a program that exposes three kinds of capability to an AI application over JSON-RPC 2.0 — Tools (model-callable functions), Resources (context data) and Prompts (reusable templates) — where the AI application, called the host, spins up one dedicated MCP client per server it connects to. Local versus remote is a property of the transport, not of the server itself.
Should I build my MCP server on stdio or Streamable HTTP?
Both, in sequence. The spec defines exactly two standard transports, stdio and Streamable HTTP, and says clients SHOULD support stdio whenever possible — so stdio is the fastest development loop, with no auth and instant Inspector testing. But Anthropic's own build-mcp-server guidance calls remote Streamable HTTP the recommended path for anything wrapping a cloud API, and calls local stdio not recommended for distribution — only a stepping stone. Develop on stdio, ship on HTTP, and use an MCPB bundle for anything that genuinely must run on the user's machine.
My server connects but no tools appear. What is wrong?
You almost certainly wrote to stdout. On the stdio transport the server MUST NOT write anything to stdout that is not a valid MCP message, and messages must be newline-delimited with no embedded newlines. A single print() or console.log() corrupts the JSON-RPC stream and the client sees a broken server. Log to stderr instead — the 2025-11-25 revision clarified that servers MAY write UTF-8 to stderr for any log level, not just errors, and clients should not assume stderr output means failure. On HTTP transports, stdout logging is harmless.
Do I need to add OAuth to my MCP server?
Only if it is remote. Authorization in MCP is optional and transport-scoped: HTTP-based transports SHOULD conform to the OAuth 2.1 authorization spec, while implementations using stdio SHOULD NOT follow it and should instead retrieve credentials from the environment. Bolting OAuth onto a local stdio server is a category error. If you do go remote, note that as of 2025-11-25 OAuth Client ID Metadata Documents is the recommended registration mechanism and Dynamic Client Registration has been demoted to MAY, retained for backwards compatibility — most existing MCP auth content still teaches the old way.
Is pip install fastmcp the same as the FastMCP in the official docs?
No, and this trips up a lot of people. There are two different libraries with the same name. The FastMCP imported in the official tutorial lives at mcp.server.fastmcp inside the official mcp package on PyPI — it is FastMCP 1.0, vendored and frozen. The fastmcp package on PyPI is a separate, actively maintained project from Prefect, currently at v3.4.4, documented at gofastmcp.com. Different install, different API. Helpers like FastMCP.from_openapi belong to the standalone project only, not to the official SDK.
Which protocol version should I target right now?
Target 2025-11-25. That is the current revision — not 2025-06-18, which circulates widely because it appears in the spec's own example JSON. A 2026-07-28 revision is in release candidate as of mid-July 2026 with final publication scheduled for 28 July 2026; it removes the initialize handshake and protocol-level sessions in favour of a server/discover method and per-request metadata, and adds required routing headers. Those changes hit remote HTTP deployments hardest; stdio framing, tools/call and inputSchema are not themselves broken.
DataLLM Lab