Skip to content

Commit ebf3933

Browse files
authored
Fix automatic log level handling for sessionless connections (#917)
1 parent 5dd7a2b commit ebf3933

File tree

2 files changed

+158
-6
lines changed

2 files changed

+158
-6
lines changed

src/server/index.test.ts

Lines changed: 154 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import {
1515
ListResourcesRequestSchema,
1616
ListToolsRequestSchema,
1717
SetLevelRequestSchema,
18-
ErrorCode
18+
ErrorCode,
19+
LoggingMessageNotification
1920
} from "../types.js";
2021
import { Transport } from "../shared/transport.js";
2122
import { InMemoryTransport } from "../inMemory.js";
@@ -569,7 +570,7 @@ test("should allow elicitation reject and cancel without validation", async () =
569570
action: "decline",
570571
});
571572

572-
// Test cancel - should not validate
573+
// Test cancel - should not validate
573574
await expect(
574575
server.elicitInput({
575576
message: "Please provide your name",
@@ -861,3 +862,154 @@ test("should handle request timeout", async () => {
861862
code: ErrorCode.RequestTimeout,
862863
});
863864
});
865+
866+
/*
867+
Test automatic log level handling for transports with and without sessionId
868+
*/
869+
test("should respect log level for transport without sessionId", async () => {
870+
871+
const server = new Server(
872+
{
873+
name: "test server",
874+
version: "1.0",
875+
},
876+
{
877+
capabilities: {
878+
prompts: {},
879+
resources: {},
880+
tools: {},
881+
logging: {},
882+
},
883+
enforceStrictCapabilities: true,
884+
},
885+
);
886+
887+
const client = new Client(
888+
{
889+
name: "test client",
890+
version: "1.0",
891+
},
892+
);
893+
894+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
895+
896+
await Promise.all([
897+
client.connect(clientTransport),
898+
server.connect(serverTransport),
899+
]);
900+
901+
expect(clientTransport.sessionId).toEqual(undefined);
902+
903+
// Client sets logging level to warning
904+
await client.setLoggingLevel("warning");
905+
906+
// This one will make it through
907+
const warningParams: LoggingMessageNotification["params"] = {
908+
level: "warning",
909+
logger: "test server",
910+
data: "Warning message",
911+
};
912+
913+
// This one will not
914+
const debugParams: LoggingMessageNotification["params"] = {
915+
level: "debug",
916+
logger: "test server",
917+
data: "Debug message",
918+
};
919+
920+
// Test the one that makes it through
921+
clientTransport.onmessage = jest.fn().mockImplementation((message) => {
922+
expect(message).toEqual({
923+
jsonrpc: "2.0",
924+
method: "notifications/message",
925+
params: warningParams
926+
});
927+
});
928+
929+
// This one will not make it through
930+
await server.sendLoggingMessage(debugParams);
931+
expect(clientTransport.onmessage).not.toHaveBeenCalled();
932+
933+
// This one will, triggering the above test in clientTransport.onmessage
934+
await server.sendLoggingMessage(warningParams);
935+
expect(clientTransport.onmessage).toHaveBeenCalled();
936+
937+
});
938+
939+
test("should respect log level for transport with sessionId", async () => {
940+
941+
const server = new Server(
942+
{
943+
name: "test server",
944+
version: "1.0",
945+
},
946+
{
947+
capabilities: {
948+
prompts: {},
949+
resources: {},
950+
tools: {},
951+
logging: {},
952+
},
953+
enforceStrictCapabilities: true,
954+
},
955+
);
956+
957+
const client = new Client(
958+
{
959+
name: "test client",
960+
version: "1.0",
961+
},
962+
);
963+
964+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
965+
966+
// Add a session id to the transports
967+
const SESSION_ID = "test-session-id";
968+
clientTransport.sessionId = SESSION_ID;
969+
serverTransport.sessionId = SESSION_ID;
970+
971+
expect(clientTransport.sessionId).toBeDefined();
972+
expect(serverTransport.sessionId).toBeDefined();
973+
974+
await Promise.all([
975+
client.connect(clientTransport),
976+
server.connect(serverTransport),
977+
]);
978+
979+
980+
// Client sets logging level to warning
981+
await client.setLoggingLevel("warning");
982+
983+
// This one will make it through
984+
const warningParams: LoggingMessageNotification["params"] = {
985+
level: "warning",
986+
logger: "test server",
987+
data: "Warning message",
988+
};
989+
990+
// This one will not
991+
const debugParams: LoggingMessageNotification["params"] = {
992+
level: "debug",
993+
logger: "test server",
994+
data: "Debug message",
995+
};
996+
997+
// Test the one that makes it through
998+
clientTransport.onmessage = jest.fn().mockImplementation((message) => {
999+
expect(message).toEqual({
1000+
jsonrpc: "2.0",
1001+
method: "notifications/message",
1002+
params: warningParams
1003+
});
1004+
});
1005+
1006+
// This one will not make it through
1007+
await server.sendLoggingMessage(debugParams, SESSION_ID);
1008+
expect(clientTransport.onmessage).not.toHaveBeenCalled();
1009+
1010+
// This one will, triggering the above test in clientTransport.onmessage
1011+
await server.sendLoggingMessage(warningParams, SESSION_ID);
1012+
expect(clientTransport.onmessage).toHaveBeenCalled();
1013+
1014+
});
1015+

src/server/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ export class Server<
117117
const transportSessionId: string | undefined = extra.sessionId || extra.requestInfo?.headers['mcp-session-id'] as string || undefined;
118118
const { level } = request.params;
119119
const parseResult = LoggingLevelSchema.safeParse(level);
120-
if (transportSessionId && parseResult.success) {
120+
if (parseResult.success) {
121121
this._loggingLevels.set(transportSessionId, parseResult.data);
122122
}
123123
return {};
@@ -126,15 +126,15 @@ export class Server<
126126
}
127127

128128
// Map log levels by session id
129-
private _loggingLevels = new Map<string, LoggingLevel>();
129+
private _loggingLevels = new Map<string | undefined, LoggingLevel>();
130130

131131
// Map LogLevelSchema to severity index
132132
private readonly LOG_LEVEL_SEVERITY = new Map(
133133
LoggingLevelSchema.options.map((level, index) => [level, index])
134134
);
135135

136136
// Is a message with the given level ignored in the log level set for the given session id?
137-
private isMessageIgnored = (level: LoggingLevel, sessionId: string): boolean => {
137+
private isMessageIgnored = (level: LoggingLevel, sessionId?: string): boolean => {
138138
const currentLevel = this._loggingLevels.get(sessionId);
139139
return (currentLevel)
140140
? this.LOG_LEVEL_SEVERITY.get(level)! < this.LOG_LEVEL_SEVERITY.get(currentLevel)!
@@ -398,7 +398,7 @@ export class Server<
398398
*/
399399
async sendLoggingMessage(params: LoggingMessageNotification["params"], sessionId?: string) {
400400
if (this._capabilities.logging) {
401-
if (!sessionId || !this.isMessageIgnored(params.level, sessionId)) {
401+
if (!this.isMessageIgnored(params.level, sessionId)) {
402402
return this.notification({method: "notifications/message", params})
403403
}
404404
}

0 commit comments

Comments
 (0)