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. */