Skip to content
Open
Show file tree
Hide file tree
Changes from all 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