Skip to content

Commit be71bee

Browse files
docs(examples): close review-1 M2-M9 + bot nits; in-repo OAuth-protected server, sessionful repl/, fix docs:check anchor
- oauth/server.ts: in-repo authorization-code AS (setupAuthServer) + RS (createMcpHandler behind requireBearerAuth(demoTokenVerifier)) so simpleOAuthClient.ts has an in-repo target (M2). README run-manually section. - legacy-routing/: CORS exposedHeaders recipe (M8), explicit GET/DELETE routes for the sessionful arm (M9), README section on direct WebStandardStreamableHTTPServerTransport construction (M6). - repl/server.ts: re-hosted on sessionful NodeStreamableHTTPServerTransport with an InMemoryEventStore so the REPL client's reconnect/resumability commands work (M7). - elicitation/: plan_trip tool chains two form elicitations inside one tool call on both eras (M4). - oauth-client-credentials/README: PrivateKeyJwtProvider section pointing at the client guide snippet (M3). - examples/README: fix #backwards-compatibility -> #sse-fallback-for-legacy-servers (docs:check broken anchor); update oauth/ row. - CONTRIBUTING + root package.json: drop stale examples-server / oauth/ simpleStreamableHttpServer.ts references. - stickynotes/client.ts: header says 2025-era (matches era pin).
1 parent 393bc07 commit be71bee

18 files changed

Lines changed: 341 additions & 54 deletions

File tree

CONTRIBUTING.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,16 +112,19 @@ Then:
112112

113113
### Running Examples
114114

115-
See [`examples/server/README.md`](examples/server/README.md) and [`examples/client/README.md`](examples/client/README.md) for a full list of runnable examples.
115+
See [`examples/README.md`](examples/README.md) for the full list of runnable examples — one self-verifying client/server pair per directory.
116116

117117
Quick start:
118118

119119
```bash
120-
# Run a server example
121-
pnpm --filter @modelcontextprotocol/examples-server exec tsx src/simpleStreamableHttp.ts
120+
# Run any story's server
121+
pnpm --filter @mcp-examples/tools server -- --http --port 3000
122122

123-
# Run a client example (in another terminal)
124-
pnpm --filter @modelcontextprotocol/examples-client exec tsx src/simpleStreamableHttp.ts
123+
# Run its client (in another terminal)
124+
pnpm --filter @mcp-examples/tools client -- --http http://127.0.0.1:3000/
125+
126+
# Run every story over every transport × era leg
127+
pnpm run:examples
125128
```
126129

127130
## Releasing v1.x Patches

examples/README.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,14 @@ Some stories mount at a different path (e.g. `/`); check the story's `package.js
5656

5757
## Excluded
5858

59-
| Directory | What it is | Why not in CI |
60-
| ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
61-
| [`oauth/`](./oauth/README.md) | Interactive authorization-code OAuth flow (`simpleOAuthClient.ts`, `dualModeAuth.ts`, `simpleTokenProvider.ts`) | Opens a real browser and runs a callback server on `:8090`. The headless machine-to-machine grant is covered by [`oauth-client-credentials/`](./oauth-client-credentials/README.md). |
62-
| [`repl/`](./repl/README.md) | Fully-featured HTTP playground server + readline client | Interactive — `client.ts` reads from stdin. Run manually in two terminals. |
63-
| [`sse-polling/`](./sse-polling/README.md), [`standalone-get/`](./standalone-get/README.md) | Legacy sessionful-2025 SSE stories (SEP-1699 reconnect/replay; standalone GET stream) | Kept for reference; long-running reconnect/timer flows that need a longer per-leg readiness wait than the harness default. Self-verifying — flip the `excluded` flag once the harness has bounded-wait knobs. |
64-
| [`guides/`](./guides/README.md) | Snippet collections synced into `docs/server.md` and `docs/client.md` | Typecheck-only; not a runnable pair. |
65-
| `server-quickstart/`, `client-quickstart/` | Website-tutorial sources | External network / API key; typecheck-only. |
66-
| `shared/` | Demo OAuth provider helper library | Not a story — imported by the OAuth examples. |
59+
| Directory | What it is | Why not in CI |
60+
| ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
61+
| [`oauth/`](./oauth/README.md) | Interactive authorization-code OAuth flow: in-repo protected `server.ts` (demo AS + RS) + browser `simpleOAuthClient.ts` | Opens a real browser and runs a callback server on `:8090`. The headless machine-to-machine grant is covered by [`oauth-client-credentials/`](./oauth-client-credentials/README.md). |
62+
| [`repl/`](./repl/README.md) | Fully-featured HTTP playground server + readline client | Interactive — `client.ts` reads from stdin. Run manually in two terminals. |
63+
| [`sse-polling/`](./sse-polling/README.md), [`standalone-get/`](./standalone-get/README.md) | Legacy sessionful-2025 SSE stories (SEP-1699 reconnect/replay; standalone GET stream) | Kept for reference; long-running reconnect/timer flows that need a longer per-leg readiness wait than the harness default. Self-verifying — flip the `excluded` flag once the harness has bounded-wait knobs. |
64+
| [`guides/`](./guides/README.md) | Snippet collections synced into `docs/server.md` and `docs/client.md` | Typecheck-only; not a runnable pair. |
65+
| `server-quickstart/`, `client-quickstart/` | Website-tutorial sources | External network / API key; typecheck-only. |
66+
| `shared/` | Demo OAuth provider helper library | Not a story — imported by the OAuth examples. |
6767

6868
## Multi-node deployment patterns
6969

@@ -165,5 +165,5 @@ For scenarios where local in-memory state must be maintained on specific nodes,
165165

166166
## Backwards compatibility (Streamable HTTP ↔ legacy SSE)
167167

168-
A client that needs to fall back from Streamable HTTP to the legacy HTTP+SSE transport (for servers that only implement the older transport) follows the [`connect_sseFallback`](../docs/client.md#backwards-compatibility) recipe in the client guide — try
168+
A client that needs to fall back from Streamable HTTP to the legacy HTTP+SSE transport (for servers that only implement the older transport) follows the [`connect_sseFallback`](../docs/client.md#sse-fallback-for-legacy-servers) recipe in the client guide — try
169169
`StreamableHTTPClientTransport` first, fall back to `SSEClientTransport` on a 4xx. There is no runnable pair for this in `examples/` (the legacy SSE server transport is deprecated); the snippet in `guides/clientGuide.examples.ts` is the complete pattern.

examples/elicitation/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ Server requests user input. One factory, both protocol eras: elicitation works o
88
| **url** (`link_account`) | `await ctx.mcpReq.elicitInput({ mode: 'url', url, elicitationId })` + `createElicitationCompletionNotifier(elicitationId)` for the out-of-band `notifications/elicitation/complete` | `return inputRequired({ inputRequests: { auth: inputRequired.elicitUrl(...) } })` — no `elicitationId` / complete notification on this era |
99
| **url, throw** (`confirm_payment`) | `throw new UrlElicitationRequiredError([...])` — the wire `-32042`; the client catches the typed error and reads `.elicitations` | n/a — a throw on this era fails loudly with a steer to `inputRequired.elicitUrl(...)` |
1010

11-
The form schema includes an `enumNames` field (display labels for the `plan` enum). For the secure `requestState` round-trip pattern see [`../mrtr/`](../mrtr/README.md).
11+
`plan_trip` chains **two** form elicitations inside one tool call (destination → dates for that destination): two sequential `ctx.mcpReq.elicitInput` pushes on 2025, two `inputRequired` rounds with `requestState` carry-over on 2026. The `register_user` form schema includes an
12+
`enumNames` field (display labels for the `plan` enum). For the secure `requestState` round-trip pattern see [`../mrtr/`](../mrtr/README.md).
1213

1314
**stdio-only** in the harness: push server→client requests need either a stdio connection or a sessionful HTTP transport (see `../legacy-routing/`); the harness's `--http` arm is the stateless per-request `createMcpHandler`.
1415

examples/elicitation/client.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ runClient('elicitation', async () => {
4444
}
4545
return { action: 'accept' };
4646
}
47+
if (params.requestedSchema?.properties?.['destination']) {
48+
return { action: 'accept', content: { destination: 'Tokyo' } };
49+
}
50+
if (params.requestedSchema?.properties?.['departure']) {
51+
return { action: 'accept', content: { departure: '2026-09-01', nights: 7 } };
52+
}
4753
check.ok(params.requestedSchema?.properties?.['username'], 'elicitation should carry the requestedSchema');
4854
if (formAction === 'decline') return { action: 'decline' };
4955
return { action: 'accept', content: { username: 'alice', email: 'alice@example.com', plan: 'pro' } };
@@ -60,6 +66,10 @@ runClient('elicitation', async () => {
6066
const declined = await client.callTool({ name: 'register_user' });
6167
check.match(declined.content?.[0]?.type === 'text' ? declined.content[0].text : '', /registration decline/);
6268

69+
// ---- Multi-step form (two chained elicitations inside one tool call) -----
70+
const trip = await client.callTool({ name: 'plan_trip' });
71+
check.match(trip.content?.[0]?.type === 'text' ? trip.content[0].text : '', /trip planned: Tokyo on 2026-09-01 for 7 nights/);
72+
6373
// ---- URL mode (push-style on 2025, inputRequired.elicitUrl on 2026) ------
6474
const linked = await client.callTool({ name: 'link_account', arguments: { provider: 'github' } });
6575
check.match(linked.content?.[0]?.type === 'text' ? linked.content[0].text : '', /linked github/);

examples/elicitation/server.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,61 @@ function buildServer(reqCtx: McpRequestContext): McpServer {
8989
}
9090
);
9191

92+
// ---- Multi-step / chained form elicitation (two sequential prompts) ------
93+
server.registerTool(
94+
'plan_trip',
95+
{ description: 'Plan a trip by collecting a destination and then dates for that destination' },
96+
async (ctx): Promise<CallToolResult | InputRequiredResult> => {
97+
const DEST: ElicitRequestFormParams['requestedSchema'] = {
98+
type: 'object',
99+
properties: { destination: { type: 'string', title: 'Destination' } },
100+
required: ['destination']
101+
};
102+
const datesFor = (dest: string): ElicitRequestFormParams['requestedSchema'] => ({
103+
type: 'object',
104+
properties: {
105+
departure: { type: 'string', title: `Departure date for ${dest}`, format: 'date' },
106+
nights: { type: 'integer', title: 'Nights', minimum: 1, maximum: 30 }
107+
},
108+
required: ['departure', 'nights']
109+
});
110+
if (reqCtx.era === 'legacy') {
111+
// 2025-era: two sequential `elicitation/create` pushes inside one tool call.
112+
const step1 = await ctx.mcpReq.elicitInput({ mode: 'form', message: 'Where to?', requestedSchema: DEST });
113+
if (step1.action !== 'accept' || !step1.content) {
114+
return { content: [{ type: 'text', text: `trip ${step1.action}` }] };
115+
}
116+
const dest = step1.content.destination as string;
117+
const step2 = await ctx.mcpReq.elicitInput({ mode: 'form', message: 'When?', requestedSchema: datesFor(dest) });
118+
if (step2.action !== 'accept' || !step2.content) {
119+
return { content: [{ type: 'text', text: `trip ${step2.action}` }] };
120+
}
121+
return {
122+
content: [
123+
{ type: 'text', text: `trip planned: ${dest} on ${step2.content.departure} for ${step2.content.nights} nights` }
124+
]
125+
};
126+
}
127+
// 2026-07-28: two `inputRequired` rounds — the second carries the
128+
// first answer back via `requestState` (an opaque server-minted
129+
// string) so the chain survives the stateless retry. See ../mrtr/
130+
// for integrity-protecting `requestState` in production.
131+
const dates = acceptedContent<{ departure: string; nights: number }>(ctx.mcpReq.inputResponses, 'dates');
132+
const destination =
133+
ctx.mcpReq.requestState ?? acceptedContent<{ destination: string }>(ctx.mcpReq.inputResponses, 'dest')?.destination;
134+
if (!destination) {
135+
return inputRequired({ inputRequests: { dest: inputRequired.elicit({ message: 'Where to?', requestedSchema: DEST }) } });
136+
}
137+
if (!dates) {
138+
return inputRequired({
139+
requestState: destination,
140+
inputRequests: { dates: inputRequired.elicit({ message: 'When?', requestedSchema: datesFor(destination) }) }
141+
});
142+
}
143+
return { content: [{ type: 'text', text: `trip planned: ${destination} on ${dates.departure} for ${dates.nights} nights` }] };
144+
}
145+
);
146+
92147
// ---- URL-mode elicitation (push style + completion notification) ---------
93148
server.registerTool(
94149
'link_account',

examples/legacy-routing/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,25 @@
22

33
`isLegacyRequest` routing: keep an **existing** sessionful 1.x Streamable HTTP deployment serving 2025-era clients, add a strict `createMcpHandler({ legacy: 'reject' })` for 2026-07-28 traffic, on the **same port**. The predicate decides per request which arm handles it.
44

5+
`server.ts` also shows the browser-client CORS `exposedHeaders` recipe and explicit `GET` (standalone SSE stream) / `DELETE` (session termination per the MCP spec) routes for the sessionful arm.
6+
57
**HTTP-only** by definition; see also `dual-era/` for the simple case where you don't have a sessionful deployment to keep.
8+
9+
## Direct transport construction (without `createMcpHandler`)
10+
11+
If you need full control over the per-request transport on a web-standards runtime (Hono, Cloudflare Workers, …) instead of `createMcpHandler`, construct `WebStandardStreamableHTTPServerTransport` directly:
12+
13+
```ts
14+
import { McpServer, WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/server';
15+
16+
const transport = new WebStandardStreamableHTTPServerTransport({
17+
sessionIdGenerator: () => crypto.randomUUID()
18+
});
19+
const server = new McpServer({ name: 'direct-transport', version: '1.0.0' });
20+
await server.connect(transport);
21+
22+
// Any Request/Response runtime (fetch handler, Hono `c.req.raw`, …):
23+
export default { fetch: (request: Request) => transport.handleRequest(request) };
24+
```
25+
26+
`NodeStreamableHTTPServerTransport` (used in this story's legacy arm) is the Node.js `IncomingMessage`/`ServerResponse` equivalent.

examples/legacy-routing/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111
"@modelcontextprotocol/express": "workspace:*",
1212
"@modelcontextprotocol/node": "workspace:*",
1313
"@modelcontextprotocol/server": "workspace:*",
14+
"cors": "catalog:runtimeServerOnly",
1415
"express": "catalog:runtimeServerOnly",
1516
"zod": "catalog:runtimeShared"
1617
},
1718
"devDependencies": {
19+
"@types/cors": "catalog:devTools",
1820
"tsx": "catalog:devTools"
1921
},
2022
"example": {

examples/legacy-routing/server.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { createMcpExpressApp } from '@modelcontextprotocol/express';
1616
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
1717
import type { McpRequestContext } from '@modelcontextprotocol/server';
1818
import { createMcpHandler, isInitializeRequest, isLegacyRequest, McpServer } from '@modelcontextprotocol/server';
19+
import cors from 'cors';
1920
import type { Request, Response } from 'express';
2021
import * as z from 'zod/v4';
2122

@@ -56,7 +57,19 @@ const handleLegacy = async (req: Request, res: Response) => {
5657
const modern = createMcpHandler((ctx: McpRequestContext) => buildServer(ctx.era), { legacy: 'reject' });
5758

5859
const app = createMcpExpressApp();
59-
app.all('/', async (req: Request, res: Response) => {
60+
// Browser-client CORS recipe: expose the response headers a browser-based MCP
61+
// client must be able to read (`Mcp-Session-Id` for session correlation,
62+
// `WWW-Authenticate` for the auth challenge, `Last-Event-Id` for resumability,
63+
// `Mcp-Protocol-Version` for negotiation). DEMO ONLY — restrict `origin` in
64+
// production.
65+
app.use(
66+
cors({
67+
origin: '*',
68+
exposedHeaders: ['Mcp-Session-Id', 'WWW-Authenticate', 'Last-Event-Id', 'Mcp-Protocol-Version']
69+
})
70+
);
71+
72+
app.post('/', async (req: Request, res: Response) => {
6073
// The predicate inspects the same headers + body the entry does. Express
6174
// has parsed the JSON body; pass it as `parsedBody` so the predicate need
6275
// not re-read the stream.
@@ -66,6 +79,11 @@ app.all('/', async (req: Request, res: Response) => {
6679
});
6780
await ((await isLegacyRequest(probe, req.body)) ? handleLegacy(req, res) : modern.node(req, res, req.body));
6881
});
82+
// GET (standalone SSE stream / reconnect with Last-Event-ID) and DELETE
83+
// (explicit session termination per the MCP spec) are sessionful-2025-only —
84+
// route them straight to the legacy arm; the transport handles each verb.
85+
app.get('/', (req, res) => void handleLegacy(req, res));
86+
app.delete('/', (req, res) => void handleLegacy(req, res));
6987

7088
const argv = process.argv.slice(2);
7189
const portIdx = argv.indexOf('--port');

0 commit comments

Comments
 (0)