diff --git a/CLAUDE.md b/CLAUDE.md index 67601a319..dea46e8e4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,6 +41,12 @@ Include what changed, why, and how to migrate. Search for related sections and g - **Testing**: Co-locate tests with source files, use descriptive test names - **Comments**: JSDoc for public APIs, inline comments for complex logic +### JSDoc `@example` Code Snippets + +JSDoc `@example` tags should pull type-checked code from companion `.examples.ts` files (e.g., `client.ts` → `client.examples.ts`). Use `` ```ts source="./file.examples.ts#regionName" `` fences referencing `//#region regionName` blocks; region names follow `exportedName_variant` or `ClassName_methodName_variant` pattern (e.g., `applyMiddlewares_basicUsage`, `Client_connect_basicUsage`). For whole-file inclusion (any file type), omit the `#regionName`. + +Run `pnpm sync:snippets` to sync example content into JSDoc comments and markdown files. + ## Architecture Overview ### Core Layers diff --git a/common/eslint-config/eslint.config.mjs b/common/eslint-config/eslint.config.mjs index 6e55630cf..32aad9275 100644 --- a/common/eslint-config/eslint.config.mjs +++ b/common/eslint-config/eslint.config.mjs @@ -86,6 +86,14 @@ export default defineConfig( 'unicorn/consistent-function-scoping': 'off' } }, + { + // Example files contain intentionally unused functions (one per region) + files: ['**/*.examples.ts'], + rules: { + '@typescript-eslint/no-unused-vars': 'off', + 'no-console': 'off' + } + }, { // Ignore generated protocol types everywhere ignores: ['**/spec.types.ts'] diff --git a/package.json b/package.json index ff9a9b350..6195e90e0 100644 --- a/package.json +++ b/package.json @@ -23,14 +23,15 @@ ], "scripts": { "fetch:spec-types": "tsx scripts/fetch-spec-types.ts", + "sync:snippets": "tsx scripts/sync-snippets.ts", "examples:simple-server:w": "pnpm --filter @modelcontextprotocol/examples-server exec tsx --watch src/simpleStreamableHttp.ts --oauth", "docs": "typedoc", "docs:check": "typedoc --emit none", "typecheck:all": "pnpm -r typecheck", "build:all": "pnpm -r build", "prepack:all": "pnpm -r prepack", - "lint:all": "pnpm -r lint", - "lint:fix:all": "pnpm -r lint:fix", + "lint:all": "pnpm sync:snippets --check && pnpm -r lint", + "lint:fix:all": "pnpm sync:snippets && pnpm -r lint:fix", "check:all": "pnpm -r typecheck && pnpm -r lint", "test:all": "pnpm -r test", "test:conformance:client": "pnpm --filter @modelcontextprotocol/test-conformance run test:conformance:client", diff --git a/packages/client/src/client/auth.examples.ts b/packages/client/src/client/auth.examples.ts new file mode 100644 index 000000000..17c04e6a0 --- /dev/null +++ b/packages/client/src/client/auth.examples.ts @@ -0,0 +1,63 @@ +/** + * Type-checked examples for `auth.ts`. + * + * These examples are synced into JSDoc comments via the sync-snippets script. + * Each function's region markers define the code snippet that appears in the docs. + * + * @module + */ + +import type { AuthorizationServerMetadata } from '@modelcontextprotocol/core'; + +import type { OAuthClientProvider } from './auth.js'; +import { fetchToken } from './auth.js'; + +/** + * Base class providing no-op implementations of required OAuthClientProvider methods. + * Used as a base for concise examples that focus on specific methods. + */ +abstract class MyProviderBase implements OAuthClientProvider { + get redirectUrl(): URL | undefined { + return; + } + get clientMetadata() { + return { redirect_uris: [] as string[] }; + } + clientInformation(): undefined { + return; + } + tokens(): undefined { + return; + } + saveTokens() { + return Promise.resolve(); + } + redirectToAuthorization() { + return Promise.resolve(); + } + saveCodeVerifier() { + return Promise.resolve(); + } + codeVerifier() { + return Promise.resolve(''); + } +} + +/** + * Example: Using fetchToken with a client_credentials provider. + */ +async function fetchToken_clientCredentials(authServerUrl: URL, metadata: AuthorizationServerMetadata) { + //#region fetchToken_clientCredentials + // Provider for client_credentials: + class MyProvider extends MyProviderBase implements OAuthClientProvider { + prepareTokenRequest(scope?: string) { + const params = new URLSearchParams({ grant_type: 'client_credentials' }); + if (scope) params.set('scope', scope); + return params; + } + } + + const tokens = await fetchToken(new MyProvider(), authServerUrl, { metadata }); + //#endregion fetchToken_clientCredentials + return tokens; +} diff --git a/packages/client/src/client/auth.ts b/packages/client/src/client/auth.ts index 03b38b327..92c76695e 100644 --- a/packages/client/src/client/auth.ts +++ b/packages/client/src/client/auth.ts @@ -1210,18 +1210,17 @@ export async function refreshAuthorization( * @throws {Error} When provider doesn't implement `prepareTokenRequest` or token fetch fails * * @example - * ```typescript + * ```ts source="./auth.examples.ts#fetchToken_clientCredentials" * // Provider for client_credentials: - * class MyProvider implements OAuthClientProvider { - * prepareTokenRequest(scope) { - * const params = new URLSearchParams({ grant_type: 'client_credentials' }); - * if (scope) params.set('scope', scope); - * return params; - * } - * // ... other methods + * class MyProvider extends MyProviderBase implements OAuthClientProvider { + * prepareTokenRequest(scope?: string) { + * const params = new URLSearchParams({ grant_type: 'client_credentials' }); + * if (scope) params.set('scope', scope); + * return params; + * } * } * - * const tokens = await fetchToken(provider, authServerUrl, { metadata }); + * const tokens = await fetchToken(new MyProvider(), authServerUrl, { metadata }); * ``` */ export async function fetchToken( diff --git a/packages/client/src/client/authExtensions.examples.ts b/packages/client/src/client/authExtensions.examples.ts new file mode 100644 index 000000000..bcb26a3d4 --- /dev/null +++ b/packages/client/src/client/authExtensions.examples.ts @@ -0,0 +1,62 @@ +/** + * Type-checked examples for `authExtensions.ts`. + * + * These examples are synced into JSDoc comments via the sync-snippets script. + * Each function's region markers define the code snippet that appears in the docs. + * + * @module + */ + +import { ClientCredentialsProvider, createPrivateKeyJwtAuth, PrivateKeyJwtProvider } from './authExtensions.js'; +import { StreamableHTTPClientTransport } from './streamableHttp.js'; + +/** + * Example: Creating a private key JWT authentication function. + */ +function createPrivateKeyJwtAuth_basicUsage(pemEncodedPrivateKey: string) { + //#region createPrivateKeyJwtAuth_basicUsage + const addClientAuth = createPrivateKeyJwtAuth({ + issuer: 'my-client', + subject: 'my-client', + privateKey: pemEncodedPrivateKey, + alg: 'RS256' + }); + // pass addClientAuth as provider.addClientAuthentication implementation + //#endregion createPrivateKeyJwtAuth_basicUsage + return addClientAuth; +} + +/** + * Example: Using ClientCredentialsProvider for OAuth client credentials flow. + */ +function ClientCredentialsProvider_basicUsage(serverUrl: URL) { + //#region ClientCredentialsProvider_basicUsage + const provider = new ClientCredentialsProvider({ + clientId: 'my-client', + clientSecret: 'my-secret' + }); + + const transport = new StreamableHTTPClientTransport(serverUrl, { + authProvider: provider + }); + //#endregion ClientCredentialsProvider_basicUsage + return transport; +} + +/** + * Example: Using PrivateKeyJwtProvider for OAuth with private key JWT. + */ +function PrivateKeyJwtProvider_basicUsage(pemEncodedPrivateKey: string, serverUrl: URL) { + //#region PrivateKeyJwtProvider_basicUsage + const provider = new PrivateKeyJwtProvider({ + clientId: 'my-client', + privateKey: pemEncodedPrivateKey, + algorithm: 'RS256' + }); + + const transport = new StreamableHTTPClientTransport(serverUrl, { + authProvider: provider + }); + //#endregion PrivateKeyJwtProvider_basicUsage + return transport; +} diff --git a/packages/client/src/client/authExtensions.ts b/packages/client/src/client/authExtensions.ts index c3c69f00c..c366947be 100644 --- a/packages/client/src/client/authExtensions.ts +++ b/packages/client/src/client/authExtensions.ts @@ -14,8 +14,13 @@ import type { AddClientAuthentication, OAuthClientProvider } from './auth.js'; * Helper to produce a `private_key_jwt` client authentication function. * * @example - * ```typescript - * const addClientAuth = createPrivateKeyJwtAuth({ issuer, subject, privateKey, alg, audience? }); + * ```ts source="./authExtensions.examples.ts#createPrivateKeyJwtAuth_basicUsage" + * const addClientAuth = createPrivateKeyJwtAuth({ + * issuer: 'my-client', + * subject: 'my-client', + * privateKey: pemEncodedPrivateKey, + * alg: 'RS256' + * }); * // pass addClientAuth as provider.addClientAuthentication implementation * ``` */ @@ -116,14 +121,14 @@ export interface ClientCredentialsProviderOptions { * the client authenticates using a `client_id` and `client_secret`. * * @example - * ```typescript + * ```ts source="./authExtensions.examples.ts#ClientCredentialsProvider_basicUsage" * const provider = new ClientCredentialsProvider({ - * clientId: 'my-client', - * clientSecret: 'my-secret' + * clientId: 'my-client', + * clientSecret: 'my-secret' * }); * * const transport = new StreamableHTTPClientTransport(serverUrl, { - * authProvider: provider + * authProvider: provider * }); * ``` */ @@ -227,15 +232,15 @@ export interface PrivateKeyJwtProviderOptions { * ({@link https://datatracker.ietf.org/doc/html/rfc7523#section-2.2 | RFC 7523 Section 2.2}). * * @example - * ```typescript + * ```ts source="./authExtensions.examples.ts#PrivateKeyJwtProvider_basicUsage" * const provider = new PrivateKeyJwtProvider({ - * clientId: 'my-client', - * privateKey: pemEncodedPrivateKey, - * algorithm: 'RS256' + * clientId: 'my-client', + * privateKey: pemEncodedPrivateKey, + * algorithm: 'RS256' * }); * * const transport = new StreamableHTTPClientTransport(serverUrl, { - * authProvider: provider + * authProvider: provider * }); * ``` */ diff --git a/packages/client/src/client/client.examples.ts b/packages/client/src/client/client.examples.ts new file mode 100644 index 000000000..b18c26701 --- /dev/null +++ b/packages/client/src/client/client.examples.ts @@ -0,0 +1,38 @@ +/** + * Type-checked examples for `client.ts`. + * + * These examples are synced into JSDoc comments via the sync-snippets script. + * Each function's region markers define the code snippet that appears in the docs. + * + * @module + */ + +import { Client } from './client.js'; + +/** + * Example: Using listChanged to automatically track tool and prompt updates. + */ +function ClientOptions_listChanged() { + //#region ClientOptions_listChanged + const client = new Client( + { name: 'my-client', version: '1.0.0' }, + { + listChanged: { + tools: { + onChanged: (error, tools) => { + if (error) { + console.error('Failed to refresh tools:', error); + return; + } + console.log('Tools updated:', tools); + } + }, + prompts: { + onChanged: (error, prompts) => console.log('Prompts updated:', prompts) + } + } + } + ); + //#endregion ClientOptions_listChanged + return client; +} diff --git a/packages/client/src/client/client.ts b/packages/client/src/client/client.ts index b6e83e359..3c434f723 100644 --- a/packages/client/src/client/client.ts +++ b/packages/client/src/client/client.ts @@ -161,25 +161,25 @@ export type ClientOptions = ProtocolOptions & { * Configure handlers for list changed notifications (tools, prompts, resources). * * @example - * ```typescript + * ```ts source="./client.examples.ts#ClientOptions_listChanged" * const client = new Client( - * { name: 'my-client', version: '1.0.0' }, - * { - * listChanged: { - * tools: { - * onChanged: (error, tools) => { - * if (error) { - * console.error('Failed to refresh tools:', error); - * return; - * } - * console.log('Tools updated:', tools); + * { name: 'my-client', version: '1.0.0' }, + * { + * listChanged: { + * tools: { + * onChanged: (error, tools) => { + * if (error) { + * console.error('Failed to refresh tools:', error); + * return; + * } + * console.log('Tools updated:', tools); + * } + * }, + * prompts: { + * onChanged: (error, prompts) => console.log('Prompts updated:', prompts) + * } * } - * }, - * prompts: { - * onChanged: (error, prompts) => console.log('Prompts updated:', prompts) - * } * } - * } * ); * ``` */ diff --git a/packages/client/src/client/middleware.examples.ts b/packages/client/src/client/middleware.examples.ts new file mode 100644 index 000000000..9ccea3abc --- /dev/null +++ b/packages/client/src/client/middleware.examples.ts @@ -0,0 +1,89 @@ +/** + * Type-checked examples for `middleware.ts`. + * + * These examples are synced into JSDoc comments via the sync-snippets script. + * Each function's region markers define the code snippet that appears in the docs. + * + * @module + */ + +import type { Middleware } from './middleware.js'; +import { applyMiddlewares, createMiddleware } from './middleware.js'; + +// Stubs for hypothetical application middleware +declare function withOAuth(provider: unknown, url: string): Middleware; +declare function withLogging(opts: { statusLevel: number }): Middleware; + +// Stubs for hypothetical application cache +declare function getFromCache(key: string): Promise; +declare function saveToCache(key: string, value: string): Promise; + +/** + * Example: Creating a middleware pipeline for OAuth and logging. + */ +async function applyMiddlewares_basicUsage(oauthProvider: unknown) { + //#region applyMiddlewares_basicUsage + // Create a middleware pipeline that handles both OAuth and logging + const enhancedFetch = applyMiddlewares(withOAuth(oauthProvider, 'https://api.example.com'), withLogging({ statusLevel: 400 }))(fetch); + + // Use the enhanced fetch - it will handle auth and log errors + const response = await enhancedFetch('https://api.example.com/data'); + //#endregion applyMiddlewares_basicUsage + return response; +} + +/** + * Example: Creating various custom middlewares with createMiddleware. + */ +function createMiddleware_examples() { + //#region createMiddleware_examples + // Create custom authentication middleware + const customAuthMiddleware = createMiddleware(async (next, input, init) => { + const headers = new Headers(init?.headers); + headers.set('X-Custom-Auth', 'my-token'); + + const response = await next(input, { ...init, headers }); + + if (response.status === 401) { + console.log('Authentication failed'); + } + + return response; + }); + + // Create conditional middleware + const conditionalMiddleware = createMiddleware(async (next, input, init) => { + const url = typeof input === 'string' ? input : input.toString(); + + // Only add headers for API routes + if (url.includes('/api/')) { + const headers = new Headers(init?.headers); + headers.set('X-API-Version', 'v2'); + return next(input, { ...init, headers }); + } + + // Pass through for non-API routes + return next(input, init); + }); + + // Create caching middleware + const cacheMiddleware = createMiddleware(async (next, input, init) => { + const cacheKey = typeof input === 'string' ? input : input.toString(); + + // Check cache first + const cached = await getFromCache(cacheKey); + if (cached) { + return new Response(cached, { status: 200 }); + } + + // Make request and cache result + const response = await next(input, init); + if (response.ok) { + await saveToCache(cacheKey, await response.clone().text()); + } + + return response; + }); + //#endregion createMiddleware_examples + return { customAuthMiddleware, conditionalMiddleware, cacheMiddleware }; +} diff --git a/packages/client/src/client/middleware.ts b/packages/client/src/client/middleware.ts index 31151b68f..749414441 100644 --- a/packages/client/src/client/middleware.ts +++ b/packages/client/src/client/middleware.ts @@ -235,12 +235,9 @@ export const withLogging = (options: LoggingOptions = {}): Middleware => { * Middleware are applied in the order they appear, creating a chain of handlers. * * @example - * ```typescript + * ```ts source="./middleware.examples.ts#applyMiddlewares_basicUsage" * // Create a middleware pipeline that handles both OAuth and logging - * const enhancedFetch = applyMiddlewares( - * withOAuth(oauthProvider, 'https://api.example.com'), - * withLogging({ statusLevel: 400 }) - * )(fetch); + * const enhancedFetch = applyMiddlewares(withOAuth(oauthProvider, 'https://api.example.com'), withLogging({ statusLevel: 400 }))(fetch); * * // Use the enhanced fetch - it will handle auth and log errors * const response = await enhancedFetch('https://api.example.com/data'); @@ -264,53 +261,53 @@ export const applyMiddlewares = (...middleware: Middleware[]): Middleware => { * Provides the next handler and request details as separate parameters for easier access. * * @example - * ```typescript + * ```ts source="./middleware.examples.ts#createMiddleware_examples" * // Create custom authentication middleware * const customAuthMiddleware = createMiddleware(async (next, input, init) => { - * const headers = new Headers(init?.headers); - * headers.set('X-Custom-Auth', 'my-token'); + * const headers = new Headers(init?.headers); + * headers.set('X-Custom-Auth', 'my-token'); * - * const response = await next(input, { ...init, headers }); + * const response = await next(input, { ...init, headers }); * - * if (response.status === 401) { - * console.log('Authentication failed'); - * } + * if (response.status === 401) { + * console.log('Authentication failed'); + * } * - * return response; + * return response; * }); * * // Create conditional middleware * const conditionalMiddleware = createMiddleware(async (next, input, init) => { - * const url = typeof input === 'string' ? input : input.toString(); + * const url = typeof input === 'string' ? input : input.toString(); * - * // Only add headers for API routes - * if (url.includes('/api/')) { - * const headers = new Headers(init?.headers); - * headers.set('X-API-Version', 'v2'); - * return next(input, { ...init, headers }); - * } + * // Only add headers for API routes + * if (url.includes('/api/')) { + * const headers = new Headers(init?.headers); + * headers.set('X-API-Version', 'v2'); + * return next(input, { ...init, headers }); + * } * - * // Pass through for non-API routes - * return next(input, init); + * // Pass through for non-API routes + * return next(input, init); * }); * * // Create caching middleware * const cacheMiddleware = createMiddleware(async (next, input, init) => { - * const cacheKey = typeof input === 'string' ? input : input.toString(); + * const cacheKey = typeof input === 'string' ? input : input.toString(); * - * // Check cache first - * const cached = await getFromCache(cacheKey); - * if (cached) { - * return new Response(cached, { status: 200 }); - * } + * // Check cache first + * const cached = await getFromCache(cacheKey); + * if (cached) { + * return new Response(cached, { status: 200 }); + * } * - * // Make request and cache result - * const response = await next(input, init); - * if (response.ok) { - * await saveToCache(cacheKey, await response.clone().text()); - * } + * // Make request and cache result + * const response = await next(input, init); + * if (response.ok) { + * await saveToCache(cacheKey, await response.clone().text()); + * } * - * return response; + * return response; * }); * ``` * diff --git a/packages/client/src/experimental/tasks/client.examples.ts b/packages/client/src/experimental/tasks/client.examples.ts new file mode 100644 index 000000000..d8146edee --- /dev/null +++ b/packages/client/src/experimental/tasks/client.examples.ts @@ -0,0 +1,71 @@ +/** + * Type-checked examples for `client.ts`. + * + * These examples are synced into JSDoc comments via the sync-snippets script. + * Each function's region markers define the code snippet that appears in the docs. + * + * @module + */ + +import type { Request, RequestOptions } from '@modelcontextprotocol/core'; +import { CallToolResultSchema } from '@modelcontextprotocol/core'; + +import type { Client } from '../../client/client.js'; + +/** + * Example: Using callToolStream to execute a tool with task lifecycle events. + */ +async function ExperimentalClientTasks_callToolStream(client: Client) { + //#region ExperimentalClientTasks_callToolStream + const stream = client.experimental.tasks.callToolStream({ name: 'myTool', arguments: {} }); + for await (const message of stream) { + switch (message.type) { + case 'taskCreated': { + console.log('Tool execution started:', message.task.taskId); + break; + } + case 'taskStatus': { + console.log('Tool status:', message.task.status); + break; + } + case 'result': { + console.log('Tool result:', message.result); + break; + } + case 'error': { + console.error('Tool error:', message.error); + break; + } + } + } + //#endregion ExperimentalClientTasks_callToolStream +} + +/** + * Example: Using requestStream to consume task lifecycle events for any request type. + */ +async function ExperimentalClientTasks_requestStream(client: Client, request: Request, options: RequestOptions) { + //#region ExperimentalClientTasks_requestStream + const stream = client.experimental.tasks.requestStream(request, CallToolResultSchema, options); + for await (const message of stream) { + switch (message.type) { + case 'taskCreated': { + console.log('Task created:', message.task.taskId); + break; + } + case 'taskStatus': { + console.log('Task status:', message.task.status); + break; + } + case 'result': { + console.log('Final result:', message.result); + break; + } + case 'error': { + console.error('Error:', message.error); + break; + } + } + } + //#endregion ExperimentalClientTasks_requestStream +} diff --git a/packages/client/src/experimental/tasks/client.ts b/packages/client/src/experimental/tasks/client.ts index 4379fc324..fe797017b 100644 --- a/packages/client/src/experimental/tasks/client.ts +++ b/packages/client/src/experimental/tasks/client.ts @@ -57,23 +57,27 @@ export class ExperimentalClientTasks { * Automatically validates structured output if the tool has an `outputSchema`. * * @example - * ```typescript + * ```ts source="./client.examples.ts#ExperimentalClientTasks_callToolStream" * const stream = client.experimental.tasks.callToolStream({ name: 'myTool', arguments: {} }); * for await (const message of stream) { - * switch (message.type) { - * case 'taskCreated': - * console.log('Tool execution started:', message.task.taskId); - * break; - * case 'taskStatus': - * console.log('Tool status:', message.task.status); - * break; - * case 'result': - * console.log('Tool result:', message.result); - * break; - * case 'error': - * console.error('Tool error:', message.error); - * break; - * } + * switch (message.type) { + * case 'taskCreated': { + * console.log('Tool execution started:', message.task.taskId); + * break; + * } + * case 'taskStatus': { + * console.log('Tool status:', message.task.status); + * break; + * } + * case 'result': { + * console.log('Tool result:', message.result); + * break; + * } + * case 'error': { + * console.error('Tool error:', message.error); + * break; + * } + * } * } * ``` * @@ -240,6 +244,31 @@ export class ExperimentalClientTasks { * This method provides streaming access to request processing, allowing you to * observe intermediate task status updates for task-augmented requests. * + * @example + * ```ts source="./client.examples.ts#ExperimentalClientTasks_requestStream" + * const stream = client.experimental.tasks.requestStream(request, CallToolResultSchema, options); + * for await (const message of stream) { + * switch (message.type) { + * case 'taskCreated': { + * console.log('Task created:', message.task.taskId); + * break; + * } + * case 'taskStatus': { + * console.log('Task status:', message.task.status); + * break; + * } + * case 'result': { + * console.log('Final result:', message.result); + * break; + * } + * case 'error': { + * console.error('Error:', message.error); + * break; + * } + * } + * } + * ``` + * * @param request - The request to send * @param resultSchema - Zod schema for validating the result * @param options - Optional request options (timeout, signal, task creation params, etc.) diff --git a/packages/core/src/errors/sdkErrors.examples.ts b/packages/core/src/errors/sdkErrors.examples.ts new file mode 100644 index 000000000..729a879ac --- /dev/null +++ b/packages/core/src/errors/sdkErrors.examples.ts @@ -0,0 +1,27 @@ +/** + * Type-checked examples for `sdkErrors.ts`. + * + * These examples are synced into JSDoc comments via the sync-snippets script. + * Each function's region markers define the code snippet that appears in the docs. + * + * @module + */ + +import { SdkError, SdkErrorCode } from './sdkErrors.js'; + +/** + * Example: Throwing and catching SDK errors. + */ +function SdkError_basicUsage() { + //#region SdkError_basicUsage + try { + // Throwing an SDK error + throw new SdkError(SdkErrorCode.NotConnected, 'Transport is not connected'); + } catch (error) { + // Checking error type by code + if (error instanceof SdkError && error.code === SdkErrorCode.RequestTimeout) { + // Handle timeout + } + } + //#endregion SdkError_basicUsage +} diff --git a/packages/core/src/errors/sdkErrors.ts b/packages/core/src/errors/sdkErrors.ts index eac276b0f..f53c07ccf 100644 --- a/packages/core/src/errors/sdkErrors.ts +++ b/packages/core/src/errors/sdkErrors.ts @@ -42,13 +42,15 @@ export enum SdkErrorCode { * that are serialized and sent as error responses. * * @example - * ```typescript - * // Throwing an SDK error - * throw new SdkError(SdkErrorCode.NotConnected, 'Transport is not connected'); - * - * // Checking error type by code - * if (error instanceof SdkError && error.code === SdkErrorCode.RequestTimeout) { - * // Handle timeout + * ```ts source="./sdkErrors.examples.ts#SdkError_basicUsage" + * try { + * // Throwing an SDK error + * throw new SdkError(SdkErrorCode.NotConnected, 'Transport is not connected'); + * } catch (error) { + * // Checking error type by code + * if (error instanceof SdkError && error.code === SdkErrorCode.RequestTimeout) { + * // Handle timeout + * } * } * ``` */ diff --git a/packages/core/src/index.examples.ts b/packages/core/src/index.examples.ts new file mode 100644 index 000000000..41952d29e --- /dev/null +++ b/packages/core/src/index.examples.ts @@ -0,0 +1,31 @@ +/** + * Type-checked examples for `index.ts`. + * + * These examples are synced into JSDoc comments via the sync-snippets script. + * Each function's region markers define the code snippet that appears in the docs. + * + * @module + */ + +import { AjvJsonSchemaValidator } from './validation/ajvProvider.js'; +import { CfWorkerJsonSchemaValidator } from './validation/cfWorkerProvider.js'; + +/** + * Example: AJV validator for Node.js. + */ +function validation_ajv() { + //#region validation_ajv + const validator = new AjvJsonSchemaValidator(); + //#endregion validation_ajv + return validator; +} + +/** + * Example: CfWorker validator for edge runtimes. + */ +function validation_cfWorker() { + //#region validation_cfWorker + const validator = new CfWorkerJsonSchemaValidator(); + //#endregion validation_cfWorker + return validator; +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 7948d6517..917e5c6b6 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -31,14 +31,13 @@ export * from './validation/cfWorkerProvider.js'; * Import from: @modelcontextprotocol/sdk/validation/cfworker * Requires peer dependency: @cfworker/json-schema * - * @example - * ```typescript - * // For Node.js with AJV - * import { AjvJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/ajv'; + * @example For Node.js with AJV + * ```ts source="./index.examples.ts#validation_ajv" * const validator = new AjvJsonSchemaValidator(); + * ``` * - * // For Cloudflare Workers - * import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/cfworker'; + * @example For Cloudflare Workers + * ```ts source="./index.examples.ts#validation_cfWorker" * const validator = new CfWorkerJsonSchemaValidator(); * ``` * diff --git a/packages/core/src/shared/protocol.ts b/packages/core/src/shared/protocol.ts index f241ce730..6c693971f 100644 --- a/packages/core/src/shared/protocol.ts +++ b/packages/core/src/shared/protocol.ts @@ -1033,27 +1033,6 @@ export abstract class Protocol { * Sends a request and returns an AsyncGenerator that yields response messages. * The generator is guaranteed to end with either a `'result'` or `'error'` message. * - * @example - * ```typescript - * const stream = protocol.requestStream(request, resultSchema, options); - * for await (const message of stream) { - * switch (message.type) { - * case 'taskCreated': - * console.log('Task created:', message.task.taskId); - * break; - * case 'taskStatus': - * console.log('Task status:', message.task.status); - * break; - * case 'result': - * console.log('Final result:', message.result); - * break; - * case 'error': - * console.error('Error:', message.error); - * break; - * } - * } - * ``` - * * @experimental Use `client.experimental.tasks.requestStream()` to access this method. */ protected async *requestStream( diff --git a/packages/core/src/validation/ajvProvider.examples.ts b/packages/core/src/validation/ajvProvider.examples.ts new file mode 100644 index 000000000..eea45bf15 --- /dev/null +++ b/packages/core/src/validation/ajvProvider.examples.ts @@ -0,0 +1,48 @@ +/** + * Type-checked examples for `ajvProvider.ts`. + * + * These examples are synced into JSDoc comments via the sync-snippets script. + * Each function's region markers define the code snippet that appears in the docs. + * + * @module + */ + +import { Ajv } from 'ajv'; +import _addFormats from 'ajv-formats'; + +import { AjvJsonSchemaValidator } from './ajvProvider.js'; + +const addFormats = _addFormats as unknown as typeof _addFormats.default; + +/** + * Example: Default AJV instance. + */ +function AjvJsonSchemaValidator_default() { + //#region AjvJsonSchemaValidator_default + const validator = new AjvJsonSchemaValidator(); + //#endregion AjvJsonSchemaValidator_default + return validator; +} + +/** + * Example: Custom AJV instance. + */ +function AjvJsonSchemaValidator_customInstance() { + //#region AjvJsonSchemaValidator_customInstance + const ajv = new Ajv({ strict: true, allErrors: true }); + const validator = new AjvJsonSchemaValidator(ajv); + //#endregion AjvJsonSchemaValidator_customInstance + return validator; +} + +/** + * Example: Constructor with advanced AJV configuration including formats. + */ +function AjvJsonSchemaValidator_constructor_withFormats() { + //#region AjvJsonSchemaValidator_constructor_withFormats + const ajv = new Ajv({ validateFormats: true }); + addFormats(ajv); + const validator = new AjvJsonSchemaValidator(ajv); + //#endregion AjvJsonSchemaValidator_constructor_withFormats + return validator; +} diff --git a/packages/core/src/validation/ajvProvider.ts b/packages/core/src/validation/ajvProvider.ts index ce9ea125e..ae57e8c7d 100644 --- a/packages/core/src/validation/ajvProvider.ts +++ b/packages/core/src/validation/ajvProvider.ts @@ -22,14 +22,13 @@ function createDefaultAjvInstance(): Ajv { } /** - * @example - * ```typescript - * // Use with default AJV instance (recommended) - * import { AjvJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/ajv'; + * @example Use with default AJV instance (recommended) + * ```ts source="./ajvProvider.examples.ts#AjvJsonSchemaValidator_default" * const validator = new AjvJsonSchemaValidator(); + * ``` * - * // Use with custom AJV instance - * import { Ajv } from 'ajv'; + * @example Use with custom AJV instance + * ```ts source="./ajvProvider.examples.ts#AjvJsonSchemaValidator_customInstance" * const ajv = new Ajv({ strict: true, allErrors: true }); * const validator = new AjvJsonSchemaValidator(ajv); * ``` @@ -44,16 +43,13 @@ export class AjvJsonSchemaValidator implements jsonSchemaValidator { * * @param ajv - Optional pre-configured AJV instance. If not provided, a default instance will be created. * - * @example - * ```typescript - * // Use default configuration (recommended for most cases) - * import { AjvJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/ajv'; + * @example Use default configuration (recommended for most cases) + * ```ts source="./ajvProvider.examples.ts#AjvJsonSchemaValidator_default" * const validator = new AjvJsonSchemaValidator(); + * ``` * - * // Or provide custom AJV instance for advanced configuration - * import { Ajv } from 'ajv'; - * import addFormats from 'ajv-formats'; - * + * @example Provide custom AJV instance for advanced configuration + * ```ts source="./ajvProvider.examples.ts#AjvJsonSchemaValidator_constructor_withFormats" * const ajv = new Ajv({ validateFormats: true }); * addFormats(ajv); * const validator = new AjvJsonSchemaValidator(ajv); diff --git a/packages/core/src/validation/cfWorkerProvider.examples.ts b/packages/core/src/validation/cfWorkerProvider.examples.ts new file mode 100644 index 000000000..a347f9b7c --- /dev/null +++ b/packages/core/src/validation/cfWorkerProvider.examples.ts @@ -0,0 +1,33 @@ +/** + * Type-checked examples for `cfWorkerProvider.ts`. + * + * These examples are synced into JSDoc comments via the sync-snippets script. + * Each function's region markers define the code snippet that appears in the docs. + * + * @module + */ + +import { CfWorkerJsonSchemaValidator } from './cfWorkerProvider.js'; + +/** + * Example: Default configuration. + */ +function CfWorkerJsonSchemaValidator_default() { + //#region CfWorkerJsonSchemaValidator_default + const validator = new CfWorkerJsonSchemaValidator(); + //#endregion CfWorkerJsonSchemaValidator_default + return validator; +} + +/** + * Example: Custom configuration with all errors reported. + */ +function CfWorkerJsonSchemaValidator_customConfig() { + //#region CfWorkerJsonSchemaValidator_customConfig + const validator = new CfWorkerJsonSchemaValidator({ + draft: '2020-12', + shortcircuit: false // Report all errors + }); + //#endregion CfWorkerJsonSchemaValidator_customConfig + return validator; +} diff --git a/packages/core/src/validation/cfWorkerProvider.ts b/packages/core/src/validation/cfWorkerProvider.ts index 982d5703b..f2cce37e8 100644 --- a/packages/core/src/validation/cfWorkerProvider.ts +++ b/packages/core/src/validation/cfWorkerProvider.ts @@ -19,15 +19,16 @@ export type CfWorkerSchemaDraft = '4' | '7' | '2019-09' | '2020-12'; /** * - * @example - * ```typescript - * // Use with default configuration (2020-12, shortcircuit) + * @example Use with default configuration (2020-12, shortcircuit) + * ```ts source="./cfWorkerProvider.examples.ts#CfWorkerJsonSchemaValidator_default" * const validator = new CfWorkerJsonSchemaValidator(); + * ``` * - * // Use with custom configuration + * @example Use with custom configuration + * ```ts source="./cfWorkerProvider.examples.ts#CfWorkerJsonSchemaValidator_customConfig" * const validator = new CfWorkerJsonSchemaValidator({ - * draft: '2020-12', - * shortcircuit: false // Report all errors + * draft: '2020-12', + * shortcircuit: false // Report all errors * }); * ``` */ diff --git a/packages/core/src/validation/types.examples.ts b/packages/core/src/validation/types.examples.ts new file mode 100644 index 000000000..b6cd76069 --- /dev/null +++ b/packages/core/src/validation/types.examples.ts @@ -0,0 +1,31 @@ +/** + * Type-checked examples for `types.ts`. + * + * These examples are synced into JSDoc comments via the sync-snippets script. + * Each function's region markers define the code snippet that appears in the docs. + * + * @module + */ + +import type { JsonSchemaType, JsonSchemaValidator, jsonSchemaValidator } from './types.js'; + +// Stub for hypothetical schema validation function +declare function isValid(schema: JsonSchemaType, input: unknown): boolean; + +/** + * Example: Implementing the jsonSchemaValidator interface. + */ +function jsonSchemaValidator_implementation() { + //#region jsonSchemaValidator_implementation + class MyValidatorProvider implements jsonSchemaValidator { + getValidator(schema: JsonSchemaType): JsonSchemaValidator { + // Compile/cache validator from schema + return (input: unknown) => + isValid(schema, input) + ? { valid: true, data: input as T, errorMessage: undefined } + : { valid: false, data: undefined, errorMessage: 'Error details' }; + } + } + //#endregion jsonSchemaValidator_implementation + return MyValidatorProvider; +} diff --git a/packages/core/src/validation/types.ts b/packages/core/src/validation/types.ts index 5864a43f2..e2202b4a6 100644 --- a/packages/core/src/validation/types.ts +++ b/packages/core/src/validation/types.ts @@ -36,19 +36,15 @@ export type JsonSchemaValidator = (input: unknown) => JsonSchemaValidatorResu * - Provide clear error messages on validation failure * * @example - * ```typescript + * ```ts source="./types.examples.ts#jsonSchemaValidator_implementation" * class MyValidatorProvider implements jsonSchemaValidator { - * getValidator(schema: JsonSchemaType): JsonSchemaValidator { - * // Compile/cache validator from schema - * return (input: unknown) => { - * // Validate input against schema - * if (valid) { - * return { valid: true, data: input as T, errorMessage: undefined }; - * } else { - * return { valid: false, data: undefined, errorMessage: 'Error details' }; - * } - * }; - * } + * getValidator(schema: JsonSchemaType): JsonSchemaValidator { + * // Compile/cache validator from schema + * return (input: unknown) => + * isValid(schema, input) + * ? { valid: true, data: input as T, errorMessage: undefined } + * : { valid: false, data: undefined, errorMessage: 'Error details' }; + * } * } * ``` */ diff --git a/packages/middleware/express/src/express.examples.ts b/packages/middleware/express/src/express.examples.ts new file mode 100644 index 000000000..8d3f8e2ff --- /dev/null +++ b/packages/middleware/express/src/express.examples.ts @@ -0,0 +1,41 @@ +/** + * Type-checked examples for `express.ts`. + * + * These examples are synced into JSDoc comments via the sync-snippets script. + * Each function's region markers define the code snippet that appears in the docs. + * + * @module + */ + +import { createMcpExpressApp } from './express.js'; + +/** + * Example: Basic usage with default DNS rebinding protection. + */ +function createMcpExpressApp_default() { + //#region createMcpExpressApp_default + const app = createMcpExpressApp(); + //#endregion createMcpExpressApp_default + return app; +} + +/** + * Example: Custom host binding with and without DNS rebinding protection. + */ +function createMcpExpressApp_customHost() { + //#region createMcpExpressApp_customHost + const appOpen = createMcpExpressApp({ host: '0.0.0.0' }); // No automatic DNS rebinding protection + const appLocal = createMcpExpressApp({ host: 'localhost' }); // DNS rebinding protection enabled + //#endregion createMcpExpressApp_customHost + return { appOpen, appLocal }; +} + +/** + * Example: Custom allowed hosts for non-localhost binding. + */ +function createMcpExpressApp_allowedHosts() { + //#region createMcpExpressApp_allowedHosts + const app = createMcpExpressApp({ host: '0.0.0.0', allowedHosts: ['myapp.local', 'localhost'] }); + //#endregion createMcpExpressApp_allowedHosts + return app; +} diff --git a/packages/middleware/express/src/express.ts b/packages/middleware/express/src/express.ts index 68dbafb76..af156a229 100644 --- a/packages/middleware/express/src/express.ts +++ b/packages/middleware/express/src/express.ts @@ -34,16 +34,19 @@ export interface CreateMcpExpressAppOptions { * @param options - Configuration options * @returns A configured Express application * - * @example - * ```typescript - * // Basic usage - defaults to 127.0.0.1 with DNS rebinding protection + * @example Basic usage - defaults to 127.0.0.1 with DNS rebinding protection + * ```ts source="./express.examples.ts#createMcpExpressApp_default" * const app = createMcpExpressApp(); + * ``` * - * // Custom host - DNS rebinding protection only applied for localhost hosts - * const app = createMcpExpressApp({ host: '0.0.0.0' }); // No automatic DNS rebinding protection - * const app = createMcpExpressApp({ host: 'localhost' }); // DNS rebinding protection enabled + * @example Custom host - DNS rebinding protection only applied for localhost hosts + * ```ts source="./express.examples.ts#createMcpExpressApp_customHost" + * const appOpen = createMcpExpressApp({ host: '0.0.0.0' }); // No automatic DNS rebinding protection + * const appLocal = createMcpExpressApp({ host: 'localhost' }); // DNS rebinding protection enabled + * ``` * - * // Custom allowed hosts for non-localhost binding + * @example Custom allowed hosts for non-localhost binding + * ```ts source="./express.examples.ts#createMcpExpressApp_allowedHosts" * const app = createMcpExpressApp({ host: '0.0.0.0', allowedHosts: ['myapp.local', 'localhost'] }); * ``` */ diff --git a/packages/middleware/express/src/middleware/hostHeaderValidation.examples.ts b/packages/middleware/express/src/middleware/hostHeaderValidation.examples.ts new file mode 100644 index 000000000..2e00f48b6 --- /dev/null +++ b/packages/middleware/express/src/middleware/hostHeaderValidation.examples.ts @@ -0,0 +1,31 @@ +/** + * Type-checked examples for `hostHeaderValidation.ts`. + * + * These examples are synced into JSDoc comments via the sync-snippets script. + * Each function's region markers define the code snippet that appears in the docs. + * + * @module + */ + +import type { Express } from 'express'; + +import { hostHeaderValidation, localhostHostValidation } from './hostHeaderValidation.js'; + +/** + * Example: Using hostHeaderValidation middleware with custom allowed hosts. + */ +function hostHeaderValidation_basicUsage(app: Express) { + //#region hostHeaderValidation_basicUsage + const middleware = hostHeaderValidation(['localhost', '127.0.0.1', '[::1]']); + app.use(middleware); + //#endregion hostHeaderValidation_basicUsage +} + +/** + * Example: Using localhostHostValidation convenience middleware. + */ +function localhostHostValidation_basicUsage(app: Express) { + //#region localhostHostValidation_basicUsage + app.use(localhostHostValidation()); + //#endregion localhostHostValidation_basicUsage +} diff --git a/packages/middleware/express/src/middleware/hostHeaderValidation.ts b/packages/middleware/express/src/middleware/hostHeaderValidation.ts index 47c2b0a94..c22ee6655 100644 --- a/packages/middleware/express/src/middleware/hostHeaderValidation.ts +++ b/packages/middleware/express/src/middleware/hostHeaderValidation.ts @@ -15,7 +15,7 @@ import type { NextFunction, Request, RequestHandler, Response } from 'express'; * @returns Express middleware function * * @example - * ```typescript + * ```ts source="./hostHeaderValidation.examples.ts#hostHeaderValidation_basicUsage" * const middleware = hostHeaderValidation(['localhost', '127.0.0.1', '[::1]']); * app.use(middleware); * ``` @@ -43,7 +43,7 @@ export function hostHeaderValidation(allowedHostnames: string[]): RequestHandler * Allows only `localhost`, `127.0.0.1`, and `[::1]` (IPv6 localhost) hostnames. * * @example - * ```typescript + * ```ts source="./hostHeaderValidation.examples.ts#localhostHostValidation_basicUsage" * app.use(localhostHostValidation()); * ``` */ diff --git a/packages/middleware/node/src/streamableHttp.examples.ts b/packages/middleware/node/src/streamableHttp.examples.ts new file mode 100644 index 000000000..fb4bef841 --- /dev/null +++ b/packages/middleware/node/src/streamableHttp.examples.ts @@ -0,0 +1,56 @@ +/** + * Type-checked examples for `streamableHttp.ts`. + * + * These examples are synced into JSDoc comments via the sync-snippets script. + * Each function's region markers define the code snippet that appears in the docs. + * + * @module + */ + +import { randomUUID } from 'node:crypto'; +import type { IncomingMessage, ServerResponse } from 'node:http'; + +import { McpServer } from '@modelcontextprotocol/server'; + +import { NodeStreamableHTTPServerTransport } from './streamableHttp.js'; + +/** + * Example: Stateful Streamable HTTP transport (Node.js). + */ +async function NodeStreamableHTTPServerTransport_stateful() { + //#region NodeStreamableHTTPServerTransport_stateful + const server = new McpServer({ name: 'my-server', version: '1.0.0' }); + + const transport = new NodeStreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID() + }); + + await server.connect(transport); + //#endregion NodeStreamableHTTPServerTransport_stateful +} + +/** + * Example: Stateless Streamable HTTP transport (Node.js). + */ +async function NodeStreamableHTTPServerTransport_stateless() { + //#region NodeStreamableHTTPServerTransport_stateless + const transport = new NodeStreamableHTTPServerTransport({ + sessionIdGenerator: undefined + }); + //#endregion NodeStreamableHTTPServerTransport_stateless + return transport; +} + +// Stubs for Express-style app +declare const app: { post(path: string, handler: (req: IncomingMessage & { body?: unknown }, res: ServerResponse) => void): void }; + +/** + * Example: Using with a pre-parsed request body (e.g. Express). + */ +function NodeStreamableHTTPServerTransport_express(transport: NodeStreamableHTTPServerTransport) { + //#region NodeStreamableHTTPServerTransport_express + app.post('/mcp', (req, res) => { + transport.handleRequest(req, res, req.body); + }); + //#endregion NodeStreamableHTTPServerTransport_express +} diff --git a/packages/middleware/node/src/streamableHttp.ts b/packages/middleware/node/src/streamableHttp.ts index 59f8d2b82..68a0c224f 100644 --- a/packages/middleware/node/src/streamableHttp.ts +++ b/packages/middleware/node/src/streamableHttp.ts @@ -28,25 +28,6 @@ export type StreamableHTTPServerTransportOptions = WebStandardStreamableHTTPServ * This is a wrapper around {@linkcode WebStandardStreamableHTTPServerTransport} that provides Node.js HTTP compatibility. * It uses the `@hono/node-server` library to convert between Node.js HTTP and Web Standard APIs. * - * Usage example: - * - * ```typescript - * // Stateful mode - server sets the session ID - * const statefulTransport = new StreamableHTTPServerTransport({ - * sessionIdGenerator: () => randomUUID(), - * }); - * - * // Stateless mode - explicitly set session ID to undefined - * const statelessTransport = new StreamableHTTPServerTransport({ - * sessionIdGenerator: undefined, - * }); - * - * // Using with pre-parsed request body - * app.post('/mcp', (req, res) => { - * transport.handleRequest(req, res, req.body); - * }); - * ``` - * * In stateful mode: * - Session ID is generated and included in response headers * - Session ID is always included in initialization responses @@ -57,6 +38,31 @@ export type StreamableHTTPServerTransportOptions = WebStandardStreamableHTTPServ * In stateless mode: * - No Session ID is included in any responses * - No session validation is performed + * + * @example Stateful setup + * ```ts source="./streamableHttp.examples.ts#NodeStreamableHTTPServerTransport_stateful" + * const server = new McpServer({ name: 'my-server', version: '1.0.0' }); + * + * const transport = new NodeStreamableHTTPServerTransport({ + * sessionIdGenerator: () => randomUUID() + * }); + * + * await server.connect(transport); + * ``` + * + * @example Stateless setup + * ```ts source="./streamableHttp.examples.ts#NodeStreamableHTTPServerTransport_stateless" + * const transport = new NodeStreamableHTTPServerTransport({ + * sessionIdGenerator: undefined + * }); + * ``` + * + * @example Using with a pre-parsed request body (e.g. Express) + * ```ts source="./streamableHttp.examples.ts#NodeStreamableHTTPServerTransport_express" + * app.post('/mcp', (req, res) => { + * transport.handleRequest(req, res, req.body); + * }); + * ``` */ export class NodeStreamableHTTPServerTransport implements Transport { private _webStandardTransport: WebStandardStreamableHTTPServerTransport; diff --git a/packages/server/src/server/completable.examples.ts b/packages/server/src/server/completable.examples.ts new file mode 100644 index 000000000..b0655d243 --- /dev/null +++ b/packages/server/src/server/completable.examples.ts @@ -0,0 +1,46 @@ +/** + * Type-checked examples for `completable.ts`. + * + * These examples are synced into JSDoc comments via the sync-snippets script. + * Each function's region markers define the code snippet that appears in the docs. + * + * @module + */ + +import * as z from 'zod/v4'; + +import { completable } from './completable.js'; +import { McpServer } from './mcp.js'; + +/** + * Example: Using completable() in a prompt registration. + */ +function completable_basicUsage() { + const server = new McpServer({ name: 'my-server', version: '1.0.0' }); + + //#region completable_basicUsage + server.registerPrompt( + 'review-code', + { + title: 'Code Review', + argsSchema: z.object({ + language: completable(z.string().describe('Programming language'), value => + ['typescript', 'javascript', 'python', 'rust', 'go'].filter(lang => lang.startsWith(value)) + ) + }) + }, + ({ language }) => ({ + messages: [ + { + role: 'user' as const, + content: { + type: 'text' as const, + text: `Review this ${language} code.` + } + } + ] + }) + ); + //#endregion completable_basicUsage + return server; +} diff --git a/packages/server/src/server/completable.ts b/packages/server/src/server/completable.ts index 126f88f55..f80cdad95 100644 --- a/packages/server/src/server/completable.ts +++ b/packages/server/src/server/completable.ts @@ -21,6 +21,32 @@ export type CompletableSchema = T & { /** * Wraps a Zod type to provide autocompletion capabilities. Useful for, e.g., prompt arguments in MCP. * + * @example + * ```ts source="./completable.examples.ts#completable_basicUsage" + * server.registerPrompt( + * 'review-code', + * { + * title: 'Code Review', + * argsSchema: z.object({ + * language: completable(z.string().describe('Programming language'), value => + * ['typescript', 'javascript', 'python', 'rust', 'go'].filter(lang => lang.startsWith(value)) + * ) + * }) + * }, + * ({ language }) => ({ + * messages: [ + * { + * role: 'user' as const, + * content: { + * type: 'text' as const, + * text: `Review this ${language} code.` + * } + * } + * ] + * }) + * ); + * ``` + * * @see {@linkcode server/mcp.McpServer.registerPrompt | McpServer.registerPrompt} for using completable schemas in prompt argument definitions */ export function completable(schema: T, complete: CompleteCallback): CompletableSchema { diff --git a/packages/server/src/server/mcp.examples.ts b/packages/server/src/server/mcp.examples.ts new file mode 100644 index 000000000..740c1bf18 --- /dev/null +++ b/packages/server/src/server/mcp.examples.ts @@ -0,0 +1,145 @@ +/** + * Type-checked examples for `mcp.ts`. + * + * These examples are synced into JSDoc comments via the sync-snippets script. + * Each function's region markers define the code snippet that appears in the docs. + * + * @module + */ + +import type { CallToolResult } from '@modelcontextprotocol/core'; +import * as z from 'zod/v4'; + +import { McpServer } from './mcp.js'; +import { StdioServerTransport } from './stdio.js'; + +/** + * Example: Creating a new McpServer. + */ +function McpServer_basicUsage() { + //#region McpServer_basicUsage + const server = new McpServer({ + name: 'my-server', + version: '1.0.0' + }); + //#endregion McpServer_basicUsage + return server; +} + +/** + * Example: Registering a tool with inputSchema and outputSchema. + */ +function McpServer_registerTool_basic(server: McpServer) { + //#region McpServer_registerTool_basic + server.registerTool( + 'calculate-bmi', + { + title: 'BMI Calculator', + description: 'Calculate Body Mass Index', + inputSchema: z.object({ + weightKg: z.number(), + heightM: z.number() + }), + outputSchema: z.object({ bmi: z.number() }) + }, + async ({ weightKg, heightM }) => { + const output = { bmi: weightKg / (heightM * heightM) }; + return { + content: [{ type: 'text', text: JSON.stringify(output) }], + structuredContent: output + }; + } + ); + //#endregion McpServer_registerTool_basic +} + +/** + * Example: Registering a static resource at a fixed URI. + */ +function McpServer_registerResource_static(server: McpServer) { + //#region McpServer_registerResource_static + server.registerResource( + 'config', + 'config://app', + { + title: 'Application Config', + mimeType: 'text/plain' + }, + async uri => ({ + contents: [{ uri: uri.href, text: 'App configuration here' }] + }) + ); + //#endregion McpServer_registerResource_static +} + +/** + * Example: Registering a prompt with an argument schema. + */ +function McpServer_registerPrompt_basic(server: McpServer) { + //#region McpServer_registerPrompt_basic + server.registerPrompt( + 'review-code', + { + title: 'Code Review', + description: 'Review code for best practices', + argsSchema: z.object({ code: z.string() }) + }, + ({ code }) => ({ + messages: [ + { + role: 'user' as const, + content: { + type: 'text' as const, + text: `Please review this code:\n\n${code}` + } + } + ] + }) + ); + //#endregion McpServer_registerPrompt_basic +} + +/** + * Example: Connecting an McpServer to a stdio transport. + */ +async function McpServer_connect_stdio() { + //#region McpServer_connect_stdio + const server = new McpServer({ name: 'my-server', version: '1.0.0' }); + const transport = new StdioServerTransport(); + await server.connect(transport); + //#endregion McpServer_connect_stdio +} + +/** + * Example: Sending a log message to the client. + */ +async function McpServer_sendLoggingMessage_basic(server: McpServer) { + //#region McpServer_sendLoggingMessage_basic + await server.sendLoggingMessage({ + level: 'info', + data: 'Processing complete' + }); + //#endregion McpServer_sendLoggingMessage_basic +} + +/** + * Example: Logging from inside a tool handler via ctx.mcpReq.log(). + */ +function McpServer_registerTool_logging(server: McpServer) { + //#region McpServer_registerTool_logging + server.registerTool( + 'fetch-data', + { + description: 'Fetch data from an API', + inputSchema: z.object({ url: z.string() }) + }, + async ({ url }, ctx): Promise => { + await ctx.mcpReq.log('info', `Fetching ${url}`); + const res = await fetch(url); + await ctx.mcpReq.log('debug', `Response status: ${res.status}`); + const text = await res.text(); + return { content: [{ type: 'text', text }] }; + } + ); + //#endregion McpServer_registerTool_logging +} diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index e35871096..b0db5bb06 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -54,6 +54,14 @@ import { Server } from './server.js'; * High-level MCP server that provides a simpler API for working with resources, tools, and prompts. * For advanced usage (like sending notifications or setting custom request handlers), use the underlying * {@linkcode Server} instance available via the {@linkcode McpServer.server | server} property. + * + * @example + * ```ts source="./mcp.examples.ts#McpServer_basicUsage" + * const server = new McpServer({ + * name: 'my-server', + * version: '1.0.0' + * }); + * ``` */ export class McpServer { /** @@ -93,6 +101,13 @@ export class McpServer { * Attaches to the given transport, starts it, and starts listening for messages. * * The `server` object assumes ownership of the {@linkcode Transport}, replacing any callbacks that have already been set, and expects that it is the only user of the {@linkcode Transport} instance going forward. + * + * @example + * ```ts source="./mcp.examples.ts#McpServer_connect_stdio" + * const server = new McpServer({ name: 'my-server', version: '1.0.0' }); + * const transport = new StdioServerTransport(); + * await server.connect(transport); + * ``` */ async connect(transport: Transport): Promise { return await this.server.connect(transport); @@ -549,6 +564,21 @@ export class McpServer { /** * Registers a resource with a config object and callback. * For static resources, use a URI string. For dynamic resources, use a {@linkcode ResourceTemplate}. + * + * @example + * ```ts source="./mcp.examples.ts#McpServer_registerResource_static" + * server.registerResource( + * 'config', + * 'config://app', + * { + * title: 'Application Config', + * mimeType: 'text/plain' + * }, + * async uri => ({ + * contents: [{ uri: uri.href, text: 'App configuration here' }] + * }) + * ); + * ``` */ registerResource(name: string, uriOrTemplate: string, config: ResourceMetadata, readCallback: ReadResourceCallback): RegisteredResource; registerResource( @@ -812,6 +842,29 @@ export class McpServer { /** * Registers a tool with a config object and callback. + * + * @example + * ```ts source="./mcp.examples.ts#McpServer_registerTool_basic" + * server.registerTool( + * 'calculate-bmi', + * { + * title: 'BMI Calculator', + * description: 'Calculate Body Mass Index', + * inputSchema: z.object({ + * weightKg: z.number(), + * heightM: z.number() + * }), + * outputSchema: z.object({ bmi: z.number() }) + * }, + * async ({ weightKg, heightM }) => { + * const output = { bmi: weightKg / (heightM * heightM) }; + * return { + * content: [{ type: 'text', text: JSON.stringify(output) }], + * structuredContent: output + * }; + * } + * ); + * ``` */ registerTool( name: string, @@ -846,6 +899,29 @@ export class McpServer { /** * Registers a prompt with a config object and callback. + * + * @example + * ```ts source="./mcp.examples.ts#McpServer_registerPrompt_basic" + * server.registerPrompt( + * 'review-code', + * { + * title: 'Code Review', + * description: 'Review code for best practices', + * argsSchema: z.object({ code: z.string() }) + * }, + * ({ code }) => ({ + * messages: [ + * { + * role: 'user' as const, + * content: { + * type: 'text' as const, + * text: `Please review this code:\n\n${code}` + * } + * } + * ] + * }) + * ); + * ``` */ registerPrompt( name: string, @@ -890,6 +966,14 @@ export class McpServer { * @see {@linkcode LoggingMessageNotification} * @param params * @param sessionId Optional for stateless transports and backward compatibility. + * + * @example + * ```ts source="./mcp.examples.ts#McpServer_sendLoggingMessage_basic" + * await server.sendLoggingMessage({ + * level: 'info', + * data: 'Processing complete' + * }); + * ``` */ async sendLoggingMessage(params: LoggingMessageNotification['params'], sessionId?: string) { return this.server.sendLoggingMessage(params, sessionId); diff --git a/packages/server/src/server/middleware/hostHeaderValidation.examples.ts b/packages/server/src/server/middleware/hostHeaderValidation.examples.ts new file mode 100644 index 000000000..fd49d9751 --- /dev/null +++ b/packages/server/src/server/middleware/hostHeaderValidation.examples.ts @@ -0,0 +1,20 @@ +/** + * Type-checked examples for `hostHeaderValidation.ts`. + * + * These examples are synced into JSDoc comments via the sync-snippets script. + * Each function's region markers define the code snippet that appears in the docs. + * + * @module + */ + +import { validateHostHeader } from './hostHeaderValidation.js'; + +/** + * Example: Validating a host header against allowed hosts. + */ +function hostHeaderValidationResponse_basicUsage(req: Request) { + //#region hostHeaderValidationResponse_basicUsage + const result = validateHostHeader(req.headers.get('host'), ['localhost']); + //#endregion hostHeaderValidationResponse_basicUsage + return result; +} diff --git a/packages/server/src/server/middleware/hostHeaderValidation.ts b/packages/server/src/server/middleware/hostHeaderValidation.ts index 75466d655..a438bea72 100644 --- a/packages/server/src/server/middleware/hostHeaderValidation.ts +++ b/packages/server/src/server/middleware/hostHeaderValidation.ts @@ -44,8 +44,8 @@ export function localhostAllowedHostnames(): string[] { /** * Web-standard `Request` helper for DNS rebinding protection. * @example - * ```typescript - * const result = validateHostHeader(req.headers.get('host'), ['localhost']) + * ```ts source="./hostHeaderValidation.examples.ts#hostHeaderValidationResponse_basicUsage" + * const result = validateHostHeader(req.headers.get('host'), ['localhost']); * ``` */ export function hostHeaderValidationResponse(req: Request, allowedHostnames: string[]): Response | undefined { diff --git a/packages/server/src/server/stdio.examples.ts b/packages/server/src/server/stdio.examples.ts new file mode 100644 index 000000000..de4603eaa --- /dev/null +++ b/packages/server/src/server/stdio.examples.ts @@ -0,0 +1,22 @@ +/** + * Type-checked examples for `stdio.ts`. + * + * These examples are synced into JSDoc comments via the sync-snippets script. + * Each function's region markers define the code snippet that appears in the docs. + * + * @module + */ + +import { McpServer } from './mcp.js'; +import { StdioServerTransport } from './stdio.js'; + +/** + * Example: Basic stdio transport usage. + */ +async function StdioServerTransport_basicUsage() { + //#region StdioServerTransport_basicUsage + const server = new McpServer({ name: 'my-server', version: '1.0.0' }); + const transport = new StdioServerTransport(); + await server.connect(transport); + //#endregion StdioServerTransport_basicUsage +} diff --git a/packages/server/src/server/stdio.ts b/packages/server/src/server/stdio.ts index e022f69ab..562c6861c 100644 --- a/packages/server/src/server/stdio.ts +++ b/packages/server/src/server/stdio.ts @@ -8,6 +8,13 @@ import { process } from '@modelcontextprotocol/server/_shims'; * Server transport for stdio: this communicates with an MCP client by reading from the current process' `stdin` and writing to `stdout`. * * This transport is only available in Node.js environments. + * + * @example + * ```ts source="./stdio.examples.ts#StdioServerTransport_basicUsage" + * const server = new McpServer({ name: 'my-server', version: '1.0.0' }); + * const transport = new StdioServerTransport(); + * await server.connect(transport); + * ``` */ export class StdioServerTransport implements Transport { private _readBuffer: ReadBuffer = new ReadBuffer(); diff --git a/packages/server/src/server/streamableHttp.examples.ts b/packages/server/src/server/streamableHttp.examples.ts new file mode 100644 index 000000000..a805c1dce --- /dev/null +++ b/packages/server/src/server/streamableHttp.examples.ts @@ -0,0 +1,66 @@ +/** + * Type-checked examples for `streamableHttp.ts`. + * + * These examples are synced into JSDoc comments via the sync-snippets script. + * Each function's region markers define the code snippet that appears in the docs. + * + * @module + */ + +import { McpServer } from './mcp.js'; +import { WebStandardStreamableHTTPServerTransport } from './streamableHttp.js'; + +/** + * Example: Stateful Streamable HTTP transport (Web Standard). + */ +async function WebStandardStreamableHTTPServerTransport_stateful() { + //#region WebStandardStreamableHTTPServerTransport_stateful + const server = new McpServer({ name: 'my-server', version: '1.0.0' }); + + const transport = new WebStandardStreamableHTTPServerTransport({ + sessionIdGenerator: () => crypto.randomUUID() + }); + + await server.connect(transport); + //#endregion WebStandardStreamableHTTPServerTransport_stateful +} + +/** + * Example: Stateless Streamable HTTP transport (Web Standard). + */ +async function WebStandardStreamableHTTPServerTransport_stateless() { + //#region WebStandardStreamableHTTPServerTransport_stateless + const transport = new WebStandardStreamableHTTPServerTransport({ + sessionIdGenerator: undefined + }); + //#endregion WebStandardStreamableHTTPServerTransport_stateless + return transport; +} + +// Stubs for framework-specific examples +declare const app: { all(path: string, handler: (c: { req: { raw: Request } }) => Promise): void }; + +/** + * Example: Using with Hono.js. + */ +function WebStandardStreamableHTTPServerTransport_hono(transport: WebStandardStreamableHTTPServerTransport) { + //#region WebStandardStreamableHTTPServerTransport_hono + app.all('/mcp', async c => { + return transport.handleRequest(c.req.raw); + }); + //#endregion WebStandardStreamableHTTPServerTransport_hono +} + +/** + * Example: Using with Cloudflare Workers. + */ +function WebStandardStreamableHTTPServerTransport_workers(transport: WebStandardStreamableHTTPServerTransport) { + //#region WebStandardStreamableHTTPServerTransport_workers + const worker = { + async fetch(request: Request): Promise { + return transport.handleRequest(request); + } + }; + //#endregion WebStandardStreamableHTTPServerTransport_workers + return worker; +} diff --git a/packages/server/src/server/streamableHttp.ts b/packages/server/src/server/streamableHttp.ts index b5038d018..74e689892 100644 --- a/packages/server/src/server/streamableHttp.ts +++ b/packages/server/src/server/streamableHttp.ts @@ -176,32 +176,6 @@ export interface HandleRequestOptions { * * This transport works on any runtime that supports Web Standards: Node.js 18+, Cloudflare Workers, Deno, Bun, etc. * - * Usage example: - * - * ```typescript - * // Stateful mode - server sets the session ID - * const statefulTransport = new WebStandardStreamableHTTPServerTransport({ - * sessionIdGenerator: () => crypto.randomUUID(), - * }); - * - * // Stateless mode - explicitly set session ID to undefined - * const statelessTransport = new WebStandardStreamableHTTPServerTransport({ - * sessionIdGenerator: undefined, - * }); - * - * // Hono.js usage - * app.all('/mcp', async (c) => { - * return transport.handleRequest(c.req.raw); - * }); - * - * // Cloudflare Workers usage - * export default { - * async fetch(request: Request): Promise { - * return transport.handleRequest(request); - * } - * }; - * ``` - * * In stateful mode: * - Session ID is generated and included in response headers * - Session ID is always included in initialization responses @@ -212,6 +186,40 @@ export interface HandleRequestOptions { * In stateless mode: * - No Session ID is included in any responses * - No session validation is performed + * + * @example Stateful setup + * ```ts source="./streamableHttp.examples.ts#WebStandardStreamableHTTPServerTransport_stateful" + * const server = new McpServer({ name: 'my-server', version: '1.0.0' }); + * + * const transport = new WebStandardStreamableHTTPServerTransport({ + * sessionIdGenerator: () => crypto.randomUUID() + * }); + * + * await server.connect(transport); + * ``` + * + * @example Stateless setup + * ```ts source="./streamableHttp.examples.ts#WebStandardStreamableHTTPServerTransport_stateless" + * const transport = new WebStandardStreamableHTTPServerTransport({ + * sessionIdGenerator: undefined + * }); + * ``` + * + * @example Hono.js + * ```ts source="./streamableHttp.examples.ts#WebStandardStreamableHTTPServerTransport_hono" + * app.all('/mcp', async c => { + * return transport.handleRequest(c.req.raw); + * }); + * ``` + * + * @example Cloudflare Workers + * ```ts source="./streamableHttp.examples.ts#WebStandardStreamableHTTPServerTransport_workers" + * const worker = { + * async fetch(request: Request): Promise { + * return transport.handleRequest(request); + * } + * }; + * ``` */ export class WebStandardStreamableHTTPServerTransport implements Transport { // when sessionId is not set (undefined), it means the transport is in stateless mode diff --git a/scripts/sync-snippets.ts b/scripts/sync-snippets.ts new file mode 100644 index 000000000..fb91eee1c --- /dev/null +++ b/scripts/sync-snippets.ts @@ -0,0 +1,595 @@ +/** + * Code Snippet Sync Script + * + * This script syncs code snippets into JSDoc comments and markdown files + * containing labeled code fences. + * + * ## Supported Source Files + * + * - **Full-file inclusion**: Any file type (e.g., `.json`, `.yaml`, `.sh`, `.ts`) + * - **Region extraction**: Only `.ts` files (using `//#region` markers) + * + * ## Code Fence Format + * + * Full-file inclusion (any file type): + * + * ``````typescript + * ```json source="./config.json" + * // entire file content is synced here + * ``` + * `````` + * + * Region extraction (.ts only): + * + * ``````typescript + * ```ts source="./path.examples.ts#regionName" + * // region content is synced here + * ``` + * `````` + * + * Optionally, a display filename can be shown before the source reference: + * + * ``````typescript + * ```ts my-app.ts source="./path.examples.ts#regionName" + * // code is synced here + * ``` + * `````` + * + * ## Region Format (in .examples.ts files) + * + * ``````typescript + * //#region regionName + * // code here + * //#endregion regionName + * `````` + * + * Run: pnpm sync:snippets + */ + +import { readFileSync, writeFileSync, readdirSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const PROJECT_ROOT = join(__dirname, '..'); +const PACKAGES_DIR = join(PROJECT_ROOT, 'packages'); +const DOCS_DIR = join(PROJECT_ROOT, 'docs'); + +/** Processing mode based on file type */ +type FileMode = 'jsdoc' | 'markdown'; + +/** + * Represents a labeled code fence found in a source file. + */ +interface LabeledCodeFence { + /** Optional display filename (e.g., "my-app.ts") */ + displayName?: string; + /** Relative path to the example file (e.g., "./app.examples.ts") */ + examplePath: string; + /** Region name (e.g., "App_basicUsage"), or undefined for whole file */ + regionName?: string; + /** Language from the code fence (e.g., "ts", "json", "yaml") */ + language: string; + /** Character index of the opening fence line start */ + openingFenceStart: number; + /** Character index after the opening fence line (after newline) */ + openingFenceEnd: number; + /** Character index of the closing fence line start */ + closingFenceStart: number; + /** The JSDoc line prefix extracted from context (e.g., " * ") */ + linePrefix: string; +} + +/** + * Cache for example file regions to avoid re-reading files. + * Key: `${absoluteExamplePath}#${regionName}` (empty regionName for whole file) + * Value: extracted code string + */ +type RegionCache = Map; + +/** + * Processing result for a source file. + */ +interface FileProcessingResult { + filePath: string; + modified: boolean; + snippetsProcessed: number; + errors: string[]; +} + +// JSDoc patterns - for code fences inside JSDoc comments with " * " prefix +// Matches: ``` [displayName] source="" or source="#" +// Example: " * ```ts my-app.ts source="./app.examples.ts#App_basicUsage"" +// Example: " * ```ts source="./app.examples.ts#App_basicUsage"" +// Example: " * ```ts source="./complete-example.ts"" (whole file) +const JSDOC_LABELED_FENCE_PATTERN = + /^(\s*\*\s*)```(\w+)(?:\s+(\S+))?\s+source="([^"#]+)(?:#([^"]+))?"/; +const JSDOC_CLOSING_FENCE_PATTERN = /^(\s*\*\s*)```\s*$/; + +// Markdown patterns - for plain code fences in markdown files (no prefix) +// Matches: ``` [displayName] source="" or source="#" +// Example: ```ts source="./patterns.ts#chunkedDataServer" +// Example: ```ts source="./complete-example.ts" (whole file) +const MARKDOWN_LABELED_FENCE_PATTERN = + /^```(\w+)(?:\s+(\S+))?\s+source="([^"#]+)(?:#([^"]+))?"/; +const MARKDOWN_CLOSING_FENCE_PATTERN = /^```\s*$/; + +/** + * Find all labeled code fences in a source file. + * @param content The file content + * @param filePath The file path (for error messages) + * @param mode The processing mode (jsdoc or markdown) + * @returns Array of labeled code fence references + */ +function findLabeledCodeFences( + content: string, + filePath: string, + mode: FileMode, +): LabeledCodeFence[] { + const results: LabeledCodeFence[] = []; + const lines = content.split('\n'); + let charIndex = 0; + + // Select patterns based on mode + const openPattern = + mode === 'jsdoc' + ? JSDOC_LABELED_FENCE_PATTERN + : MARKDOWN_LABELED_FENCE_PATTERN; + const closePattern = + mode === 'jsdoc' + ? JSDOC_CLOSING_FENCE_PATTERN + : MARKDOWN_CLOSING_FENCE_PATTERN; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const openMatch = line.match(openPattern); + + if (openMatch) { + let linePrefix: string; + let language: string; + let displayName: string | undefined; + let examplePath: string; + let regionName: string; + + if (mode === 'jsdoc') { + // JSDoc: group 1=prefix, 2=lang, 3=displayName, 4=path, 5=region + [, linePrefix, language, displayName, examplePath, regionName] = + openMatch; + } else { + // Markdown: group 1=lang, 2=displayName, 3=path, 4=region (no prefix) + [, language, displayName, examplePath, regionName] = openMatch; + linePrefix = ''; + } + + const openingFenceStart = charIndex; + const openingFenceEnd = charIndex + line.length + 1; // +1 for newline + + // Find closing fence + let closingFenceStart = -1; + let searchIndex = openingFenceEnd; + + for (let j = i + 1; j < lines.length; j++) { + const closeLine = lines[j]; + if (closePattern.test(closeLine)) { + closingFenceStart = searchIndex; + break; + } + searchIndex += closeLine.length + 1; + } + + if (closingFenceStart === -1) { + throw new Error( + `${filePath}: No closing fence for ${examplePath}#${regionName}`, + ); + } + + results.push({ + displayName, + examplePath, + regionName, + language, + openingFenceStart, + openingFenceEnd, + closingFenceStart, + linePrefix, + }); + } + + charIndex += line.length + 1; + } + + return results; +} + +/** + * Dedent content by removing a base indentation prefix from each line. + * @param content The content to dedent + * @param baseIndent The indentation to remove + * @returns The dedented content + */ +function dedent(content: string, baseIndent: string): string { + if (!baseIndent) return content; + + const lines = content.split('\n'); + const dedentedLines = lines.map((line) => { + // Preserve empty lines as-is + if (line.trim() === '') return ''; + // Remove the base indentation if present + if (line.startsWith(baseIndent)) { + return line.slice(baseIndent.length); + } + // Line has less indentation than base - keep as-is + return line; + }); + + // Trim trailing empty lines + while ( + dedentedLines.length > 0 && + dedentedLines[dedentedLines.length - 1] === '' + ) { + dedentedLines.pop(); + } + + return dedentedLines.join('\n'); +} + +/** + * Extract a region from an example file. + * @param exampleContent The content of the example file + * @param regionName The region name to extract + * @param examplePath The example file path (for error messages) + * @returns The dedented region content + */ +function extractRegion( + exampleContent: string, + regionName: string, + examplePath: string, +): string { + // Region extraction only supported for .ts files (uses //#region syntax) + if (!examplePath.endsWith('.ts')) { + throw new Error( + `Region extraction (#${regionName}) is only supported for .ts files. ` + + `Use full-file inclusion (without #regionName) for: ${examplePath}`, + ); + } + + const lineEnding = exampleContent.includes('\r\n') ? '\r\n' : '\n'; + const regionStart = `//#region ${regionName}${lineEnding}`; + const regionEnd = `//#endregion ${regionName}${lineEnding}`; + + const startIndex = exampleContent.indexOf(regionStart); + if (startIndex === -1) { + throw new Error(`Region "${regionName}" not found in ${examplePath}`); + } + + const endIndex = exampleContent.indexOf(regionEnd, startIndex); + if (endIndex === -1) { + throw new Error( + `Region end marker for "${regionName}" not found in ${examplePath}`, + ); + } + + // Get content after the region start line + const afterStart = exampleContent.indexOf('\n', startIndex); + if (afterStart === -1 || afterStart >= endIndex) { + return ''; // Empty region + } + + // Extract the raw content + const rawContent = exampleContent.slice(afterStart + 1, endIndex); + + // Determine base indentation from the //#region line + let lineStart = exampleContent.lastIndexOf('\n', startIndex); + lineStart = lineStart === -1 ? 0 : lineStart + 1; + const regionLine = exampleContent.slice(lineStart, startIndex); + + // The base indent is the whitespace before //#region + const baseIndent = regionLine; + + return dedent(rawContent, baseIndent); +} + +/** + * Get or load a region from the cache. + * @param sourceFilePath The source file requesting the region + * @param examplePath The relative path to the example file + * @param regionName The region name to extract, or undefined for whole file + * @param cache The region cache + * @returns The extracted code string + */ +function getOrLoadRegion( + sourceFilePath: string, + examplePath: string, + regionName: string | undefined, + cache: RegionCache, +): string { + // Resolve the example path relative to the source file + const sourceDir = dirname(sourceFilePath); + const absoluteExamplePath = resolve(sourceDir, examplePath); + + // File content is always cached with key ending in "#" (empty region) + const fileKey = `${absoluteExamplePath}#`; + let fileContent = cache.get(fileKey); + + if (fileContent === undefined) { + try { + fileContent = readFileSync(absoluteExamplePath, 'utf-8').trim(); + } catch { + throw new Error(`Example file not found: ${absoluteExamplePath}`); + } + cache.set(fileKey, fileContent); + } + + // If no region name, return whole file + if (!regionName) { + return fileContent; + } + + // Extract region from cached file content, cache the result + const regionKey = `${absoluteExamplePath}#${regionName}`; + let regionContent = cache.get(regionKey); + + if (regionContent === undefined) { + regionContent = extractRegion(fileContent, regionName, examplePath); + cache.set(regionKey, regionContent); + } + + return regionContent; +} + +/** + * Format code lines for insertion into a JSDoc comment. + * @param code The code to format + * @param linePrefix The JSDoc line prefix (e.g., " * ") + * @returns The formatted code with JSDoc prefixes + */ +function formatCodeLines(code: string, linePrefix: string): string { + const lines = code.split('\n'); + return lines + .map((line) => + line === '' ? linePrefix.trimEnd() : `${linePrefix}${line}`, + ) + .join('\n'); +} + +interface ProcessFileOptions { + check?: boolean; +} + +/** + * Process a single source file to sync snippets. + * @param filePath The source file path + * @param cache The region cache + * @param mode The processing mode (jsdoc or markdown) + * @returns The processing result + */ +function processFile( + filePath: string, + cache: RegionCache, + mode: FileMode, + options?: ProcessFileOptions, +): FileProcessingResult { + const result: FileProcessingResult = { + filePath, + modified: false, + snippetsProcessed: 0, + errors: [], + }; + + let content: string; + try { + content = readFileSync(filePath, 'utf-8'); + } catch (err) { + result.errors.push(`Failed to read file: ${err}`); + return result; + } + + let fences: LabeledCodeFence[]; + try { + fences = findLabeledCodeFences(content, filePath, mode); + } catch (err) { + result.errors.push(err instanceof Error ? err.message : String(err)); + return result; + } + + if (fences.length === 0) { + return result; + } + + const originalContent = content; + + // Process fences in reverse order to preserve positions + for (let i = fences.length - 1; i >= 0; i--) { + const fence = fences[i]; + + try { + const code = getOrLoadRegion( + filePath, + fence.examplePath, + fence.regionName, + cache, + ); + + const formattedCode = formatCodeLines(code, fence.linePrefix); + + // Replace content between opening fence end and closing fence start + content = + content.slice(0, fence.openingFenceEnd) + + formattedCode + + '\n' + + content.slice(fence.closingFenceStart); + + result.snippetsProcessed++; + } catch (err) { + result.errors.push( + `${filePath}: ${err instanceof Error ? err.message : String(err)}`, + ); + } + } + + if ( + result.snippetsProcessed > 0 && + result.errors.length === 0 && + content !== originalContent + ) { + if (!options?.check) { + writeFileSync(filePath, content); + } + result.modified = true; + } + + return result; +} + +/** + * Find all TypeScript source files in a directory, excluding examples, tests, and generated files. + * @param dir The directory to search + * @returns Array of absolute file paths + */ +function findSourceFiles(dir: string): string[] { + const files: string[] = []; + const entries = readdirSync(dir, { withFileTypes: true, recursive: true }); + + for (const entry of entries) { + if (!entry.isFile()) continue; + + const name = entry.name; + + // Only process .ts files + if (!name.endsWith('.ts')) continue; + + // Exclude example files, test files + if (name.endsWith('.examples.ts')) continue; + if (name.endsWith('.test.ts')) continue; + + // Get the relative path from the parent directory + const parentPath = entry.parentPath; + + // Exclude generated directory + if (parentPath.includes('/generated') || parentPath.includes('\\generated')) + continue; + + const fullPath = join(parentPath, name); + files.push(fullPath); + } + + return files; +} + +/** + * Find all markdown files in a directory. + * @param dir The directory to search + * @returns Array of absolute file paths + */ +function findMarkdownFiles(dir: string): string[] { + const files: string[] = []; + const entries = readdirSync(dir, { withFileTypes: true, recursive: true }); + + for (const entry of entries) { + if (!entry.isFile()) continue; + + // Only process .md files + if (!entry.name.endsWith('.md')) continue; + + const fullPath = join(entry.parentPath, entry.name); + files.push(fullPath); + } + + return files; +} + +/** + * Find all package src directories under the packages directory. + * @param packagesDir The packages directory + * @returns Array of absolute paths to src directories + */ +function findPackageSrcDirs(packagesDir: string): string[] { + const srcDirs: string[] = []; + const entries = readdirSync(packagesDir, { + withFileTypes: true, + recursive: true, + }); + + for (const entry of entries) { + if (!entry.isDirectory()) continue; + if (entry.name !== 'src') continue; + + const fullPath = join(entry.parentPath, entry.name); + + // Only include src dirs that are direct children of a package + // (e.g., packages/core/src, packages/middleware/express/src) + // Skip nested src dirs like node_modules/*/src + if (fullPath.includes('node_modules')) continue; + + srcDirs.push(fullPath); + } + + return srcDirs; +} + +async function main() { + const checkMode = process.argv.includes('--check'); + console.log( + checkMode + ? 'Checking code snippets are in sync...\n' + : 'Syncing code snippets from example files...\n', + ); + + const cache: RegionCache = new Map(); + const results: FileProcessingResult[] = []; + + // Process TypeScript source files (JSDoc mode) across all packages + const packageSrcDirs = findPackageSrcDirs(PACKAGES_DIR); + for (const srcDir of packageSrcDirs) { + const sourceFiles = findSourceFiles(srcDir); + for (const filePath of sourceFiles) { + const result = processFile(filePath, cache, 'jsdoc', { check: checkMode }); + results.push(result); + } + } + + // Process markdown documentation files + const markdownFiles = findMarkdownFiles(DOCS_DIR); + for (const filePath of markdownFiles) { + const result = processFile(filePath, cache, 'markdown', { check: checkMode }); + results.push(result); + } + + // Report results + const modified = results.filter((r) => r.modified); + const errors = results.flatMap((r) => r.errors); + + if (modified.length > 0) { + if (checkMode) { + console.error(`${modified.length} file(s) out of sync:`); + } else { + console.log(`Modified ${modified.length} file(s):`); + } + for (const r of modified) { + console.log(` ${r.filePath} (${r.snippetsProcessed} snippet(s))`); + } + } else { + console.log('All snippets are up to date'); + } + + if (errors.length > 0) { + console.error('\nErrors:'); + for (const error of errors) { + console.error(` ${error}`); + } + process.exit(1); + } + + if (checkMode && modified.length > 0) { + console.error('\nRun "pnpm sync:snippets" to fix.'); + process.exit(1); + } + + console.log('\nSnippet sync complete!'); +} + +main().catch((error) => { + console.error('Snippet sync failed:', error); + process.exit(1); +}); diff --git a/typedoc.config.mjs b/typedoc.config.mjs index 33fe9e267..2fa7289ba 100644 --- a/typedoc.config.mjs +++ b/typedoc.config.mjs @@ -28,7 +28,8 @@ export default { entryPointStrategy: 'packages', entryPoints, packageOptions: { - blockTags: [...OptionDefaults.blockTags, '@format'] + blockTags: [...OptionDefaults.blockTags, '@format'], + exclude: ['**/*.examples.ts'] }, projectDocuments: ['docs/documents.md'], navigation: {