diff --git a/CLAUDE.md b/CLAUDE.md index 272c4d990..6e768e559 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,13 +1,18 @@ -# MCP TypeScript SDK Guide +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Build & Test Commands ```sh npm run build # Build ESM and CJS versions -npm run lint # Run ESLint -npm test # Run all tests -npx jest path/to/file.test.ts # Run specific test file -npx jest -t "test name" # Run tests matching pattern +npm run lint # Run ESLint and Prettier check +npm run lint:fix # Auto-fix lint and formatting issues +npm test # Run all tests (vitest) +npm run test:watch # Run tests in watch mode +npx vitest path/to/file.test.ts # Run specific test file +npx vitest -t "test name" # Run tests matching pattern +npm run typecheck # Type-check without emitting ``` ## Code Style Guidelines @@ -16,13 +21,206 @@ npx jest -t "test name" # Run tests matching pattern - **Naming**: PascalCase for classes/types, camelCase for functions/variables - **Files**: Lowercase with hyphens, test files with `.test.ts` suffix - **Imports**: ES module style, include `.js` extension, group imports logically -- **Error Handling**: Use TypeScript's strict mode, explicit error checking in tests - **Formatting**: 2-space indentation, semicolons required, single quotes preferred - **Testing**: Co-locate tests with source files, use descriptive test names - **Comments**: JSDoc for public APIs, inline comments for complex logic -## Project Structure +## Architecture Overview + +### Core Layers + +The SDK is organized into three main layers: + +1. **Types Layer** (`src/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** (`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` (`src/client/index.ts`) - Low-level client extending Protocol with typed methods for all MCP operations + - `Server` (`src/server/index.ts`) - Low-level server extending Protocol with request handler registration + - `McpServer` (`src/server/mcp.ts`) - High-level server API with simplified resource/tool/prompt registration + +### Transport System + +Transports (`src/shared/transport.ts`) provide the communication layer: + +- **Streamable HTTP** (`src/server/streamableHttp.ts`, `src/client/streamableHttp.ts`) - Recommended transport for remote servers, supports SSE for streaming +- **SSE** (`src/server/sse.ts`, `src/client/sse.ts`) - Legacy HTTP+SSE transport for backwards compatibility +- **stdio** (`src/server/stdio.ts`, `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 `src/server/auth/` +- **Completions**: Auto-completion support via `src/server/completable.ts` + +### Client-Side Features + +- **Auth**: OAuth client support in `src/client/auth.ts` and `src/client/auth-extensions.ts` +- **Middleware**: Request middleware in `src/client/middleware.ts` +- **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` + +### Experimental Features + +Located in `src/experimental/`: + +- **Tasks**: Long-running task support with polling/resumption (`src/experimental/tasks/`) + +### Zod Compatibility + +The SDK uses `zod/v4` internally but supports both v3 and v4 APIs. Compatibility utilities: + +- `src/server/zod-compat.ts` - Schema parsing helpers that work across versions +- `src/server/zod-json-schema-compat.ts` - Converts Zod schemas to JSON Schema + +### Validation + +Pluggable JSON Schema validation (`src/validation/`): + +- `ajv-provider.ts` - Default Ajv-based validator +- `cfworker-provider.ts` - Cloudflare Workers-compatible alternative + +### Examples + +Runnable examples in `src/examples/`: + +- `server/` - Various server configurations (stateful, stateless, OAuth, etc.) +- `client/` - Client examples (basic, OAuth, parallel calls, etc.) +- `shared/` - Shared utilities like in-memory event store + +## 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 (src/client/index.ts) - can send requests TO server, handle requests FROM server +└── Server (src/server/index.ts) - can send requests TO client, handle requests FROM client + └── McpServer (src/server/mcp.ts) - high-level wrapper around Server +``` + +### Outbound Flow: Sending Requests -- `/src`: Source code with client, server, and shared modules -- Tests alongside source files with `.test.ts` suffix -- Node.js >= 18 required +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 `RequestHandlerExtra` with `signal`, `sessionId`, `sendNotification`, `sendRequest` + - Invokes handler, sends JSON-RPC response back via transport +4. **Handler** was registered via `setRequestHandler(Schema, handler)` + +### Handler Registration + +```typescript +// In Client (for server→client requests like sampling, elicitation) +client.setRequestHandler(CreateMessageRequestSchema, async (request, extra) => { + // Handle sampling request from server + return { role: "assistant", content: {...}, model: "..." }; +}); + +// In Server (for client→server requests like tools/call) +server.setRequestHandler(CallToolRequestSchema, async (request, extra) => { + // Handle tool call from client + return { content: [...] }; +}); +``` + +### Request Handler Extra + +The `extra` parameter in handlers (`RequestHandlerExtra`) provides: + +- `signal`: AbortSignal for cancellation +- `sessionId`: Transport session identifier +- `authInfo`: Validated auth token info (if authenticated) +- `requestId`: JSON-RPC message ID +- `sendNotification(notification)`: Send related notification back +- `sendRequest(request, schema)`: Send related request (for bidirectional flows) +- `taskStore`: Task storage interface (if tasks enabled) + +### 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(CreateMessageRequestSchema, async (request, extra) => { + // Client-side LLM call + return { role: "assistant", content: {...} }; +}); +``` + +## Key Patterns + +### Request Handler Registration (Low-Level Server) + +```typescript +server.setRequestHandler(SomeRequestSchema, 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 +const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() }); +await server.connect(transport); + +// Client +const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3000/mcp')); +await client.connect(transport); +``` diff --git a/docs/server.md b/docs/server.md index b319e6f0f..bfb8dad21 100644 --- a/docs/server.md +++ b/docs/server.md @@ -71,7 +71,7 @@ For more detailed patterns (stateless vs stateful, JSON response mode, CORS, DNS MCP servers running on localhost are vulnerable to DNS rebinding attacks. Use `createMcpExpressApp()` to create an Express app with DNS rebinding protection enabled by default: ```typescript -import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/index.js'; +import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js'; // Protection auto-enabled (default host is 127.0.0.1) const app = createMcpExpressApp(); diff --git a/package-lock.json b/package-lock.json index e408792b9..7f8288a42 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@modelcontextprotocol/sdk", - "version": "1.24.1", + "version": "1.24.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@modelcontextprotocol/sdk", - "version": "1.24.1", + "version": "1.24.2", "license": "MIT", "dependencies": { "ajv": "^8.17.1", diff --git a/package.json b/package.json index 00f0d897a..18ee23d40 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/sdk", - "version": "1.24.1", + "version": "1.24.2", "description": "Model Context Protocol implementation for TypeScript", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", diff --git a/src/examples/server/elicitationFormExample.ts b/src/examples/server/elicitationFormExample.ts index e3ce083d1..6c0800949 100644 --- a/src/examples/server/elicitationFormExample.ts +++ b/src/examples/server/elicitationFormExample.ts @@ -12,7 +12,7 @@ import { type Request, type Response } from 'express'; import { McpServer } from '../../server/mcp.js'; import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; import { isInitializeRequest } from '../../types.js'; -import { createMcpExpressApp } from '../../server/index.js'; +import { createMcpExpressApp } from '../../server/express.js'; // Create MCP server - it will automatically use AjvJsonSchemaValidator with sensible defaults // The validator supports format validation (email, date, etc.) if ajv-formats is installed diff --git a/src/examples/server/elicitationUrlExample.ts b/src/examples/server/elicitationUrlExample.ts index e4d3d2268..5ddecc4e1 100644 --- a/src/examples/server/elicitationUrlExample.ts +++ b/src/examples/server/elicitationUrlExample.ts @@ -11,7 +11,7 @@ import express, { Request, Response } from 'express'; import { randomUUID } from 'node:crypto'; import { z } from 'zod'; import { McpServer } from '../../server/mcp.js'; -import { createMcpExpressApp } from '../../server/index.js'; +import { createMcpExpressApp } from '../../server/express.js'; import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; import { getOAuthProtectedResourceMetadataUrl, mcpAuthMetadataRouter } from '../../server/auth/router.js'; import { requireBearerAuth } from '../../server/auth/middleware/bearerAuth.js'; diff --git a/src/examples/server/jsonResponseStreamableHttp.ts b/src/examples/server/jsonResponseStreamableHttp.ts index 9be3d7204..224955c46 100644 --- a/src/examples/server/jsonResponseStreamableHttp.ts +++ b/src/examples/server/jsonResponseStreamableHttp.ts @@ -4,7 +4,7 @@ import { McpServer } from '../../server/mcp.js'; import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; import * as z from 'zod/v4'; import { CallToolResult, isInitializeRequest } from '../../types.js'; -import { createMcpExpressApp } from '../../server/index.js'; +import { createMcpExpressApp } from '../../server/express.js'; // Create an MCP server with implementation details const getServer = () => { diff --git a/src/examples/server/simpleSseServer.ts b/src/examples/server/simpleSseServer.ts index bc6fd2cab..1cd10cd2d 100644 --- a/src/examples/server/simpleSseServer.ts +++ b/src/examples/server/simpleSseServer.ts @@ -3,7 +3,7 @@ import { McpServer } from '../../server/mcp.js'; import { SSEServerTransport } from '../../server/sse.js'; import * as z from 'zod/v4'; import { CallToolResult } from '../../types.js'; -import { createMcpExpressApp } from '../../server/index.js'; +import { createMcpExpressApp } from '../../server/express.js'; /** * This example server demonstrates the deprecated HTTP+SSE transport diff --git a/src/examples/server/simpleStatelessStreamableHttp.ts b/src/examples/server/simpleStatelessStreamableHttp.ts index e2cefffd8..748d82fda 100644 --- a/src/examples/server/simpleStatelessStreamableHttp.ts +++ b/src/examples/server/simpleStatelessStreamableHttp.ts @@ -3,7 +3,7 @@ import { McpServer } from '../../server/mcp.js'; import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; import * as z from 'zod/v4'; import { CallToolResult, GetPromptResult, ReadResourceResult } from '../../types.js'; -import { createMcpExpressApp } from '../../server/index.js'; +import { createMcpExpressApp } from '../../server/express.js'; const getServer = () => { // Create an MCP server with implementation details diff --git a/src/examples/server/simpleStreamableHttp.ts b/src/examples/server/simpleStreamableHttp.ts index 3500ac066..ca1363198 100644 --- a/src/examples/server/simpleStreamableHttp.ts +++ b/src/examples/server/simpleStreamableHttp.ts @@ -5,7 +5,7 @@ import { McpServer } from '../../server/mcp.js'; import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; import { getOAuthProtectedResourceMetadataUrl, mcpAuthMetadataRouter } from '../../server/auth/router.js'; import { requireBearerAuth } from '../../server/auth/middleware/bearerAuth.js'; -import { createMcpExpressApp } from '../../server/index.js'; +import { createMcpExpressApp } from '../../server/express.js'; import { CallToolResult, ElicitResultSchema, diff --git a/src/examples/server/simpleTaskInteractive.ts b/src/examples/server/simpleTaskInteractive.ts index 51e97b7e9..c35126dc0 100644 --- a/src/examples/server/simpleTaskInteractive.ts +++ b/src/examples/server/simpleTaskInteractive.ts @@ -11,7 +11,8 @@ import { Request, Response } from 'express'; import { randomUUID } from 'node:crypto'; -import { createMcpExpressApp, Server } from '../../server/index.js'; +import { Server } from '../../server/index.js'; +import { createMcpExpressApp } from '../../server/express.js'; import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; import { CallToolResult, diff --git a/src/examples/server/sseAndStreamableHttpCompatibleServer.ts b/src/examples/server/sseAndStreamableHttpCompatibleServer.ts index 317cb2bfe..5c91b7e33 100644 --- a/src/examples/server/sseAndStreamableHttpCompatibleServer.ts +++ b/src/examples/server/sseAndStreamableHttpCompatibleServer.ts @@ -6,7 +6,7 @@ import { SSEServerTransport } from '../../server/sse.js'; import * as z from 'zod/v4'; import { CallToolResult, isInitializeRequest } from '../../types.js'; import { InMemoryEventStore } from '../shared/inMemoryEventStore.js'; -import { createMcpExpressApp } from '../../server/index.js'; +import { createMcpExpressApp } from '../../server/express.js'; /** * This example server demonstrates backwards compatibility with both: diff --git a/src/examples/server/ssePollingExample.ts b/src/examples/server/ssePollingExample.ts index 83ef8e4b1..bbecf2fdb 100644 --- a/src/examples/server/ssePollingExample.ts +++ b/src/examples/server/ssePollingExample.ts @@ -15,7 +15,7 @@ import { Request, Response } from 'express'; import { randomUUID } from 'node:crypto'; import { McpServer } from '../../server/mcp.js'; -import { createMcpExpressApp } from '../../server/index.js'; +import { createMcpExpressApp } from '../../server/express.js'; import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; import { CallToolResult } from '../../types.js'; import { InMemoryEventStore } from '../shared/inMemoryEventStore.js'; diff --git a/src/examples/server/standaloneSseWithGetStreamableHttp.ts b/src/examples/server/standaloneSseWithGetStreamableHttp.ts index 33bd73d04..546d35c70 100644 --- a/src/examples/server/standaloneSseWithGetStreamableHttp.ts +++ b/src/examples/server/standaloneSseWithGetStreamableHttp.ts @@ -3,7 +3,7 @@ import { randomUUID } from 'node:crypto'; import { McpServer } from '../../server/mcp.js'; import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; import { isInitializeRequest, ReadResourceResult } from '../../types.js'; -import { createMcpExpressApp } from '../../server/index.js'; +import { createMcpExpressApp } from '../../server/express.js'; // Create an MCP server with implementation details const server = new McpServer({ diff --git a/src/server/express.ts b/src/server/express.ts new file mode 100644 index 000000000..a542acd7a --- /dev/null +++ b/src/server/express.ts @@ -0,0 +1,74 @@ +import express, { Express } from 'express'; +import { hostHeaderValidation, localhostHostValidation } from './middleware/hostHeaderValidation.js'; + +/** + * Options for creating an MCP Express application. + */ +export interface CreateMcpExpressAppOptions { + /** + * The hostname to bind to. Defaults to '127.0.0.1'. + * When set to '127.0.0.1', 'localhost', or '::1', DNS rebinding protection is automatically enabled. + */ + host?: string; + + /** + * List of allowed hostnames for DNS rebinding protection. + * If provided, host header validation will be applied using this list. + * For IPv6, provide addresses with brackets (e.g., '[::1]'). + * + * This is useful when binding to '0.0.0.0' or '::' but still wanting + * to restrict which hostnames are allowed. + */ + allowedHosts?: string[]; +} + +/** + * Creates an Express application pre-configured for MCP servers. + * + * When the host is '127.0.0.1', 'localhost', or '::1' (the default is '127.0.0.1'), + * DNS rebinding protection middleware is automatically applied to protect against + * DNS rebinding attacks on localhost servers. + * + * @param options - Configuration options + * @returns A configured Express application + * + * @example + * ```typescript + * // Basic usage - defaults to 127.0.0.1 with DNS rebinding protection + * const app = createMcpExpressApp(); + * + * // Custom host - DNS rebinding protection only applied for localhost hosts + * const app = createMcpExpressApp({ host: '0.0.0.0' }); // No automatic DNS rebinding protection + * const app = createMcpExpressApp({ host: 'localhost' }); // DNS rebinding protection enabled + * + * // Custom allowed hosts for non-localhost binding + * const app = createMcpExpressApp({ host: '0.0.0.0', allowedHosts: ['myapp.local', 'localhost'] }); + * ``` + */ +export function createMcpExpressApp(options: CreateMcpExpressAppOptions = {}): Express { + const { host = '127.0.0.1', allowedHosts } = options; + + const app = express(); + app.use(express.json()); + + // If allowedHosts is explicitly provided, use that for validation + if (allowedHosts) { + app.use(hostHeaderValidation(allowedHosts)); + } else { + // Apply DNS rebinding protection automatically for localhost hosts + const localhostHosts = ['127.0.0.1', 'localhost', '::1']; + if (localhostHosts.includes(host)) { + app.use(localhostHostValidation()); + } else if (host === '0.0.0.0' || host === '::') { + // Warn when binding to all interfaces without DNS rebinding protection + // eslint-disable-next-line no-console + console.warn( + `Warning: Server is binding to ${host} without DNS rebinding protection. ` + + 'Consider using the allowedHosts option to restrict allowed hosts, ' + + 'or use authentication to protect your server.' + ); + } + } + + return app; +} diff --git a/src/server/index.test.ts b/src/server/index.test.ts index c01e638d0..035754a47 100644 --- a/src/server/index.test.ts +++ b/src/server/index.test.ts @@ -3,7 +3,7 @@ import supertest from 'supertest'; import { Client } from '../client/index.js'; import { InMemoryTransport } from '../inMemory.js'; import type { Transport } from '../shared/transport.js'; -import { createMcpExpressApp } from './index.js'; +import { createMcpExpressApp } from './express.js'; import { CreateMessageRequestSchema, CreateMessageResultSchema, diff --git a/src/server/index.ts b/src/server/index.ts index 43aca3b93..aa1a62d00 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,6 +1,4 @@ -import express, { Express } from 'express'; import { mergeCapabilities, Protocol, type NotificationOptions, type ProtocolOptions, type RequestOptions } from '../shared/protocol.js'; -import { hostHeaderValidation, localhostHostValidation } from './middleware/hostHeaderValidation.js'; import { type ClientCapabilities, type CreateMessageRequest, @@ -669,75 +667,3 @@ export class Server< return this.notification({ method: 'notifications/prompts/list_changed' }); } } - -/** - * Options for creating an MCP Express application. - */ -export interface CreateMcpExpressAppOptions { - /** - * The hostname to bind to. Defaults to '127.0.0.1'. - * When set to '127.0.0.1', 'localhost', or '::1', DNS rebinding protection is automatically enabled. - */ - host?: string; - - /** - * List of allowed hostnames for DNS rebinding protection. - * If provided, host header validation will be applied using this list. - * For IPv6, provide addresses with brackets (e.g., '[::1]'). - * - * This is useful when binding to '0.0.0.0' or '::' but still wanting - * to restrict which hostnames are allowed. - */ - allowedHosts?: string[]; -} - -/** - * Creates an Express application pre-configured for MCP servers. - * - * When the host is '127.0.0.1', 'localhost', or '::1' (the default is '127.0.0.1'), - * DNS rebinding protection middleware is automatically applied to protect against - * DNS rebinding attacks on localhost servers. - * - * @param options - Configuration options - * @returns A configured Express application - * - * @example - * ```typescript - * // Basic usage - defaults to 127.0.0.1 with DNS rebinding protection - * const app = createMcpExpressApp(); - * - * // Custom host - DNS rebinding protection only applied for localhost hosts - * const app = createMcpExpressApp({ host: '0.0.0.0' }); // No automatic DNS rebinding protection - * const app = createMcpExpressApp({ host: 'localhost' }); // DNS rebinding protection enabled - * - * // Custom allowed hosts for non-localhost binding - * const app = createMcpExpressApp({ host: '0.0.0.0', allowedHosts: ['myapp.local', 'localhost'] }); - * ``` - */ -export function createMcpExpressApp(options: CreateMcpExpressAppOptions = {}): Express { - const { host = '127.0.0.1', allowedHosts } = options; - - const app = express(); - app.use(express.json()); - - // If allowedHosts is explicitly provided, use that for validation - if (allowedHosts) { - app.use(hostHeaderValidation(allowedHosts)); - } else { - // Apply DNS rebinding protection automatically for localhost hosts - const localhostHosts = ['127.0.0.1', 'localhost', '::1']; - if (localhostHosts.includes(host)) { - app.use(localhostHostValidation()); - } else if (host === '0.0.0.0' || host === '::') { - // Warn when binding to all interfaces without DNS rebinding protection - // eslint-disable-next-line no-console - console.warn( - `Warning: Server is binding to ${host} without DNS rebinding protection. ` + - 'Consider using the allowedHosts option to restrict allowed hosts, ' + - 'or use authentication to protect your server.' - ); - } - } - - return app; -} diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index 981768ec5..7dc4742e6 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -2315,12 +2315,18 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { version: '1.0' }); + const mockDate = new Date().toISOString(); mcpServer.resource( 'test', 'test://resource', { description: 'Test resource', - mimeType: 'text/plain' + mimeType: 'text/plain', + annotations: { + audience: ['user'], + priority: 0.5, + lastModified: mockDate + } }, async () => ({ contents: [ @@ -2346,6 +2352,11 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { expect(result.resources).toHaveLength(1); expect(result.resources[0].description).toBe('Test resource'); expect(result.resources[0].mimeType).toBe('text/plain'); + expect(result.resources[0].annotations).toEqual({ + audience: ['user'], + priority: 0.5, + lastModified: mockDate + }); }); /*** diff --git a/src/server/sse.ts b/src/server/sse.ts index 270eebc19..b7450a09e 100644 --- a/src/server/sse.ts +++ b/src/server/sse.ts @@ -17,7 +17,7 @@ export interface SSEServerTransportOptions { * List of allowed host header values for DNS rebinding protection. * If not specified, host validation is disabled. * @deprecated Use the `hostHeaderValidation` middleware from `@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js` instead, - * or use `createMcpExpressApp` from `@modelcontextprotocol/sdk/server/index.js` which includes localhost protection by default. + * or use `createMcpExpressApp` from `@modelcontextprotocol/sdk/server/express.js` which includes localhost protection by default. */ allowedHosts?: string[]; @@ -25,7 +25,7 @@ export interface SSEServerTransportOptions { * List of allowed origin header values for DNS rebinding protection. * If not specified, origin validation is disabled. * @deprecated Use the `hostHeaderValidation` middleware from `@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js` instead, - * or use `createMcpExpressApp` from `@modelcontextprotocol/sdk/server/index.js` which includes localhost protection by default. + * or use `createMcpExpressApp` from `@modelcontextprotocol/sdk/server/express.js` which includes localhost protection by default. */ allowedOrigins?: string[]; @@ -33,7 +33,7 @@ export interface SSEServerTransportOptions { * Enable DNS rebinding protection (requires allowedHosts and/or allowedOrigins to be configured). * Default is false for backwards compatibility. * @deprecated Use the `hostHeaderValidation` middleware from `@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js` instead, - * or use `createMcpExpressApp` from `@modelcontextprotocol/sdk/server/index.js` which includes localhost protection by default. + * or use `createMcpExpressApp` from `@modelcontextprotocol/sdk/server/express.js` which includes localhost protection by default. */ enableDnsRebindingProtection?: boolean; } diff --git a/src/server/streamableHttp.ts b/src/server/streamableHttp.ts index 841d6654d..658592c19 100644 --- a/src/server/streamableHttp.ts +++ b/src/server/streamableHttp.ts @@ -105,7 +105,7 @@ export interface StreamableHTTPServerTransportOptions { * List of allowed host header values for DNS rebinding protection. * If not specified, host validation is disabled. * @deprecated Use the `hostHeaderValidation` middleware from `@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js` instead, - * or use `createMcpExpressApp` from `@modelcontextprotocol/sdk/server/index.js` which includes localhost protection by default. + * or use `createMcpExpressApp` from `@modelcontextprotocol/sdk/server/express.js` which includes localhost protection by default. */ allowedHosts?: string[]; @@ -113,7 +113,7 @@ export interface StreamableHTTPServerTransportOptions { * List of allowed origin header values for DNS rebinding protection. * If not specified, origin validation is disabled. * @deprecated Use the `hostHeaderValidation` middleware from `@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js` instead, - * or use `createMcpExpressApp` from `@modelcontextprotocol/sdk/server/index.js` which includes localhost protection by default. + * or use `createMcpExpressApp` from `@modelcontextprotocol/sdk/server/express.js` which includes localhost protection by default. */ allowedOrigins?: string[]; @@ -121,7 +121,7 @@ export interface StreamableHTTPServerTransportOptions { * Enable DNS rebinding protection (requires allowedHosts and/or allowedOrigins to be configured). * Default is false for backwards compatibility. * @deprecated Use the `hostHeaderValidation` middleware from `@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js` instead, - * or use `createMcpExpressApp` from `@modelcontextprotocol/sdk/server/index.js` which includes localhost protection by default. + * or use `createMcpExpressApp` from `@modelcontextprotocol/sdk/server/express.js` which includes localhost protection by default. */ enableDnsRebindingProtection?: boolean; diff --git a/src/shared/metadataUtils.ts b/src/shared/metadataUtils.ts index 7e9846aa8..18f84a4c9 100644 --- a/src/shared/metadataUtils.ts +++ b/src/shared/metadataUtils.ts @@ -10,14 +10,14 @@ import { BaseMetadata } from '../types.js'; * For other objects: title → name * This implements the spec requirement: "if no title is provided, name should be used for display purposes" */ -export function getDisplayName(metadata: BaseMetadata & { annotations?: { title?: string } }): string { +export function getDisplayName(metadata: BaseMetadata | (BaseMetadata & { annotations?: { title?: string } })): string { // First check for title (not undefined and not empty string) if (metadata.title !== undefined && metadata.title !== '') { return metadata.title; } // Then check for annotations.title (only present in Tool objects) - if (metadata.annotations?.title) { + if ('annotations' in metadata && metadata.annotations?.title) { return metadata.annotations.title; } diff --git a/src/spec.types.test.ts b/src/spec.types.test.ts index 66e0da207..539ea38ff 100644 --- a/src/spec.types.test.ts +++ b/src/spec.types.test.ts @@ -656,6 +656,10 @@ const sdkTypeChecks = { ) => { sdk = spec; spec = sdk; + }, + Annotations: (sdk: SDKTypes.Annotations, spec: SpecTypes.Annotations) => { + sdk = spec; + spec = sdk; } }; @@ -667,10 +671,7 @@ const MISSING_SDK_TYPES = [ // These are inlined in the SDK: 'Role', 'Error', // The inner error object of a JSONRPCError - 'URLElicitationRequiredError', // In the SDK, but with a custom definition - // These aren't supported by the SDK yet: - // TODO: Add definitions to the SDK - 'Annotations' + 'URLElicitationRequiredError' // In the SDK, but with a custom definition ]; function extractExportedTypes(source: string): string[] { diff --git a/src/types.test.ts b/src/types.test.ts index 4570a443a..e0b17c628 100644 --- a/src/types.test.ts +++ b/src/types.test.ts @@ -95,68 +95,118 @@ describe('Types', () => { describe('ContentBlock', () => { test('should validate text content', () => { + const mockDate = new Date().toISOString(); const textContent = { type: 'text', - text: 'Hello, world!' + text: 'Hello, world!', + annotations: { + audience: ['user'], + priority: 0.5, + lastModified: mockDate + } }; const result = ContentBlockSchema.safeParse(textContent); expect(result.success).toBe(true); if (result.success) { expect(result.data.type).toBe('text'); + expect(result.data.annotations).toEqual({ + audience: ['user'], + priority: 0.5, + lastModified: mockDate + }); } }); test('should validate image content', () => { + const mockDate = new Date().toISOString(); const imageContent = { type: 'image', data: 'aGVsbG8=', // base64 encoded "hello" - mimeType: 'image/png' + mimeType: 'image/png', + annotations: { + audience: ['user'], + priority: 0.5, + lastModified: mockDate + } }; const result = ContentBlockSchema.safeParse(imageContent); expect(result.success).toBe(true); if (result.success) { expect(result.data.type).toBe('image'); + expect(result.data.annotations).toEqual({ + audience: ['user'], + priority: 0.5, + lastModified: mockDate + }); } }); test('should validate audio content', () => { + const mockDate = new Date().toISOString(); const audioContent = { type: 'audio', data: 'aGVsbG8=', // base64 encoded "hello" - mimeType: 'audio/mp3' + mimeType: 'audio/mp3', + annotations: { + audience: ['user'], + priority: 0.5, + lastModified: mockDate + } }; const result = ContentBlockSchema.safeParse(audioContent); expect(result.success).toBe(true); if (result.success) { expect(result.data.type).toBe('audio'); + expect(result.data.annotations).toEqual({ + audience: ['user'], + priority: 0.5, + lastModified: mockDate + }); } }); test('should validate resource link content', () => { + const mockDate = new Date().toISOString(); const resourceLink = { type: 'resource_link', uri: 'file:///path/to/file.txt', name: 'file.txt', - mimeType: 'text/plain' + mimeType: 'text/plain', + annotations: { + audience: ['user'], + priority: 0.5, + lastModified: new Date().toISOString() + } }; const result = ContentBlockSchema.safeParse(resourceLink); expect(result.success).toBe(true); if (result.success) { expect(result.data.type).toBe('resource_link'); + expect(result.data.annotations).toEqual({ + audience: ['user'], + priority: 0.5, + lastModified: mockDate + }); } }); test('should validate embedded resource content', () => { + const mockDate = new Date().toISOString(); const embeddedResource = { type: 'resource', resource: { uri: 'file:///path/to/file.txt', mimeType: 'text/plain', text: 'File contents' + }, + annotations: { + audience: ['user'], + priority: 0.5, + lastModified: mockDate } }; @@ -164,6 +214,11 @@ describe('Types', () => { expect(result.success).toBe(true); if (result.success) { expect(result.data.type).toBe('resource'); + expect(result.data.annotations).toEqual({ + audience: ['user'], + priority: 0.5, + lastModified: mockDate + }); } }); }); diff --git a/src/types.ts b/src/types.ts index 923da5447..744877db1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -790,6 +790,26 @@ export const BlobResourceContentsSchema = ResourceContentsSchema.extend({ blob: Base64Schema }); +/** + * Optional annotations providing clients additional context about a resource. + */ +export const AnnotationsSchema = z.object({ + /** + * Intended audience(s) for the resource. + */ + audience: z.array(z.enum(['user', 'assistant'])).optional(), + + /** + * Importance hint for the resource, from 0 (least) to 1 (most). + */ + priority: z.number().min(0).max(1).optional(), + + /** + * ISO 8601 timestamp for the most recent modification. + */ + lastModified: z.iso.datetime({ offset: true }).optional() +}); + /** * A known resource that the server is capable of reading. */ @@ -813,6 +833,11 @@ export const ResourceSchema = z.object({ */ mimeType: z.optional(z.string()), + /** + * Optional annotations for the client. + */ + annotations: AnnotationsSchema.optional(), + /** * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) * for notes on _meta usage. @@ -843,6 +868,11 @@ export const ResourceTemplateSchema = z.object({ */ mimeType: z.optional(z.string()), + /** + * Optional annotations for the client. + */ + annotations: AnnotationsSchema.optional(), + /** * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) * for notes on _meta usage. @@ -1035,6 +1065,11 @@ export const TextContentSchema = z.object({ */ text: z.string(), + /** + * Optional annotations for the client. + */ + annotations: AnnotationsSchema.optional(), + /** * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) * for notes on _meta usage. @@ -1056,6 +1091,11 @@ export const ImageContentSchema = z.object({ */ mimeType: z.string(), + /** + * Optional annotations for the client. + */ + annotations: AnnotationsSchema.optional(), + /** * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) * for notes on _meta usage. @@ -1077,6 +1117,11 @@ export const AudioContentSchema = z.object({ */ mimeType: z.string(), + /** + * Optional annotations for the client. + */ + annotations: AnnotationsSchema.optional(), + /** * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) * for notes on _meta usage. @@ -1120,6 +1165,10 @@ export const ToolUseContentSchema = z export const EmbeddedResourceSchema = z.object({ type: z.literal('resource'), resource: z.union([TextResourceContentsSchema, BlobResourceContentsSchema]), + /** + * Optional annotations for the client. + */ + annotations: AnnotationsSchema.optional(), /** * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) * for notes on _meta usage. @@ -2219,6 +2268,7 @@ export type CancelledNotification = Infer; export type Icon = Infer; export type Icons = Infer; export type BaseMetadata = Infer; +export type Annotations = Infer; /* Initialization */ export type Implementation = Infer;