15 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Build & Test Commands
pnpm install # Install all workspace dependencies
pnpm build:all # Build all packages
pnpm lint:all # Run ESLint + Prettier checks across all packages
pnpm lint:fix:all # Auto-fix lint and formatting issues across all packages
pnpm typecheck:all # Type-check all packages
pnpm test:all # Run all tests (vitest) across all packages
pnpm check:all # typecheck + lint across all packages
# Run a single package script (examples)
# Run a single package script from the repo root with pnpm filter
pnpm --filter @modelcontextprotocol/core-internal test # vitest run (core)
pnpm --filter @modelcontextprotocol/core-internal test:watch # vitest (watch)
pnpm --filter @modelcontextprotocol/core-internal test -- path/to/file.test.ts
pnpm --filter @modelcontextprotocol/core-internal test -- -t "test name"
Breaking Changes
When making breaking changes, add to the relevant subsystem section in
docs/migration/upgrade-to-v2.md (or docs/migration/support-2026-07-28.md if the
change is 2026-07-28-only). Mechanical renames go in
packages/codemod/src/migrations/v1-to-v2/mappings/ and the codemod handles them — do
not reproduce mapping tables in the guide; link to the mapping file instead.
Include what changed, why, and how to migrate. Search for related sections and group related changes together rather than adding new standalone sections.
Code Style Guidelines
- TypeScript: Strict type checking, ES modules, explicit return types
- Naming: PascalCase for classes/types, camelCase for functions/variables
- Files: Lowercase with hyphens, test files with
.test.tssuffix - Imports: ES module style, no
.jsextension on relative imports (project usesmoduleResolution: bundler), group imports logically - Formatting: 2-space indentation, semicolons required, single quotes preferred
- Testing: Place tests under each package's
test/directory (vitest only includestest/**/*.test.ts), use descriptive test names - Comments: JSDoc for public APIs, inline comments for complex logic
JSDoc @example Code Snippets
JSDoc @example tags should pull type-checked code from companion .examples.ts files (e.g., client.ts → client.examples.ts). Use ```ts source="./file.examples.ts#regionName" fences referencing //#region regionName blocks; region names follow exportedName_variant or ClassName_methodName_variant pattern (e.g., applyMiddlewares_basicUsage, Client_connect_basicUsage). For whole-file inclusion (any file type), omit the #regionName.
Run pnpm sync:snippets to sync example content into JSDoc comments and markdown files.
Architecture Overview
Core Layers
The SDK is organized into three main layers:
-
Types Layer (
packages/core-internal/src/types/types.ts) - Protocol types generated from the MCP specification. All JSON-RPC message types, schemas, and protocol constants are defined here using Zod v4. -
Protocol Layer (
packages/core-internal/src/shared/protocol.ts) - The abstractProtocolclass that handles JSON-RPC message routing, request/response correlation, capability negotiation, and transport management. BothClientandServerextend this class. -
High-Level APIs:
Client(packages/client/src/client/client.ts) - Client implementation extending Protocol with typed methods for MCP operationsServer(packages/server/src/server/server.ts) - Server implementation extending Protocol with request handler registrationMcpServer(packages/server/src/server/mcp.ts) - High-level server API with simplified resource/tool/prompt registration
Public API Exports
The SDK separates internal code from the public API surface:
@modelcontextprotocol/core-internal(main entry,packages/core-internal/src/index.ts) — Internal barrel. Exports everything (including Zod schemas, Protocol class, stdio utils). Only consumed by sibling packages within the monorepo (private: true).@modelcontextprotocol/core-internal/public(packages/core-internal/src/exports/public/index.ts) — Curated public API. Exports only TypeScript types, error classes, constants, and guards. Re-exported by client and server packages.@modelcontextprotocol/clientand@modelcontextprotocol/server(packages/*/src/index.ts) — Final public surface. Package-specific exports (named explicitly) plus re-exports fromcore-internal/public.@modelcontextprotocol/core(packages/core/src/index.ts) — Public Zod-schema package and the canonical home of the schema source modules (src/schemas.ts,src/auth.ts,src/constants.ts). The root entry re-exports only the*SchemaZod constants (MCP spec + OAuth/OpenID) — the published home for raw runtime validation (CallToolResultSchema.parse(...)); runtime-neutral (zodis its only dependency). The./internalsubpath re-exports the schema modules wholesale for the sibling packages:core-internalre-exports them at the old module paths, and theclient/server/server-legacybundles resolve@modelcontextprotocol/core/internalas a real external dependency instead of carrying their own schema copies (their public surfaces stay Zod-free).
When modifying exports:
- Use explicit named exports, not
export *, in packageindex.tsfiles andcore-internal/public. - Adding a symbol to a package
index.tsmakes it public API — do so intentionally. - Internal helpers should stay in the core internal barrel and not be added to
core-internal/publicor package index files. - The package root entry must stay runtime-neutral so browser and Cloudflare Workers bundlers can consume it. Exports whose module graph transitively touches unpolyfillable Node builtins (
node:child_process,node:net,cross-spawn, etc.) must live at a named subpath export (e.g../stdio) and be covered by abarrelCleantest in that package.
Transport System
Transports (packages/core-internal/src/shared/transport.ts) provide the communication layer:
- Streamable HTTP (
packages/server/src/server/streamableHttp.ts,packages/client/src/client/streamableHttp.ts) - Recommended transport for remote servers, supports SSE for streaming - SSE (
packages/server/src/server/sse.ts,packages/client/src/client/sse.ts) - Legacy HTTP+SSE transport for backwards compatibility - stdio (
packages/server/src/server/stdio.ts,packages/client/src/client/stdio.ts) - For local process-spawned integrations
Server-Side Features
- Tools/Resources/Prompts: Registered via
McpServer.tool(),.resource(),.prompt()methods - OAuth/Auth: Full OAuth 2.0 server implementation in
packages/server/src/server/auth/ - Completions: Auto-completion support via
packages/server/src/server/completable.ts
Client-Side Features
- Auth: OAuth client support in
packages/client/src/client/auth.tsandpackages/client/src/client/auth-extensions.ts - Client middleware: Request middleware in
packages/client/src/client/middleware.ts(unrelated to the framework adapter packages below) - Sampling: Clients can handle
sampling/createMessagerequests from servers (LLM completions) - Elicitation: Clients can handle
elicitation/createrequests for user input (form or URL mode) - Roots: Clients can expose filesystem roots to servers via
roots/list
Middleware packages (framework/runtime adapters)
The repo also ships “middleware” packages under packages/middleware/ (e.g. @modelcontextprotocol/express, @modelcontextprotocol/hono, @modelcontextprotocol/node). These are thin integration layers for specific frameworks/runtimes and should not add new MCP functionality.
Experimental Features
Located in packages/*/src/experimental/. Currently empty.
Zod Schemas
The SDK uses zod/v4 internally. Schema utilities live in:
packages/core-internal/src/util/schema.ts- AnySchema alias and helpers for inspecting Zod objects
Validation
Pluggable JSON Schema validation (packages/core-internal/src/validators/):
ajvProvider.ts- Default Ajv-based validatorcfWorkerProvider.ts- Cloudflare Workers-compatible alternative
Examples
Runnable examples in examples/<story>/{server.ts,client.ts} — each story is its own
@mcp-examples/<story> workspace package and a self-verifying e2e test (the client connects,
asserts results, exits non-zero on mismatch). pnpm run:examples runs every story over its
configured transport×era legs; the examples (build + e2e) CI job is part of the per-PR gate
basket. See examples/README.md for the full story matrix.
examples/shared/—@mcp-examples/sharedpackage. Root export is args-only (parseExampleArgs,check,siblingPath); the demo OAuth provider andInMemoryEventStorelive at the@mcp-examples/shared/authsubpath so non-auth stories don't eagerly evaluate better-auth/express/better-sqlite3. Stories import only this plumbing and inline the SDK transport setup themselves — seeexamples/CONTRIBUTING.md.scripts/examples/— runner (run-examples.ts)examples/guides/— per-page snippet companions for thedocs/guide pages (one<section>/<page>.examples.tsper page); fences sync viapnpm sync:snippets, and the runnable ones are executed in CI bypnpm docs:examples
Message Flow (Bidirectional Protocol)
MCP is bidirectional: both client and server can send requests. Understanding this flow is essential when implementing new request types.
Class Hierarchy
Protocol (abstract base)
├── Client (packages/client/src/client/client.ts) - can send requests TO server, handle requests FROM server
└── Server (packages/server/src/server/server.ts) - can send requests TO client, handle requests FROM client
└── McpServer (packages/server/src/server/mcp.ts) - high-level wrapper around Server
Outbound Flow: Sending Requests
When code calls client.callTool() or server.createMessage():
- High-level method (e.g.,
Client.callTool()) callsthis.request() Protocol.request():- Assigns unique message ID
- Checks capabilities via
assertCapabilityForMethod()(abstract, implemented by Client/Server) - Creates response handler promise
- Calls
transport.send()with JSON-RPC request - Waits for response handler to resolve
- Transport serializes and sends over wire (HTTP, stdio, etc.)
Protocol._onresponse()resolves the promise when response arrives
Inbound Flow: Handling Requests
When a request arrives from the remote side:
- Transport receives message, calls
transport.onmessage() Protocol.connect()routes to_onrequest(),_onresponse(), or_onnotification()Protocol._onrequest():- Looks up handler in
_requestHandlersmap (keyed by method name) - Creates
BaseContextwithsignal,sessionId,sendNotification,sendRequest, etc. - Calls
buildContext()to let subclasses enrich the context (e.g., Server adds HTTP request info) - Invokes handler, sends JSON-RPC response back via transport
- Looks up handler in
- Handler was registered via
setRequestHandler('method', handler)
Handler Registration
// In Client (for server→client requests like sampling, elicitation)
client.setRequestHandler('sampling/createMessage', async (request, ctx) => {
// Handle sampling request from server
return { role: "assistant", content: {...}, model: "..." };
});
// In Server (for client→server requests like tools/call)
server.setRequestHandler('tools/call', async (request, ctx) => {
// Handle tool call from client
return { content: [...] };
});
Request Handler Context
The ctx parameter in handlers provides a structured context:
BaseContext (common to both Server and Client), fields organized into nested groups:
sessionId?: Transport session identifiermcpReq: Request-level concernsid: JSON-RPC message IDmethod: Request method string (e.g., 'tools/call')_meta?: Request metadatasignal: AbortSignal for cancellationsend(request, schema, options?): Send related request (for bidirectional flows)notify(notification): Send related notification back
http?: HTTP transport info (undefined for stdio)authInfo?: Validated auth token info
ServerContext extends BaseContext.mcpReq and BaseContext.http? via type intersection:
mcpReqadds:log(level, data, logger?),elicitInput(params, options?),requestSampling(params, options?)http?adds:req?(HTTP request info),closeSSE?,closeStandaloneSSE?
ClientContext is currently identical to BaseContext.
Capability Checking
Both sides declare capabilities during initialization. The SDK enforces these:
- Client→Server:
Client.assertCapabilityForMethod()checks_serverCapabilities - Server→Client:
Server.assertCapabilityForMethod()checks_clientCapabilities - Handler registration:
assertRequestHandlerCapability()validates local capabilities
Adding a New Request Type
- Define schema in
src/types.ts(request params, result schema) - Add capability to
ClientCapabilitiesorServerCapabilitiesin types - Implement sender method in Client or Server class
- Add capability check in the appropriate
assertCapabilityForMethod() - Register handler on the receiving side with
setRequestHandler() - For McpServer: Add high-level wrapper method if needed
Server-Initiated Requests (Sampling, Elicitation)
Server can request actions from client (requires client capability):
// Server sends sampling request to client
const result = await server.createMessage({
messages: [...],
maxTokens: 100
});
// Client must have registered handler:
client.setRequestHandler('sampling/createMessage', async (request, extra) => {
// Client-side LLM call
return { role: "assistant", content: {...} };
});
Key Patterns
Request Handler Registration (Low-Level Server)
server.setRequestHandler('tools/call', async (request, extra) => {
// extra contains sessionId, authInfo, sendNotification, etc.
return {
/* result */
};
});
Tool Registration (High-Level McpServer)
mcpServer.tool('tool-name', { param: z.string() }, async ({ param }, extra) => {
return { content: [{ type: 'text', text: 'result' }] };
});
Transport Connection
// Server
// (Node.js IncomingMessage/ServerResponse wrapper; exported by @modelcontextprotocol/node)
const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() });
await server.connect(transport);
// Client
const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3000/mcp'));
await client.connect(transport);