Skip to content

Commit a0e03b2

Browse files
committed
Add setLevel handling to Server when logging capability is advertised. Update sendLoggingMessage method to respect current log level.
Add sendLoggingMessage passthrough method on McpServer class since it doesn't use inheritance. Update examples to use the sendLoggingMessage method so that it can be filtered according to the log level. * In src/server/index.ts - in constructor - if capabilities object has logging, set request handler for SetLevelRequestSchema which sets this.logLevel to the level value from the request params and return empty object as per spec - define private _logLevel property of type LoggingLevel initialized to "debug" - define private _msgLevels property with list of objects with level property set to the ascending logging levels according to RFC 5424 as per spec. This gives us a lookup to determine if level is greater or less than a target level - add private isMessageIgnored method which takes a LoggingLevel type arg returns boolean, and looks up the currently set level and the target level, returning true if the target level is below the current level - in sendLoggingMessage method, only send the notification if the params.level value is not ignored. * In mcp.ts - import LoggingMessageNotification - add async sendLoggingMessage method that returns takes a params object from LoggingMessageNotification, and returns the result of a call to this.server.sendLoggingMessage (since McpServer doesn't extend Server, but rather uses composition. * In src/examples/server/ - jsonResponseStreamableHttp.ts - simpleSseServer.ts - simpleStreamableHttp.ts - simpleStatelessStreamableHttp.ts - in tool call callback functions that send logging message via the included sendNotification function, replace with calls to server.sendLoggingMessage, passing only the params of the message.
1 parent abb2f0a commit a0e03b2

File tree

7 files changed

+86
-72
lines changed

7 files changed

+86
-72
lines changed

src/examples/server/jsonResponseStreamableHttp.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,26 +44,26 @@ const getServer = () => {
4444
{
4545
name: z.string().describe('Name to greet'),
4646
},
47-
async ({ name }, { sendNotification }): Promise<CallToolResult> => {
47+
async ({ name }): 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}` }
50+
await server.sendLoggingMessage({
51+
level: "debug",
52+
data: `Starting multi-greet for ${name}`
5353
});
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}` }
57+
await server.sendLoggingMessage({
58+
level: "info",
59+
data: `Sending first greeting to ${name}`
6060
});
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}` }
64+
await server.sendLoggingMessage({
65+
level: "info",
66+
data: `Sending second greeting to ${name}`
6767
});
6868

6969
return {
@@ -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: 10 additions & 16 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,17 +28,14 @@ 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 }): 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-
}
36+
await server.sendLoggingMessage({
37+
level: "info",
38+
data: `Starting notification stream with ${count} messages every ${interval}ms`
4239
});
4340

4441
// Send periodic notifications
@@ -47,13 +44,10 @@ const getServer = () => {
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+
});
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: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,19 +42,16 @@ 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 }): 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-
}
52+
await server.sendLoggingMessage({
53+
level: "info",
54+
data: `Periodic notification #${counter} at ${new Date().toISOString()}`
5855
});
5956
}
6057
catch (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: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -58,27 +58,18 @@ const getServer = () => {
5858
readOnlyHint: true,
5959
openWorldHint: false
6060
},
61-
async ({ name }, { sendNotification }): Promise<CallToolResult> => {
61+
async ({ name }): 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({ level: "debug", data: `Starting multi-greet for ${name}`});
6865

6966
await sleep(1000); // Wait 1 second before first greeting
7067

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

7670
await sleep(1000); // Wait another second before second greeting
7771

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

8374
return {
8475
content: [
@@ -273,20 +264,17 @@ const getServer = () => {
273264
interval: z.number().describe('Interval in milliseconds between notifications').default(100),
274265
count: z.number().describe('Number of notifications to send (0 for 100)').default(50),
275266
},
276-
async ({ interval, count }, { sendNotification }): Promise<CallToolResult> => {
267+
async ({ interval, count }): Promise<CallToolResult> => {
277268
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
278269
let counter = 0;
279270

280271
while (count === 0 || counter < count) {
281272
counter++;
282273
try {
283-
await sendNotification({
284-
method: "notifications/message",
285-
params: {
286-
level: "info",
287-
data: `Periodic notification #${counter} at ${new Date().toISOString()}`
288-
}
289-
});
274+
await server.sendLoggingMessage( {
275+
level: "info",
276+
data: `Periodic notification #${counter} at ${new Date().toISOString()}`
277+
});
290278
}
291279
catch (error) {
292280
console.error("Error sending notification:", error);

src/examples/server/sseAndStreamableHttpCompatibleServer.ts

Lines changed: 6 additions & 9 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,19 +33,16 @@ 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 }): 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-
}
43+
await server.sendLoggingMessage({
44+
level: "info",
45+
data: `Periodic notification #${counter} at ${new Date().toISOString()}`
4946
});
5047
}
5148
catch (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: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
ServerRequest,
3333
ServerResult,
3434
SUPPORTED_PROTOCOL_VERSIONS,
35+
LoggingLevel, SetLevelRequestSchema,
3536
} from "../types.js";
3637
import Ajv from "ajv";
3738

@@ -108,8 +109,34 @@ export class Server<
108109
this.setNotificationHandler(InitializedNotificationSchema, () =>
109110
this.oninitialized?.(),
110111
);
112+
113+
if (this._capabilities.logging) {
114+
this.setRequestHandler(SetLevelRequestSchema, async (request) => {
115+
const { level } = request.params;
116+
this._logLevel = level;
117+
return {};
118+
})
119+
}
111120
}
112121

122+
private _logLevel: LoggingLevel = "debug";
123+
private _msgLevels = [
124+
{ level: "debug" },
125+
{ level: "info" },
126+
{ level: "notice" },
127+
{ level: "warning" },
128+
{ level: "error" },
129+
{ level: "critical" },
130+
{ level: "alert" },
131+
{ level: "emergency" },
132+
];
133+
134+
private isMessageIgnored = (level: LoggingLevel): boolean => {
135+
const currentLevel = this._msgLevels.findIndex((msg) => this._logLevel === msg.level);
136+
const messageLevel = this._msgLevels.findIndex((msg) => level === msg.level);
137+
return messageLevel < currentLevel;
138+
};
139+
113140
/**
114141
* Registers new capabilities. This can only be called before connecting to a transport.
115142
*
@@ -121,7 +148,6 @@ export class Server<
121148
"Cannot register capabilities after connecting to transport",
122149
);
123150
}
124-
125151
this._capabilities = mergeCapabilities(this._capabilities, capabilities);
126152
}
127153

@@ -324,10 +350,10 @@ export class Server<
324350
if (result.action === "accept" && result.content) {
325351
try {
326352
const ajv = new Ajv();
327-
353+
328354
const validate = ajv.compile(params.requestedSchema);
329355
const isValid = validate(result.content);
330-
356+
331357
if (!isValid) {
332358
throw new McpError(
333359
ErrorCode.InvalidParams,
@@ -360,7 +386,9 @@ export class Server<
360386
}
361387

362388
async sendLoggingMessage(params: LoggingMessageNotification["params"]) {
363-
return this.notification({ method: "notifications/message", params });
389+
return (!this.isMessageIgnored(params.level))
390+
? this.notification({ method: "notifications/message", params })
391+
: undefined
364392
}
365393

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

src/server/mcp.ts

Lines changed: 13 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,15 @@ 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
1054+
* @see LoggingMessageNotification
1055+
* @param params
1056+
*/
1057+
async sendLoggingMessage(params: LoggingMessageNotification["params"]) {
1058+
return this.server.sendLoggingMessage(params);
1059+
}
10501060
/**
10511061
* Sends a resource list changed event to the client, if connected.
10521062
*/

0 commit comments

Comments
 (0)