diff --git a/src/examples/server/simpleStreamableHttp.ts b/src/examples/server/simpleStreamableHttp.ts index 35c04fe5..2b25f4b2 100644 --- a/src/examples/server/simpleStreamableHttp.ts +++ b/src/examples/server/simpleStreamableHttp.ts @@ -12,6 +12,21 @@ const server = new McpServer({ version: '1.0.0', }, { capabilities: { logging: {} } }); +// Log the capability invocation details +server.onCapabilityChange((event) => { + switch (event.action) { + case 'invoked': + console.log(`${event.capabilityType} invocation ${event.invocationIndex}: '${event.capabilityName}' started`); + break; + case 'completed': + console.log(`${event.capabilityType} invocation ${event.invocationIndex}: '${event.capabilityName}' completed in ${event.durationMs}ms`); + break; + case 'error': + console.log(`${event.capabilityType} invocation ${event.invocationIndex}: '${event.capabilityName}' failed in ${event.durationMs}ms: ${event.error}`); + break; + } +}); + // Register a simple tool that returns a greeting server.tool( 'greet', @@ -291,4 +306,4 @@ process.on('SIGINT', async () => { await server.close(); console.log('Server shutdown complete'); process.exit(0); -}); \ No newline at end of file +}); diff --git a/src/server/mcp.ts b/src/server/mcp.ts index 3627892b..b713d0a2 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -65,8 +65,11 @@ export class McpServer { private _registeredPrompts: { [name: string]: RegisteredPrompt } = {}; private _onCapabilityChange = createEventNotifier(); + /** Counter for unique resource invocation indexes, used to correlate resource invocation events */ private _resourceInvocationIndex = 0; + /** Counter for unique tool invocation indexes, used to correlate tool invocation events */ private _toolInvocationIndex = 0; + /** Counter for unique prompt invocation indexes, used to correlate prompt invocation events */ private _promptInvocationIndex = 0; constructor(serverInfo: Implementation, options?: ServerOptions) { @@ -94,9 +97,18 @@ export class McpServer { * Event notifier for capability changes. Listeners will be notified when capabilities are added, updated, removed, * enabled, disabled, invoked, completed, or when errors occur. * + * This provides a way to monitor and respond to all capability-related activities in the server, + * including both lifecycle changes and invocation events. Each capability type (resource, tool, prompt) + * maintains its own sequence of invocation indexes, which can be used to correlate invocations + * with their completions or errors. + * * @example * const subscription = server.onCapabilityChange((event) => { - * console.log(`${event.capabilityType} ${event.capabilityName} ${event.action}`); + * if (event.action === "invoked") { + * console.log(`${event.capabilityType} ${event.capabilityName} invoked with index ${event.invocationIndex}`); + * } else if (event.action === "completed" || event.action === "error") { + * console.log(`${event.capabilityType} operation completed in ${event.durationMs}ms`); + * } * }); * * // Later, to stop listening: @@ -1799,29 +1811,71 @@ const EMPTY_COMPLETION_RESULT: CompleteResult = { }, }; +/** + * Represents events emitted when capabilities (tools, resources, prompts) change state or are invoked. + * + * These events allow tracking the lifecycle and usage of all capabilities registered with an McpServer. + * Events include capability registration, updates, invocation, completion, and errors. + * + * Each capability type (tool, resource, prompt) maintains its own sequence of invocation indexes. + * The invocationIndex can be used to correlate "invoked" events with their corresponding + * "completed" or "error" events for the same capability type. + */ export type CapabilityEvent = { + /** Information about the server that generated this event */ readonly serverInfo: { readonly name: string; readonly version: string }; + /** The type of capability this event relates to */ readonly capabilityType: "resource" | "tool" | "prompt"; + /** The name (or URI for resources) of the specific capability */ readonly capabilityName: string; } & ( | { + /** + * Lifecycle events for capability registration and status changes. + * - "added": The capability was registered + * - "updated": The capability was modified + * - "removed": The capability was unregistered + * - "enabled": The capability was enabled + * - "disabled": The capability was disabled + */ readonly action: "added" | "updated" | "removed" | "enabled" | "disabled"; } | { + /** Emitted when a capability is invoked */ readonly action: "invoked"; + /** + * Monotonically increasing index for each invocation, per capability type. + * This index can be used to correlate this "invoked" event with a later + * "completed" or "error" event with the same capabilityType and invocationIndex. + */ readonly invocationIndex: number; + /** The arguments passed to the capability, if any */ readonly arguments?: unknown; } | { + /** Emitted when a capability invocation completes successfully */ readonly action: "completed"; + /** + * The invocationIndex from the corresponding "invoked" event. + * This allows correlating the completion with its invocation. + */ readonly invocationIndex: number; + /** The result returned by the capability, if any */ readonly result?: unknown; + /** The duration of the operation in milliseconds, measured from invocation to completion */ readonly durationMs?: number; } | { + /** Emitted when a capability invocation fails with an error */ readonly action: "error"; + /** + * The invocationIndex from the corresponding "invoked" event. + * This allows correlating the error with its invocation. + */ readonly invocationIndex: number; + /** The error that occurred during capability execution */ readonly error: unknown; + /** The duration from invocation to error in milliseconds */ readonly durationMs?: number; } ); diff --git a/src/shared/eventNotifier.ts b/src/shared/eventNotifier.ts index 349a9446..877f4757 100644 --- a/src/shared/eventNotifier.ts +++ b/src/shared/eventNotifier.ts @@ -35,7 +35,7 @@ export type EventNotifier = { * ``` * * @param listener A function that will be called with the notified event - * @returns A SyncCloseable that, when closed, will unregister the listener + * @returns A closeable to unregister the listener (all listeners are unregistered when the notifier is closed). */ onEvent: (listener: (event: T) => unknown) => { close: () => void }; @@ -69,7 +69,7 @@ export type EventNotifier = { /** * Sets an error handler for the notifier. This handler will be called when a listener throws an error. * - * @param handler A function that will be called with any errors thrown by listeners + * @param handler A function that will be called with any errors thrown by listeners. */ onError: (handler: (error: E) => void) => void; @@ -101,7 +101,7 @@ export type EventNotifier = { * * @template T The type of events this notifier will handle * @template E The type of error that can be handled (defaults to Error) - * @returns A new EventNotifier instance + * @returns A new EventNotifier instance. */ export const createEventNotifier = (): EventNotifier => { const listeners = new Set<(event: T) => unknown>();