The spec version is a date string negotiated during initialize.
Servers should accept anything they support and reply with the highest version both sides understand. The
legacy SSE transport was deprecated 2025-03-26 — new servers use streamable HTTP or stdio.
Spec lives at
modelcontextprotocol.io.
SDKs · CLISetup
bash
# Python SDK (FastMCP) — recommended for new servers
pip install "mcp[cli]"
# Run a server (stdio transport — Claude Desktop default)
mcp dev server.py # interactive inspector
mcp install server.py # register with Claude Desktop
# TypeScript SDK
npm install @modelcontextprotocol/sdk
# Pick a transport
# stdio - desktop apps, subprocess-style
# http - streamable HTTP, web hosts
# sse - legacy server-sent events (deprecated 2025-03)
three rolesArchitecture
Host
The user-facing app (Claude Desktop, an IDE). Owns the LLM and config. Spawns clients.
Client
One-to-one connection to a server. Lives inside the host. Routes JSON-RPC.
Server
Exposes tools / resources / prompts. Stateless across connections; long-lived per session.
Server → client primitives
Tools, resources, prompts, logging. The classic “what does this server expose?”.
Client → server primitives
Sampling (ask the host’s LLM), roots (filesystem scope), elicitation (ask the user).
Mental model: MCP is “LSP for tool use”. The host is the editor, servers are the language servers,
primitives are completions / hovers / actions.
wire protocolJSON-RPC
MCP is JSON-RPC 2.0 over a transport. Every message is either a request (has id),
a response (has id + result or
error), or a notification (no id).
{type:"resource", resource:{uri,…}} — embed a known resource.
isError: true
Soft tool failure. Model sees the message; not a JSON-RPC error.
annotations.destructiveHint
Hint for the host UI to require user confirmation.
notifications/tools/list_changed
Server tells client to re-fetch the tool list.
Use JSON-RPC errors for protocol failures (bad params, internal). Use
isError: true for “the tool ran but couldn’t fulfil the
request” so the model can see + recover.
URI templates the server can serve (e.g. file:///{path}).
resources/read
Params {uri}. Result {contents:[{uri,text|blob,mimeType}]}.
resources/subscribe
Watch a resource. Server emits notifications/resources/updated.
notifications/resources/list_changed
Hint the client to re-list resources.
file:// / https:// / custom://
URIs are free-form. Pick a scheme that makes sense.
Tools vs resources. Tools are “model-controlled” (the model decides to call them).
Resources are “app-controlled” (the host attaches them to context). When in doubt, expose both:
@resource for browse + attach, @tool for search.
Params {name, arguments}. Result {messages:[{role,content}]}.
arguments: required vs optional
Declare which args the user must supply.
content: text / resource
Prompt messages can embed a resource by URI.
notifications/prompts/list_changed
Tell the client the catalogue moved.
Prompts surface in hosts as slash commands or quick actions — not in the model’s normal tool-use loop.
They’re the “user picks this from a menu” primitive.
server → host LLMSampling
The reverse direction: a server asks the client’s LLM for a completion. The host MUST mediate (show the
user what’s being asked, allow modification). Powers agentic patterns without the server holding API keys.
sampling/createMessage
Server-initiated request to the client.
messages: [{role, content}]
Conversation history to feed the model.
modelPreferences.hints
Soft hints (e.g. claude-3-5-sonnet) the client may honour.
Whether the host should add MCP context to the call.
stopReason
endTurn, stopSequence, maxTokens.
Sampling is gated. Most hosts (incl. Claude Desktop today) don’t expose it without an
explicit opt-in. Don’t design a server that requires sampling unless you control the host.
client capabilitiesRoots & elicitation
roots/list
Server asks the client which filesystem roots it’s allowed to see.
notifications/roots/list_changed
Client tells server the roots changed.
elicitation/create
Server asks the host to prompt the user for structured input.
requestedSchema
JSON Schema describing the answer the server expects.
action: "accept" | "decline" | "cancel"
User decisions surfaced to the server.
how bytes moveTransports
stdio
Preferred for desktop. Server is a subprocess; host writes JSON-RPC to stdin, reads from stdout.
Streamable HTTP
Preferred for remote. Single /mcp endpoint; POST request, response can be JSON or an SSE stream.
HTTP+SSE
Legacy Two endpoints (POST /messages, GET /sse). Deprecated 2025-03-26.
Origin / Host header checks
Required on HTTP transports. Prevents DNS-rebinding attacks.
Mcp-Session-Id
Header used by Streamable HTTP to multiplex sessions on one endpoint.
stderr
Log to stderr in stdio servers — stdout is for JSON-RPC only.
build a serverSDKs · server skeletons
Python · FastMCP
python
from mcp.server.fastmcp import FastMCP
from pydantic import Field
mcp = FastMCP("Weather")
@mcp.tool()
def get_forecast(
city: str = Field(..., description="City name, English"),
units: str = Field("metric", description="metric | imperial"),
) -> dict:
"""Return the next-48h forecast for the given city."""
return fetch_forecast(city, units)
@mcp.resource("weather://stations")
def list_stations() -> str:
"""Static directory of weather stations as Markdown."""
return load_stations_markdown()
@mcp.prompt()
def daily_briefing(city: str) -> str:
"""Prompt template — caller substitutes {city}."""
return f"Give me a 3-line weather briefing for {city}."
if __name__ == "__main__":
mcp.run() # stdio by default; mcp.run(transport="http") for HTTP
TypeScript
javascript
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: "0.1.0" });
server.tool(
"get_forecast",
"Next-48h forecast for a city.",
{
city: z.string().describe("City name, English"),
units: z.enum(["metric", "imperial"]).default("metric"),
},
async ({ city, units }) => ({
content: [{ type: "text", text: await fetchForecast(city, units) }],
}),
);
server.resource("stations", "weather://stations", async () => ({
contents: [{ uri: "weather://stations", text: await loadStations() }],
}));
await server.connect(new StdioServerTransport());
Where to look when a server “disappears” from the UI.
server → tool → resource → promptEnd-to-end · FastMCP server
A complete weather server with one tool, one resource, one prompt. Wire it into Claude Desktop with the
JSON above and the model can call it.
python
from mcp.server.fastmcp import FastMCP
from pydantic import Field
mcp = FastMCP("Weather")
@mcp.tool()
def get_forecast(
city: str = Field(..., description="City name, English"),
units: str = Field("metric", description="metric | imperial"),
) -> dict:
"""Return the next-48h forecast for the given city."""
return fetch_forecast(city, units)
@mcp.resource("weather://stations")
def list_stations() -> str:
"""Static directory of weather stations as Markdown."""
return load_stations_markdown()
@mcp.prompt()
def daily_briefing(city: str) -> str:
"""Prompt template — caller substitutes {city}."""
return f"Give me a 3-line weather briefing for {city}."
if __name__ == "__main__":
mcp.run() # stdio by default; mcp.run(transport="http") for HTTP
Best practiceGood to know
Describe inputs with JSON Schema, including description.
The model only sees inputSchema; a tool with bare types
({"type":"string"}) gets hallucinated arguments. Always include
description, enum, and required.
Use mcp dev early.
The inspector lets you fire tools/call by hand and see exact JSON-RPC.
Catches schema bugs faster than driving through the host.
Keep servers stateless across connections.
Connection state is fine; persist nothing else there. If the host restarts you, a fresh subprocess must
behave identically — store durable state on disk or a DB.
Common trapsWatch out for
Never write to stdout in a stdio server (except JSON-RPC).
A stray print() in your tool corrupts the transport. The host disconnects
with a parse error. Log to stderr or a file.
Tools must declare required params.
Missing required in the schema is the #1 reason models call your tool with
empty args — JSON Schema treats every field as optional by default.
Don’t ship the legacy SSE transport for new servers.
It’s deprecated and many newer clients don’t implement it. Use Streamable HTTP for remote, stdio for
local.
MCP is an open protocol (published by Anthropic) that standardizes how AI applications connect to external tools, data sources, and context providers. It defines a JSON-RPC 2.0 message layer with primitives for tools (executable functions), resources (readable data), prompts (reusable templates), and sampling (LLM inference requests). Any MCP client (Claude Desktop, Cursor, custom apps) can talk to any MCP server.
What is the difference between MCP tools and resources?
Tools are server-side functions the LLM can invoke to take actions or query live data — they have a name, description, and JSON Schema for inputs. Resources are read-only data sources (files, database rows, API responses) the host can fetch by URI and inject as context. Tools are for doing; resources are for reading.
What transports does MCP support?
MCP ships with two standard transports: stdio (default for local tools — the client spawns the server as a subprocess and communicates over stdin/stdout) and SSE over HTTP (for remote or multi-client servers). The Python and TypeScript SDKs abstract the transport, so you define tools with decorators and call mcp.run(transport='stdio') or mcp.run(transport='sse').
What is sampling in MCP?
Sampling lets an MCP server request LLM inference from the connected client (e.g., Claude Desktop). Instead of calling an LLM API directly from the server, the server sends a sampling/createMessage request and the client routes it through its own model. This keeps API keys on the client side and lets servers leverage the host's model without needing separate credentials.
How do I test an MCP server locally?
Run mcp dev server.py to launch the MCP Inspector — a browser UI that lets you call your tools, read resources, and inspect JSON-RPC traffic in real time without connecting a full LLM client. Use mcp install server.py to register the server with Claude Desktop so it appears in the menu for end-to-end testing.
Is MCP open source and free to use?
Yes. The MCP specification and official SDKs (Python, TypeScript) are MIT-licensed and free. The spec is maintained by Anthropic but designed to be implemented by any vendor or developer. There are no licencing fees to build or deploy MCP servers.