@modelcontextprotocol/sdk
The official TypeScript SDK for the Model Context Protocol, the standard that lets AI applications (Claude, Cursor, VS Code, and most agent frameworks) talk to external tools, resources, and prompts through a common wire format. You use it to build MCP servers that expose your tools to any MCP client, or MCP clients that consume them, over stdio or Streamable HTTP. Important context: this package is the v1 line. The project's main branch is now v2, which splits the SDK into @modelcontextprotocol/server and @modelcontextprotocol/client, targeting the 2026-07-28 spec.
The canonical way TypeScript speaks MCP, maintained by the protocol's own team, but the package is now the previous generation. Keep it for existing 1.x servers; start anything new on the v2 split packages.
Use it if
- You maintain an existing MCP server or client built on the 1.x API and need continued bug and security fixes, which the team has committed to for at least 6 months after the v2 release
- You depend on tooling, templates, or hosting platforms that still expect the single @modelcontextprotocol/sdk package and its 1.x import paths
- You want the reference implementation of the protocol in TypeScript, with both server and client sides, stdio and Streamable HTTP transports, and OAuth helpers in one package
- You are pinned to Zod-shaped tool schemas that 1.x uses directly (v2 moved to Standard Schema, which accepts Zod v4, Valibot, or ArkType)
- You are starting a new project today: the maintainers call v2 the stable release line, and it ships as separate @modelcontextprotocol/server and @modelcontextprotocol/client packages implementing the 2026-07-28 spec; building new work on 1.x buys you a migration
- You want a lean dependency tree: the 1.x package pulls in express, hono, ajv, jose, cors, and more even if you only build a stdio server
- Your stack is Python: the official mcp package (and FastMCP, now part of it) is the native path, not this SDK behind a subprocess
- You expect a settled API: the MCP spec itself still revs roughly twice a year, and SDK majors track spec releases, so churn is structural, not incidental
Setup reality
npm install @modelcontextprotocol/sdk zod and you can have a stdio server running in 20 lines; Node 18+ required and zod (v3.25+ or v4) is a peer dependency you install yourself. The annoyances: all imports use explicit .js subpaths (@modelcontextprotocol/sdk/server/mcp.js) which trips up TypeScript configs without moduleResolution node16/bundler; the package drags in a large dependency set including express and hono whether you use HTTP or not; Streamable HTTP session management (session IDs, resumability, DNS-rebinding protection) is your code to wire; and with v2 now published, a growing share of docs, examples, and blog posts show APIs that do not exist in 1.x.
Patterns
Minimal stdio MCP server (1.x API)stdio-server
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: "demo", version: "1.0.0" });
server.registerTool("add",
{
description: "Add two numbers",
inputSchema: { a: z.number(), b: z.number() },
},
async ({ a, b }) => ({ content: [{ type: "text", text: String(a + b) }] })
);
await server.connect(new StdioServerTransport());In 1.x, inputSchema is a plain object of Zod fields (a raw shape), not z.object(...); v2 takes a full Standard Schema object instead.
Tool that returns structured contentstructured-output
server.registerTool("get-weather",
{
description: "Weather for a city",
inputSchema: { city: z.string() },
outputSchema: { temperature: z.number(), conditions: z.string() },
},
async ({ city }) => {
const output = { temperature: 22.5, conditions: "sunny" };
return {
content: [{ type: "text", text: JSON.stringify(output) }],
structuredContent: output,
};
}
);When outputSchema is declared, you must return structuredContent that validates against it; keep the text content as a JSON mirror for older clients.
Expose a resource with a URI templateregister-resource
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
server.registerResource(
"greeting",
new ResourceTemplate("greeting://{name}", { list: undefined }),
{ title: "Greeting", description: "Personal greeting" },
async (uri, { name }) => ({
contents: [{ uri: uri.href, text: `Hello, ${name}!` }],
})
);Resources are for data the client chooses to read; if the model should decide to fetch it during a conversation, make it a tool instead.
Reusable prompt templateregister-prompt
server.registerPrompt("review-code",
{
title: "Code Review",
description: "Review code for issues",
argsSchema: { code: z.string() },
},
({ code }) => ({
messages: [{
role: "user",
content: { type: "text", text: `Please review this code:\n\n${code}` },
}],
})
);Prompts surface as user-invoked commands (slash commands in many clients), not as things the model calls on its own.
Stateless Streamable HTTP server with Expresshttp-stateless
import express from "express";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
const app = express();
app.use(express.json());
app.post("/mcp", async (req, res) => {
const server = buildServer(); // fresh McpServer per request
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // stateless mode
});
res.on("close", () => { transport.close(); server.close(); });
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});
app.listen(3000);sessionIdGenerator: undefined disables sessions entirely; create a new server and transport per request or concurrent clients will collide.
Session-aware Streamable HTTP (multi-request clients)http-sessions
import { randomUUID } from "node:crypto";
const transports = {};
// inside app.post("/mcp", ...):
const sessionId = req.headers["mcp-session-id"];
let transport = sessionId ? transports[sessionId] : undefined;
if (!transport) {
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (id) => { transports[id] = transport; },
});
await server.connect(transport);
}
await transport.handleRequest(req, res, req.body);You own the session store and its cleanup; also enable enableDnsRebindingProtection and allowedHosts for locally exposed servers.
Client: spawn a stdio server and call a toolclient-stdio
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
const transport = new StdioClientTransport({
command: "node",
args: ["server.js"],
});
const client = new Client({ name: "my-client", version: "1.0.0" });
await client.connect(transport);
const tools = await client.listTools();
const result = await client.callTool({
name: "add",
arguments: { a: 1, b: 2 },
});StdioClientTransport spawns the server as a child process; the server must never write logs to stdout or it corrupts the JSON-RPC stream (use stderr).
Client: connect to a remote server over Streamable HTTPclient-http
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const client = new Client({ name: "my-client", version: "1.0.0" });
await client.connect(
new StreamableHTTPClientTransport(new URL("https://example.com/mcp"))
);Streamable HTTP replaced the older SSE transport; for servers that still speak SSE only, 1.x keeps SSEClientTransport as a deprecated fallback.
Return a tool error the model can readtool-error
server.registerTool("fetch-user",
{ description: "Fetch a user", inputSchema: { id: z.string() } },
async ({ id }) => {
const user = await db.find(id);
if (!user) {
return {
isError: true,
content: [{ type: "text", text: `No user with id ${id}` }],
};
}
return { content: [{ type: "text", text: JSON.stringify(user) }] };
}
);Throwing raises a protocol-level error; returning isError: true keeps the failure inside the tool result so the model can recover from it.
Enable, disable, and update tools after connectdynamic-tools
const adminTool = server.registerTool("delete-user",
{ description: "Delete a user", inputSchema: { id: z.string() } },
handler
);
adminTool.disable(); // hidden from listTools
// later, after auth upgrade:
adminTool.enable(); // emits listChanged notification automaticallyregisterTool returns a handle; enable/disable/update automatically send tools/listChanged so connected clients refresh.
Send log messages to the clientserver-logging
const server = new McpServer(
{ name: "demo", version: "1.0.0" },
{ capabilities: { logging: {} } }
);
// inside a tool handler:
await server.server.sendLoggingMessage({
level: "info",
data: "processing started",
});Declare the logging capability at construction; console.log is not an option on stdio transports because stdout carries the protocol.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| @modelcontextprotocol/server | npm | New MCP servers should start here: the v2 stable line for the 2026-07-28 spec, with Standard Schema support |
| fastmcp | npm | You want a higher-level TypeScript framework with sessions, auth, and CLI testing conveniences on top of MCP |
| mcp | PyPI | Your tools live in Python; this is the official Python SDK with the FastMCP decorator API built in |