diff --git a/package-lock.json b/package-lock.json index 8759a701e..c9380e731 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@modelcontextprotocol/sdk", - "version": "1.17.4", + "version": "1.17.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@modelcontextprotocol/sdk", - "version": "1.17.4", + "version": "1.17.5", "license": "MIT", "dependencies": { "ajv": "^6.12.6", diff --git a/package.json b/package.json index 8be8f1002..b421b6eac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/sdk", - "version": "1.17.4", + "version": "1.17.5", "description": "Model Context Protocol implementation for TypeScript", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", diff --git a/src/examples/server/jsonResponseStreamableHttp.ts b/src/examples/server/jsonResponseStreamableHttp.ts index d6501d275..bc740c5fa 100644 --- a/src/examples/server/jsonResponseStreamableHttp.ts +++ b/src/examples/server/jsonResponseStreamableHttp.ts @@ -44,27 +44,27 @@ const getServer = () => { { name: z.string().describe('Name to greet'), }, - async ({ name }, { sendNotification }): Promise => { + async ({ name }, extra): Promise => { const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); - await sendNotification({ - method: "notifications/message", - params: { level: "debug", data: `Starting multi-greet for ${name}` } - }); + await server.sendLoggingMessage({ + level: "debug", + data: `Starting multi-greet for ${name}` + }, extra.sessionId); await sleep(1000); // Wait 1 second before first greeting - await sendNotification({ - method: "notifications/message", - params: { level: "info", data: `Sending first greeting to ${name}` } - }); + await server.sendLoggingMessage({ + level: "info", + data: `Sending first greeting to ${name}` + }, extra.sessionId); await sleep(1000); // Wait another second before second greeting - await sendNotification({ - method: "notifications/message", - params: { level: "info", data: `Sending second greeting to ${name}` } - }); + await server.sendLoggingMessage({ + level: "info", + data: `Sending second greeting to ${name}` + }, extra.sessionId); return { content: [ @@ -170,4 +170,4 @@ app.listen(PORT, (error) => { process.on('SIGINT', async () => { console.log('Shutting down server...'); process.exit(0); -}); \ No newline at end of file +}); diff --git a/src/examples/server/simpleSseServer.ts b/src/examples/server/simpleSseServer.ts index f8bdd4662..664b15008 100644 --- a/src/examples/server/simpleSseServer.ts +++ b/src/examples/server/simpleSseServer.ts @@ -5,13 +5,13 @@ import { z } from 'zod'; import { CallToolResult } from '../../types.js'; /** - * This example server demonstrates the deprecated HTTP+SSE transport + * This example server demonstrates the deprecated HTTP+SSE transport * (protocol version 2024-11-05). It mainly used for testing backward compatible clients. - * + * * The server exposes two endpoints: * - /mcp: For establishing the SSE stream (GET) * - /messages: For receiving client messages (POST) - * + * */ // Create an MCP server instance @@ -28,18 +28,15 @@ const getServer = () => { interval: z.number().describe('Interval in milliseconds between notifications').default(1000), count: z.number().describe('Number of notifications to send').default(10), }, - async ({ interval, count }, { sendNotification }): Promise => { + async ({ interval, count }, extra): Promise => { const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); let counter = 0; // Send the initial notification - await sendNotification({ - method: "notifications/message", - params: { - level: "info", - data: `Starting notification stream with ${count} messages every ${interval}ms` - } - }); + await server.sendLoggingMessage({ + level: "info", + data: `Starting notification stream with ${count} messages every ${interval}ms` + }, extra.sessionId); // Send periodic notifications while (counter < count) { @@ -47,13 +44,10 @@ const getServer = () => { await sleep(interval); try { - await sendNotification({ - method: "notifications/message", - params: { + await server.sendLoggingMessage({ level: "info", data: `Notification #${counter} at ${new Date().toISOString()}` - } - }); + }, extra.sessionId); } catch (error) { console.error("Error sending notification:", error); @@ -169,4 +163,4 @@ process.on('SIGINT', async () => { } console.log('Server shutdown complete'); process.exit(0); -}); \ No newline at end of file +}); diff --git a/src/examples/server/simpleStatelessStreamableHttp.ts b/src/examples/server/simpleStatelessStreamableHttp.ts index b5a1e291e..d91f3a7b5 100644 --- a/src/examples/server/simpleStatelessStreamableHttp.ts +++ b/src/examples/server/simpleStatelessStreamableHttp.ts @@ -42,20 +42,17 @@ const getServer = () => { interval: z.number().describe('Interval in milliseconds between notifications').default(100), count: z.number().describe('Number of notifications to send (0 for 100)').default(10), }, - async ({ interval, count }, { sendNotification }): Promise => { + async ({ interval, count }, extra): Promise => { const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); let counter = 0; while (count === 0 || counter < count) { counter++; try { - await sendNotification({ - method: "notifications/message", - params: { - level: "info", - data: `Periodic notification #${counter} at ${new Date().toISOString()}` - } - }); + await server.sendLoggingMessage({ + level: "info", + data: `Periodic notification #${counter} at ${new Date().toISOString()}` + }, extra.sessionId); } catch (error) { console.error("Error sending notification:", error); @@ -170,4 +167,4 @@ app.listen(PORT, (error) => { process.on('SIGINT', async () => { console.log('Shutting down server...'); process.exit(0); -}); \ No newline at end of file +}); diff --git a/src/examples/server/simpleStreamableHttp.ts b/src/examples/server/simpleStreamableHttp.ts index 98f9d351c..3271e6213 100644 --- a/src/examples/server/simpleStreamableHttp.ts +++ b/src/examples/server/simpleStreamableHttp.ts @@ -58,27 +58,27 @@ const getServer = () => { readOnlyHint: true, openWorldHint: false }, - async ({ name }, { sendNotification }): Promise => { + async ({ name }, extra): Promise => { const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); - await sendNotification({ - method: "notifications/message", - params: { level: "debug", data: `Starting multi-greet for ${name}` } - }); + await server.sendLoggingMessage({ + level: "debug", + data: `Starting multi-greet for ${name}` + }, extra.sessionId); await sleep(1000); // Wait 1 second before first greeting - await sendNotification({ - method: "notifications/message", - params: { level: "info", data: `Sending first greeting to ${name}` } - }); + await server.sendLoggingMessage({ + level: "info", + data: `Sending first greeting to ${name}` + }, extra.sessionId); await sleep(1000); // Wait another second before second greeting - await sendNotification({ - method: "notifications/message", - params: { level: "info", data: `Sending second greeting to ${name}` } - }); + await server.sendLoggingMessage({ + level: "info", + data: `Sending second greeting to ${name}` + }, extra.sessionId); return { content: [ @@ -273,20 +273,17 @@ const getServer = () => { interval: z.number().describe('Interval in milliseconds between notifications').default(100), count: z.number().describe('Number of notifications to send (0 for 100)').default(50), }, - async ({ interval, count }, { sendNotification }): Promise => { + async ({ interval, count }, extra): Promise => { const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); let counter = 0; while (count === 0 || counter < count) { counter++; try { - await sendNotification({ - method: "notifications/message", - params: { - level: "info", - data: `Periodic notification #${counter} at ${new Date().toISOString()}` - } - }); + await server.sendLoggingMessage( { + level: "info", + data: `Periodic notification #${counter} at ${new Date().toISOString()}` + }, extra.sessionId); } catch (error) { console.error("Error sending notification:", error); diff --git a/src/examples/server/sseAndStreamableHttpCompatibleServer.ts b/src/examples/server/sseAndStreamableHttpCompatibleServer.ts index e097ca70e..a9d9b63d7 100644 --- a/src/examples/server/sseAndStreamableHttpCompatibleServer.ts +++ b/src/examples/server/sseAndStreamableHttpCompatibleServer.ts @@ -12,7 +12,7 @@ import cors from 'cors'; * This example server demonstrates backwards compatibility with both: * 1. The deprecated HTTP+SSE transport (protocol version 2024-11-05) * 2. The Streamable HTTP transport (protocol version 2025-03-26) - * + * * It maintains a single MCP server instance but exposes two transport options: * - /mcp: The new Streamable HTTP endpoint (supports GET/POST/DELETE) * - /sse: The deprecated SSE endpoint for older clients (GET to establish stream) @@ -33,20 +33,17 @@ const getServer = () => { interval: z.number().describe('Interval in milliseconds between notifications').default(100), count: z.number().describe('Number of notifications to send (0 for 100)').default(50), }, - async ({ interval, count }, { sendNotification }): Promise => { + async ({ interval, count }, extra): Promise => { const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); let counter = 0; while (count === 0 || counter < count) { counter++; try { - await sendNotification({ - method: "notifications/message", - params: { - level: "info", - data: `Periodic notification #${counter} at ${new Date().toISOString()}` - } - }); + await server.sendLoggingMessage({ + level: "info", + data: `Periodic notification #${counter} at ${new Date().toISOString()}` + }, extra.sessionId); } catch (error) { console.error("Error sending notification:", error); @@ -254,4 +251,4 @@ process.on('SIGINT', async () => { } console.log('Server shutdown complete'); process.exit(0); -}); \ No newline at end of file +}); diff --git a/src/server/index.ts b/src/server/index.ts index 10ae2fadc..b1f71ea28 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -32,6 +32,9 @@ import { ServerRequest, ServerResult, SUPPORTED_PROTOCOL_VERSIONS, + LoggingLevel, + SetLevelRequestSchema, + LoggingLevelSchema } from "../types.js"; import Ajv from "ajv"; @@ -108,8 +111,36 @@ export class Server< this.setNotificationHandler(InitializedNotificationSchema, () => this.oninitialized?.(), ); + + if (this._capabilities.logging) { + this.setRequestHandler(SetLevelRequestSchema, async (request, extra) => { + const transportSessionId: string | undefined = extra.sessionId || extra.requestInfo?.headers['mcp-session-id'] as string || undefined; + const { level } = request.params; + const parseResult = LoggingLevelSchema.safeParse(level); + if (transportSessionId && parseResult.success) { + this._loggingLevels.set(transportSessionId, parseResult.data); + } + return {}; + }) + } } + // Map log levels by session id + private _loggingLevels = new Map(); + + // Map LogLevelSchema to severity index + private readonly LOG_LEVEL_SEVERITY = new Map( + LoggingLevelSchema.options.map((level, index) => [level, index]) + ); + + // Is a message with the given level ignored in the log level set for the given session id? + private isMessageIgnored = (level: LoggingLevel, sessionId: string): boolean => { + const currentLevel = this._loggingLevels.get(sessionId); + return (currentLevel) + ? this.LOG_LEVEL_SEVERITY.get(level)! < this.LOG_LEVEL_SEVERITY.get(currentLevel)! + : false; + }; + /** * Registers new capabilities. This can only be called before connecting to a transport. * @@ -121,7 +152,6 @@ export class Server< "Cannot register capabilities after connecting to transport", ); } - this._capabilities = mergeCapabilities(this._capabilities, capabilities); } @@ -324,10 +354,10 @@ export class Server< if (result.action === "accept" && result.content) { try { const ajv = new Ajv(); - + const validate = ajv.compile(params.requestedSchema); const isValid = validate(result.content); - + if (!isValid) { throw new McpError( ErrorCode.InvalidParams, @@ -359,8 +389,19 @@ export class Server< ); } - async sendLoggingMessage(params: LoggingMessageNotification["params"]) { - return this.notification({ method: "notifications/message", params }); + /** + * Sends a logging message to the client, if connected. + * Note: You only need to send the parameters object, not the entire JSON RPC message + * @see LoggingMessageNotification + * @param params + * @param sessionId optional for stateless and backward compatibility + */ + async sendLoggingMessage(params: LoggingMessageNotification["params"], sessionId?: string) { + if (this._capabilities.logging) { + if (!sessionId || !this.isMessageIgnored(params.level, sessionId)) { + return this.notification({method: "notifications/message", params}) + } + } } async sendResourceUpdated(params: ResourceUpdatedNotification["params"]) { diff --git a/src/server/mcp.ts b/src/server/mcp.ts index 791facef1..fb797a8b4 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -41,6 +41,7 @@ import { ServerRequest, ServerNotification, ToolAnnotations, + LoggingMessageNotification, } from "../types.js"; import { Completable, CompletableDef } from "./completable.js"; import { UriTemplate, Variables } from "../shared/uriTemplate.js"; @@ -822,7 +823,7 @@ export class McpServer { /** * Registers a tool taking either a parameter schema for validation or annotations for additional metadata. * This unified overload handles both `tool(name, paramsSchema, cb)` and `tool(name, annotations, cb)` cases. - * + * * Note: We use a union type for the second parameter because TypeScript cannot reliably disambiguate * between ToolAnnotations and ZodRawShape during overload resolution, as both are plain object types. */ @@ -834,9 +835,9 @@ export class McpServer { /** * Registers a tool `name` (with a description) taking either parameter schema or annotations. - * This unified overload handles both `tool(name, description, paramsSchema, cb)` and + * This unified overload handles both `tool(name, description, paramsSchema, cb)` and * `tool(name, description, annotations, cb)` cases. - * + * * Note: We use a union type for the third parameter because TypeScript cannot reliably disambiguate * between ToolAnnotations and ZodRawShape during overload resolution, as both are plain object types. */ @@ -1047,6 +1048,16 @@ export class McpServer { return this.server.transport !== undefined } + /** + * Sends a logging message to the client, if connected. + * Note: You only need to send the parameters object, not the entire JSON RPC message + * @see LoggingMessageNotification + * @param params + * @param sessionId optional for stateless and backward compatibility + */ + async sendLoggingMessage(params: LoggingMessageNotification["params"], sessionId?: string) { + return this.server.sendLoggingMessage(params, sessionId); + } /** * Sends a resource list changed event to the client, if connected. */ diff --git a/src/spec.types.test.ts b/src/spec.types.test.ts index 09cd6c2d0..09e411894 100644 --- a/src/spec.types.test.ts +++ b/src/spec.types.test.ts @@ -21,6 +21,12 @@ type RemovePassthrough = T extends object : {[K in keyof T as string extends K ? never : K]: RemovePassthrough} : T; +// Adds the `jsonrpc` property to a type, to match the on-wire format of notifications. +type WithJSONRPC = T & { jsonrpc: "2.0" }; + +// Adds the `jsonrpc` and `id` properties to a type, to match the on-wire format of requests. +type WithJSONRPCRequest = T & { jsonrpc: "2.0"; id: SDKTypes.RequestId }; + type IsUnknown = [unknown] extends [T] ? [T] extends [unknown] ? true : false : false; // Turns {x?: unknown} into {x: unknown} but keeps {_meta?: unknown} unchanged (and leaves other optional properties unchanged, e.g. {x?: string}). @@ -48,7 +54,7 @@ type MakeUnknownsNotOptional = : T); function checkCancelledNotification( - sdk: SDKTypes.CancelledNotification, + sdk: WithJSONRPC, spec: SpecTypes.CancelledNotification ) { sdk = spec; @@ -69,7 +75,7 @@ function checkImplementation( spec = sdk; } function checkProgressNotification( - sdk: SDKTypes.ProgressNotification, + sdk: WithJSONRPC, spec: SpecTypes.ProgressNotification ) { sdk = spec; @@ -77,21 +83,21 @@ function checkProgressNotification( } function checkSubscribeRequest( - sdk: SDKTypes.SubscribeRequest, + sdk: WithJSONRPCRequest, spec: SpecTypes.SubscribeRequest ) { sdk = spec; spec = sdk; } function checkUnsubscribeRequest( - sdk: SDKTypes.UnsubscribeRequest, + sdk: WithJSONRPCRequest, spec: SpecTypes.UnsubscribeRequest ) { sdk = spec; spec = sdk; } function checkPaginatedRequest( - sdk: SDKTypes.PaginatedRequest, + sdk: WithJSONRPCRequest, spec: SpecTypes.PaginatedRequest ) { sdk = spec; @@ -105,7 +111,7 @@ function checkPaginatedResult( spec = sdk; } function checkListRootsRequest( - sdk: SDKTypes.ListRootsRequest, + sdk: WithJSONRPCRequest, spec: SpecTypes.ListRootsRequest ) { sdk = spec; @@ -126,7 +132,7 @@ function checkRoot( spec = sdk; } function checkElicitRequest( - sdk: RemovePassthrough, + sdk: WithJSONRPCRequest>, spec: SpecTypes.ElicitRequest ) { sdk = spec; @@ -140,7 +146,7 @@ function checkElicitResult( spec = sdk; } function checkCompleteRequest( - sdk: RemovePassthrough, + sdk: WithJSONRPCRequest>, spec: SpecTypes.CompleteRequest ) { sdk = spec; @@ -231,7 +237,7 @@ function checkClientResult( spec = sdk; } function checkClientNotification( - sdk: SDKTypes.ClientNotification, + sdk: WithJSONRPC, spec: SpecTypes.ClientNotification ) { sdk = spec; @@ -273,7 +279,7 @@ function checkTool( spec = sdk; } function checkListToolsRequest( - sdk: SDKTypes.ListToolsRequest, + sdk: WithJSONRPCRequest, spec: SpecTypes.ListToolsRequest ) { sdk = spec; @@ -294,42 +300,42 @@ function checkCallToolResult( spec = sdk; } function checkCallToolRequest( - sdk: SDKTypes.CallToolRequest, + sdk: WithJSONRPCRequest, spec: SpecTypes.CallToolRequest ) { sdk = spec; spec = sdk; } function checkToolListChangedNotification( - sdk: SDKTypes.ToolListChangedNotification, + sdk: WithJSONRPC, spec: SpecTypes.ToolListChangedNotification ) { sdk = spec; spec = sdk; } function checkResourceListChangedNotification( - sdk: SDKTypes.ResourceListChangedNotification, + sdk: WithJSONRPC, spec: SpecTypes.ResourceListChangedNotification ) { sdk = spec; spec = sdk; } function checkPromptListChangedNotification( - sdk: SDKTypes.PromptListChangedNotification, + sdk: WithJSONRPC, spec: SpecTypes.PromptListChangedNotification ) { sdk = spec; spec = sdk; } function checkRootsListChangedNotification( - sdk: SDKTypes.RootsListChangedNotification, + sdk: WithJSONRPC, spec: SpecTypes.RootsListChangedNotification ) { sdk = spec; spec = sdk; } function checkResourceUpdatedNotification( - sdk: SDKTypes.ResourceUpdatedNotification, + sdk: WithJSONRPC, spec: SpecTypes.ResourceUpdatedNotification ) { sdk = spec; @@ -350,28 +356,28 @@ function checkCreateMessageResult( spec = sdk; } function checkSetLevelRequest( - sdk: SDKTypes.SetLevelRequest, + sdk: WithJSONRPCRequest, spec: SpecTypes.SetLevelRequest ) { sdk = spec; spec = sdk; } function checkPingRequest( - sdk: SDKTypes.PingRequest, + sdk: WithJSONRPCRequest, spec: SpecTypes.PingRequest ) { sdk = spec; spec = sdk; } function checkInitializedNotification( - sdk: SDKTypes.InitializedNotification, + sdk: WithJSONRPC, spec: SpecTypes.InitializedNotification ) { sdk = spec; spec = sdk; } function checkListResourcesRequest( - sdk: SDKTypes.ListResourcesRequest, + sdk: WithJSONRPCRequest, spec: SpecTypes.ListResourcesRequest ) { sdk = spec; @@ -385,7 +391,7 @@ function checkListResourcesResult( spec = sdk; } function checkListResourceTemplatesRequest( - sdk: SDKTypes.ListResourceTemplatesRequest, + sdk: WithJSONRPCRequest, spec: SpecTypes.ListResourceTemplatesRequest ) { sdk = spec; @@ -399,7 +405,7 @@ function checkListResourceTemplatesResult( spec = sdk; } function checkReadResourceRequest( - sdk: SDKTypes.ReadResourceRequest, + sdk: WithJSONRPCRequest, spec: SpecTypes.ReadResourceRequest ) { sdk = spec; @@ -462,7 +468,7 @@ function checkPrompt( spec = sdk; } function checkListPromptsRequest( - sdk: SDKTypes.ListPromptsRequest, + sdk: WithJSONRPCRequest, spec: SpecTypes.ListPromptsRequest ) { sdk = spec; @@ -476,7 +482,7 @@ function checkListPromptsResult( spec = sdk; } function checkGetPromptRequest( - sdk: SDKTypes.GetPromptRequest, + sdk: WithJSONRPCRequest, spec: SpecTypes.GetPromptRequest ) { sdk = spec; @@ -588,14 +594,14 @@ function checkJSONRPCMessage( spec = sdk; } function checkCreateMessageRequest( - sdk: RemovePassthrough, + sdk: WithJSONRPCRequest>, spec: SpecTypes.CreateMessageRequest ) { sdk = spec; spec = sdk; } function checkInitializeRequest( - sdk: RemovePassthrough, + sdk: WithJSONRPCRequest>, spec: SpecTypes.InitializeRequest ) { sdk = spec; @@ -623,28 +629,28 @@ function checkServerCapabilities( spec = sdk; } function checkClientRequest( - sdk: RemovePassthrough, + sdk: WithJSONRPCRequest>, spec: SpecTypes.ClientRequest ) { sdk = spec; spec = sdk; } function checkServerRequest( - sdk: RemovePassthrough, + sdk: WithJSONRPCRequest>, spec: SpecTypes.ServerRequest ) { sdk = spec; spec = sdk; } function checkLoggingMessageNotification( - sdk: MakeUnknownsNotOptional, + sdk: MakeUnknownsNotOptional>, spec: SpecTypes.LoggingMessageNotification ) { sdk = spec; spec = sdk; } function checkServerNotification( - sdk: MakeUnknownsNotOptional, + sdk: MakeUnknownsNotOptional>, spec: SpecTypes.ServerNotification ) { sdk = spec; @@ -665,6 +671,7 @@ const SDK_TYPES_FILE = 'src/types.ts'; const MISSING_SDK_TYPES = [ // These are inlined in the SDK: 'Role', + 'Error', // The inner error object of a JSONRPCError // These aren't supported by the SDK yet: // TODO: Add definitions to the SDK @@ -685,7 +692,7 @@ describe('Spec Types', () => { it('should define some expected types', () => { expect(specTypes).toContain('JSONRPCNotification'); expect(specTypes).toContain('ElicitResult'); - expect(specTypes).toHaveLength(91); + expect(specTypes).toHaveLength(92); }); it('should have up to date list of missing sdk types', () => {