From 36048aa6d26375e7075e862035d9c87c174bd12e Mon Sep 17 00:00:00 2001 From: cliffhall Date: Wed, 3 Sep 2025 17:55:25 -0400 Subject: [PATCH 1/5] * In .gitignore - add .idea/ for JetBrains IDEs * In src/server/index.ts - in Server class - in automatic request handler for SetLevelRequestSchema set in constructor, use "NO_SESSION" as the transportSessionId / map key for _loggingLevels and set the level. - This allows an STDIO server to have automatic log filtering if sendLoggingMessage is used. - in isMessageIgnored method - default value of sessionId is "NO_SESSION" if not supplied - in sendLoggingMessage method, - remove sessionId check from condition, since an undefined sessionId will be converted to "NO_SESSION" in the call to isMessageIgnored - this allows STDIO servers to have automatic log filtering * In src/server/index.test.ts - added tests - "should respect log level for transport without sessionId" - "should respect log level for transport with sessionId" --- .gitignore | 3 + src/server/index.test.ts | 178 ++++++++++++++++++++++++++++++++++++--- src/server/index.ts | 6 +- 3 files changed, 171 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index 694735b68..50343a2b6 100644 --- a/.gitignore +++ b/.gitignore @@ -124,6 +124,9 @@ out # Stores VSCode versions used for testing VSCode extensions .vscode-test +# Jetbrains IDEs +.idea/ + # yarn v2 .yarn/cache .yarn/unplugged diff --git a/src/server/index.test.ts b/src/server/index.test.ts index 46205d726..f110f0dc5 100644 --- a/src/server/index.test.ts +++ b/src/server/index.test.ts @@ -4,22 +4,23 @@ import { Server } from "./index.js"; import { z } from "zod"; import { - RequestSchema, - NotificationSchema, - ResultSchema, - LATEST_PROTOCOL_VERSION, - SUPPORTED_PROTOCOL_VERSIONS, - CreateMessageRequestSchema, - ElicitRequestSchema, - ListPromptsRequestSchema, - ListResourcesRequestSchema, - ListToolsRequestSchema, - SetLevelRequestSchema, - ErrorCode + RequestSchema, + NotificationSchema, + ResultSchema, + LATEST_PROTOCOL_VERSION, + SUPPORTED_PROTOCOL_VERSIONS, + CreateMessageRequestSchema, + ElicitRequestSchema, + ListPromptsRequestSchema, + ListResourcesRequestSchema, + ListToolsRequestSchema, + SetLevelRequestSchema, + ErrorCode, LoggingMessageNotificationSchema, JSONRPCMessage, LoggingMessageNotification } from "../types.js"; import { Transport } from "../shared/transport.js"; import { InMemoryTransport } from "../inMemory.js"; import { Client } from "../client/index.js"; +import {AuthInfo} from "./auth/types.js"; test("should accept latest protocol version", async () => { let sendPromiseResolve: (value: unknown) => void; @@ -569,7 +570,7 @@ test("should allow elicitation reject and cancel without validation", async () = action: "decline", }); - // Test cancel - should not validate + // Test cancel - should not validate await expect( server.elicitInput({ message: "Please provide your name", @@ -861,3 +862,154 @@ test("should handle request timeout", async () => { code: ErrorCode.RequestTimeout, }); }); + +/* + Test automatic log level handling for transports with and without sessionId + */ +test("should respect log level for transport without sessionId", async () => { + + const server = new Server( + { + name: "test server", + version: "1.0", + }, + { + capabilities: { + prompts: {}, + resources: {}, + tools: {}, + logging: {}, + }, + enforceStrictCapabilities: true, + }, + ); + + const client = new Client( + { + name: "test client", + version: "1.0", + }, + ); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + + expect(clientTransport.sessionId).toEqual(undefined); + + // Client sets logging level to warning + await client.setLoggingLevel("warning"); + + // This one will make it through + const warningParams: LoggingMessageNotification["params"] = { + level: "warning", + logger: "test server", + data: "Warning message", + }; + + // This one will not + const debugParams: LoggingMessageNotification["params"] = { + level: "debug", + logger: "test server", + data: "Debug message", + }; + + // Test the one that makes it through + clientTransport.onmessage = jest.fn().mockImplementation((message) => { + expect(message).toEqual({ + jsonrpc: "2.0", + method: "notifications/message", + params: warningParams + }); + }); + + // This one will not make it through + await server.sendLoggingMessage(debugParams); + expect(clientTransport.onmessage).not.toHaveBeenCalled(); + + // This one will, triggering the above test in clientTransport.onmessage + await server.sendLoggingMessage(warningParams); + expect(clientTransport.onmessage).toHaveBeenCalled(); + +}); + +test("should respect log level for transport with sessionId", async () => { + + const server = new Server( + { + name: "test server", + version: "1.0", + }, + { + capabilities: { + prompts: {}, + resources: {}, + tools: {}, + logging: {}, + }, + enforceStrictCapabilities: true, + }, + ); + + const client = new Client( + { + name: "test client", + version: "1.0", + }, + ); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + // Add a session id to the transports + const SESSION_ID = "test-session-id"; + clientTransport.sessionId = SESSION_ID; + serverTransport.sessionId = SESSION_ID; + + expect(clientTransport.sessionId).toBeDefined(); + expect(serverTransport.sessionId).toBeDefined(); + + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + + + // Client sets logging level to warning + await client.setLoggingLevel("warning"); + + // This one will make it through + const warningParams: LoggingMessageNotification["params"] = { + level: "warning", + logger: "test server", + data: "Warning message", + }; + + // This one will not + const debugParams: LoggingMessageNotification["params"] = { + level: "debug", + logger: "test server", + data: "Debug message", + }; + + // Test the one that makes it through + clientTransport.onmessage = jest.fn().mockImplementation((message) => { + expect(message).toEqual({ + jsonrpc: "2.0", + method: "notifications/message", + params: warningParams + }); + }); + + // This one will not make it through + await server.sendLoggingMessage(debugParams, SESSION_ID); + expect(clientTransport.onmessage).not.toHaveBeenCalled(); + + // This one will, triggering the above test in clientTransport.onmessage + await server.sendLoggingMessage(warningParams, SESSION_ID); + expect(clientTransport.onmessage).toHaveBeenCalled(); + +}); + diff --git a/src/server/index.ts b/src/server/index.ts index b1f71ea28..abe17dc95 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -114,7 +114,7 @@ export class Server< 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 transportSessionId: string | undefined = extra.sessionId || extra.requestInfo?.headers['mcp-session-id'] as string || "NO_SESSION"; const { level } = request.params; const parseResult = LoggingLevelSchema.safeParse(level); if (transportSessionId && parseResult.success) { @@ -134,7 +134,7 @@ export class Server< ); // 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 => { + private isMessageIgnored = (level: LoggingLevel, sessionId: string = "NO_SESSION"): boolean => { const currentLevel = this._loggingLevels.get(sessionId); return (currentLevel) ? this.LOG_LEVEL_SEVERITY.get(level)! < this.LOG_LEVEL_SEVERITY.get(currentLevel)! @@ -398,7 +398,7 @@ export class Server< */ async sendLoggingMessage(params: LoggingMessageNotification["params"], sessionId?: string) { if (this._capabilities.logging) { - if (!sessionId || !this.isMessageIgnored(params.level, sessionId)) { + if (!this.isMessageIgnored(params.level, sessionId)) { return this.notification({method: "notifications/message", params}) } } From 06c804e5041c8e2f295d10fb7a729adb3e4ab79f Mon Sep 17 00:00:00 2001 From: cliffhall Date: Wed, 3 Sep 2025 18:54:16 -0400 Subject: [PATCH 2/5] * In src/server/index.test.ts - adjust imports to minimize diff impact --- src/server/index.test.ts | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/server/index.test.ts b/src/server/index.test.ts index f110f0dc5..5ec0ac74f 100644 --- a/src/server/index.test.ts +++ b/src/server/index.test.ts @@ -4,18 +4,19 @@ import { Server } from "./index.js"; import { z } from "zod"; import { - RequestSchema, - NotificationSchema, - ResultSchema, - LATEST_PROTOCOL_VERSION, - SUPPORTED_PROTOCOL_VERSIONS, - CreateMessageRequestSchema, - ElicitRequestSchema, - ListPromptsRequestSchema, - ListResourcesRequestSchema, - ListToolsRequestSchema, - SetLevelRequestSchema, - ErrorCode, LoggingMessageNotificationSchema, JSONRPCMessage, LoggingMessageNotification + RequestSchema, + NotificationSchema, + ResultSchema, + LATEST_PROTOCOL_VERSION, + SUPPORTED_PROTOCOL_VERSIONS, + CreateMessageRequestSchema, + ElicitRequestSchema, + ListPromptsRequestSchema, + ListResourcesRequestSchema, + ListToolsRequestSchema, + SetLevelRequestSchema, + ErrorCode, + LoggingMessageNotification } from "../types.js"; import { Transport } from "../shared/transport.js"; import { InMemoryTransport } from "../inMemory.js"; From 6e2a2e713e64ff8d50f58721f27ad5320188da23 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Wed, 3 Sep 2025 18:55:31 -0400 Subject: [PATCH 3/5] * In src/server/index.test.ts - remove unused import --- src/server/index.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/server/index.test.ts b/src/server/index.test.ts index 5ec0ac74f..664ed4520 100644 --- a/src/server/index.test.ts +++ b/src/server/index.test.ts @@ -21,7 +21,6 @@ import { import { Transport } from "../shared/transport.js"; import { InMemoryTransport } from "../inMemory.js"; import { Client } from "../client/index.js"; -import {AuthInfo} from "./auth/types.js"; test("should accept latest protocol version", async () => { let sendPromiseResolve: (value: unknown) => void; From d5f1e81345c298d496f6d2a3d1ebcc4220c6f63a Mon Sep 17 00:00:00 2001 From: cliffhall Date: Tue, 9 Sep 2025 12:04:58 -0400 Subject: [PATCH 4/5] * In .gitignore - remove Jetbrains IDE / .idea * In src/server/index.test.ts - in Server constructor, When setting request handler for SetLevelRequestSchema - remove "NO_SESSION" key if sessionId is otherwise not found - remove check for transportSessionId when calling _loggingLevels.set - change _loggingLevels Map to allow string or undefined as a key - in isMessageIgnored method - remove default value of "NO_SESSION" for optional sessionId argument --- .gitignore | 3 --- src/server/index.ts | 8 ++++---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 50343a2b6..694735b68 100644 --- a/.gitignore +++ b/.gitignore @@ -124,9 +124,6 @@ out # Stores VSCode versions used for testing VSCode extensions .vscode-test -# Jetbrains IDEs -.idea/ - # yarn v2 .yarn/cache .yarn/unplugged diff --git a/src/server/index.ts b/src/server/index.ts index abe17dc95..8f5d34875 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -114,10 +114,10 @@ export class Server< if (this._capabilities.logging) { this.setRequestHandler(SetLevelRequestSchema, async (request, extra) => { - const transportSessionId: string | undefined = extra.sessionId || extra.requestInfo?.headers['mcp-session-id'] as string || "NO_SESSION"; + const transportSessionId: string | undefined = extra.sessionId || extra.requestInfo?.headers['mcp-session-id'] as string; const { level } = request.params; const parseResult = LoggingLevelSchema.safeParse(level); - if (transportSessionId && parseResult.success) { + if (parseResult.success) { this._loggingLevels.set(transportSessionId, parseResult.data); } return {}; @@ -126,7 +126,7 @@ export class Server< } // Map log levels by session id - private _loggingLevels = new Map(); + private _loggingLevels = new Map(); // Map LogLevelSchema to severity index private readonly LOG_LEVEL_SEVERITY = new Map( @@ -134,7 +134,7 @@ export class Server< ); // Is a message with the given level ignored in the log level set for the given session id? - private isMessageIgnored = (level: LoggingLevel, sessionId: string = "NO_SESSION"): boolean => { + 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)! From e81820672bf3d98a8365e3e50dcaa1e9342158fc Mon Sep 17 00:00:00 2001 From: cliffhall Date: Tue, 9 Sep 2025 12:07:56 -0400 Subject: [PATCH 5/5] * In src/server/index.test.ts - in Server constructor, When setting request handler for SetLevelRequestSchema - replace literal final resort of undefined if sessionId is otherwise not found --- 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 8f5d34875..970657358 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -114,7 +114,7 @@ export class Server< if (this._capabilities.logging) { this.setRequestHandler(SetLevelRequestSchema, async (request, extra) => { - const transportSessionId: string | undefined = extra.sessionId || extra.requestInfo?.headers['mcp-session-id'] as string; + 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 (parseResult.success) {