From a0e03b2e58df0f4f1f0dd0aa84ffd802cab969a0 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Mon, 18 Aug 2025 18:37:26 -0400 Subject: [PATCH 1/5] Add setLevel handling to Server when logging capability is advertised. Update sendLoggingMessage method to respect current log level. Add sendLoggingMessage passthrough method on McpServer class since it doesn't use inheritance. Update examples to use the sendLoggingMessage method so that it can be filtered according to the log level. * In src/server/index.ts - in constructor - if capabilities object has logging, set request handler for SetLevelRequestSchema which sets this.logLevel to the level value from the request params and return empty object as per spec - define private _logLevel property of type LoggingLevel initialized to "debug" - define private _msgLevels property with list of objects with level property set to the ascending logging levels according to RFC 5424 as per spec. This gives us a lookup to determine if level is greater or less than a target level - add private isMessageIgnored method which takes a LoggingLevel type arg returns boolean, and looks up the currently set level and the target level, returning true if the target level is below the current level - in sendLoggingMessage method, only send the notification if the params.level value is not ignored. * In mcp.ts - import LoggingMessageNotification - add async sendLoggingMessage method that returns takes a params object from LoggingMessageNotification, and returns the result of a call to this.server.sendLoggingMessage (since McpServer doesn't extend Server, but rather uses composition. * In src/examples/server/ - jsonResponseStreamableHttp.ts - simpleSseServer.ts - simpleStreamableHttp.ts - simpleStatelessStreamableHttp.ts - in tool call callback functions that send logging message via the included sendNotification function, replace with calls to server.sendLoggingMessage, passing only the params of the message. --- .../server/jsonResponseStreamableHttp.ts | 22 ++++++------ src/examples/server/simpleSseServer.ts | 26 ++++++-------- .../server/simpleStatelessStreamableHttp.ts | 13 +++---- src/examples/server/simpleStreamableHttp.ts | 30 +++++----------- .../sseAndStreamableHttpCompatibleServer.ts | 15 ++++---- src/server/index.ts | 36 ++++++++++++++++--- src/server/mcp.ts | 16 +++++++-- 7 files changed, 86 insertions(+), 72 deletions(-) diff --git a/src/examples/server/jsonResponseStreamableHttp.ts b/src/examples/server/jsonResponseStreamableHttp.ts index d6501d275..38902cb61 100644 --- a/src/examples/server/jsonResponseStreamableHttp.ts +++ b/src/examples/server/jsonResponseStreamableHttp.ts @@ -44,26 +44,26 @@ const getServer = () => { { name: z.string().describe('Name to greet'), }, - async ({ name }, { sendNotification }): Promise => { + async ({ name }): 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}` }); 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}` }); 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}` }); return { @@ -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..77955af10 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,17 +28,14 @@ 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 }): 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` }); // Send periodic notifications @@ -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()}` - } - }); + }); } 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..657759539 100644 --- a/src/examples/server/simpleStatelessStreamableHttp.ts +++ b/src/examples/server/simpleStatelessStreamableHttp.ts @@ -42,19 +42,16 @@ 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 }): 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()}` }); } catch (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..36d50ad57 100644 --- a/src/examples/server/simpleStreamableHttp.ts +++ b/src/examples/server/simpleStreamableHttp.ts @@ -58,27 +58,18 @@ const getServer = () => { readOnlyHint: true, openWorldHint: false }, - async ({ name }, { sendNotification }): Promise => { + async ({ name }): 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}`}); 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}`}); 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}`}); return { content: [ @@ -273,20 +264,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 }): 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()}` + }); } catch (error) { console.error("Error sending notification:", error); diff --git a/src/examples/server/sseAndStreamableHttpCompatibleServer.ts b/src/examples/server/sseAndStreamableHttpCompatibleServer.ts index e097ca70e..6d04f91b1 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,19 +33,16 @@ 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 }): 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()}` }); } catch (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..b861c59a6 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -32,6 +32,7 @@ import { ServerRequest, ServerResult, SUPPORTED_PROTOCOL_VERSIONS, + LoggingLevel, SetLevelRequestSchema, } from "../types.js"; import Ajv from "ajv"; @@ -108,8 +109,34 @@ export class Server< this.setNotificationHandler(InitializedNotificationSchema, () => this.oninitialized?.(), ); + + if (this._capabilities.logging) { + this.setRequestHandler(SetLevelRequestSchema, async (request) => { + const { level } = request.params; + this._logLevel = level; + return {}; + }) + } } + private _logLevel: LoggingLevel = "debug"; + private _msgLevels = [ + { level: "debug" }, + { level: "info" }, + { level: "notice" }, + { level: "warning" }, + { level: "error" }, + { level: "critical" }, + { level: "alert" }, + { level: "emergency" }, + ]; + + private isMessageIgnored = (level: LoggingLevel): boolean => { + const currentLevel = this._msgLevels.findIndex((msg) => this._logLevel === msg.level); + const messageLevel = this._msgLevels.findIndex((msg) => level === msg.level); + return messageLevel < currentLevel; + }; + /** * Registers new capabilities. This can only be called before connecting to a transport. * @@ -121,7 +148,6 @@ export class Server< "Cannot register capabilities after connecting to transport", ); } - this._capabilities = mergeCapabilities(this._capabilities, capabilities); } @@ -324,10 +350,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, @@ -360,7 +386,9 @@ export class Server< } async sendLoggingMessage(params: LoggingMessageNotification["params"]) { - return this.notification({ method: "notifications/message", params }); + return (!this.isMessageIgnored(params.level)) + ? this.notification({ method: "notifications/message", params }) + : undefined } async sendResourceUpdated(params: ResourceUpdatedNotification["params"]) { diff --git a/src/server/mcp.ts b/src/server/mcp.ts index 791facef1..8afe08517 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,15 @@ 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 + * @see LoggingMessageNotification + * @param params + */ + async sendLoggingMessage(params: LoggingMessageNotification["params"]) { + return this.server.sendLoggingMessage(params); + } /** * Sends a resource list changed event to the client, if connected. */ From 8f3e203dbcfb1f9ac78e247142334641c254afe9 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Tue, 19 Aug 2025 11:19:51 -0400 Subject: [PATCH 2/5] This commit ties logging level to session * In src/server/index.ts - in Server class constructor when setting request handler for SetLevelRequestSchema - take the sessionId OR 'mcp-session-id' header and store the session id in the _loggingLevels map by session id. Doesn't store the level if there is no session id - change _logLevel variable to a Map, string to LoggingLevel type - rename _msgLevels array to _levelNames and just make it strings for simplicity - in isMessageIgnored function - take a sessionId argument - change _msgLevels to _levelNames - match against the level stored in the _loggingLevels map by session id - in sendLoggingLevelMessage, - add optional sessionId argument - make all action conditional upon _capabilities.logging existing - if there is no session id OR if the message is not ignored based on the level and session id, send the notification * In src/server/mcp.ts - in sendLoggingMessage - add optional sessionId argument - pass sessionId to server.sendLoggingMessage * In src/examples/server - jsonResponseStreamableHttp.ts - simpleSseServer.ts - simpleStatelessStreamableHttp.ts - simpleStreamableHttp.ts - sseAndStreamableHttpCompatibleServer.ts - update tool callbacks to get the extra param - update erver.sendLoggingMessage calls to include extra.sessionId --- .../server/jsonResponseStreamableHttp.ts | 8 +-- src/examples/server/simpleSseServer.ts | 6 +-- .../server/simpleStatelessStreamableHttp.ts | 4 +- src/examples/server/simpleStreamableHttp.ts | 21 +++++--- .../sseAndStreamableHttpCompatibleServer.ts | 4 +- src/server/index.ts | 50 ++++++++++++------- src/server/mcp.ts | 7 +-- 7 files changed, 61 insertions(+), 39 deletions(-) diff --git a/src/examples/server/jsonResponseStreamableHttp.ts b/src/examples/server/jsonResponseStreamableHttp.ts index 38902cb61..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 }): Promise => { + async ({ name }, extra): Promise => { const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); await server.sendLoggingMessage({ level: "debug", data: `Starting multi-greet for ${name}` - }); + }, extra.sessionId); await sleep(1000); // Wait 1 second before first greeting await server.sendLoggingMessage({ level: "info", data: `Sending first greeting to ${name}` - }); + }, extra.sessionId); await sleep(1000); // Wait another second before second greeting await server.sendLoggingMessage({ level: "info", data: `Sending second greeting to ${name}` - }); + }, extra.sessionId); return { content: [ diff --git a/src/examples/server/simpleSseServer.ts b/src/examples/server/simpleSseServer.ts index 77955af10..664b15008 100644 --- a/src/examples/server/simpleSseServer.ts +++ b/src/examples/server/simpleSseServer.ts @@ -28,7 +28,7 @@ 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 }): Promise => { + async ({ interval, count }, extra): Promise => { const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); let counter = 0; @@ -36,7 +36,7 @@ const getServer = () => { await server.sendLoggingMessage({ level: "info", data: `Starting notification stream with ${count} messages every ${interval}ms` - }); + }, extra.sessionId); // Send periodic notifications while (counter < count) { @@ -47,7 +47,7 @@ const getServer = () => { await server.sendLoggingMessage({ level: "info", data: `Notification #${counter} at ${new Date().toISOString()}` - }); + }, extra.sessionId); } catch (error) { console.error("Error sending notification:", error); diff --git a/src/examples/server/simpleStatelessStreamableHttp.ts b/src/examples/server/simpleStatelessStreamableHttp.ts index 657759539..d91f3a7b5 100644 --- a/src/examples/server/simpleStatelessStreamableHttp.ts +++ b/src/examples/server/simpleStatelessStreamableHttp.ts @@ -42,7 +42,7 @@ 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 }): Promise => { + async ({ interval, count }, extra): Promise => { const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); let counter = 0; @@ -52,7 +52,7 @@ const getServer = () => { 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/simpleStreamableHttp.ts b/src/examples/server/simpleStreamableHttp.ts index 36d50ad57..3271e6213 100644 --- a/src/examples/server/simpleStreamableHttp.ts +++ b/src/examples/server/simpleStreamableHttp.ts @@ -58,18 +58,27 @@ const getServer = () => { readOnlyHint: true, openWorldHint: false }, - async ({ name }): Promise => { + async ({ name }, extra): Promise => { const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); - await server.sendLoggingMessage({ 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 server.sendLoggingMessage( { 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 server.sendLoggingMessage( { level: "info", data: `Sending second greeting to ${name}`}); + await server.sendLoggingMessage({ + level: "info", + data: `Sending second greeting to ${name}` + }, extra.sessionId); return { content: [ @@ -264,7 +273,7 @@ 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 }): Promise => { + async ({ interval, count }, extra): Promise => { const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); let counter = 0; @@ -274,7 +283,7 @@ const getServer = () => { 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 6d04f91b1..a9d9b63d7 100644 --- a/src/examples/server/sseAndStreamableHttpCompatibleServer.ts +++ b/src/examples/server/sseAndStreamableHttpCompatibleServer.ts @@ -33,7 +33,7 @@ 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 }): Promise => { + async ({ interval, count }, extra): Promise => { const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); let counter = 0; @@ -43,7 +43,7 @@ const getServer = () => { 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/server/index.ts b/src/server/index.ts index b861c59a6..8695d43d6 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -111,29 +111,32 @@ export class Server< ); if (this._capabilities.logging) { - this.setRequestHandler(SetLevelRequestSchema, async (request) => { + 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; - this._logLevel = level; + if (transportSessionId && this._levelNames.some(l => l === level)) { + this._loggingLevels.set(transportSessionId, level); + } return {}; }) } } - private _logLevel: LoggingLevel = "debug"; - private _msgLevels = [ - { level: "debug" }, - { level: "info" }, - { level: "notice" }, - { level: "warning" }, - { level: "error" }, - { level: "critical" }, - { level: "alert" }, - { level: "emergency" }, + private _loggingLevels = new Map(); + private _levelNames = [ + "debug", + "info", + "notice", + "warning", + "error", + "critical", + "alert", + "emergency", ]; - private isMessageIgnored = (level: LoggingLevel): boolean => { - const currentLevel = this._msgLevels.findIndex((msg) => this._logLevel === msg.level); - const messageLevel = this._msgLevels.findIndex((msg) => level === msg.level); + private isMessageIgnored = (level: LoggingLevel, sessionId: string): boolean => { + const currentLevel = this._levelNames.findIndex((l) => this._loggingLevels.get(sessionId) === l); + const messageLevel = this._levelNames.findIndex((l) => level === l); return messageLevel < currentLevel; }; @@ -385,10 +388,19 @@ export class Server< ); } - async sendLoggingMessage(params: LoggingMessageNotification["params"]) { - return (!this.isMessageIgnored(params.level)) - ? this.notification({ method: "notifications/message", params }) - : 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) { + 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 8afe08517..fb797a8b4 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -1050,12 +1050,13 @@ export class McpServer { /** * Sends a logging message to the client, if connected. - * Note: You only need to send the parameters object + * 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"]) { - return this.server.sendLoggingMessage(params); + async sendLoggingMessage(params: LoggingMessageNotification["params"], sessionId?: string) { + return this.server.sendLoggingMessage(params, sessionId); } /** * Sends a resource list changed event to the client, if connected. From 184ce5a2b4d904312cf79c7b0c9048cac8514719 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Fri, 22 Aug 2025 14:05:48 -0400 Subject: [PATCH 3/5] * In src/server/index.ts - In Server class - in SetLevelRequestSchema request handler - parse level against LoggingLevelSchema rather than checking obviated array - replace _levelNames array with LOG_LEVEL_SEVERITY map, mapped from LoggingLevelSchema.options - In isMessageIgnored method, - get the current level set for the given session id - if no level set, return false - otherwise, compare LOG_LEVEL_SEVERITY map entries for level and currentLevel, returning true if level is less than current level. --- src/server/index.ts | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/src/server/index.ts b/src/server/index.ts index 8695d43d6..38c02fcc4 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -32,7 +32,9 @@ import { ServerRequest, ServerResult, SUPPORTED_PROTOCOL_VERSIONS, - LoggingLevel, SetLevelRequestSchema, + LoggingLevel, + SetLevelRequestSchema, + LoggingLevelSchema } from "../types.js"; import Ajv from "ajv"; @@ -114,33 +116,32 @@ export class Server< 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; - if (transportSessionId && this._levelNames.some(l => l === level)) { - this._loggingLevels.set(transportSessionId, level); + 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(); - private _levelNames = [ - "debug", - "info", - "notice", - "warning", - "error", - "critical", - "alert", - "emergency", - ]; + // 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._levelNames.findIndex((l) => this._loggingLevels.get(sessionId) === l); - const messageLevel = this._levelNames.findIndex((l) => level === l); - return messageLevel < currentLevel; - }; + 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. * * The new capabilities will be merged with any existing capabilities previously given (e.g., at initialization). From df02c0f7a4f93b5b3c27bc9cd2573286c1079d69 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Fri, 22 Aug 2025 14:13:11 -0400 Subject: [PATCH 4/5] whitespace --- src/server/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/index.ts b/src/server/index.ts index 38c02fcc4..3d55d1af6 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -141,7 +141,7 @@ export class Server< : false; }; - /** + /** * Registers new capabilities. This can only be called before connecting to a transport. * * The new capabilities will be merged with any existing capabilities previously given (e.g., at initialization). From 576a85301c95e5eb7c44503a14376dc3548568fd Mon Sep 17 00:00:00 2001 From: cliffhall Date: Fri, 22 Aug 2025 14:13:43 -0400 Subject: [PATCH 5/5] whitespace --- src/server/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/index.ts b/src/server/index.ts index 3d55d1af6..b1f71ea28 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -139,7 +139,7 @@ export class Server< 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.