-
Notifications
You must be signed in to change notification settings - Fork 2.2k
docs: state the principles in CLAUDE.md and shorten REVIEW.md #2938
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,284 +1,14 @@ | ||
| # CLAUDE.md | ||
|
|
||
| This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. | ||
|
|
||
| ## Build & Test Commands | ||
|
|
||
| ```sh | ||
| 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.ts` suffix | ||
| - **Imports**: ES module style, no `.js` extension on relative imports (project uses `moduleResolution: bundler`), group imports logically | ||
| - **Formatting**: 2-space indentation, semicolons required, single quotes preferred | ||
| - **Testing**: Place tests under each package's `test/` directory (vitest only includes `test/**/*.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: | ||
|
|
||
| 1. **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. | ||
|
|
||
| 2. **Protocol Layer** (`packages/core-internal/src/shared/protocol.ts`) - The abstract `Protocol` class that handles JSON-RPC message routing, request/response correlation, capability negotiation, and transport management. Both `Client` and `Server` extend this class. | ||
|
|
||
| 3. **High-Level APIs**: | ||
| - `Client` (`packages/client/src/client/client.ts`) - Client implementation extending Protocol with typed methods for MCP operations | ||
| - `Server` (`packages/server/src/server/server.ts`) - Server implementation extending Protocol with request handler registration | ||
| - `McpServer` (`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 TypeScript types, error classes, constants, guards, and the `Protocol` base class (+ `mergeCapabilities`). Re-exported by client and server packages. | ||
| - **`@modelcontextprotocol/client`** and **`@modelcontextprotocol/server`** (`packages/*/src/index.ts`) — Final public surface. Package-specific exports (named explicitly) plus re-exports from `core-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 `*Schema` Zod constants (MCP spec + OAuth/OpenID) — the published home for raw runtime validation (`CallToolResultSchema.parse(...)`); runtime-neutral (`zod` is its only dependency). The `./internal` subpath re-exports the schema modules wholesale for the sibling packages: `core-internal` re-exports them at the old module paths, and the `client`/`server`/`server-legacy` bundles resolve `@modelcontextprotocol/core/internal` as 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 package `index.ts` files and `core-internal/public`. | ||
| - Adding a symbol to a package `index.ts` makes it public API — do so intentionally. | ||
| - Internal helpers should stay in the core internal barrel and not be added to `core-internal/public` or 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 a `barrelClean` test 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.ts` and `packages/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/createMessage` requests from servers (LLM completions) | ||
| - **Elicitation**: Clients can handle `elicitation/create` requests 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 validator | ||
| - `cfWorkerProvider.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/shared` package. Root export is args-only (`parseExampleArgs`, `check`, `siblingPath`); the demo OAuth provider and `InMemoryEventStore` live at the `@mcp-examples/shared/auth` subpath 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 — see `examples/CONTRIBUTING.md`. | ||
| - `scripts/examples/` — runner (`run-examples.ts`) | ||
| - `examples/guides/` — per-page snippet companions for the `docs/` guide pages (one `<section>/<page>.examples.ts` per page); fences sync via `pnpm sync:snippets`, and the runnable ones are executed in CI by `pnpm 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()`: | ||
|
|
||
| 1. **High-level method** (e.g., `Client.callTool()`) calls `this.request()` | ||
| 2. **`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 | ||
| 3. **Transport** serializes and sends over wire (HTTP, stdio, etc.) | ||
| 4. **`Protocol._onresponse()`** resolves the promise when response arrives | ||
|
|
||
| ### Inbound Flow: Handling Requests | ||
|
|
||
| When a request arrives from the remote side: | ||
|
|
||
| 1. **Transport** receives message, calls `transport.onmessage()` | ||
| 2. **`Protocol.connect()`** routes to `_onrequest()`, `_onresponse()`, or `_onnotification()` | ||
| 3. **`Protocol._onrequest()`**: | ||
| - Looks up handler in `_requestHandlers` map (keyed by method name) | ||
| - Creates `BaseContext` with `signal`, `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 | ||
| 4. **Handler** was registered via `setRequestHandler('method', handler)` | ||
|
|
||
| ### Handler Registration | ||
|
|
||
| ```typescript | ||
| // 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 identifier | ||
| - `mcpReq`: Request-level concerns | ||
| - `id`: JSON-RPC message ID | ||
| - `method`: Request method string (e.g., 'tools/call') | ||
| - `_meta?`: Request metadata | ||
| - `signal`: AbortSignal for cancellation | ||
| - `send(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: | ||
|
|
||
| - `mcpReq` adds: `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 | ||
|
|
||
| 1. **Define schema** in `src/types.ts` (request params, result schema) | ||
| 2. **Add capability** to `ClientCapabilities` or `ServerCapabilities` in types | ||
| 3. **Implement sender** method in Client or Server class | ||
| 4. **Add capability check** in the appropriate `assertCapabilityForMethod()` | ||
| 5. **Register handler** on the receiving side with `setRequestHandler()` | ||
| 6. **For McpServer**: Add high-level wrapper method if needed | ||
|
|
||
| ### Server-Initiated Requests (Sampling, Elicitation) | ||
|
|
||
| Server can request actions from client (requires client capability): | ||
|
|
||
| ```typescript | ||
| // 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) | ||
|
|
||
| ```typescript | ||
| server.setRequestHandler('tools/call', async (request, extra) => { | ||
| // extra contains sessionId, authInfo, sendNotification, etc. | ||
| return { | ||
| /* result */ | ||
| }; | ||
| }); | ||
| ``` | ||
|
|
||
| ### Tool Registration (High-Level McpServer) | ||
|
|
||
| ```typescript | ||
| mcpServer.tool('tool-name', { param: z.string() }, async ({ param }, extra) => { | ||
| return { content: [{ type: 'text', text: 'result' }] }; | ||
| }); | ||
| ``` | ||
|
|
||
| ### Transport Connection | ||
|
|
||
| ```typescript | ||
| // 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); | ||
| ``` | ||
| The principles this SDK is built and reviewed by. They are not a checklist: apply them with judgement. | ||
|
|
||
| 1. **Minimalism** — The SDK should do less, not more: prefer changes that add no API or option unless justified. | ||
| 2. **Spec is the anchor** — The spec decides; where it leaves a choice open, check what the other official SDKs do before making your own choice. | ||
| 3. **Strict by default** — Accept what the spec allows, and add no leniency for peers that break it. | ||
| 4. **Backwards compatible** — New features and changes arrive in backwards compatible ways; when a bug, a spec violation or an unsafe default forces a break, the changeset says what breaks. | ||
| 5. **Small changes** — Guard scope hard: a smaller change that lands beats a bigger one that doesn't, and small, independent pull requests can land in any order. | ||
| 6. **The words are true** — Changesets, docs and comments say what the code does. | ||
| 7. **Test what users see** — Prefer tests that cover the end-to-end usage a user would actually see, where sensible; `test/e2e/requirements.ts` lists those behaviours. | ||
| 8. **Brief comments** — Keep inline comments to one line and JSDoc to the public API; the reasoning goes in the commit message or PR description. | ||
|
|
||
| Style belongs to Prettier, ESLint and the compiler. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.