Automatic handling of logging level (#882)
Co-authored-by: Felix Weinberger <3823880+felixweinberger@users.noreply.github.com>
This commit is contained in:
parent
3bc2235d74
commit
79d11dc53f
7 changed files with 116 additions and 79 deletions
|
|
@ -44,27 +44,27 @@ const getServer = () => {
|
|||
{
|
||||
name: z.string().describe('Name to greet'),
|
||||
},
|
||||
async ({ name }, { sendNotification }): Promise<CallToolResult> => {
|
||||
async ({ name }, extra): Promise<CallToolResult> => {
|
||||
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
await sendNotification({
|
||||
method: "notifications/message",
|
||||
params: { level: "debug", data: `Starting multi-greet for ${name}` }
|
||||
});
|
||||
await server.sendLoggingMessage({
|
||||
level: "debug",
|
||||
data: `Starting multi-greet for ${name}`
|
||||
}, extra.sessionId);
|
||||
|
||||
await sleep(1000); // Wait 1 second before first greeting
|
||||
|
||||
await sendNotification({
|
||||
method: "notifications/message",
|
||||
params: { level: "info", data: `Sending first greeting to ${name}` }
|
||||
});
|
||||
await server.sendLoggingMessage({
|
||||
level: "info",
|
||||
data: `Sending first greeting to ${name}`
|
||||
}, extra.sessionId);
|
||||
|
||||
await sleep(1000); // Wait another second before second greeting
|
||||
|
||||
await sendNotification({
|
||||
method: "notifications/message",
|
||||
params: { level: "info", data: `Sending second greeting to ${name}` }
|
||||
});
|
||||
await server.sendLoggingMessage({
|
||||
level: "info",
|
||||
data: `Sending second greeting to ${name}`
|
||||
}, extra.sessionId);
|
||||
|
||||
return {
|
||||
content: [
|
||||
|
|
@ -170,4 +170,4 @@ app.listen(PORT, (error) => {
|
|||
process.on('SIGINT', async () => {
|
||||
console.log('Shutting down server...');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,13 +5,13 @@ import { z } from 'zod';
|
|||
import { CallToolResult } from '../../types.js';
|
||||
|
||||
/**
|
||||
* This example server demonstrates the deprecated HTTP+SSE transport
|
||||
* This example server demonstrates the deprecated HTTP+SSE transport
|
||||
* (protocol version 2024-11-05). It mainly used for testing backward compatible clients.
|
||||
*
|
||||
*
|
||||
* The server exposes two endpoints:
|
||||
* - /mcp: For establishing the SSE stream (GET)
|
||||
* - /messages: For receiving client messages (POST)
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
// Create an MCP server instance
|
||||
|
|
@ -28,18 +28,15 @@ const getServer = () => {
|
|||
interval: z.number().describe('Interval in milliseconds between notifications').default(1000),
|
||||
count: z.number().describe('Number of notifications to send').default(10),
|
||||
},
|
||||
async ({ interval, count }, { sendNotification }): Promise<CallToolResult> => {
|
||||
async ({ interval, count }, extra): Promise<CallToolResult> => {
|
||||
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
let counter = 0;
|
||||
|
||||
// Send the initial notification
|
||||
await sendNotification({
|
||||
method: "notifications/message",
|
||||
params: {
|
||||
level: "info",
|
||||
data: `Starting notification stream with ${count} messages every ${interval}ms`
|
||||
}
|
||||
});
|
||||
await server.sendLoggingMessage({
|
||||
level: "info",
|
||||
data: `Starting notification stream with ${count} messages every ${interval}ms`
|
||||
}, extra.sessionId);
|
||||
|
||||
// Send periodic notifications
|
||||
while (counter < count) {
|
||||
|
|
@ -47,13 +44,10 @@ const getServer = () => {
|
|||
await sleep(interval);
|
||||
|
||||
try {
|
||||
await sendNotification({
|
||||
method: "notifications/message",
|
||||
params: {
|
||||
await server.sendLoggingMessage({
|
||||
level: "info",
|
||||
data: `Notification #${counter} at ${new Date().toISOString()}`
|
||||
}
|
||||
});
|
||||
}, extra.sessionId);
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Error sending notification:", error);
|
||||
|
|
@ -169,4 +163,4 @@ process.on('SIGINT', async () => {
|
|||
}
|
||||
console.log('Server shutdown complete');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -42,20 +42,17 @@ const getServer = () => {
|
|||
interval: z.number().describe('Interval in milliseconds between notifications').default(100),
|
||||
count: z.number().describe('Number of notifications to send (0 for 100)').default(10),
|
||||
},
|
||||
async ({ interval, count }, { sendNotification }): Promise<CallToolResult> => {
|
||||
async ({ interval, count }, extra): Promise<CallToolResult> => {
|
||||
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
let counter = 0;
|
||||
|
||||
while (count === 0 || counter < count) {
|
||||
counter++;
|
||||
try {
|
||||
await sendNotification({
|
||||
method: "notifications/message",
|
||||
params: {
|
||||
level: "info",
|
||||
data: `Periodic notification #${counter} at ${new Date().toISOString()}`
|
||||
}
|
||||
});
|
||||
await server.sendLoggingMessage({
|
||||
level: "info",
|
||||
data: `Periodic notification #${counter} at ${new Date().toISOString()}`
|
||||
}, extra.sessionId);
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Error sending notification:", error);
|
||||
|
|
@ -170,4 +167,4 @@ app.listen(PORT, (error) => {
|
|||
process.on('SIGINT', async () => {
|
||||
console.log('Shutting down server...');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -58,27 +58,27 @@ const getServer = () => {
|
|||
readOnlyHint: true,
|
||||
openWorldHint: false
|
||||
},
|
||||
async ({ name }, { sendNotification }): Promise<CallToolResult> => {
|
||||
async ({ name }, extra): Promise<CallToolResult> => {
|
||||
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
await sendNotification({
|
||||
method: "notifications/message",
|
||||
params: { level: "debug", data: `Starting multi-greet for ${name}` }
|
||||
});
|
||||
await server.sendLoggingMessage({
|
||||
level: "debug",
|
||||
data: `Starting multi-greet for ${name}`
|
||||
}, extra.sessionId);
|
||||
|
||||
await sleep(1000); // Wait 1 second before first greeting
|
||||
|
||||
await sendNotification({
|
||||
method: "notifications/message",
|
||||
params: { level: "info", data: `Sending first greeting to ${name}` }
|
||||
});
|
||||
await server.sendLoggingMessage({
|
||||
level: "info",
|
||||
data: `Sending first greeting to ${name}`
|
||||
}, extra.sessionId);
|
||||
|
||||
await sleep(1000); // Wait another second before second greeting
|
||||
|
||||
await sendNotification({
|
||||
method: "notifications/message",
|
||||
params: { level: "info", data: `Sending second greeting to ${name}` }
|
||||
});
|
||||
await server.sendLoggingMessage({
|
||||
level: "info",
|
||||
data: `Sending second greeting to ${name}`
|
||||
}, extra.sessionId);
|
||||
|
||||
return {
|
||||
content: [
|
||||
|
|
@ -273,20 +273,17 @@ const getServer = () => {
|
|||
interval: z.number().describe('Interval in milliseconds between notifications').default(100),
|
||||
count: z.number().describe('Number of notifications to send (0 for 100)').default(50),
|
||||
},
|
||||
async ({ interval, count }, { sendNotification }): Promise<CallToolResult> => {
|
||||
async ({ interval, count }, extra): Promise<CallToolResult> => {
|
||||
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
let counter = 0;
|
||||
|
||||
while (count === 0 || counter < count) {
|
||||
counter++;
|
||||
try {
|
||||
await sendNotification({
|
||||
method: "notifications/message",
|
||||
params: {
|
||||
level: "info",
|
||||
data: `Periodic notification #${counter} at ${new Date().toISOString()}`
|
||||
}
|
||||
});
|
||||
await server.sendLoggingMessage( {
|
||||
level: "info",
|
||||
data: `Periodic notification #${counter} at ${new Date().toISOString()}`
|
||||
}, extra.sessionId);
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Error sending notification:", error);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import cors from 'cors';
|
|||
* This example server demonstrates backwards compatibility with both:
|
||||
* 1. The deprecated HTTP+SSE transport (protocol version 2024-11-05)
|
||||
* 2. The Streamable HTTP transport (protocol version 2025-03-26)
|
||||
*
|
||||
*
|
||||
* It maintains a single MCP server instance but exposes two transport options:
|
||||
* - /mcp: The new Streamable HTTP endpoint (supports GET/POST/DELETE)
|
||||
* - /sse: The deprecated SSE endpoint for older clients (GET to establish stream)
|
||||
|
|
@ -33,20 +33,17 @@ const getServer = () => {
|
|||
interval: z.number().describe('Interval in milliseconds between notifications').default(100),
|
||||
count: z.number().describe('Number of notifications to send (0 for 100)').default(50),
|
||||
},
|
||||
async ({ interval, count }, { sendNotification }): Promise<CallToolResult> => {
|
||||
async ({ interval, count }, extra): Promise<CallToolResult> => {
|
||||
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
let counter = 0;
|
||||
|
||||
while (count === 0 || counter < count) {
|
||||
counter++;
|
||||
try {
|
||||
await sendNotification({
|
||||
method: "notifications/message",
|
||||
params: {
|
||||
level: "info",
|
||||
data: `Periodic notification #${counter} at ${new Date().toISOString()}`
|
||||
}
|
||||
});
|
||||
await server.sendLoggingMessage({
|
||||
level: "info",
|
||||
data: `Periodic notification #${counter} at ${new Date().toISOString()}`
|
||||
}, extra.sessionId);
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Error sending notification:", error);
|
||||
|
|
@ -254,4 +251,4 @@ process.on('SIGINT', async () => {
|
|||
}
|
||||
console.log('Server shutdown complete');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@ import {
|
|||
ServerRequest,
|
||||
ServerResult,
|
||||
SUPPORTED_PROTOCOL_VERSIONS,
|
||||
LoggingLevel,
|
||||
SetLevelRequestSchema,
|
||||
LoggingLevelSchema
|
||||
} from "../types.js";
|
||||
import Ajv from "ajv";
|
||||
|
||||
|
|
@ -108,8 +111,36 @@ export class Server<
|
|||
this.setNotificationHandler(InitializedNotificationSchema, () =>
|
||||
this.oninitialized?.(),
|
||||
);
|
||||
|
||||
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 { level } = request.params;
|
||||
const parseResult = LoggingLevelSchema.safeParse(level);
|
||||
if (transportSessionId && parseResult.success) {
|
||||
this._loggingLevels.set(transportSessionId, parseResult.data);
|
||||
}
|
||||
return {};
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Map log levels by session id
|
||||
private _loggingLevels = new Map<string, LoggingLevel>();
|
||||
|
||||
// Map LogLevelSchema to severity index
|
||||
private readonly LOG_LEVEL_SEVERITY = new Map(
|
||||
LoggingLevelSchema.options.map((level, index) => [level, index])
|
||||
);
|
||||
|
||||
// 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 => {
|
||||
const currentLevel = this._loggingLevels.get(sessionId);
|
||||
return (currentLevel)
|
||||
? this.LOG_LEVEL_SEVERITY.get(level)! < this.LOG_LEVEL_SEVERITY.get(currentLevel)!
|
||||
: false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Registers new capabilities. This can only be called before connecting to a transport.
|
||||
*
|
||||
|
|
@ -121,7 +152,6 @@ export class Server<
|
|||
"Cannot register capabilities after connecting to transport",
|
||||
);
|
||||
}
|
||||
|
||||
this._capabilities = mergeCapabilities(this._capabilities, capabilities);
|
||||
}
|
||||
|
||||
|
|
@ -324,10 +354,10 @@ export class Server<
|
|||
if (result.action === "accept" && result.content) {
|
||||
try {
|
||||
const ajv = new Ajv();
|
||||
|
||||
|
||||
const validate = ajv.compile(params.requestedSchema);
|
||||
const isValid = validate(result.content);
|
||||
|
||||
|
||||
if (!isValid) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
|
|
@ -359,8 +389,19 @@ export class Server<
|
|||
);
|
||||
}
|
||||
|
||||
async sendLoggingMessage(params: LoggingMessageNotification["params"]) {
|
||||
return this.notification({ method: "notifications/message", params });
|
||||
/**
|
||||
* Sends a logging message to the client, if connected.
|
||||
* Note: You only need to send the parameters object, not the entire JSON RPC message
|
||||
* @see LoggingMessageNotification
|
||||
* @param params
|
||||
* @param sessionId optional for stateless and backward compatibility
|
||||
*/
|
||||
async sendLoggingMessage(params: LoggingMessageNotification["params"], sessionId?: string) {
|
||||
if (this._capabilities.logging) {
|
||||
if (!sessionId || !this.isMessageIgnored(params.level, sessionId)) {
|
||||
return this.notification({method: "notifications/message", params})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async sendResourceUpdated(params: ResourceUpdatedNotification["params"]) {
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ import {
|
|||
ServerRequest,
|
||||
ServerNotification,
|
||||
ToolAnnotations,
|
||||
LoggingMessageNotification,
|
||||
} from "../types.js";
|
||||
import { Completable, CompletableDef } from "./completable.js";
|
||||
import { UriTemplate, Variables } from "../shared/uriTemplate.js";
|
||||
|
|
@ -822,7 +823,7 @@ export class McpServer {
|
|||
/**
|
||||
* Registers a tool taking either a parameter schema for validation or annotations for additional metadata.
|
||||
* This unified overload handles both `tool(name, paramsSchema, cb)` and `tool(name, annotations, cb)` cases.
|
||||
*
|
||||
*
|
||||
* Note: We use a union type for the second parameter because TypeScript cannot reliably disambiguate
|
||||
* between ToolAnnotations and ZodRawShape during overload resolution, as both are plain object types.
|
||||
*/
|
||||
|
|
@ -834,9 +835,9 @@ export class McpServer {
|
|||
|
||||
/**
|
||||
* Registers a tool `name` (with a description) taking either parameter schema or annotations.
|
||||
* This unified overload handles both `tool(name, description, paramsSchema, cb)` and
|
||||
* This unified overload handles both `tool(name, description, paramsSchema, cb)` and
|
||||
* `tool(name, description, annotations, cb)` cases.
|
||||
*
|
||||
*
|
||||
* Note: We use a union type for the third parameter because TypeScript cannot reliably disambiguate
|
||||
* between ToolAnnotations and ZodRawShape during overload resolution, as both are plain object types.
|
||||
*/
|
||||
|
|
@ -1047,6 +1048,16 @@ export class McpServer {
|
|||
return this.server.transport !== undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a logging message to the client, if connected.
|
||||
* Note: You only need to send the parameters object, not the entire JSON RPC message
|
||||
* @see LoggingMessageNotification
|
||||
* @param params
|
||||
* @param sessionId optional for stateless and backward compatibility
|
||||
*/
|
||||
async sendLoggingMessage(params: LoggingMessageNotification["params"], sessionId?: string) {
|
||||
return this.server.sendLoggingMessage(params, sessionId);
|
||||
}
|
||||
/**
|
||||
* Sends a resource list changed event to the client, if connected.
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in a new issue