Automatic handling of logging level (#882)

Co-authored-by: Felix Weinberger <3823880+felixweinberger@users.noreply.github.com>
This commit is contained in:
Cliff Hall 2025-08-27 15:09:07 -04:00 • committed by GitHub
commit 79d11dc53f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 116 additions and 79 deletions

View file

@ -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"]) {

View file

@ -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.
*/