Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ out
# Stores VSCode versions used for testing VSCode extensions
.vscode-test

# Jetbrains IDEs
.idea/

# yarn v2
.yarn/cache
.yarn/unplugged
Expand Down
156 changes: 154 additions & 2 deletions src/server/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import {
ListResourcesRequestSchema,
ListToolsRequestSchema,
SetLevelRequestSchema,
ErrorCode
ErrorCode,
LoggingMessageNotification
} from "../types.js";
import { Transport } from "../shared/transport.js";
import { InMemoryTransport } from "../inMemory.js";
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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();

});

6 changes: 3 additions & 3 deletions src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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)!
Expand Down Expand Up @@ -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})
}
}
Expand Down