Skip to content

Commit f301b69

Browse files
Add sync-snippets script for type-checked examples
JSDoc `@example` code blocks were previously inlined and not type-checked, meaning they could silently drift from the actual API. This adds a `scripts/sync-snippets.ts` script (`pnpm sync:snippets`) that extracts code from co-located `.examples.ts` files — using `//#region` markers — and syncs them into JSDoc `@example` fences and markdown files. The script supports a `--check` mode (wired into `lint:all`) to catch drift in CI. Code fences use a `source="./file.examples.ts#regionName"` attribute to reference their source of truth. Includes `.examples.ts` companion files for: - `packages/core`: `SdkError`, validation providers (`AjvJsonSchemaValidator`, `CfWorkerJsonSchemaValidator`, `jsonSchemaValidator`), and module-level validator examples - `packages/client`: `Client` options, `fetchToken`, auth extensions (`createPrivateKeyJwtAuth`, `ClientCredentialsProvider`, `PrivateKeyJwtProvider`), client middleware (`applyMiddlewares`, `createMiddleware`), and experimental task streaming - `packages/server`: `hostHeaderValidation` response helper - `packages/middleware/express`: `createMcpExpressApp` and host header validation middleware Also removes the duplicate `@example` from `Protocol.requestStream()` (now covered by `ExperimentalClientTasks.requestStream()`), adds ESLint overrides for `.examples.ts` files (`no-unused-vars`, `no-console`), and updates `CLAUDE.md` with snippet conventions. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
1 parent 7778c74 commit f301b69

31 files changed

+1367
-164
lines changed

CLAUDE.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ Include what changed, why, and how to migrate. Search for related sections and g
4141
- **Testing**: Co-locate tests with source files, use descriptive test names
4242
- **Comments**: JSDoc for public APIs, inline comments for complex logic
4343

44+
### JSDoc `@example` Code Snippets
45+
46+
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`.
47+
48+
Run `pnpm sync:snippets` to sync example content into JSDoc comments and markdown files.
49+
4450
## Architecture Overview
4551

4652
### Core Layers

common/eslint-config/eslint.config.mjs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,14 @@ export default defineConfig(
8686
'unicorn/consistent-function-scoping': 'off'
8787
}
8888
},
89+
{
90+
// Example files contain intentionally unused functions (one per region)
91+
files: ['**/*.examples.ts'],
92+
rules: {
93+
'@typescript-eslint/no-unused-vars': 'off',
94+
'no-console': 'off'
95+
}
96+
},
8997
{
9098
// Ignore generated protocol types everywhere
9199
ignores: ['**/spec.types.ts']

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,15 @@
2323
],
2424
"scripts": {
2525
"fetch:spec-types": "tsx scripts/fetch-spec-types.ts",
26+
"sync:snippets": "tsx scripts/sync-snippets.ts",
2627
"examples:simple-server:w": "pnpm --filter @modelcontextprotocol/examples-server exec tsx --watch src/simpleStreamableHttp.ts --oauth",
2728
"docs": "typedoc",
2829
"docs:check": "typedoc --emit none",
2930
"typecheck:all": "pnpm -r typecheck",
3031
"build:all": "pnpm -r build",
3132
"prepack:all": "pnpm -r prepack",
32-
"lint:all": "pnpm -r lint",
33-
"lint:fix:all": "pnpm -r lint:fix",
33+
"lint:all": "pnpm sync:snippets --check && pnpm -r lint",
34+
"lint:fix:all": "pnpm sync:snippets && pnpm -r lint:fix",
3435
"check:all": "pnpm -r typecheck && pnpm -r lint",
3536
"test:all": "pnpm -r test",
3637
"test:conformance:client": "pnpm --filter @modelcontextprotocol/test-conformance run test:conformance:client",
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* Type-checked examples for {@linkcode fetchToken} in {@link ./auth.ts `auth.ts`}.
3+
*
4+
* These examples are synced into JSDoc comments via the sync-snippets script.
5+
* Each function's region markers define the code snippet that appears in the docs.
6+
*
7+
* @module
8+
*/
9+
10+
import type { AuthorizationServerMetadata } from '@modelcontextprotocol/core';
11+
12+
import type { OAuthClientProvider } from './auth.js';
13+
import { fetchToken } from './auth.js';
14+
15+
/**
16+
* Base class providing no-op implementations of required OAuthClientProvider methods.
17+
* Used as a base for concise examples that focus on specific methods.
18+
*/
19+
abstract class MyProviderBase implements OAuthClientProvider {
20+
get redirectUrl(): URL | undefined {
21+
return;
22+
}
23+
get clientMetadata() {
24+
return { redirect_uris: [] as string[] };
25+
}
26+
clientInformation(): undefined {
27+
return;
28+
}
29+
tokens(): undefined {
30+
return;
31+
}
32+
saveTokens() {
33+
return Promise.resolve();
34+
}
35+
redirectToAuthorization() {
36+
return Promise.resolve();
37+
}
38+
saveCodeVerifier() {
39+
return Promise.resolve();
40+
}
41+
codeVerifier() {
42+
return Promise.resolve('');
43+
}
44+
}
45+
46+
/**
47+
* Example: Using fetchToken with a client_credentials provider.
48+
*/
49+
async function fetchToken_clientCredentials(authServerUrl: URL, metadata: AuthorizationServerMetadata) {
50+
//#region fetchToken_clientCredentials
51+
// Provider for client_credentials:
52+
class MyProvider extends MyProviderBase implements OAuthClientProvider {
53+
prepareTokenRequest(scope?: string) {
54+
const params = new URLSearchParams({ grant_type: 'client_credentials' });
55+
if (scope) params.set('scope', scope);
56+
return params;
57+
}
58+
}
59+
60+
const tokens = await fetchToken(new MyProvider(), authServerUrl, { metadata });
61+
//#endregion fetchToken_clientCredentials
62+
return tokens;
63+
}

packages/client/src/client/auth.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1210,18 +1210,17 @@ export async function refreshAuthorization(
12101210
* @throws {Error} When provider doesn't implement prepareTokenRequest or token fetch fails
12111211
*
12121212
* @example
1213-
* ```typescript
1213+
* ```ts source="./auth.examples.ts#fetchToken_clientCredentials"
12141214
* // Provider for client_credentials:
1215-
* class MyProvider implements OAuthClientProvider {
1216-
* prepareTokenRequest(scope) {
1217-
* const params = new URLSearchParams({ grant_type: 'client_credentials' });
1218-
* if (scope) params.set('scope', scope);
1219-
* return params;
1220-
* }
1221-
* // ... other methods
1215+
* class MyProvider extends MyProviderBase implements OAuthClientProvider {
1216+
* prepareTokenRequest(scope?: string) {
1217+
* const params = new URLSearchParams({ grant_type: 'client_credentials' });
1218+
* if (scope) params.set('scope', scope);
1219+
* return params;
1220+
* }
12221221
* }
12231222
*
1224-
* const tokens = await fetchToken(provider, authServerUrl, { metadata });
1223+
* const tokens = await fetchToken(new MyProvider(), authServerUrl, { metadata });
12251224
* ```
12261225
*/
12271226
export async function fetchToken(
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* Type-checked examples for auth extensions in {@link ./authExtensions.ts `authExtensions.ts`}.
3+
*
4+
* These examples are synced into JSDoc comments via the sync-snippets script.
5+
* Each function's region markers define the code snippet that appears in the docs.
6+
*
7+
* @module
8+
*/
9+
10+
import { ClientCredentialsProvider, createPrivateKeyJwtAuth, PrivateKeyJwtProvider } from './authExtensions.js';
11+
import { StreamableHTTPClientTransport } from './streamableHttp.js';
12+
13+
/**
14+
* Example: Creating a private key JWT authentication function.
15+
*/
16+
function createPrivateKeyJwtAuth_basicUsage(pemEncodedPrivateKey: string) {
17+
//#region createPrivateKeyJwtAuth_basicUsage
18+
const addClientAuth = createPrivateKeyJwtAuth({
19+
issuer: 'my-client',
20+
subject: 'my-client',
21+
privateKey: pemEncodedPrivateKey,
22+
alg: 'RS256'
23+
});
24+
// pass addClientAuth as provider.addClientAuthentication implementation
25+
//#endregion createPrivateKeyJwtAuth_basicUsage
26+
return addClientAuth;
27+
}
28+
29+
/**
30+
* Example: Using ClientCredentialsProvider for OAuth client credentials flow.
31+
*/
32+
function ClientCredentialsProvider_basicUsage(serverUrl: URL) {
33+
//#region ClientCredentialsProvider_basicUsage
34+
const provider = new ClientCredentialsProvider({
35+
clientId: 'my-client',
36+
clientSecret: 'my-secret'
37+
});
38+
39+
const transport = new StreamableHTTPClientTransport(serverUrl, {
40+
authProvider: provider
41+
});
42+
//#endregion ClientCredentialsProvider_basicUsage
43+
return transport;
44+
}
45+
46+
/**
47+
* Example: Using PrivateKeyJwtProvider for OAuth with private key JWT.
48+
*/
49+
function PrivateKeyJwtProvider_basicUsage(pemEncodedPrivateKey: string, serverUrl: URL) {
50+
//#region PrivateKeyJwtProvider_basicUsage
51+
const provider = new PrivateKeyJwtProvider({
52+
clientId: 'my-client',
53+
privateKey: pemEncodedPrivateKey,
54+
algorithm: 'RS256'
55+
});
56+
57+
const transport = new StreamableHTTPClientTransport(serverUrl, {
58+
authProvider: provider
59+
});
60+
//#endregion PrivateKeyJwtProvider_basicUsage
61+
return transport;
62+
}

packages/client/src/client/authExtensions.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,13 @@ import type { AddClientAuthentication, OAuthClientProvider } from './auth.js';
1414
* Helper to produce a private_key_jwt client authentication function.
1515
*
1616
* @example
17-
* ```typescript
18-
* const addClientAuth = createPrivateKeyJwtAuth({ issuer, subject, privateKey, alg, audience? });
17+
* ```ts source="./authExtensions.examples.ts#createPrivateKeyJwtAuth_basicUsage"
18+
* const addClientAuth = createPrivateKeyJwtAuth({
19+
* issuer: 'my-client',
20+
* subject: 'my-client',
21+
* privateKey: pemEncodedPrivateKey,
22+
* alg: 'RS256'
23+
* });
1924
* // pass addClientAuth as provider.addClientAuthentication implementation
2025
* ```
2126
*/
@@ -116,14 +121,14 @@ export interface ClientCredentialsProviderOptions {
116121
* the client authenticates using a client_id and client_secret.
117122
*
118123
* @example
119-
* ```typescript
124+
* ```ts source="./authExtensions.examples.ts#ClientCredentialsProvider_basicUsage"
120125
* const provider = new ClientCredentialsProvider({
121-
* clientId: 'my-client',
122-
* clientSecret: 'my-secret'
126+
* clientId: 'my-client',
127+
* clientSecret: 'my-secret'
123128
* });
124129
*
125130
* const transport = new StreamableHTTPClientTransport(serverUrl, {
126-
* authProvider: provider
131+
* authProvider: provider
127132
* });
128133
* ```
129134
*/
@@ -227,15 +232,15 @@ export interface PrivateKeyJwtProviderOptions {
227232
* ({@link https://datatracker.ietf.org/doc/html/rfc7523#section-2.2 | RFC 7523 Section 2.2}).
228233
*
229234
* @example
230-
* ```typescript
235+
* ```ts source="./authExtensions.examples.ts#PrivateKeyJwtProvider_basicUsage"
231236
* const provider = new PrivateKeyJwtProvider({
232-
* clientId: 'my-client',
233-
* privateKey: pemEncodedPrivateKey,
234-
* algorithm: 'RS256'
237+
* clientId: 'my-client',
238+
* privateKey: pemEncodedPrivateKey,
239+
* algorithm: 'RS256'
235240
* });
236241
*
237242
* const transport = new StreamableHTTPClientTransport(serverUrl, {
238-
* authProvider: provider
243+
* authProvider: provider
239244
* });
240245
* ```
241246
*/
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* Type-checked examples for {@linkcode Client} in {@link ./client.ts `client.ts`}.
3+
*
4+
* These examples are synced into JSDoc comments via the sync-snippets script.
5+
* Each function's region markers define the code snippet that appears in the docs.
6+
*
7+
* @module
8+
*/
9+
10+
import { Client } from './client.js';
11+
12+
/**
13+
* Example: Using listChanged to automatically track tool and prompt updates.
14+
*/
15+
function ClientOptions_listChanged() {
16+
//#region ClientOptions_listChanged
17+
const client = new Client(
18+
{ name: 'my-client', version: '1.0.0' },
19+
{
20+
listChanged: {
21+
tools: {
22+
onChanged: (error, tools) => {
23+
if (error) {
24+
console.error('Failed to refresh tools:', error);
25+
return;
26+
}
27+
console.log('Tools updated:', tools);
28+
}
29+
},
30+
prompts: {
31+
onChanged: (error, prompts) => console.log('Prompts updated:', prompts)
32+
}
33+
}
34+
}
35+
);
36+
//#endregion ClientOptions_listChanged
37+
return client;
38+
}

packages/client/src/client/client.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -161,25 +161,25 @@ export type ClientOptions = ProtocolOptions & {
161161
* Configure handlers for list changed notifications (tools, prompts, resources).
162162
*
163163
* @example
164-
* ```typescript
164+
* ```ts source="./client.examples.ts#ClientOptions_listChanged"
165165
* const client = new Client(
166-
* { name: 'my-client', version: '1.0.0' },
167-
* {
168-
* listChanged: {
169-
* tools: {
170-
* onChanged: (error, tools) => {
171-
* if (error) {
172-
* console.error('Failed to refresh tools:', error);
173-
* return;
174-
* }
175-
* console.log('Tools updated:', tools);
166+
* { name: 'my-client', version: '1.0.0' },
167+
* {
168+
* listChanged: {
169+
* tools: {
170+
* onChanged: (error, tools) => {
171+
* if (error) {
172+
* console.error('Failed to refresh tools:', error);
173+
* return;
174+
* }
175+
* console.log('Tools updated:', tools);
176+
* }
177+
* },
178+
* prompts: {
179+
* onChanged: (error, prompts) => console.log('Prompts updated:', prompts)
180+
* }
176181
* }
177-
* },
178-
* prompts: {
179-
* onChanged: (error, prompts) => console.log('Prompts updated:', prompts)
180-
* }
181182
* }
182-
* }
183183
* );
184184
* ```
185185
*/

0 commit comments

Comments
 (0)