Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 24 additions & 23 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,29 +31,30 @@ The one exception to the generic commands is the reference pair: [`cli-client/`]

## Feature stories

| Story | What it teaches | Transports | Era |
| ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | -------------- |
| [`mrtr/`](./mrtr/README.md) | Multi-round-trip write-once tool, secure `requestState` | stdio + http | modern |
| [`subscriptions/`](./subscriptions/README.md) | `subscriptions/listen`: `client.listen()` + auto-open, `handler.notify` / `ServerEventBus` | stdio + http | modern |
| [`streaming/`](./streaming/README.md) | In-flight progress, logging, cancellation | stdio + http | dual |
| [`elicitation/`](./elicitation/README.md) | Elicitation (form + URL mode), both eras: push-style on 2025, `inputRequired` on 2026 | stdio + http | dual |
| [`sampling/`](./sampling/README.md) | Tool that requests LLM sampling from the client, both eras: push-style on 2025, `inputRequired` on 2026 | stdio + http | dual |
| [`stickynotes/`](./stickynotes/README.md) | "Real app" capstone: tools mutate state, a resource per note, listChanged, elicitation-confirmed clear | stdio + http | dual |
| [`cli-client/`](./cli-client/README.md) | **Reference host**: LLM chat CLI with provider seam — tool loop, @-mention resources, prompt commands, sampling, elicitation, roots, OAuth, cancellation | stdio + http | dual |
| [`todos-server/`](./todos-server/README.md) | **Reference server** (pairs with cli-client): every server feature with a real job — CRUD tools, sampling, multi-round elicitation, subscriptions, progress | stdio + http | dual |
| [`caching/`](./caching/README.md) | `cacheHints` stamping on cacheable results (2026-07-28) | stdio + http | modern |
| [`gateway/`](./gateway/README.md) | `connect({ prior })` — probe once, zero-round-trip connect for every worker (gateway pattern) | http | modern |
| [`custom-methods/`](./custom-methods/README.md) | Vendor-prefixed methods + custom notifications | stdio + http | dual |
| [`extension-capabilities/`](./extension-capabilities/README.md) | Declaring `capabilities.extensions` and reading the negotiated map | stdio + http | dual |
| [`schema-validators/`](./schema-validators/README.md) | ArkType, Valibot, Zod, and `outputSchema` | stdio + http | dual |
| [`custom-version/`](./custom-version/README.md) | `supportedProtocolVersions` / version negotiation | stdio + http | legacy |
| [`parallel-calls/`](./parallel-calls/README.md) | Multiple clients / parallel tool calls, per-client notifications | stdio + http | dual |
| [`legacy-routing/`](./legacy-routing/README.md) | `isLegacyRequest` in front of an existing sessionful 1.x deployment + a strict modern entry on one port | http | dual (in-body) |
| [`bearer-auth/`](./bearer-auth/README.md) | Resource server with bearer token; `401` + `WWW-Authenticate` | http | dual |
| [`bearer-auth-web/`](./bearer-auth-web/README.md) | Web-standard twin: host/origin guards + `requireBearerAuth` + `createMcpHandler` as one fetch handler | http | dual |
| [`oauth/`](./oauth/README.md) | OAuth `authorization_code`: in-repo AS (auto-consent) + headless redirect-following client | http | dual |
| [`oauth-client-credentials/`](./oauth-client-credentials/README.md) | OAuth `client_credentials` (machine-to-machine): in-repo AS + `ClientCredentialsProvider` | http | dual |
| [`scoped-tools/`](./scoped-tools/README.md) | Per-tool scope on `createMcpHandler` — bearer-verify gate + handler-level `ctx.http?.authInfo` checks | http | modern |
| Story | What it teaches | Transports | Era |
| ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | -------------- |
| [`mrtr/`](./mrtr/README.md) | Multi-round-trip write-once tool, secure `requestState` | stdio + http | modern |
| [`subscriptions/`](./subscriptions/README.md) | `subscriptions/listen`: `client.listen()` + auto-open, `handler.notify` / `ServerEventBus` | stdio + http | modern |
| [`streaming/`](./streaming/README.md) | In-flight progress, logging, cancellation | stdio + http | dual |
| [`elicitation/`](./elicitation/README.md) | Elicitation (form + URL mode), both eras: push-style on 2025, `inputRequired` on 2026 | stdio + http | dual |
| [`sampling/`](./sampling/README.md) | Tool that requests LLM sampling from the client, both eras: push-style on 2025, `inputRequired` on 2026 | stdio + http | dual |
| [`stickynotes/`](./stickynotes/README.md) | "Real app" capstone: tools mutate state, a resource per note, listChanged, elicitation-confirmed clear | stdio + http | dual |
| [`cli-client/`](./cli-client/README.md) | **Reference host**: LLM chat CLI with provider seam — tool loop, @-mention resources, prompt commands, sampling, elicitation, roots, OAuth, cancellation | stdio + http | dual |
| [`todos-server/`](./todos-server/README.md) | **Reference server** (pairs with cli-client): every server feature with a real job — CRUD tools, sampling, multi-round elicitation, subscriptions, progress | stdio + http | dual |
| [`caching/`](./caching/README.md) | `cacheHints` stamping on cacheable results (2026-07-28) | stdio + http | modern |
| [`gateway/`](./gateway/README.md) | `connect({ prior })` — probe once, zero-round-trip connect for every worker (gateway pattern) | http | modern |
| [`custom-methods/`](./custom-methods/README.md) | Vendor-prefixed methods + custom notifications | stdio + http | dual |
| [`extension-capabilities/`](./extension-capabilities/README.md) | Declaring `capabilities.extensions` and reading the negotiated map | stdio + http | dual |
| [`schema-validators/`](./schema-validators/README.md) | ArkType, Valibot, Zod, and `outputSchema` | stdio + http | dual |
| [`custom-version/`](./custom-version/README.md) | `supportedProtocolVersions` / version negotiation | stdio + http | legacy |
| [`parallel-calls/`](./parallel-calls/README.md) | Multiple clients / parallel tool calls, per-client notifications | stdio + http | dual |
| [`legacy-routing/`](./legacy-routing/README.md) | `isLegacyRequest` in front of an existing sessionful 1.x deployment + a strict modern entry on one port | http | dual (in-body) |
| [`bearer-auth/`](./bearer-auth/README.md) | Resource server with bearer token; `401` + `WWW-Authenticate` | http | dual |
| [`bearer-auth-web/`](./bearer-auth-web/README.md) | Web-standard twin: host/origin guards + `requireBearerAuth` + `createMcpHandler` as one fetch handler | http | dual |
| [`oauth/`](./oauth/README.md) | OAuth `authorization_code`: in-repo AS (auto-consent) + headless redirect-following client | http | dual |
| [`oauth-client-credentials/`](./oauth-client-credentials/README.md) | OAuth `client_credentials` (machine-to-machine): in-repo AS + `ClientCredentialsProvider` | http | dual |
| [`scoped-tools/`](./scoped-tools/README.md) | Per-tool scope on `createMcpHandler` — bearer-verify gate + handler-level `ctx.http?.authInfo` checks | http | modern |
| [`ext-tasks/`](./ext-tasks/README.md) | Tasks extension: `TasksExtension` + `InMemoryTaskStore` on the server, `TasksClientExtension` + `waitFor` on the client, in-process work with `input_required` and cancel | stdio + http | modern |

## HTTP hosting variants

Expand Down
13 changes: 13 additions & 0 deletions examples/ext-tasks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# ext-tasks

A minimal server for the [MCP Tasks extension](https://github.com/modelcontextprotocol/ext-tasks) (`io.modelcontextprotocol/tasks`), everything in-process. `TasksExtension` over an `InMemoryTaskStore` serves the wire; the work behind the one tool, `bake_cake`, is a plain async function driven by timers that reports progress, asks the client which frosting to use (`input_required`), and honours cancellation between steps.

The client installs `TasksClientExtension`, calls the tool, follows the task with `waitFor` (answering the frosting question through `tasks/update` on the way), then starts a second task and cancels it while it waits for input.

```bash
pnpm tsx examples/ext-tasks/client.ts # stdio, spawns the server
pnpm tsx examples/ext-tasks/server.ts --http --port 3000 # or serve over HTTP …
pnpm tsx examples/ext-tasks/client.ts --http http://127.0.0.1:3000/mcp
```

Modern era only: task handles and the `tasks/*` methods ride the 2026-07-28 per-request capabilities envelope.
75 changes: 75 additions & 0 deletions examples/ext-tasks/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* Connects to `./server.ts` with the Tasks client extension, calls `bake_cake`
* twice, and asserts both outcomes: one task followed to completion through
* `waitFor` (answering the frosting question on the way), one cancelled
* mid-bake.
*
* Spawns the sibling `server.ts` over stdio by default, or connects to a
* running endpoint under `--http <url>`. See `examples/CONTRIBUTING.md` for
* the canonical shape.
*/
import { check, parseExampleArgs, siblingPath } from '@mcp-examples/shared';
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
import { TasksClientExtension } from '@modelcontextprotocol/client/ext/tasks';
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';

const { transport, url } = parseExampleArgs();

const tasks = new TasksClientExtension();
const client = new Client(
{ name: 'tasks-example-client', version: '1.0.0' },
{ versionNegotiation: { mode: 'auto' }, extensions: [tasks] }
);

await client.connect(
transport === 'stdio'
? new StdioClientTransport({ command: 'npx', args: ['-y', 'tsx', siblingPath(import.meta.url, 'server.ts')] })
: new StreamableHTTPClientTransport(new URL(url))
);

check.ok('io.modelcontextprotocol/tasks' in (client.getServerCapabilities()?.extensions ?? {}));

// 1. A task followed to the end. `tools/call` answers a handle at once.
const started = await tasks.callTool({ name: 'bake_cake', arguments: { layers: 3 } });
check.equal(started.kind, 'task');
if (started.kind !== 'task') throw new Error('unreachable');
console.log(`[client] task ${started.task.taskId} created: ${started.task.status}`);

const seen: string[] = [];
const done = await tasks.waitFor(started.task.taskId, {
onUpdate: async task => {
seen.push(`${task.status}${task.statusMessage ? ` (${task.statusMessage})` : ''}`);
if (task.status === 'input_required') {
// The server asked which frosting; answer through tasks/update.
check.deepEqual(Object.keys(task.inputRequests), ['frosting']);
await tasks.update(task.taskId, { frosting: { action: 'accept', content: { frosting: 'chocolate' } } });
}
}
});
console.log(`[client] saw: ${[...new Set(seen)].join(' -> ')}`);
check.equal(done.status, 'completed');
if (done.status !== 'completed') throw new Error('unreachable');
check.deepEqual(done.result['structuredContent'], { layers: 3, frosting: 'chocolate' });
check.ok(seen.some(s => s.startsWith('input_required')));

// 2. A task cancelled while it waits for the frosting answer.
const second = await tasks.callTool({ name: 'bake_cake', arguments: { layers: 1 } });
if (second.kind !== 'task') throw new Error('expected a task handle');
await tasks
.waitFor(second.task.taskId, {
onUpdate: async task => {
if (task.status === 'input_required') await tasks.cancel(task.taskId);
}
})
.then(task => {
console.log(`[client] task ${task.taskId} ended: ${task.status}`);
check.equal(task.status, 'cancelled');
});

// 3. Unknown task ids are -32602.
await tasks.get('no-such-task').then(
() => check.fail('expected tasks/get to reject'),
(error: unknown) => check.equal((error as { code?: number }).code, -32_602)
);

await client.close();
24 changes: 24 additions & 0 deletions examples/ext-tasks/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "@mcp-examples/ext-tasks",
"private": true,
"type": "module",
"scripts": {
"server": "tsx server.ts",
"client": "tsx client.ts"
},
"dependencies": {
"@hono/node-server": "catalog:runtimeServerOnly",
"@mcp-examples/shared": "workspace:*",
"@modelcontextprotocol/client": "workspace:*",
"@modelcontextprotocol/hono": "workspace:*",
"@modelcontextprotocol/server": "workspace:*",
"zod": "catalog:runtimeShared"
},
"devDependencies": {
"tsx": "catalog:devTools"
},
"example": {
"era": "modern",
"//": "The Tasks extension (io.modelcontextprotocol/tasks) is a 2026-07-28 extension: task handles and tasks/* ride the per-request capabilities envelope."
}
}
Loading
Loading