Skip to content

Commit cb7b55c

Browse files
Merge branch 'main' into fweinberger/expose-auth-server-discovery
2 parents 864c44a + 288eaf8 commit cb7b55c

48 files changed

Lines changed: 3104 additions & 343 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

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']

docs/client.md

Lines changed: 355 additions & 39 deletions
Large diffs are not rendered by default.

docs/server.md

Lines changed: 218 additions & 87 deletions
Large diffs are not rendered by default.
Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
1+
/**
2+
* Type-checked examples for docs/client.md.
3+
*
4+
* Regions are synced into markdown code fences via `pnpm sync:snippets`.
5+
* Each function wraps a single region. The function name matches the region name.
6+
*
7+
* @module
8+
*/
9+
10+
import {
11+
applyMiddlewares,
12+
CallToolResultSchema,
13+
Client,
14+
ClientCredentialsProvider,
15+
createMiddleware,
16+
PrivateKeyJwtProvider,
17+
SSEClientTransport,
18+
StdioClientTransport,
19+
StreamableHTTPClientTransport
20+
} from '@modelcontextprotocol/client';
21+
22+
// ---------------------------------------------------------------------------
23+
// Connecting to a server
24+
// ---------------------------------------------------------------------------
25+
26+
/** Example: Streamable HTTP transport. */
27+
async function connect_streamableHttp() {
28+
//#region connect_streamableHttp
29+
const client = new Client({ name: 'my-client', version: '1.0.0' });
30+
31+
const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3000/mcp'));
32+
33+
await client.connect(transport);
34+
//#endregion connect_streamableHttp
35+
}
36+
37+
/** Example: stdio transport for local process-spawned servers. */
38+
async function connect_stdio() {
39+
//#region connect_stdio
40+
const client = new Client({ name: 'my-client', version: '1.0.0' });
41+
42+
const transport = new StdioClientTransport({
43+
command: 'node',
44+
args: ['server.js']
45+
});
46+
47+
await client.connect(transport);
48+
//#endregion connect_stdio
49+
}
50+
51+
/** Example: Try Streamable HTTP, fall back to legacy SSE. */
52+
async function connect_sseFallback(url: string) {
53+
//#region connect_sseFallback
54+
const baseUrl = new URL(url);
55+
56+
try {
57+
// Try modern Streamable HTTP transport first
58+
const client = new Client({ name: 'my-client', version: '1.0.0' });
59+
const transport = new StreamableHTTPClientTransport(baseUrl);
60+
await client.connect(transport);
61+
return { client, transport };
62+
} catch {
63+
// Fall back to legacy SSE transport
64+
const client = new Client({ name: 'my-client', version: '1.0.0' });
65+
const transport = new SSEClientTransport(baseUrl);
66+
await client.connect(transport);
67+
return { client, transport };
68+
}
69+
//#endregion connect_sseFallback
70+
}
71+
72+
// ---------------------------------------------------------------------------
73+
// Authentication
74+
// ---------------------------------------------------------------------------
75+
76+
/** Example: Client credentials auth for service-to-service communication. */
77+
async function auth_clientCredentials() {
78+
//#region auth_clientCredentials
79+
const authProvider = new ClientCredentialsProvider({
80+
clientId: 'my-service',
81+
clientSecret: 'my-secret'
82+
});
83+
84+
const client = new Client({ name: 'my-client', version: '1.0.0' });
85+
86+
const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3000/mcp'), { authProvider });
87+
88+
await client.connect(transport);
89+
//#endregion auth_clientCredentials
90+
}
91+
92+
/** Example: Private key JWT auth. */
93+
async function auth_privateKeyJwt(pemEncodedKey: string) {
94+
//#region auth_privateKeyJwt
95+
const authProvider = new PrivateKeyJwtProvider({
96+
clientId: 'my-service',
97+
privateKey: pemEncodedKey,
98+
algorithm: 'RS256'
99+
});
100+
101+
const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3000/mcp'), { authProvider });
102+
//#endregion auth_privateKeyJwt
103+
return transport;
104+
}
105+
106+
// ---------------------------------------------------------------------------
107+
// Using server features
108+
// ---------------------------------------------------------------------------
109+
110+
/** Example: List and call tools. */
111+
async function callTool_basic(client: Client) {
112+
//#region callTool_basic
113+
const { tools } = await client.listTools();
114+
console.log(
115+
'Available tools:',
116+
tools.map(t => t.name)
117+
);
118+
119+
const result = await client.callTool({
120+
name: 'calculate-bmi',
121+
arguments: { weightKg: 70, heightM: 1.75 }
122+
});
123+
console.log(result.content);
124+
//#endregion callTool_basic
125+
}
126+
127+
/** Example: List and read resources. */
128+
async function readResource_basic(client: Client) {
129+
//#region readResource_basic
130+
const { resources } = await client.listResources();
131+
console.log(
132+
'Available resources:',
133+
resources.map(r => r.name)
134+
);
135+
136+
const { contents } = await client.readResource({ uri: 'config://app' });
137+
for (const item of contents) {
138+
console.log(item);
139+
}
140+
//#endregion readResource_basic
141+
}
142+
143+
/** Example: List and get prompts. */
144+
async function getPrompt_basic(client: Client) {
145+
//#region getPrompt_basic
146+
const { prompts } = await client.listPrompts();
147+
console.log(
148+
'Available prompts:',
149+
prompts.map(p => p.name)
150+
);
151+
152+
const { messages } = await client.getPrompt({
153+
name: 'review-code',
154+
arguments: { code: 'console.log("hello")' }
155+
});
156+
console.log(messages);
157+
//#endregion getPrompt_basic
158+
}
159+
160+
/** Example: Request argument completions. */
161+
async function complete_basic(client: Client) {
162+
//#region complete_basic
163+
const { completion } = await client.complete({
164+
ref: {
165+
type: 'ref/prompt',
166+
name: 'review-code'
167+
},
168+
argument: {
169+
name: 'language',
170+
value: 'type'
171+
}
172+
});
173+
console.log(completion.values); // e.g. ['typescript']
174+
//#endregion complete_basic
175+
}
176+
177+
// ---------------------------------------------------------------------------
178+
// Notifications
179+
// ---------------------------------------------------------------------------
180+
181+
/** Example: Handle log messages and list-change notifications. */
182+
function notificationHandler_basic(client: Client) {
183+
//#region notificationHandler_basic
184+
// Server log messages (e.g. from ctx.mcpReq.log() in tool handlers)
185+
client.setNotificationHandler('notifications/message', notification => {
186+
const { level, data } = notification.params;
187+
console.log(`[${level}]`, data);
188+
});
189+
190+
// Server's resource list changed — re-fetch the list
191+
client.setNotificationHandler('notifications/resources/list_changed', async () => {
192+
const { resources } = await client.listResources();
193+
console.log('Resources changed:', resources.length);
194+
});
195+
//#endregion notificationHandler_basic
196+
}
197+
198+
/** Example: Automatic list-change tracking via the listChanged option. */
199+
async function listChanged_basic() {
200+
//#region listChanged_basic
201+
const client = new Client(
202+
{ name: 'my-client', version: '1.0.0' },
203+
{
204+
listChanged: {
205+
tools: {
206+
onChanged: (error, tools) => {
207+
if (error) {
208+
console.error('Failed to refresh tools:', error);
209+
return;
210+
}
211+
console.log('Tools updated:', tools);
212+
}
213+
},
214+
prompts: {
215+
onChanged: (error, prompts) => console.log('Prompts updated:', prompts)
216+
}
217+
}
218+
}
219+
);
220+
//#endregion listChanged_basic
221+
return client;
222+
}
223+
224+
// ---------------------------------------------------------------------------
225+
// Handling server-initiated requests
226+
// ---------------------------------------------------------------------------
227+
228+
/** Example: Declare client capabilities for sampling and elicitation. */
229+
function capabilities_declaration() {
230+
//#region capabilities_declaration
231+
const client = new Client(
232+
{ name: 'my-client', version: '1.0.0' },
233+
{
234+
capabilities: {
235+
sampling: {},
236+
elicitation: { form: {} }
237+
}
238+
}
239+
);
240+
//#endregion capabilities_declaration
241+
return client;
242+
}
243+
244+
/** Example: Handle a sampling request from the server. */
245+
function sampling_handler(client: Client) {
246+
//#region sampling_handler
247+
client.setRequestHandler('sampling/createMessage', async request => {
248+
const lastMessage = request.params.messages.at(-1);
249+
console.log('Sampling request:', lastMessage);
250+
251+
// In production, send messages to your LLM here
252+
return {
253+
model: 'my-model',
254+
role: 'assistant' as const,
255+
content: {
256+
type: 'text' as const,
257+
text: 'Response from the model'
258+
}
259+
};
260+
});
261+
//#endregion sampling_handler
262+
}
263+
264+
/** Example: Handle an elicitation request from the server. */
265+
function elicitation_handler(client: Client) {
266+
//#region elicitation_handler
267+
client.setRequestHandler('elicitation/create', async request => {
268+
console.log('Server asks:', request.params.message);
269+
270+
if (request.params.mode === 'form') {
271+
// Present the schema-driven form to the user
272+
console.log('Schema:', request.params.requestedSchema);
273+
return { action: 'accept', content: { confirm: true } };
274+
}
275+
276+
return { action: 'decline' };
277+
});
278+
//#endregion elicitation_handler
279+
}
280+
281+
// ---------------------------------------------------------------------------
282+
// Advanced patterns
283+
// ---------------------------------------------------------------------------
284+
285+
/** Example: Client middleware that adds a custom header. */
286+
async function middleware_basic() {
287+
//#region middleware_basic
288+
const authMiddleware = createMiddleware(async (next, input, init) => {
289+
const headers = new Headers(init?.headers);
290+
headers.set('X-Custom-Header', 'my-value');
291+
return next(input, { ...init, headers });
292+
});
293+
294+
const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3000/mcp'), {
295+
fetch: applyMiddlewares(authMiddleware)(fetch)
296+
});
297+
//#endregion middleware_basic
298+
return transport;
299+
}
300+
301+
/** Example: Track resumption tokens for SSE reconnection. */
302+
async function resumptionToken_basic(client: Client) {
303+
//#region resumptionToken_basic
304+
let lastToken: string | undefined;
305+
306+
const result = await client.request(
307+
{
308+
method: 'tools/call',
309+
params: { name: 'long-running-task', arguments: {} }
310+
},
311+
CallToolResultSchema,
312+
{
313+
resumptionToken: lastToken,
314+
onresumptiontoken: (token: string) => {
315+
lastToken = token;
316+
// Persist token to survive restarts
317+
}
318+
}
319+
);
320+
console.log(result);
321+
//#endregion resumptionToken_basic
322+
}
323+
324+
// Suppress unused-function warnings (functions exist solely for type-checking)
325+
void connect_streamableHttp;
326+
void connect_stdio;
327+
void connect_sseFallback;
328+
void auth_clientCredentials;
329+
void auth_privateKeyJwt;
330+
void callTool_basic;
331+
void readResource_basic;
332+
void getPrompt_basic;
333+
void complete_basic;
334+
void notificationHandler_basic;
335+
void listChanged_basic;
336+
void capabilities_declaration;
337+
void sampling_handler;
338+
void elicitation_handler;
339+
void middleware_basic;
340+
void resumptionToken_basic;

0 commit comments

Comments
 (0)