Skip to content

Commit 79d11dc

Browse files
Automatic handling of logging level (modelcontextprotocol#882)
Co-authored-by: Felix Weinberger <3823880+felixweinberger@users.noreply.github.com>
1 parent 3bc2235 commit 79d11dc

File tree

7 files changed

+116
-79
lines changed

7 files changed

+116
-79
lines changed

src/examples/server/jsonResponseStreamableHttp.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -44,27 +44,27 @@ const getServer = () => {
4444
{
4545
name: z.string().describe('Name to greet'),
4646
},
47-
async ({ name }, { sendNotification }): Promise<CallToolResult> => {
47+
async ({ name }, extra): Promise<CallToolResult> => {
4848
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
4949

50-
await sendNotification({
51-
method: "notifications/message",
52-
params: { level: "debug", data: `Starting multi-greet for ${name}` }
53-
});
50+
await server.sendLoggingMessage({
51+
level: "debug",
52+
data: `Starting multi-greet for ${name}`
53+
}, extra.sessionId);
5454

5555
await sleep(1000); // Wait 1 second before first greeting
5656

57-
await sendNotification({
58-
method: "notifications/message",
59-
params: { level: "info", data: `Sending first greeting to ${name}` }
60-
});
57+
await server.sendLoggingMessage({
58+
level: "info",
59+
data: `Sending first greeting to ${name}`
60+
}, extra.sessionId);
6161

6262
await sleep(1000); // Wait another second before second greeting
6363

64-
await sendNotification({
65-
method: "notifications/message",
66-
params: { level: "info", data: `Sending second greeting to ${name}` }
67-
});
64+
await server.sendLoggingMessage({
65+
level: "info",
66+
data: `Sending second greeting to ${name}`
67+
}, extra.sessionId);
6868

6969
return {
7070
content: [
@@ -170,4 +170,4 @@ app.listen(PORT, (error) => {
170170
process.on('SIGINT', async () => {
171171
console.log('Shutting down server...');
172172
process.exit(0);
173-
});
173+
});

src/examples/server/simpleSseServer.ts

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ import { z } from 'zod';
55
import { CallToolResult } from '../../types.js';
66

77
/**
8-
* This example server demonstrates the deprecated HTTP+SSE transport
8+
* This example server demonstrates the deprecated HTTP+SSE transport
99
* (protocol version 2024-11-05). It mainly used for testing backward compatible clients.
10-
*
10+
*
1111
* The server exposes two endpoints:
1212
* - /mcp: For establishing the SSE stream (GET)
1313
* - /messages: For receiving client messages (POST)
14-
*
14+
*
1515
*/
1616

1717
// Create an MCP server instance
@@ -28,32 +28,26 @@ const getServer = () => {
2828
interval: z.number().describe('Interval in milliseconds between notifications').default(1000),
2929
count: z.number().describe('Number of notifications to send').default(10),
3030
},
31-
async ({ interval, count }, { sendNotification }): Promise<CallToolResult> => {
31+
async ({ interval, count }, extra): Promise<CallToolResult> => {
3232
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
3333
let counter = 0;
3434

3535
// Send the initial notification
36-
await sendNotification({
37-
method: "notifications/message",
38-
params: {
39-
level: "info",
40-
data: `Starting notification stream with ${count} messages every ${interval}ms`
41-
}
42-
});
36+
await server.sendLoggingMessage({
37+
level: "info",
38+
data: `Starting notification stream with ${count} messages every ${interval}ms`
39+
}, extra.sessionId);
4340

4441
// Send periodic notifications
4542
while (counter < count) {
4643
counter++;
4744
await sleep(interval);
4845

4946
try {
50-
await sendNotification({
51-
method: "notifications/message",
52-
params: {
47+
await server.sendLoggingMessage({
5348
level: "info",
5449
data: `Notification #${counter} at ${new Date().toISOString()}`
55-
}
56-
});
50+
}, extra.sessionId);
5751
}
5852
catch (error) {
5953
console.error("Error sending notification:", error);
@@ -169,4 +163,4 @@ process.on('SIGINT', async () => {
169163
}
170164
console.log('Server shutdown complete');
171165
process.exit(0);
172-
});
166+
});

src/examples/server/simpleStatelessStreamableHttp.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,20 +42,17 @@ const getServer = () => {
4242
interval: z.number().describe('Interval in milliseconds between notifications').default(100),
4343
count: z.number().describe('Number of notifications to send (0 for 100)').default(10),
4444
},
45-
async ({ interval, count }, { sendNotification }): Promise<CallToolResult> => {
45+
async ({ interval, count }, extra): Promise<CallToolResult> => {
4646
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
4747
let counter = 0;
4848

4949
while (count === 0 || counter < count) {
5050
counter++;
5151
try {
52-
await sendNotification({
53-
method: "notifications/message",
54-
params: {
55-
level: "info",
56-
data: `Periodic notification #${counter} at ${new Date().toISOString()}`
57-
}
58-
});
52+
await server.sendLoggingMessage({
53+
level: "info",
54+
data: `Periodic notification #${counter} at ${new Date().toISOString()}`
55+
}, extra.sessionId);
5956
}
6057
catch (error) {
6158
console.error("Error sending notification:", error);
@@ -170,4 +167,4 @@ app.listen(PORT, (error) => {
170167
process.on('SIGINT', async () => {
171168
console.log('Shutting down server...');
172169
process.exit(0);
173-
});
170+
});

src/examples/server/simpleStreamableHttp.ts

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -58,27 +58,27 @@ const getServer = () => {
5858
readOnlyHint: true,
5959
openWorldHint: false
6060
},
61-
async ({ name }, { sendNotification }): Promise<CallToolResult> => {
61+
async ({ name }, extra): Promise<CallToolResult> => {
6262
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
6363

64-
await sendNotification({
65-
method: "notifications/message",
66-
params: { level: "debug", data: `Starting multi-greet for ${name}` }
67-
});
64+
await server.sendLoggingMessage({
65+
level: "debug",
66+
data: `Starting multi-greet for ${name}`
67+
}, extra.sessionId);
6868

6969
await sleep(1000); // Wait 1 second before first greeting
7070

71-
await sendNotification({
72-
method: "notifications/message",
73-
params: { level: "info", data: `Sending first greeting to ${name}` }
74-
});
71+
await server.sendLoggingMessage({
72+
level: "info",
73+
data: `Sending first greeting to ${name}`
74+
}, extra.sessionId);
7575

7676
await sleep(1000); // Wait another second before second greeting
7777

78-
await sendNotification({
79-
method: "notifications/message",
80-
params: { level: "info", data: `Sending second greeting to ${name}` }
81-
});
78+
await server.sendLoggingMessage({
79+
level: "info",
80+
data: `Sending second greeting to ${name}`
81+
}, extra.sessionId);
8282

8383
return {
8484
content: [
@@ -273,20 +273,17 @@ const getServer = () => {
273273
interval: z.number().describe('Interval in milliseconds between notifications').default(100),
274274
count: z.number().describe('Number of notifications to send (0 for 100)').default(50),
275275
},
276-
async ({ interval, count }, { sendNotification }): Promise<CallToolResult> => {
276+
async ({ interval, count }, extra): Promise<CallToolResult> => {
277277
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
278278
let counter = 0;
279279

280280
while (count === 0 || counter < count) {
281281
counter++;
282282
try {
283-
await sendNotification({
284-
method: "notifications/message",
285-
params: {
286-
level: "info",
287-
data: `Periodic notification #${counter} at ${new Date().toISOString()}`
288-
}
289-
});
283+
await server.sendLoggingMessage( {
284+
level: "info",
285+
data: `Periodic notification #${counter} at ${new Date().toISOString()}`
286+
}, extra.sessionId);
290287
}
291288
catch (error) {
292289
console.error("Error sending notification:", error);

src/examples/server/sseAndStreamableHttpCompatibleServer.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import cors from 'cors';
1212
* This example server demonstrates backwards compatibility with both:
1313
* 1. The deprecated HTTP+SSE transport (protocol version 2024-11-05)
1414
* 2. The Streamable HTTP transport (protocol version 2025-03-26)
15-
*
15+
*
1616
* It maintains a single MCP server instance but exposes two transport options:
1717
* - /mcp: The new Streamable HTTP endpoint (supports GET/POST/DELETE)
1818
* - /sse: The deprecated SSE endpoint for older clients (GET to establish stream)
@@ -33,20 +33,17 @@ const getServer = () => {
3333
interval: z.number().describe('Interval in milliseconds between notifications').default(100),
3434
count: z.number().describe('Number of notifications to send (0 for 100)').default(50),
3535
},
36-
async ({ interval, count }, { sendNotification }): Promise<CallToolResult> => {
36+
async ({ interval, count }, extra): Promise<CallToolResult> => {
3737
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
3838
let counter = 0;
3939

4040
while (count === 0 || counter < count) {
4141
counter++;
4242
try {
43-
await sendNotification({
44-
method: "notifications/message",
45-
params: {
46-
level: "info",
47-
data: `Periodic notification #${counter} at ${new Date().toISOString()}`
48-
}
49-
});
43+
await server.sendLoggingMessage({
44+
level: "info",
45+
data: `Periodic notification #${counter} at ${new Date().toISOString()}`
46+
}, extra.sessionId);
5047
}
5148
catch (error) {
5249
console.error("Error sending notification:", error);
@@ -254,4 +251,4 @@ process.on('SIGINT', async () => {
254251
}
255252
console.log('Server shutdown complete');
256253
process.exit(0);
257-
});
254+
});

src/server/index.ts

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ import {
3232
ServerRequest,
3333
ServerResult,
3434
SUPPORTED_PROTOCOL_VERSIONS,
35+
LoggingLevel,
36+
SetLevelRequestSchema,
37+
LoggingLevelSchema
3538
} from "../types.js";
3639
import Ajv from "ajv";
3740

@@ -108,8 +111,36 @@ export class Server<
108111
this.setNotificationHandler(InitializedNotificationSchema, () =>
109112
this.oninitialized?.(),
110113
);
114+
115+
if (this._capabilities.logging) {
116+
this.setRequestHandler(SetLevelRequestSchema, async (request, extra) => {
117+
const transportSessionId: string | undefined = extra.sessionId || extra.requestInfo?.headers['mcp-session-id'] as string || undefined;
118+
const { level } = request.params;
119+
const parseResult = LoggingLevelSchema.safeParse(level);
120+
if (transportSessionId && parseResult.success) {
121+
this._loggingLevels.set(transportSessionId, parseResult.data);
122+
}
123+
return {};
124+
})
125+
}
111126
}
112127

128+
// Map log levels by session id
129+
private _loggingLevels = new Map<string, LoggingLevel>();
130+
131+
// Map LogLevelSchema to severity index
132+
private readonly LOG_LEVEL_SEVERITY = new Map(
133+
LoggingLevelSchema.options.map((level, index) => [level, index])
134+
);
135+
136+
// 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 => {
138+
const currentLevel = this._loggingLevels.get(sessionId);
139+
return (currentLevel)
140+
? this.LOG_LEVEL_SEVERITY.get(level)! < this.LOG_LEVEL_SEVERITY.get(currentLevel)!
141+
: false;
142+
};
143+
113144
/**
114145
* Registers new capabilities. This can only be called before connecting to a transport.
115146
*
@@ -121,7 +152,6 @@ export class Server<
121152
"Cannot register capabilities after connecting to transport",
122153
);
123154
}
124-
125155
this._capabilities = mergeCapabilities(this._capabilities, capabilities);
126156
}
127157

@@ -324,10 +354,10 @@ export class Server<
324354
if (result.action === "accept" && result.content) {
325355
try {
326356
const ajv = new Ajv();
327-
357+
328358
const validate = ajv.compile(params.requestedSchema);
329359
const isValid = validate(result.content);
330-
360+
331361
if (!isValid) {
332362
throw new McpError(
333363
ErrorCode.InvalidParams,
@@ -359,8 +389,19 @@ export class Server<
359389
);
360390
}
361391

362-
async sendLoggingMessage(params: LoggingMessageNotification["params"]) {
363-
return this.notification({ method: "notifications/message", params });
392+
/**
393+
* Sends a logging message to the client, if connected.
394+
* Note: You only need to send the parameters object, not the entire JSON RPC message
395+
* @see LoggingMessageNotification
396+
* @param params
397+
* @param sessionId optional for stateless and backward compatibility
398+
*/
399+
async sendLoggingMessage(params: LoggingMessageNotification["params"], sessionId?: string) {
400+
if (this._capabilities.logging) {
401+
if (!sessionId || !this.isMessageIgnored(params.level, sessionId)) {
402+
return this.notification({method: "notifications/message", params})
403+
}
404+
}
364405
}
365406

366407
async sendResourceUpdated(params: ResourceUpdatedNotification["params"]) {

src/server/mcp.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
ServerRequest,
4242
ServerNotification,
4343
ToolAnnotations,
44+
LoggingMessageNotification,
4445
} from "../types.js";
4546
import { Completable, CompletableDef } from "./completable.js";
4647
import { UriTemplate, Variables } from "../shared/uriTemplate.js";
@@ -822,7 +823,7 @@ export class McpServer {
822823
/**
823824
* Registers a tool taking either a parameter schema for validation or annotations for additional metadata.
824825
* This unified overload handles both `tool(name, paramsSchema, cb)` and `tool(name, annotations, cb)` cases.
825-
*
826+
*
826827
* Note: We use a union type for the second parameter because TypeScript cannot reliably disambiguate
827828
* between ToolAnnotations and ZodRawShape during overload resolution, as both are plain object types.
828829
*/
@@ -834,9 +835,9 @@ export class McpServer {
834835

835836
/**
836837
* Registers a tool `name` (with a description) taking either parameter schema or annotations.
837-
* This unified overload handles both `tool(name, description, paramsSchema, cb)` and
838+
* This unified overload handles both `tool(name, description, paramsSchema, cb)` and
838839
* `tool(name, description, annotations, cb)` cases.
839-
*
840+
*
840841
* Note: We use a union type for the third parameter because TypeScript cannot reliably disambiguate
841842
* between ToolAnnotations and ZodRawShape during overload resolution, as both are plain object types.
842843
*/
@@ -1047,6 +1048,16 @@ export class McpServer {
10471048
return this.server.transport !== undefined
10481049
}
10491050

1051+
/**
1052+
* Sends a logging message to the client, if connected.
1053+
* Note: You only need to send the parameters object, not the entire JSON RPC message
1054+
* @see LoggingMessageNotification
1055+
* @param params
1056+
* @param sessionId optional for stateless and backward compatibility
1057+
*/
1058+
async sendLoggingMessage(params: LoggingMessageNotification["params"], sessionId?: string) {
1059+
return this.server.sendLoggingMessage(params, sessionId);
1060+
}
10501061
/**
10511062
* Sends a resource list changed event to the client, if connected.
10521063
*/

0 commit comments

Comments
 (0)