From 0ea1aed35d676b9ed040601b344c60900d1c6f59 Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Wed, 18 Feb 2026 17:32:36 -0600 Subject: [PATCH] Add JSDoc descriptions and `@example` blocks to `Client` public methods Add JSDoc to the 12 previously-undocumented public methods on `Client`, and type-checked `@example` blocks to the most important ones (`callTool`, `connect`, `setRequestHandler`, `listTools`, `listPrompts`, `listResources`). Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/client/src/client/client.examples.ts | 156 ++++++++++++++++ packages/client/src/client/client.ts | 167 +++++++++++++++++- 2 files changed, 321 insertions(+), 2 deletions(-) diff --git a/packages/client/src/client/client.examples.ts b/packages/client/src/client/client.examples.ts index b18c26701..b08694cfb 100644 --- a/packages/client/src/client/client.examples.ts +++ b/packages/client/src/client/client.examples.ts @@ -7,7 +7,12 @@ * @module */ +import type { Prompt, Resource, Tool } from '@modelcontextprotocol/core'; + import { Client } from './client.js'; +import { SSEClientTransport } from './sse.js'; +import { StdioClientTransport } from './stdio.js'; +import { StreamableHTTPClientTransport } from './streamableHttp.js'; /** * Example: Using listChanged to automatically track tool and prompt updates. @@ -36,3 +41,154 @@ function ClientOptions_listChanged() { //#endregion ClientOptions_listChanged return client; } + +/** + * Example: Connect to a local server process over stdio. + */ +async function Client_connect_stdio() { + //#region Client_connect_stdio + const client = new Client({ name: 'my-client', version: '1.0.0' }); + const transport = new StdioClientTransport({ command: 'my-mcp-server' }); + await client.connect(transport); + //#endregion Client_connect_stdio + return client; +} + +/** + * Example: Connect with Streamable HTTP, falling back to legacy SSE. + */ +async function Client_connect_sseFallback(url: string) { + //#region Client_connect_sseFallback + const baseUrl = new URL(url); + + try { + // Try modern Streamable HTTP transport first + const client = new Client({ name: 'my-client', version: '1.0.0' }); + const transport = new StreamableHTTPClientTransport(baseUrl); + await client.connect(transport); + return { client, transport }; + } catch { + // Fall back to legacy SSE transport + const client = new Client({ name: 'my-client', version: '1.0.0' }); + const transport = new SSEClientTransport(baseUrl); + await client.connect(transport); + return { client, transport }; + } + //#endregion Client_connect_sseFallback +} + +/** + * Example: Call a tool on the connected server. + */ +async function Client_callTool_basic(client: Client) { + //#region Client_callTool_basic + const result = await client.callTool({ + name: 'calculate-bmi', + arguments: { weightKg: 70, heightM: 1.75 } + }); + + // Tool-level errors are returned in the result, not thrown + if (result.isError) { + console.error('Tool error:', result.content); + return; + } + + console.log(result.content); + //#endregion Client_callTool_basic +} + +/** + * Example: Access machine-readable structured output from a tool call. + */ +async function Client_callTool_structuredOutput(client: Client) { + //#region Client_callTool_structuredOutput + const result = await client.callTool({ + name: 'calculate-bmi', + arguments: { weightKg: 70, heightM: 1.75 } + }); + + // Machine-readable output for the client application + if (result.structuredContent) { + console.log(result.structuredContent); // e.g. { bmi: 22.86 } + } + //#endregion Client_callTool_structuredOutput +} + +/** + * Example: Handle a sampling request from the server. + */ +function Client_setRequestHandler_sampling(client: Client) { + //#region Client_setRequestHandler_sampling + client.setRequestHandler('sampling/createMessage', async request => { + const lastMessage = request.params.messages.at(-1); + console.log('Sampling request:', lastMessage); + + // In production, send messages to your LLM here + return { + model: 'my-model', + role: 'assistant' as const, + content: { + type: 'text' as const, + text: 'Response from the model' + } + }; + }); + //#endregion Client_setRequestHandler_sampling +} + +/** + * Example: List tools with cursor-based pagination. + */ +async function Client_listTools_pagination(client: Client) { + //#region Client_listTools_pagination + const allTools: Tool[] = []; + let cursor: string | undefined; + do { + const { tools, nextCursor } = await client.listTools({ cursor }); + allTools.push(...tools); + cursor = nextCursor; + } while (cursor); + console.log( + 'Available tools:', + allTools.map(t => t.name) + ); + //#endregion Client_listTools_pagination +} + +/** + * Example: List prompts with cursor-based pagination. + */ +async function Client_listPrompts_pagination(client: Client) { + //#region Client_listPrompts_pagination + const allPrompts: Prompt[] = []; + let cursor: string | undefined; + do { + const { prompts, nextCursor } = await client.listPrompts({ cursor }); + allPrompts.push(...prompts); + cursor = nextCursor; + } while (cursor); + console.log( + 'Available prompts:', + allPrompts.map(p => p.name) + ); + //#endregion Client_listPrompts_pagination +} + +/** + * Example: List resources with cursor-based pagination. + */ +async function Client_listResources_pagination(client: Client) { + //#region Client_listResources_pagination + const allResources: Resource[] = []; + let cursor: string | undefined; + do { + const { resources, nextCursor } = await client.listResources({ cursor }); + allResources.push(...resources); + cursor = nextCursor; + } while (cursor); + console.log( + 'Available resources:', + allResources.map(r => r.name) + ); + //#endregion Client_listResources_pagination +} diff --git a/packages/client/src/client/client.ts b/packages/client/src/client/client.ts index 3c434f723..c2d999624 100644 --- a/packages/client/src/client/client.ts +++ b/packages/client/src/client/client.ts @@ -287,7 +287,30 @@ export class Client extends Protocol { } /** - * Override request handler registration to enforce client-side validation for elicitation. + * Registers a handler for server-initiated requests (sampling, elicitation, roots). + * The client must declare the corresponding capability for the handler to be accepted. + * Replaces any previously registered handler for the same method. + * + * For `sampling/createMessage` and `elicitation/create`, the handler is automatically + * wrapped with schema validation for both the incoming request and the returned result. + * + * @example Handling a sampling request + * ```ts source="./client.examples.ts#Client_setRequestHandler_sampling" + * client.setRequestHandler('sampling/createMessage', async request => { + * const lastMessage = request.params.messages.at(-1); + * console.log('Sampling request:', lastMessage); + * + * // In production, send messages to your LLM here + * return { + * model: 'my-model', + * role: 'assistant' as const, + * content: { + * type: 'text' as const, + * text: 'Response from the model' + * } + * }; + * }); + * ``` */ public override setRequestHandler( method: M, @@ -416,6 +439,35 @@ export class Client extends Protocol { } } + /** + * Connects to a server via the given transport and performs the MCP initialization handshake. + * + * @example Basic usage (stdio) + * ```ts source="./client.examples.ts#Client_connect_stdio" + * const client = new Client({ name: 'my-client', version: '1.0.0' }); + * const transport = new StdioClientTransport({ command: 'my-mcp-server' }); + * await client.connect(transport); + * ``` + * + * @example Streamable HTTP with SSE fallback + * ```ts source="./client.examples.ts#Client_connect_sseFallback" + * const baseUrl = new URL(url); + * + * try { + * // Try modern Streamable HTTP transport first + * const client = new Client({ name: 'my-client', version: '1.0.0' }); + * const transport = new StreamableHTTPClientTransport(baseUrl); + * await client.connect(transport); + * return { client, transport }; + * } catch { + * // Fall back to legacy SSE transport + * const client = new Client({ name: 'my-client', version: '1.0.0' }); + * const transport = new SSEClientTransport(baseUrl); + * await client.connect(transport); + * return { client, transport }; + * } + * ``` + */ override async connect(transport: Transport, options?: RequestOptions): Promise { await super.connect(transport); // When transport sessionId is already set this means we are trying to reconnect. @@ -655,22 +707,47 @@ export class Client extends Protocol { assertClientRequestTaskCapability(this._capabilities.tasks?.requests, method, 'Client'); } + /** Sends a ping to the server to check connectivity. */ async ping(options?: RequestOptions) { return this.request({ method: 'ping' }, EmptyResultSchema, options); } + /** Requests argument autocompletion suggestions from the server for a prompt or resource. */ async complete(params: CompleteRequest['params'], options?: RequestOptions) { return this.request({ method: 'completion/complete', params }, CompleteResultSchema, options); } + /** Sets the minimum severity level for log messages sent by the server. */ async setLoggingLevel(level: LoggingLevel, options?: RequestOptions) { return this.request({ method: 'logging/setLevel', params: { level } }, EmptyResultSchema, options); } + /** Retrieves a prompt by name from the server, passing the given arguments for template substitution. */ async getPrompt(params: GetPromptRequest['params'], options?: RequestOptions) { return this.request({ method: 'prompts/get', params }, GetPromptResultSchema, options); } + /** + * Lists available prompts. Results may be paginated — loop on `nextCursor` to collect all pages. + * + * Returns an empty list if the server does not advertise prompts capability + * (or throws if {@linkcode ClientOptions.enforceStrictCapabilities} is enabled). + * + * @example + * ```ts source="./client.examples.ts#Client_listPrompts_pagination" + * const allPrompts: Prompt[] = []; + * let cursor: string | undefined; + * do { + * const { prompts, nextCursor } = await client.listPrompts({ cursor }); + * allPrompts.push(...prompts); + * cursor = nextCursor; + * } while (cursor); + * console.log( + * 'Available prompts:', + * allPrompts.map(p => p.name) + * ); + * ``` + */ async listPrompts(params?: ListPromptsRequest['params'], options?: RequestOptions) { if (!this._serverCapabilities?.prompts && !this._enforceStrictCapabilities) { // Respect capability negotiation: server does not support prompts @@ -680,6 +757,27 @@ export class Client extends Protocol { return this.request({ method: 'prompts/list', params }, ListPromptsResultSchema, options); } + /** + * Lists available resources. Results may be paginated — loop on `nextCursor` to collect all pages. + * + * Returns an empty list if the server does not advertise resources capability + * (or throws if {@linkcode ClientOptions.enforceStrictCapabilities} is enabled). + * + * @example + * ```ts source="./client.examples.ts#Client_listResources_pagination" + * const allResources: Resource[] = []; + * let cursor: string | undefined; + * do { + * const { resources, nextCursor } = await client.listResources({ cursor }); + * allResources.push(...resources); + * cursor = nextCursor; + * } while (cursor); + * console.log( + * 'Available resources:', + * allResources.map(r => r.name) + * ); + * ``` + */ async listResources(params?: ListResourcesRequest['params'], options?: RequestOptions) { if (!this._serverCapabilities?.resources && !this._enforceStrictCapabilities) { // Respect capability negotiation: server does not support resources @@ -689,6 +787,12 @@ export class Client extends Protocol { return this.request({ method: 'resources/list', params }, ListResourcesResultSchema, options); } + /** + * Lists available resource URI templates for dynamic resources. Results may be paginated — see {@linkcode listResources | listResources()} for the cursor pattern. + * + * Returns an empty list if the server does not advertise resources capability + * (or throws if {@linkcode ClientOptions.enforceStrictCapabilities} is enabled). + */ async listResourceTemplates(params?: ListResourceTemplatesRequest['params'], options?: RequestOptions) { if (!this._serverCapabilities?.resources && !this._enforceStrictCapabilities) { // Respect capability negotiation: server does not support resources @@ -700,22 +804,59 @@ export class Client extends Protocol { return this.request({ method: 'resources/templates/list', params }, ListResourceTemplatesResultSchema, options); } + /** Reads the contents of a resource by URI. */ async readResource(params: ReadResourceRequest['params'], options?: RequestOptions) { return this.request({ method: 'resources/read', params }, ReadResourceResultSchema, options); } + /** Subscribes to change notifications for a resource. The server must support resource subscriptions. */ async subscribeResource(params: SubscribeRequest['params'], options?: RequestOptions) { return this.request({ method: 'resources/subscribe', params }, EmptyResultSchema, options); } + /** Unsubscribes from change notifications for a resource. */ async unsubscribeResource(params: UnsubscribeRequest['params'], options?: RequestOptions) { return this.request({ method: 'resources/unsubscribe', params }, EmptyResultSchema, options); } /** - * Calls a tool and waits for the result. Automatically validates structured output if the tool has an `outputSchema`. + * Calls a tool on the connected server and returns the result. Automatically validates structured output + * if the tool has an `outputSchema`. + * + * Tool results have two error surfaces: `result.isError` for tool-level failures (the tool ran but reported + * a problem), and thrown {@linkcode ProtocolError} for protocol-level failures or {@linkcode SdkError} for + * SDK-level issues (timeouts, missing capabilities). * * For task-based execution with streaming behavior, use {@linkcode ExperimentalClientTasks.callToolStream | client.experimental.tasks.callToolStream()} instead. + * + * @example Basic usage + * ```ts source="./client.examples.ts#Client_callTool_basic" + * const result = await client.callTool({ + * name: 'calculate-bmi', + * arguments: { weightKg: 70, heightM: 1.75 } + * }); + * + * // Tool-level errors are returned in the result, not thrown + * if (result.isError) { + * console.error('Tool error:', result.content); + * return; + * } + * + * console.log(result.content); + * ``` + * + * @example Structured output + * ```ts source="./client.examples.ts#Client_callTool_structuredOutput" + * const result = await client.callTool({ + * name: 'calculate-bmi', + * arguments: { weightKg: 70, heightM: 1.75 } + * }); + * + * // Machine-readable output for the client application + * if (result.structuredContent) { + * console.log(result.structuredContent); // e.g. { bmi: 22.86 } + * } + * ``` */ async callTool( params: CallToolRequest['params'], @@ -820,6 +961,27 @@ export class Client extends Protocol { return this._cachedToolOutputValidators.get(toolName); } + /** + * Lists available tools. Results may be paginated — loop on `nextCursor` to collect all pages. + * + * Returns an empty list if the server does not advertise tools capability + * (or throws if {@linkcode ClientOptions.enforceStrictCapabilities} is enabled). + * + * @example + * ```ts source="./client.examples.ts#Client_listTools_pagination" + * const allTools: Tool[] = []; + * let cursor: string | undefined; + * do { + * const { tools, nextCursor } = await client.listTools({ cursor }); + * allTools.push(...tools); + * cursor = nextCursor; + * } while (cursor); + * console.log( + * 'Available tools:', + * allTools.map(t => t.name) + * ); + * ``` + */ async listTools(params?: ListToolsRequest['params'], options?: RequestOptions) { if (!this._serverCapabilities?.tools && !this._enforceStrictCapabilities) { // Respect capability negotiation: server does not support tools @@ -894,6 +1056,7 @@ export class Client extends Protocol { this.setNotificationHandler(notificationMethod, handler); } + /** Notifies the server that the client's root list has changed. Requires the `roots.listChanged` capability. */ async sendRootsListChanged() { return this.notification({ method: 'notifications/roots/list_changed' }); }