What the MCP TypeScript SDK Covers
The Model Context Protocol TypeScript SDK (@modelcontextprotocol/sdk) is the official library for building MCP servers and clients in TypeScript and Node.js. It handles the JSON-RPC 2.0 transport layer, capability negotiation, and message routing — so you focus on tools, resources, and prompts.
This reference covers every major API surface in SDK v1.x (current as of March 2026).
You'll learn:
- How to create servers with tools, resources, and prompts
- Transport options: stdio, HTTP/SSE, and in-process
- Client API for connecting to MCP servers
- Auth, error handling, and production patterns
Time: 25 min | Difficulty: Intermediate
Installation
# npm
npm install @modelcontextprotocol/sdk
# pnpm
pnpm add @modelcontextprotocol/sdk
# Verify install
node -e "const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js'); console.log('ok')"
Minimum requirements: Node.js 18+, TypeScript 5.0+ (if using TS)
// tsconfig.json minimum config
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"strict": true
}
}
Server API
McpServer
McpServer is the high-level class for building MCP servers. It wraps the lower-level Server class and provides ergonomic .tool(), .resource(), and .prompt() registration methods.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
const server = new McpServer({
name: "my-server", // shown to clients during handshake
version: "1.0.0", // semver string
});
Constructor Options
| Option | Type | Required | Description |
|---|---|---|---|
name | string | ✅ | Server name shown to clients |
version | string | ✅ | Server version (semver) |
capabilities | ServerCapabilities | ❌ | Explicitly declare capabilities (usually auto-inferred) |
server.tool()
Register a callable tool. Tools are the primary way MCP servers expose functionality to LLMs.
import { z } from "zod";
server.tool(
"get_weather", // tool name
"Get current weather for a city", // description (shown to LLM)
{
city: z.string().describe("City name, e.g. 'Tokyo'"),
units: z.enum(["celsius", "fahrenheit"]).default("celsius"),
},
async ({ city, units }) => {
const data = await fetchWeather(city, units);
return {
content: [
{
type: "text",
text: `${city}: ${data.temp}°${units === "celsius" ? "C" : "F"}, ${data.condition}`,
},
],
};
}
);
tool() Signature
server.tool(
name: string,
description: string,
schema: ZodRawShape, // Zod schema object (NOT z.object() — raw shape)
handler: (args, extra) => Promise<CallToolResult>
): void
The extra parameter contains request metadata:
server.tool("my-tool", "desc", { q: z.string() }, async (args, extra) => {
console.log(extra.requestId); // unique request ID
console.log(extra.signal); // AbortSignal — respect this for cancellation
return { content: [{ type: "text", text: "done" }] };
});
CallToolResult Shape
// Text response
return {
content: [{ type: "text", text: "your response here" }],
};
// Image response
return {
content: [{ type: "image", data: base64String, mimeType: "image/png" }],
};
// Resource response (reference to a resource URI)
return {
content: [{ type: "resource", resource: { uri: "file:///path/to/file", text: "content" } }],
};
// Multiple content blocks
return {
content: [
{ type: "text", text: "Here is the chart:" },
{ type: "image", data: chartBase64, mimeType: "image/png" },
],
};
// Signal error to the LLM (not a thrown exception)
return {
content: [{ type: "text", text: "City not found" }],
isError: true,
};
server.resource()
Register a static or dynamic resource. Resources expose data that clients and LLMs can read by URI.
Static Resource
server.resource(
"app-config", // resource name
"config://app", // URI — clients use this to read it
async (uri) => ({
contents: [
{
uri: uri.href,
text: JSON.stringify({ version: "1.0", env: "production" }),
mimeType: "application/json",
},
],
})
);
Dynamic Resource with URI Template
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
server.resource(
"user-profile",
new ResourceTemplate("users://{userId}/profile", { list: undefined }),
async (uri, { userId }) => ({
contents: [
{
uri: uri.href,
text: JSON.stringify(await db.users.findById(userId)),
mimeType: "application/json",
},
],
})
);
ResourceTemplate Options
new ResourceTemplate(
"pattern://{param}", // URI template — {param} is extracted and passed to handler
{
list: async () => ({ // optional: enables resource listing
resources: [
{ uri: "users://123/profile", name: "Alice" },
{ uri: "users://456/profile", name: "Bob" },
],
}),
}
);
server.prompt()
Register a prompt template. Prompts let servers expose reusable message templates to LLM clients.
server.prompt(
"debug-error",
"Generate a debugging prompt for a given error",
{
error: z.string().describe("The error message or stack trace"),
language: z.string().optional().describe("Programming language"),
},
({ error, language }) => ({
messages: [
{
role: "user",
content: {
type: "text",
text: `Debug this ${language ?? "code"} error:\n\n${error}\n\nExplain the root cause and provide a fix.`,
},
},
],
})
);
Prompt Handler Return Shape
{
description?: string; // optional override shown to client
messages: PromptMessage[]; // one or more messages
}
// PromptMessage
{
role: "user" | "assistant";
content: TextContent | ImageContent | EmbeddedResource;
}
Low-Level Server API
Use Server directly when you need manual control over request handling — for proxies, middleware, or custom protocol extensions.
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
const server = new Server(
{ name: "low-level-server", version: "1.0.0" },
{
capabilities: {
tools: {}, // declare tool capability
resources: {}, // declare resource capability
prompts: {}, // declare prompt capability
},
}
);
// Handle tool list requests
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "ping",
description: "Returns pong",
inputSchema: { type: "object", properties: {}, required: [] },
},
],
}));
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "ping") {
return { content: [{ type: "text", text: "pong" }] };
}
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
});
Transport API
StdioServerTransport
The standard transport for CLI tools and local integrations (used by Cursor, Claude Desktop, etc.).
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const transport = new StdioServerTransport();
await server.connect(transport);
// Server now listens on stdin, writes to stdout
No configuration needed. The process must not write anything else to stdout — use stderr for logs.
// ✅ Log to stderr, never stdout
console.error("Server started"); // ok
console.log("Server started"); // breaks the transport
StreamableHTTPServerTransport
HTTP transport with optional SSE streaming. Use for web-based MCP deployments.
import express from "express";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
const app = express();
app.use(express.json());
// Stateless mode — new transport per request (simplest, recommended for REST)
app.post("/mcp", async (req, res) => {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // undefined = stateless
});
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});
app.listen(3000);
Stateful Mode (SSE streaming)
import { randomUUID } from "crypto";
const sessions = new Map<string, StreamableHTTPServerTransport>();
app.post("/mcp", async (req, res) => {
const sessionId = req.headers["mcp-session-id"] as string | undefined;
if (sessionId && sessions.has(sessionId)) {
// Reuse existing session
await sessions.get(sessionId)!.handleRequest(req, res, req.body);
return;
}
// New session
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (id) => sessions.set(id, transport),
});
transport.onclose = () => sessions.delete(transport.sessionId!);
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});
// SSE stream endpoint
app.get("/mcp", async (req, res) => {
const sessionId = req.headers["mcp-session-id"] as string;
const transport = sessions.get(sessionId);
if (!transport) return res.status(404).send("Session not found");
await transport.handleRequest(req, res);
});
SSEServerTransport (Legacy)
The older SSE-only transport. Use StreamableHTTPServerTransport for new projects. Kept here for compatibility with older clients.
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
app.get("/sse", (req, res) => {
const transport = new SSEServerTransport("/message", res);
server.connect(transport);
});
app.post("/message", (req, res) => {
transport.handlePostMessage(req, res);
});
Client API
Client
Use Client to connect to an MCP server from your own application — useful for testing, orchestration, and multi-agent setups.
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
const transport = new StdioClientTransport({
command: "node",
args: ["./my-server.js"],
env: { ...process.env }, // pass env to subprocess
});
const client = new Client(
{ name: "my-client", version: "1.0.0" },
{ capabilities: {} } // declare what the client supports
);
await client.connect(transport);
client.listTools()
const { tools } = await client.listTools();
for (const tool of tools) {
console.log(tool.name, tool.description);
// tool.inputSchema — JSON Schema of expected arguments
}
client.callTool()
const result = await client.callTool({
name: "get_weather",
arguments: { city: "Tokyo", units: "celsius" },
});
for (const block of result.content) {
if (block.type === "text") console.log(block.text);
}
if (result.isError) {
console.error("Tool reported an error");
}
client.listResources()
const { resources } = await client.listResources();
// resources[].uri, resources[].name, resources[].mimeType
client.readResource()
const { contents } = await client.readResource({ uri: "config://app" });
for (const content of contents) {
if ("text" in content) console.log(content.text);
}
client.listPrompts() / client.getPrompt()
const { prompts } = await client.listPrompts();
const result = await client.getPrompt({
name: "debug-error",
arguments: { error: "TypeError: Cannot read property 'x' of undefined" },
});
// result.messages — array of PromptMessage ready to send to an LLM
Client Transports
StdioClientTransport
Spawns a local process and communicates over stdin/stdout.
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
const transport = new StdioClientTransport({
command: "npx",
args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
env: process.env as Record<string, string>,
stderr: "pipe", // "pipe" | "inherit" | "ignore"
});
StreamableHTTPClientTransport
Connect to an HTTP MCP server.
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const transport = new StreamableHTTPClientTransport(
new URL("http://localhost:3000/mcp"),
{
requestInit: {
headers: { Authorization: `Bearer ${token}` },
},
}
);
SSEClientTransport (Legacy)
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
const transport = new SSEClientTransport(
new URL("http://localhost:3000/sse")
);
Error Handling
McpError
Throw McpError inside handlers to return structured JSON-RPC errors to the client.
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
server.tool("get_user", "Get user by ID", { id: z.string() }, async ({ id }) => {
const user = await db.users.findById(id);
if (!user) {
// Option 1: Signal soft error to LLM (tool ran, but result is an error)
return {
content: [{ type: "text", text: `User ${id} not found` }],
isError: true,
};
}
return { content: [{ type: "text", text: JSON.stringify(user) }] };
});
// Option 2: Throw for protocol-level errors (invalid params, server fault)
server.setRequestHandler(CallToolRequestSchema, async (req) => {
if (!req.params.name) {
throw new McpError(ErrorCode.InvalidParams, "Tool name is required");
}
});
Standard Error Codes
| Code | Constant | When to use |
|---|---|---|
-32700 | ErrorCode.ParseError | Invalid JSON received |
-32600 | ErrorCode.InvalidRequest | Request shape is wrong |
-32601 | ErrorCode.MethodNotFound | Unknown method or tool name |
-32602 | ErrorCode.InvalidParams | Arguments fail validation |
-32603 | ErrorCode.InternalError | Unhandled server exception |
Rule of thumb: use isError: true in content when the tool ran but got a bad result. Throw McpError only when the request itself is malformed.
Authentication
The SDK doesn't enforce auth — you add it at the transport layer.
API Key (HTTP)
app.post("/mcp", (req, res, next) => {
const key = req.headers["x-api-key"];
if (key !== process.env.MCP_API_KEY) {
return res.status(401).json({ error: "Unauthorized" });
}
next();
}, async (req, res) => {
// handle MCP request
});
OAuth 2.0 with OAuthServerProvider
For full OAuth support, the SDK exports OAuthServerProvider:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { OAuthServerProvider } from "@modelcontextprotocol/sdk/server/auth/provider.js";
// Implement the provider interface
class MyOAuthProvider implements OAuthServerProvider {
async authorize(req, res) { /* redirect to auth */ }
async token(req, res) { /* exchange code for token */ }
async verifyAccessToken(token: string) {
const payload = await verifyJwt(token);
return {
token,
clientId: payload.sub,
scopes: payload.scopes,
expiresAt: payload.exp,
};
}
// ... clientsStore, revoke etc.
}
// Attach to server
const provider = new MyOAuthProvider();
// Pass provider when creating transport or route middleware
For a full OAuth example, see the MCP OAuth example in the SDK repo.
Progress and Logging
Progress Notifications
Long-running tools can report progress back to the client:
server.tool(
"process_files",
"Process a batch of files",
{ paths: z.array(z.string()) },
async ({ paths }, extra) => {
for (let i = 0; i < paths.length; i++) {
// Check for cancellation before each unit of work
extra.signal.throwIfAborted();
await processFile(paths[i]);
// Report progress — client can show a progress bar
await server.server.notification({
method: "notifications/progress",
params: {
progressToken: extra.requestId,
progress: i + 1,
total: paths.length,
},
});
}
return { content: [{ type: "text", text: `Processed ${paths.length} files` }] };
}
);
Logging
Use the MCP logging notification to send structured logs to the client (avoids polluting stdout):
await server.server.notification({
method: "notifications/message",
params: {
level: "info", // "debug" | "info" | "warning" | "error"
logger: "my-server",
data: { message: "Connected to database", host: dbHost },
},
});
Resource Change Notifications
When your resources update, notify connected clients so they can re-read:
// Notify a specific resource changed
await server.server.notification({
method: "notifications/resources/updated",
params: { uri: "config://app" },
});
// Notify the resource list itself changed (new or removed resources)
await server.server.notification({
method: "notifications/resources/list_changed",
params: {},
});
// Same pattern for tools and prompts
await server.server.notification({ method: "notifications/tools/list_changed", params: {} });
await server.server.notification({ method: "notifications/prompts/list_changed", params: {} });
In-Process Transport (Testing)
Skip the network entirely — connect a client and server in the same process. Ideal for unit tests.
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
const server = new McpServer({ name: "test-server", version: "0.0.1" });
server.tool("add", "Add two numbers", { a: z.number(), b: z.number() }, async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }],
}));
const client = new Client({ name: "test-client", version: "0.0.1" }, { capabilities: {} });
// Create linked transport pair
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([
client.connect(clientTransport),
server.connect(serverTransport),
]);
// Now test your tools directly
const result = await client.callTool({ name: "add", arguments: { a: 2, b: 3 } });
console.log(result.content[0].text); // "5"
await client.close();
TypeScript Types Reference
Key types exported from @modelcontextprotocol/sdk/types.js:
// Tool result content blocks
type TextContent = { type: "text"; text: string };
type ImageContent = { type: "image"; data: string; mimeType: string };
type EmbeddedResource = { type: "resource"; resource: TextResourceContents | BlobResourceContents };
// Resource contents
type TextResourceContents = { uri: string; text: string; mimeType?: string };
type BlobResourceContents = { uri: string; blob: string; mimeType?: string }; // blob = base64
// Prompt message
type PromptMessage = { role: "user" | "assistant"; content: TextContent | ImageContent | EmbeddedResource };
// Tool call result
type CallToolResult = { content: (TextContent | ImageContent | EmbeddedResource)[]; isError?: boolean };
// Error
class McpError extends Error {
constructor(code: ErrorCode, message: string, data?: unknown);
code: number;
data?: unknown;
}
Production Checklist
Server
- Use
StdioServerTransportfor CLI / local tools,StreamableHTTPServerTransportfor web - Write logs to
stderr, neverstdout(stdio transport) - Respect
extra.signal(AbortSignal) in long-running tools - Return
isError: truefor tool-level errors, throwMcpErrorfor protocol errors - Add
try/catcharound external calls inside handlers — unhandled throws crash the server - Test with
InMemoryTransportbefore wiring up real transports
Client
- Always
await client.close()on shutdown — flushes pending messages - Handle
result.isError === trueincallToolresponses - Cache
listTools()result — don't call it on every tool invocation - Set
stderr: "pipe"onStdioClientTransportso subprocess errors don't leak to your stdout
What You Learned
McpServerwith.tool(),.resource(),.prompt()covers 90% of use casesStdioServerTransportfor local tools;StreamableHTTPServerTransportfor web APIsisError: truein content vs throwingMcpErrorserve different purposesInMemoryTransportmakes server testing fast and reliable- Progress, logging, and list-change notifications require calling
server.server.notification()directly
Next step: If you're building a tool-heavy server, check out the MCP server examples for filesystem, GitHub, and database reference implementations.
Tested on @modelcontextprotocol/sdk v1.8.0, Node.js 22.x, TypeScript 5.7