[SEP-2663] refactor!: remove 2025-11 experimental tasks (#2128)

This commit is contained in:
Felix Weinberger 2026-06-02 13:04:18 +01:00 • committed by GitHub
commit c8d7401b46
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
73 changed files with 423 additions and 18280 deletions

View file

@ -1,5 +0,0 @@
---
'@modelcontextprotocol/core': patch
---
Fix `requestStream` to call `tasks/result` for failed tasks instead of yielding a hardcoded `ProtocolError`. When a task reaches the `failed` terminal status, the stream now retrieves and yields the actual stored result (matching the behavior for `completed` tasks), as required by the spec.

View file

@ -0,0 +1,6 @@
---
'@modelcontextprotocol/core': major
'@modelcontextprotocol/server': major
'@modelcontextprotocol/client': major
---
SEP-2663: remove 2025-11 experimental tasks (TaskManager, experimental.tasks.* accessors). Tasks are now Extensions Track.

View file

@ -104,9 +104,7 @@ The repo also ships “middleware” packages under `packages/middleware/` (e.g.
### Experimental Features
Located in `packages/*/src/experimental/`:
- **Tasks**: Long-running task support with polling/resumption (`packages/core/src/experimental/tasks/`)
Located in `packages/*/src/experimental/`. Currently empty.
### Zod Schemas
@ -201,7 +199,6 @@ The `ctx` parameter in handlers provides a structured context:
- `notify(notification)`: Send related notification back
- `http?`: HTTP transport info (undefined for stdio)
- `authInfo?`: Validated auth token info
- `task?`: Task context (`{ id?, store, requestedTtl? }`) when task storage is configured
**`ServerContext`** extends `BaseContext.mcpReq` and `BaseContext.http?` via type intersection:

View file

@ -544,7 +544,7 @@ All requests have a 60-second default timeout. Pass a custom `timeout` in the op
```ts source="../examples/client/src/clientGuide.examples.ts#errorHandling_timeout"
try {
const result = await client.callTool(
{ name: 'slow-task', arguments: {} },
{ name: 'slow-operation', arguments: {} },
{ timeout: 120_000 } // 2 minutes instead of the default 60 seconds
);
console.log(result.content);
@ -581,7 +581,7 @@ let lastToken: string | undefined;
const result = await client.request(
{
method: 'tools/call',
params: { name: 'long-running-task', arguments: {} }
params: { name: 'long-running-operation', arguments: {} }
},
{
resumptionToken: lastToken,
@ -596,18 +596,6 @@ console.log(result);
For an end-to-end example of server-initiated SSE disconnection and automatic client reconnection with event replay, see [`ssePollingClient.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/ssePollingClient.ts).
## Tasks (experimental)
> [!WARNING]
> The tasks API is experimental and may change without notice.
Task-based execution enables "call-now, fetch-later" patterns for long-running operations (see [Tasks](https://modelcontextprotocol.io/specification/latest/basic/utilities/tasks) in the MCP specification). Instead of returning a result immediately, a tool creates a task that can be polled or resumed later. To use tasks:
- Call {@linkcode @modelcontextprotocol/client!experimental/tasks/client.ExperimentalClientTasks#callToolStream | client.experimental.tasks.callToolStream(...)} to start a tool call that may create a task and emit status updates over time.
- Call {@linkcode @modelcontextprotocol/client!experimental/tasks/client.ExperimentalClientTasks#getTask | client.experimental.tasks.getTask(...)} and {@linkcode @modelcontextprotocol/client!experimental/tasks/client.ExperimentalClientTasks#getTaskResult | getTaskResult(...)} to check status and fetch results after reconnecting.
For a full runnable example, see [`simpleTaskInteractiveClient.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/client/src/simpleTaskInteractiveClient.ts).
## See also
- [`examples/client/`](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/examples/client) — Full runnable client examples

View file

@ -47,25 +47,25 @@ Replace all `@modelcontextprotocol/sdk/...` imports using this table.
### Server imports
| v1 import path | v2 package |
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `@modelcontextprotocol/sdk/server/mcp.js` | `@modelcontextprotocol/server` |
| `@modelcontextprotocol/sdk/server/index.js` | `@modelcontextprotocol/server` |
| `@modelcontextprotocol/sdk/server/stdio.js` | `@modelcontextprotocol/server/stdio` |
| `@modelcontextprotocol/sdk/server/streamableHttp.js` | `@modelcontextprotocol/node` (class renamed to `NodeStreamableHTTPServerTransport`) OR `@modelcontextprotocol/server` (web-standard `WebStandardStreamableHTTPServerTransport` for Cloudflare Workers, Deno, etc.) |
| `@modelcontextprotocol/sdk/server/sse.js` | REMOVED (migrate to Streamable HTTP); legacy bridge: `@modelcontextprotocol/server-legacy/sse` |
| v1 import path | v2 package |
| ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `@modelcontextprotocol/sdk/server/mcp.js` | `@modelcontextprotocol/server` |
| `@modelcontextprotocol/sdk/server/index.js` | `@modelcontextprotocol/server` |
| `@modelcontextprotocol/sdk/server/stdio.js` | `@modelcontextprotocol/server/stdio` |
| `@modelcontextprotocol/sdk/server/streamableHttp.js` | `@modelcontextprotocol/node` (class renamed to `NodeStreamableHTTPServerTransport`) OR `@modelcontextprotocol/server` (web-standard `WebStandardStreamableHTTPServerTransport` for Cloudflare Workers, Deno, etc.) |
| `@modelcontextprotocol/sdk/server/sse.js` | REMOVED (migrate to Streamable HTTP); legacy bridge: `@modelcontextprotocol/server-legacy/sse` |
| `@modelcontextprotocol/sdk/server/auth/*` | RS helpers (`requireBearerAuth`, `mcpAuthMetadataRouter`, `OAuthTokenVerifier`) → `@modelcontextprotocol/express`; AS helpers (`mcpAuthRouter`, `OAuthServerProvider`, etc.) → `@modelcontextprotocol/server-legacy/auth` (deprecated); migrate AS to an external IdP/OAuth library |
| `@modelcontextprotocol/sdk/server/middleware.js` | `@modelcontextprotocol/express` (signature changed, see section 8) |
| `@modelcontextprotocol/sdk/server/middleware.js` | `@modelcontextprotocol/express` (signature changed, see section 8) |
### Types / shared imports
| v1 import path | v2 package |
| ------------------------------------------------- | ---------------------------------------------------------------- |
| `@modelcontextprotocol/sdk/types.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` |
| `@modelcontextprotocol/sdk/shared/protocol.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` |
| `@modelcontextprotocol/sdk/shared/transport.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` |
| `@modelcontextprotocol/sdk/shared/uriTemplate.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` |
| `@modelcontextprotocol/sdk/shared/auth.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` |
| v1 import path | v2 package |
| ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `@modelcontextprotocol/sdk/types.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` |
| `@modelcontextprotocol/sdk/shared/protocol.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` |
| `@modelcontextprotocol/sdk/shared/transport.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` |
| `@modelcontextprotocol/sdk/shared/uriTemplate.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` |
| `@modelcontextprotocol/sdk/shared/auth.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` |
| `@modelcontextprotocol/sdk/shared/stdio.js` | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` (`ReadBuffer`, `serializeMessage`, `deserializeMessage` are in the root barrel; the `./stdio` subpath only has the transport class) |
Notes:
@ -109,20 +109,20 @@ Three error classes now exist:
- **`SdkError`** (new): Local SDK errors that never cross the wire
- **`SdkHttpError`** (extends `SdkError`): HTTP transport errors with typed `.status` and `.statusText` accessors
| Error scenario | v1 type | v2 type |
| --------------------------------- | -------------------------------------------- | ----------------------------------------------------------------- |
| Request timeout | `McpError` with `ErrorCode.RequestTimeout` | `SdkError` with `SdkErrorCode.RequestTimeout` |
| Connection closed | `McpError` with `ErrorCode.ConnectionClosed` | `SdkError` with `SdkErrorCode.ConnectionClosed` |
| Capability not supported | `new Error(...)` | `SdkError` with `SdkErrorCode.CapabilityNotSupported` |
| Not connected | `new Error('Not connected')` | `SdkError` with `SdkErrorCode.NotConnected` |
| Invalid params (server response) | `McpError` with `ErrorCode.InvalidParams` | `ProtocolError` with `ProtocolErrorCode.InvalidParams` |
| HTTP transport error | `StreamableHTTPError` | `SdkHttpError` with `SdkErrorCode.ClientHttp*` |
| Failed to open SSE stream | `StreamableHTTPError` | `SdkHttpError` with `SdkErrorCode.ClientHttpFailedToOpenStream` |
| 401 after re-auth (circuit break) | `StreamableHTTPError` | `SdkHttpError` with `SdkErrorCode.ClientHttpAuthentication` |
| 403 after upscoping | `StreamableHTTPError` | `SdkHttpError` with `SdkErrorCode.ClientHttpForbidden` |
| Unexpected content type | `StreamableHTTPError` | `SdkError` with `SdkErrorCode.ClientHttpUnexpectedContent` |
| Error scenario | v1 type | v2 type |
| --------------------------------- | -------------------------------------------- | --------------------------------------------------------------------- |
| Request timeout | `McpError` with `ErrorCode.RequestTimeout` | `SdkError` with `SdkErrorCode.RequestTimeout` |
| Connection closed | `McpError` with `ErrorCode.ConnectionClosed` | `SdkError` with `SdkErrorCode.ConnectionClosed` |
| Capability not supported | `new Error(...)` | `SdkError` with `SdkErrorCode.CapabilityNotSupported` |
| Not connected | `new Error('Not connected')` | `SdkError` with `SdkErrorCode.NotConnected` |
| Invalid params (server response) | `McpError` with `ErrorCode.InvalidParams` | `ProtocolError` with `ProtocolErrorCode.InvalidParams` |
| HTTP transport error | `StreamableHTTPError` | `SdkHttpError` with `SdkErrorCode.ClientHttp*` |
| Failed to open SSE stream | `StreamableHTTPError` | `SdkHttpError` with `SdkErrorCode.ClientHttpFailedToOpenStream` |
| 401 after re-auth (circuit break) | `StreamableHTTPError` | `SdkHttpError` with `SdkErrorCode.ClientHttpAuthentication` |
| 403 after upscoping | `StreamableHTTPError` | `SdkHttpError` with `SdkErrorCode.ClientHttpForbidden` |
| Unexpected content type | `StreamableHTTPError` | `SdkError` with `SdkErrorCode.ClientHttpUnexpectedContent` |
| Session termination failed | `StreamableHTTPError` | `SdkHttpError` with `SdkErrorCode.ClientHttpFailedToTerminateSession` |
| Response result fails schema | `ZodError` (raw) | `SdkError` with `SdkErrorCode.InvalidResult` |
| Response result fails schema | `ZodError` (raw) | `SdkError` with `SdkErrorCode.InvalidResult` |
New `SdkErrorCode` enum values:
@ -164,11 +164,11 @@ if (error instanceof StreamableHTTPError) {
// v2
import { SdkHttpError, SdkErrorCode } from '@modelcontextprotocol/client';
if (error instanceof SdkHttpError) {
console.log('HTTP status:', error.status); // number — typed accessor
console.log('HTTP status:', error.status); // number — typed accessor
console.log('Status text:', error.statusText); // string | undefined
switch (error.code) {
case SdkErrorCode.ClientHttpAuthentication: // 401 after re-auth
case SdkErrorCode.ClientHttpForbidden: // 403 after upscoping
case SdkErrorCode.ClientHttpAuthentication: // 401 after re-auth
case SdkErrorCode.ClientHttpForbidden: // 403 after upscoping
case SdkErrorCode.ClientHttpFailedToOpenStream:
case SdkErrorCode.ClientHttpNotImplemented:
break;
@ -328,11 +328,13 @@ new URL(ctx.http?.req?.url).searchParams.get('debug')
### SSE server transport
`SSEServerTransport` removed entirely. Migrate to `NodeStreamableHTTPServerTransport` (from `@modelcontextprotocol/node`). Client-side `SSEClientTransport` still available for connecting to legacy servers. Legacy bridge: `import { SSEServerTransport } from '@modelcontextprotocol/server-legacy/sse'` (deprecated, frozen v1 copy).
`SSEServerTransport` removed entirely. Migrate to `NodeStreamableHTTPServerTransport` (from `@modelcontextprotocol/node`). Client-side `SSEClientTransport` still available for connecting to legacy servers. Legacy bridge:
`import { SSEServerTransport } from '@modelcontextprotocol/server-legacy/sse'` (deprecated, frozen v1 copy).
### Server-side auth
Resource Server helpers (`requireBearerAuth`, `mcpAuthMetadataRouter`, `getOAuthProtectedResourceMetadataUrl`, `OAuthTokenVerifier`) are first-class in `@modelcontextprotocol/express`. Authorization Server helpers (`mcpAuthRouter`, `OAuthServerProvider`, `ProxyOAuthServerProvider`, `authenticateClient`, `allowedMethods`, etc.) are available from `@modelcontextprotocol/server-legacy/auth` (deprecated, frozen v1 copy). Migrate AS to an external IdP/OAuth library for production use. See `examples/server/src/` for demos.
Resource Server helpers (`requireBearerAuth`, `mcpAuthMetadataRouter`, `getOAuthProtectedResourceMetadataUrl`, `OAuthTokenVerifier`) are first-class in `@modelcontextprotocol/express`. Authorization Server helpers (`mcpAuthRouter`, `OAuthServerProvider`,
`ProxyOAuthServerProvider`, `authenticateClient`, `allowedMethods`, etc.) are available from `@modelcontextprotocol/server-legacy/auth` (deprecated, frozen v1 copy). Migrate AS to an external IdP/OAuth library for production use. See `examples/server/src/` for demos.
### Host header validation (Express)
@ -415,23 +417,21 @@ Request/notification params remain fully typed. Remove unused schema imports aft
`RequestHandlerExtra` → structured context types with nested groups. Rename `extra` → `ctx` in all handler callbacks.
| v1 | v2 |
| -------------------------------- | -------------------------------------------------------------------------- |
| `RequestHandlerExtra` | `ServerContext` (server) / `ClientContext` (client) / `BaseContext` (base) |
| `extra` (param name) | `ctx` |
| `extra.signal` | `ctx.mcpReq.signal` |
| `extra.requestId` | `ctx.mcpReq.id` |
| `extra._meta` | `ctx.mcpReq._meta` |
| `extra.sendRequest(...)` | `ctx.mcpReq.send(...)` |
| `extra.sendNotification(...)` | `ctx.mcpReq.notify(...)` |
| `extra.authInfo` | `ctx.http?.authInfo` |
| `extra.sessionId` | `ctx.sessionId` |
| `extra.requestInfo` | `ctx.http?.req` (standard Web `Request`, only `ServerContext`) |
| `extra.closeSSEStream` | `ctx.http?.closeSSE` (only `ServerContext`) |
| `extra.closeStandaloneSSEStream` | `ctx.http?.closeStandaloneSSE` (only `ServerContext`) |
| `extra.taskStore` | `ctx.task?.store` |
| `extra.taskId` | `ctx.task?.id` |
| `extra.taskRequestedTtl` | `ctx.task?.requestedTtl` |
| v1 | v2 |
| ------------------------------------------------- | -------------------------------------------------------------------------- |
| `RequestHandlerExtra` | `ServerContext` (server) / `ClientContext` (client) / `BaseContext` (base) |
| `extra` (param name) | `ctx` |
| `extra.signal` | `ctx.mcpReq.signal` |
| `extra.requestId` | `ctx.mcpReq.id` |
| `extra._meta` | `ctx.mcpReq._meta` |
| `extra.sendRequest(...)` | `ctx.mcpReq.send(...)` |
| `extra.sendNotification(...)` | `ctx.mcpReq.notify(...)` |
| `extra.authInfo` | `ctx.http?.authInfo` |
| `extra.sessionId` | `ctx.sessionId` |
| `extra.requestInfo` | `ctx.http?.req` (standard Web `Request`, only `ServerContext`) |
| `extra.closeSSEStream` | `ctx.http?.closeSSE` (only `ServerContext`) |
| `extra.closeStandaloneSSEStream` | `ctx.http?.closeStandaloneSSE` (only `ServerContext`) |
| `extra.taskStore` / `taskId` / `taskRequestedTtl` | _removed; see §12_ |
`ServerContext` convenience methods (new in v2, no v1 equivalent):
@ -473,33 +473,33 @@ Remove unused schema imports: `CallToolResultSchema`, `CompatibilityCallToolResu
If a `*Schema` constant was used for **runtime validation** (not just as a `request()` argument), replace with `isSpecType` / `specTypeSchemas`:
| v1 pattern | v2 replacement |
| -------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `CallToolResultSchema.safeParse(value).success` | `isSpecType.CallToolResult(value)` |
| `<TypeName>Schema.safeParse(value).success` | `isSpecType.<TypeName>(value)` |
| v1 pattern | v2 replacement |
| -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `CallToolResultSchema.safeParse(value).success` | `isSpecType.CallToolResult(value)` |
| `<TypeName>Schema.safeParse(value).success` | `isSpecType.<TypeName>(value)` |
| `<TypeName>Schema.parse(value)` | `specTypeSchemas.<TypeName>['~standard'].validate(value)` (returns a `Result` synchronously, not the value) |
| Passing `<TypeName>Schema` as a validator argument | `specTypeSchemas.<TypeName>` (a `StandardSchemaV1Sync<In, Out>`) |
| Passing `<TypeName>Schema` as a validator argument | `specTypeSchemas.<TypeName>` (a `StandardSchemaV1Sync<In, Out>`) |
`isCallToolResult(value)` still works, but `isSpecType` covers every spec type by name.
## 12. Experimental: `TaskCreationParams.ttl` no longer accepts `null`
## 12. Experimental tasks interception removed
`TaskCreationParams.ttl` changed from `z.union([z.number(), z.null()]).optional()` to `z.number().optional()`. Per the MCP spec, `null` TTL (unlimited lifetime) is only valid in server responses (`Task.ttl`), not in client requests. Omit `ttl` to let the server decide.
The 2025-11 task side-channel through `Protocol` is removed (was always `@experimental`). No mechanical migration; remove usages.
| v1 | v2 |
| ---------------------- | ---------------------------------- |
| `task: { ttl: null }` | `task: {}` (omit ttl) |
| `task: { ttl: 60000 }` | `task: { ttl: 60000 }` (unchanged) |
| Removed | Notes |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
| `ProtocolOptions.tasks` | drop the option |
| `protocol.taskManager` | gone |
| `RequestOptions.task` / `.relatedTask`, `NotificationOptions.relatedTask` | drop the option |
| `BaseContext.task` (`ctx.task?.*`) | gone |
| `assertTaskCapability` / `assertTaskHandlerCapability` overrides | delete the override |
| `*.experimental.tasks.*` accessors, `Experimental{Client,Server,McpServer}Tasks` | removed |
| `requestStream` / `callToolStream` / `createMessageStream` / `elicitInputStream` | removed; no streaming variant |
| `registerToolTask`, `ToolTaskHandler`, `TaskRequestHandler`, `CreateTaskRequestHandler` | removed |
| `TaskMessageQueue`, `InMemoryTaskMessageQueue`, `BaseQueuedMessage`, `Queued*`, `CreateTaskServerContext`, `TaskServerContext`, `TaskToolExecution` | removed |
| `ResponseMessage`, `BaseResponseMessage`, `ErrorMessage`, `AsyncGeneratorValue`, `TaskStatusMessage`, `TaskCreatedMessage`, `ResultMessage`, `takeResult`, `toArrayAsync` | removed |
Type changes in handler context:
| Type | v1 | v2 |
| ------------------------------------------- | ----------------------------- | --------------------- |
| `TaskContext.requestedTtl` | `number \| null \| undefined` | `number \| undefined` |
| `CreateTaskServerContext.task.requestedTtl` | `number \| null \| undefined` | `number \| undefined` |
| `TaskServerContext.task.requestedTtl` | `number \| null \| undefined` | `number \| undefined` |
> These task APIs are `@experimental` and may change without notice.
`TaskStore` / `InMemoryTaskStore` / `CreateTaskOptions` / `isTerminal` (storage layer) and `TaskCreationParams` are also removed; they will return with the SEP-2663 server-directed plugin.
## 13. Client Behavioral Changes
@ -531,7 +531,8 @@ Validator behavior:
- Do not add validator imports for normal migrations.
- Do not install `ajv`, `ajv-formats`, or `@cfworker/json-schema` for the default path; client/server bundle the runtime-selected defaults and the root entry point does not pull either dep in.
- To customize the built-in backend (e.g. register custom AJV formats, change `@cfworker/json-schema` draft), import the named class from the package subpath: `@modelcontextprotocol/{client,server}/validators/ajv` for `AjvJsonSchemaValidator`, `@modelcontextprotocol/{client,server}/validators/cf-worker` for `CfWorkerJsonSchemaValidator`. Importing from a subpath means the corresponding peer dep must be in your `package.json`.
- To customize the built-in backend (e.g. register custom AJV formats, change `@cfworker/json-schema` draft), import the named class from the package subpath: `@modelcontextprotocol/{client,server}/validators/ajv` for `AjvJsonSchemaValidator`,
`@modelcontextprotocol/{client,server}/validators/cf-worker` for `CfWorkerJsonSchemaValidator`. Importing from a subpath means the corresponding peer dep must be in your `package.json`.
- To replace validation entirely, pass `jsonSchemaValidator: myCustomValidator` with your own implementation of the `jsonSchemaValidator` interface.
## 15. Migration Steps (apply in this order)

View file

@ -145,7 +145,8 @@ const transport = new StreamableHTTPClientTransport(new URL('http://localhost:30
Resource Server helpers (`requireBearerAuth`, `mcpAuthMetadataRouter`, `getOAuthProtectedResourceMetadataUrl`, `OAuthTokenVerifier`) are first-class in `@modelcontextprotocol/express`.
Authorization Server helpers (`mcpAuthRouter`, `OAuthServerProvider`, `ProxyOAuthServerProvider`, `authenticateClient`, `allowedMethods`, etc.) have been removed from the core SDK; new code should use a dedicated IdP/OAuth library. See the [examples](../examples/server/src/) for a working demo with `better-auth`.
Authorization Server helpers (`mcpAuthRouter`, `OAuthServerProvider`, `ProxyOAuthServerProvider`, `authenticateClient`, `allowedMethods`, etc.) have been removed from the core SDK; new code should use a dedicated IdP/OAuth library. See the [examples](../examples/server/src/) for
a working demo with `better-auth`.
Note: `AuthInfo` has moved from `server/auth/types.ts` to the core types and is now re-exported by `@modelcontextprotocol/client` and `@modelcontextprotocol/server`.
@ -387,7 +388,11 @@ const AcmeSearch = z.object({
params: z.object({ query: z.string(), limit: z.number().int() })
});
server.setRequestHandler(AcmeSearch, async request => {
return { items: [/* ... */] };
return {
items: [
/* ... */
]
};
});
```
@ -398,7 +403,11 @@ const SearchParams = z.object({ query: z.string(), limit: z.number().int() });
const SearchResult = z.object({ items: z.array(z.string()) });
server.setRequestHandler('acme/search', { params: SearchParams, result: SearchResult }, async (params, ctx) => {
return { items: [/* ... */] };
return {
items: [
/* ... */
]
};
});
```
@ -437,8 +446,8 @@ Common method string replacements:
### `Protocol.request()`, `ctx.mcpReq.send()`, and `Client.callTool()` no longer require a schema parameter for spec methods
For **spec** methods, the public `Protocol.request()`, `BaseContext.mcpReq.send()`, and `Client.callTool()` methods no longer require a Zod result schema argument. The SDK now resolves the correct result schema internally based on the method name. This means you no longer need to import result schemas
like `CallToolResultSchema` or `ElicitResultSchema` when making spec-method requests.
For **spec** methods, the public `Protocol.request()`, `BaseContext.mcpReq.send()`, and `Client.callTool()` methods no longer require a Zod result schema argument. The SDK now resolves the correct result schema internally based on the method name. This means you no longer need to
import result schemas like `CallToolResultSchema` or `ElicitResultSchema` when making spec-method requests.
**`client.request()` — Before (v1):**
@ -493,7 +502,7 @@ const result = await client.callTool({ name: 'my-tool', arguments: {} }, Compati
const result = await client.callTool({ name: 'my-tool', arguments: {} });
```
The return type is now inferred from the method name via `ResultTypeMap`. For example, `client.request({ method: 'tools/call', ... })` returns `Promise<CallToolResult | CreateTaskResult>`.
The return type is now inferred from the method name via `ResultTypeMap`. For example, `client.request({ method: 'tools/call', ... })` returns `Promise<CallToolResult>`.
For **custom (non-spec)** methods, keep the result-schema argument — see [Sending custom-method requests](#sending-custom-method-requests). Only drop the schema when calling a spec method.
@ -518,7 +527,8 @@ import { specTypeSchemas } from '@modelcontextprotocol/client';
const result = specTypeSchemas.CallToolResult['~standard'].validate(value);
```
`isSpecType` and `specTypeSchemas` are keyed by `SpecTypeName` — a literal union of every named type in the MCP spec — so you get autocomplete and a compile error on typos. `specTypeSchemas.X` is a `StandardSchemaV1Sync<In, Out>` — `validate()` returns the result synchronously, so you can access `.issues` / `.value` without `await`. It composes with any Standard-Schema-aware library. The pre-existing `isCallToolResult(value)` guard still works.
`isSpecType` and `specTypeSchemas` are keyed by `SpecTypeName` — a literal union of every named type in the MCP spec — so you get autocomplete and a compile error on typos. `specTypeSchemas.X` is a `StandardSchemaV1Sync<In, Out>` — `validate()` returns the result synchronously,
so you can access `.issues` / `.value` without `await`. It composes with any Standard-Schema-aware library. The pre-existing `isCallToolResult(value)` guard still works.
### Client list methods return empty results for missing capabilities
@ -585,23 +595,21 @@ import { JSONRPCErrorResponse, ResourceTemplateReference, isJSONRPCErrorResponse
The `RequestHandlerExtra` type has been replaced with a structured context type hierarchy using nested groups:
| v1 | v2 |
| ---------------------------------------- | ---------------------------------------------------------------------- |
| `RequestHandlerExtra` (flat, all fields) | `ServerContext` (server handlers) or `ClientContext` (client handlers) |
| `extra` parameter name | `ctx` parameter name |
| `extra.signal` | `ctx.mcpReq.signal` |
| `extra.requestId` | `ctx.mcpReq.id` |
| `extra._meta` | `ctx.mcpReq._meta` |
| `extra.sendRequest(...)` | `ctx.mcpReq.send(...)` |
| `extra.sendNotification(...)` | `ctx.mcpReq.notify(...)` |
| `extra.authInfo` | `ctx.http?.authInfo` |
| `extra.requestInfo` | `ctx.http?.req` (standard Web `Request`, only on `ServerContext`) |
| `extra.closeSSEStream` | `ctx.http?.closeSSE` (only on `ServerContext`) |
| `extra.closeStandaloneSSEStream` | `ctx.http?.closeStandaloneSSE` (only on `ServerContext`) |
| `extra.sessionId` | `ctx.sessionId` |
| `extra.taskStore` | `ctx.task?.store` |
| `extra.taskId` | `ctx.task?.id` |
| `extra.taskRequestedTtl` | `ctx.task?.requestedTtl` |
| v1 | v2 |
| ------------------------------------------------- | ---------------------------------------------------------------------- |
| `RequestHandlerExtra` (flat, all fields) | `ServerContext` (server handlers) or `ClientContext` (client handlers) |
| `extra` parameter name | `ctx` parameter name |
| `extra.signal` | `ctx.mcpReq.signal` |
| `extra.requestId` | `ctx.mcpReq.id` |
| `extra._meta` | `ctx.mcpReq._meta` |
| `extra.sendRequest(...)` | `ctx.mcpReq.send(...)` |
| `extra.sendNotification(...)` | `ctx.mcpReq.notify(...)` |
| `extra.authInfo` | `ctx.http?.authInfo` |
| `extra.requestInfo` | `ctx.http?.req` (standard Web `Request`, only on `ServerContext`) |
| `extra.closeSSEStream` | `ctx.http?.closeSSE` (only on `ServerContext`) |
| `extra.closeStandaloneSSEStream` | `ctx.http?.closeStandaloneSSE` (only on `ServerContext`) |
| `extra.sessionId` | `ctx.sessionId` |
| `extra.taskStore` / `taskId` / `taskRequestedTtl` | _removed — see "Experimental tasks interception removed" below_ |
**Before (v1):**
@ -619,17 +627,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
```typescript
server.setRequestHandler('tools/call', async (request, ctx) => {
const headers = ctx.http?.req?.headers; // standard Web Request object
const taskStore = ctx.task?.store;
await ctx.mcpReq.notify({ method: 'notifications/progress', params: { progressToken: 'abc', progress: 50, total: 100 } });
return { content: [{ type: 'text', text: 'result' }] };
});
```
Context fields are organized into 4 groups:
Context fields are organized into 3 groups:
- **`mcpReq`** — request-level concerns: `id`, `method`, `_meta`, `signal`, `send()`, `notify()`, plus server-only `log()`, `elicitInput()`, and `requestSampling()`
- **`http?`** — HTTP transport concerns (undefined for stdio): `authInfo`, plus server-only `req`, `closeSSE`, `closeStandaloneSSE`
- **`task?`** — task lifecycle: `id`, `store`, `requestedTtl`
- **`sessionId?`** — transport session identifier (top-level)
`BaseContext` is the common base type shared by both `ServerContext` and `ClientContext`. `ServerContext` extends each group with server-specific additions via type intersection.
@ -715,22 +722,22 @@ try {
The new `SdkErrorCode` enum contains string-valued codes for local SDK errors:
| Code | Description |
| ------------------------------------------------- | ------------------------------------------- |
| `SdkErrorCode.NotConnected` | Transport is not connected |
| `SdkErrorCode.AlreadyConnected` | Transport is already connected |
| `SdkErrorCode.NotInitialized` | Protocol is not initialized |
| `SdkErrorCode.CapabilityNotSupported` | Required capability is not supported |
| `SdkErrorCode.RequestTimeout` | Request timed out waiting for response |
| `SdkErrorCode.ConnectionClosed` | Connection was closed |
| `SdkErrorCode.SendFailed` | Failed to send message |
| Code | Description |
| ------------------------------------------------- | ---------------------------------------------- |
| `SdkErrorCode.NotConnected` | Transport is not connected |
| `SdkErrorCode.AlreadyConnected` | Transport is already connected |
| `SdkErrorCode.NotInitialized` | Protocol is not initialized |
| `SdkErrorCode.CapabilityNotSupported` | Required capability is not supported |
| `SdkErrorCode.RequestTimeout` | Request timed out waiting for response |
| `SdkErrorCode.ConnectionClosed` | Connection was closed |
| `SdkErrorCode.SendFailed` | Failed to send message |
| `SdkErrorCode.InvalidResult` | Response result failed local schema validation |
| `SdkErrorCode.ClientHttpNotImplemented` | HTTP POST request failed |
| `SdkErrorCode.ClientHttpAuthentication` | Server returned 401 after re-authentication |
| `SdkErrorCode.ClientHttpForbidden` | Server returned 403 after trying upscoping |
| `SdkErrorCode.ClientHttpUnexpectedContent` | Unexpected content type in HTTP response |
| `SdkErrorCode.ClientHttpFailedToOpenStream` | Failed to open SSE stream |
| `SdkErrorCode.ClientHttpFailedToTerminateSession` | Failed to terminate session |
| `SdkErrorCode.ClientHttpNotImplemented` | HTTP POST request failed |
| `SdkErrorCode.ClientHttpAuthentication` | Server returned 401 after re-authentication |
| `SdkErrorCode.ClientHttpForbidden` | Server returned 403 after trying upscoping |
| `SdkErrorCode.ClientHttpUnexpectedContent` | Unexpected content type in HTTP response |
| `SdkErrorCode.ClientHttpFailedToOpenStream` | Failed to open SSE stream |
| `SdkErrorCode.ClientHttpFailedToTerminateSession` | Failed to terminate session |
#### `StreamableHTTPError` removed
@ -759,7 +766,7 @@ try {
await transport.send(message);
} catch (error) {
if (error instanceof SdkHttpError) {
console.log('HTTP status:', error.status); // number — no cast needed
console.log('HTTP status:', error.status); // number — no cast needed
console.log('Status text:', error.statusText); // string | undefined
switch (error.code) {
case SdkErrorCode.ClientHttpAuthentication:
@ -870,46 +877,24 @@ try {
}
```
### Experimental: `TaskCreationParams.ttl` no longer accepts `null`
### Experimental tasks interception removed
The `ttl` field in `TaskCreationParams` (used when requesting the server to create a task) no longer accepts `null`. Per the MCP spec, `null` TTL (meaning unlimited lifetime) is only valid in server responses (`Task.ttl`), not in client requests. Clients should omit `ttl` to let
the server decide the lifetime.
The 2025-11 experimental tasks side-channel woven through `Protocol` has been removed in preparation for the SEP-2663 Tasks Extension. The following are gone with no in-place replacement:
This also narrows the type of `requestedTtl` in `TaskContext`, `CreateTaskServerContext`, and `TaskServerContext` from `number | null | undefined` to `number | undefined`.
- `ProtocolOptions.tasks` (the `{ taskStore, taskMessageQueue }` constructor option)
- `protocol.taskManager` getter, `Protocol#_bindTaskManager`
- `RequestOptions.task` / `RequestOptions.relatedTask`, `NotificationOptions.relatedTask`
- `BaseContext.task` (`ctx.task?.store` / `ctx.task?.id` / `ctx.task?.requestedTtl`)
- abstract `assertTaskCapability` / `assertTaskHandlerCapability`
- `client.experimental.tasks.*` / `server.experimental.tasks.*` / `mcpServer.experimental.tasks.*` accessors and the `Experimental{Client,Server,McpServer}Tasks` classes
- streaming methods (`requestStream`, `callToolStream`, `createMessageStream`, `elicitInputStream`) and the `ResponseMessage` types they yielded (`BaseResponseMessage`, `ErrorMessage`, `AsyncGeneratorValue`)
- `mcpServer.experimental.tasks.registerToolTask(...)`, `ToolTaskHandler`, `TaskRequestHandler`, `CreateTaskRequestHandler`
- `TaskMessageQueue`, `InMemoryTaskMessageQueue`, `BaseQueuedMessage` and the `Queued*` message types, `CreateTaskServerContext`, `TaskServerContext`, `TaskToolExecution`
- `examples/{client,server}/src/simpleTaskInteractive*.ts`
**Before (v1):**
**Also removed:** the storage layer (`TaskStore`, `InMemoryTaskStore`, `CreateTaskOptions`, `isTerminal`) and `TaskCreationParams`. They will return as part of the SEP-2663 server-directed plugin in a follow-up.
```typescript
// Requesting unlimited lifetime by passing null
const result = await client.callTool({
name: 'long-task',
arguments: {},
task: { ttl: null }
});
// Handler context had number | null | undefined
server.setRequestHandler('tools/call', async (request, ctx) => {
const ttl: number | null | undefined = ctx.task?.requestedTtl;
});
```
**After (v2):**
```typescript
// Omit ttl to let the server decide (server may return null for unlimited)
const result = await client.callTool({
name: 'long-task',
arguments: {},
task: {}
});
// Handler context is now number | undefined
server.setRequestHandler('tools/call', async (request, ctx) => {
const ttl: number | undefined = ctx.task?.requestedTtl;
});
```
> **Note:** These task APIs are marked `@experimental` and may change without notice.
There is no migration path for the removed surface; it was always `@experimental`. Task support is planned to return as an opt-in extension plugin per SEP-2663.
## Enhancements
@ -949,7 +934,8 @@ const server = new McpServer(
);
```
You do not need to install or import validator packages for the default behavior. The client and server packages bundle the validator backend selected by the runtime shim, so a normal `import { McpServer } from '@modelcontextprotocol/server'` does not pull `ajv` or `@cfworker/json-schema` into your bundle until you choose to customize.
You do not need to install or import validator packages for the default behavior. The client and server packages bundle the validator backend selected by the runtime shim, so a normal `import { McpServer } from '@modelcontextprotocol/server'` does not pull `ajv` or
`@cfworker/json-schema` into your bundle until you choose to customize.
If you want to customize the **built-in** backend (for example, pre-register schemas by `$id`, register custom AJV formats, or change the `@cfworker/json-schema` draft), import the named class from the explicit subpath and pass an instance through `jsonSchemaValidator`:
@ -984,7 +970,8 @@ const server = new McpServer(
(both subpaths are also available on `@modelcontextprotocol/client/validators/...`)
If you import from one of these subpaths in your own code, the corresponding peer dep (`ajv` + `ajv-formats`, or `@cfworker/json-schema`) needs to be installed in your `package.json`. The runtime shim continues to vendor a copy for the default code path, so you can use the subpath in some files and rely on the default in others.
If you import from one of these subpaths in your own code, the corresponding peer dep (`ajv` + `ajv-formats`, or `@cfworker/json-schema`) needs to be installed in your `package.json`. The runtime shim continues to vendor a copy for the default code path, so you can use the
subpath in some files and rely on the default in others.
To replace validation wholesale rather than customizing the built-in classes, implement the `jsonSchemaValidator` interface and pass your own implementation through the option above.

View file

@ -495,19 +495,6 @@ server.registerTool(
);
```
## Tasks (experimental)
> [!WARNING]
> The tasks API is experimental and may change without notice.
Task-based execution enables "call-now, fetch-later" patterns for long-running operations (see [Tasks](https://modelcontextprotocol.io/specification/latest/basic/utilities/tasks) in the MCP specification). Instead of returning a result immediately, a tool creates a task that can be polled or resumed later. To use tasks:
- Provide a {@linkcode @modelcontextprotocol/server!index.TaskStore | TaskStore} implementation that persists task metadata and results (see {@linkcode @modelcontextprotocol/server!index.InMemoryTaskStore | InMemoryTaskStore} for reference).
- Enable the `tasks` capability when constructing the server.
- Register tools with {@linkcode @modelcontextprotocol/server!experimental/tasks/mcpServer.ExperimentalMcpServerTasks#registerToolTask | server.experimental.tasks.registerToolTask(...)}.
For a full runnable example, see [`simpleTaskInteractive.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/src/simpleTaskInteractive.ts).
## Shutdown
For stateful multi-session HTTP servers, capture the `http.Server` from `app.listen()` so you can stop accepting connections, then close each session transport:

View file

@ -24,18 +24,17 @@ Most clients expect a server to be running. Start one from [`../server/README.md
## Example index
| Scenario | Description | File |
| --------------------------------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| Interactive Streamable HTTP client | CLI client that exercises tools/resources/prompts, notifications, elicitation, and tasks. | [`src/simpleStreamableHttp.ts`](src/simpleStreamableHttp.ts) |
| Backwards-compatible client (Streamable HTTP → SSE) | Tries Streamable HTTP first, falls back to legacy SSE on 4xx responses. | [`src/streamableHttpWithSseFallbackClient.ts`](src/streamableHttpWithSseFallbackClient.ts) |
| SSE polling client (legacy) | Polls a legacy HTTP+SSE server and demonstrates notification handling. | [`src/ssePollingClient.ts`](src/ssePollingClient.ts) |
| Parallel tool calls | Runs multiple tool calls in parallel. | [`src/parallelToolCallsClient.ts`](src/parallelToolCallsClient.ts) |
| Multiple clients in parallel | Connects multiple clients concurrently to the same server. | [`src/multipleClientsParallel.ts`](src/multipleClientsParallel.ts) |
| OAuth client (interactive) | OAuth-enabled client (dynamic registration, auth flow). | [`src/simpleOAuthClient.ts`](src/simpleOAuthClient.ts) |
| OAuth provider helper | Demonstrates reusable OAuth providers. | [`src/simpleOAuthClientProvider.ts`](src/simpleOAuthClientProvider.ts) |
| Client credentials (M2M) | Machine-to-machine OAuth client credentials example. | [`src/simpleClientCredentials.ts`](src/simpleClientCredentials.ts) |
| URL elicitation client | Drives URL-mode elicitation flows (sensitive input in a browser). | [`src/elicitationUrlExample.ts`](src/elicitationUrlExample.ts) |
| Task interactive client | Demonstrates task-based execution + interactive server→client requests. | [`src/simpleTaskInteractiveClient.ts`](src/simpleTaskInteractiveClient.ts) |
| Scenario | Description | File |
| --------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| Interactive Streamable HTTP client | CLI client that exercises tools/resources/prompts, notifications, and elicitation. | [`src/simpleStreamableHttp.ts`](src/simpleStreamableHttp.ts) |
| Backwards-compatible client (Streamable HTTP → SSE) | Tries Streamable HTTP first, falls back to legacy SSE on 4xx responses. | [`src/streamableHttpWithSseFallbackClient.ts`](src/streamableHttpWithSseFallbackClient.ts) |
| SSE polling client (legacy) | Polls a legacy HTTP+SSE server and demonstrates notification handling. | [`src/ssePollingClient.ts`](src/ssePollingClient.ts) |
| Parallel tool calls | Runs multiple tool calls in parallel. | [`src/parallelToolCallsClient.ts`](src/parallelToolCallsClient.ts) |
| Multiple clients in parallel | Connects multiple clients concurrently to the same server. | [`src/multipleClientsParallel.ts`](src/multipleClientsParallel.ts) |
| OAuth client (interactive) | OAuth-enabled client (dynamic registration, auth flow). | [`src/simpleOAuthClient.ts`](src/simpleOAuthClient.ts) |
| OAuth provider helper | Demonstrates reusable OAuth providers. | [`src/simpleOAuthClientProvider.ts`](src/simpleOAuthClientProvider.ts) |
| Client credentials (M2M) | Machine-to-machine OAuth client credentials example. | [`src/simpleClientCredentials.ts`](src/simpleClientCredentials.ts) |
| URL elicitation client | Drives URL-mode elicitation flows (sensitive input in a browser). | [`src/elicitationUrlExample.ts`](src/elicitationUrlExample.ts) |
## URL elicitation example (server + client)

View file

@ -490,7 +490,7 @@ async function errorHandling_timeout(client: Client) {
//#region errorHandling_timeout
try {
const result = await client.callTool(
{ name: 'slow-task', arguments: {} },
{ name: 'slow-operation', arguments: {} },
{ timeout: 120_000 } // 2 minutes instead of the default 60 seconds
);
console.log(result.content);
@ -530,7 +530,7 @@ async function resumptionToken_basic(client: Client) {
const result = await client.request(
{
method: 'tools/call',
params: { name: 'long-running-task', arguments: {} }
params: { name: 'long-running-operation', arguments: {} }
},
{
resumptionToken: lastToken,

View file

@ -4,7 +4,7 @@ import { createServer } from 'node:http';
import { createInterface } from 'node:readline';
import { URL } from 'node:url';
import type { CallToolResult, ListToolsRequest, OAuthClientMetadata } from '@modelcontextprotocol/client';
import type { ListToolsRequest, OAuthClientMetadata } from '@modelcontextprotocol/client';
import { Client, StreamableHTTPClientTransport, UnauthorizedError } from '@modelcontextprotocol/client';
import open from 'open';
@ -209,7 +209,7 @@ class InteractiveOAuthClient {
console.log('Commands:');
console.log(' list - List available tools');
console.log(' call <tool_name> [args] - Call a tool');
console.log(' stream <tool_name> [args] - Call a tool with streaming (shows task status)');
console.log(' stream <tool_name> [args] - (disabled; returns when the SEP-2663 tasks extension lands)');
console.log(' quit - Exit the client');
console.log();
@ -232,7 +232,7 @@ class InteractiveOAuthClient {
} else if (command.startsWith('stream ')) {
await this.handleStreamTool(command);
} else {
console.log("❌ Unknown command. Try 'list', 'call <tool_name>', 'stream <tool_name>', or 'quit'");
console.log("❌ Unknown command. Try 'list', 'call <tool_name>', or 'quit'");
}
} catch (error) {
if (error instanceof Error && error.message === 'SIGINT') {
@ -358,62 +358,11 @@ class InteractiveOAuthClient {
return;
}
try {
// Using the experimental tasks API - WARNING: may change without notice
console.log(`\n🔧 Streaming tool '${toolName}'...`);
const stream = this.client.experimental.tasks.callToolStream(
{
name: toolName,
arguments: toolArgs
},
{
task: {
taskId: `task-${Date.now()}`,
ttl: 60_000
}
}
);
// Iterate through all messages yielded by the generator
for await (const message of stream) {
switch (message.type) {
case 'taskCreated': {
console.log(`✓ Task created: ${message.task.taskId}`);
break;
}
case 'taskStatus': {
console.log(`⟳ Status: ${message.task.status}`);
if (message.task.statusMessage) {
console.log(` ${message.task.statusMessage}`);
}
break;
}
case 'result': {
console.log('✓ Completed!');
const toolResult = message.result as CallToolResult;
for (const content of toolResult.content) {
if (content.type === 'text') {
console.log(content.text);
} else {
console.log(content);
}
}
break;
}
case 'error': {
console.log('✗ Error:');
console.log(` ${message.error.message}`);
break;
}
}
}
} catch (error) {
console.error(`❌ Failed to stream tool '${toolName}':`, error);
}
// The streaming-tool demo (callToolStream) was removed with the 2025-11
// experimental tasks (SEP-2663); it returns when the tasks extension lands.
void toolName;
void toolArgs;
console.log('Streaming tool demo removed with the 2025-11 experimental tasks (SEP-2663); returns when the tasks extension lands.');
}
close(): void {

View file

@ -1,7 +1,6 @@
import { createInterface } from 'node:readline';
import type {
CallToolResult,
GetPromptRequest,
ListPromptsRequest,
ListResourcesRequest,
@ -9,15 +8,7 @@ import type {
ReadResourceRequest,
ResourceLink
} from '@modelcontextprotocol/client';
import {
Client,
getDisplayName,
InMemoryTaskStore,
ProtocolError,
ProtocolErrorCode,
RELATED_TASK_META_KEY,
StreamableHTTPClientTransport
} from '@modelcontextprotocol/client';
import { Client, getDisplayName, ProtocolError, ProtocolErrorCode, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
import { Ajv } from 'ajv';
// Create readline interface for user input
@ -56,11 +47,9 @@ function printHelp(): void {
console.log(' reconnect - Reconnect to the server');
console.log(' list-tools - List available tools');
console.log(' call-tool <name> [args] - Call a tool with optional JSON arguments');
console.log(' call-tool-task <name> [args] - Call a tool with task-based execution (example: call-tool-task delay {"duration":3000})');
console.log(' greet [name] - Call the greet tool');
console.log(' multi-greet [name] - Call the multi-greet tool with notifications');
console.log(' collect-info [type] - Test form elicitation with collect-user-info tool (contact/preferences/feedback)');
console.log(' collect-info-task [type] - Test bidirectional task support (server+client tasks) with elicitation');
console.log(' start-notifications [interval] [count] - Start periodic notifications');
console.log(' run-notifications-tool-with-resumability [interval] [count] - Run notification tool with resumability');
console.log(' list-prompts - List available prompts');
@ -135,12 +124,6 @@ function commandLoop(): void {
await callCollectInfoTool(args[1] || 'contact');
break;
}
case 'collect-info-task': {
await callCollectInfoWithTask(args[1] || 'contact');
break;
}
case 'start-notifications': {
const interval = args[1] ? Number.parseInt(args[1], 10) : 2000;
const count = args[2] ? Number.parseInt(args[2], 10) : 10;
@ -154,25 +137,6 @@ function commandLoop(): void {
await runNotificationsToolWithResumability(interval, count);
break;
}
case 'call-tool-task': {
if (args.length < 2) {
console.log('Usage: call-tool-task <name> [args]');
} else {
const toolName = args[1]!;
let toolArgs = {};
if (args.length > 2) {
try {
toolArgs = JSON.parse(args.slice(2).join(' '));
} catch {
console.log('Invalid JSON arguments. Using empty args.');
}
}
await callToolTask(toolName, toolArgs);
}
break;
}
case 'list-prompts': {
await listPrompts();
break;
@ -250,10 +214,7 @@ async function connect(url?: string): Promise<void> {
console.log(`Connecting to ${serverUrl}...`);
try {
// Create task store for client-side task support
const clientTaskStore = new InMemoryTaskStore();
// Create a new client with form elicitation capability and task support
// Create a new client with form elicitation capability
client = new Client(
{
name: 'example-client',
@ -263,14 +224,6 @@ async function connect(url?: string): Promise<void> {
capabilities: {
elicitation: {
form: {}
},
tasks: {
taskStore: clientTaskStore,
requests: {
elicitation: {
create: {}
}
}
}
}
}
@ -279,33 +232,16 @@ async function connect(url?: string): Promise<void> {
console.error('\u001B[31mClient error:', error, '\u001B[0m');
};
// Set up elicitation request handler with proper validation and task support
client.setRequestHandler('elicitation/create', async (request, extra) => {
// Set up elicitation request handler with proper validation
client.setRequestHandler('elicitation/create', async request => {
if (request.params.mode !== 'form') {
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Unsupported elicitation mode: ${request.params.mode}`);
}
console.log('\n🔔 Elicitation (form) Request Received:');
console.log(`Message: ${request.params.message}`);
console.log(`Related Task: ${request.params._meta?.[RELATED_TASK_META_KEY]?.taskId}`);
console.log(`Task Creation Requested: ${request.params.task ? 'yes' : 'no'}`);
console.log('Requested Schema:');
console.log(JSON.stringify(request.params.requestedSchema, null, 2));
// Helper to return result, optionally creating a task if requested
const returnResult = async (result: {
action: 'accept' | 'decline' | 'cancel';
content?: Record<string, string | number | boolean | string[]>;
}) => {
if (request.params.task && extra.task?.store) {
// Create a task and store the result
const task = await extra.task.store.createTask({ ttl: extra.task.requestedTtl });
await extra.task.store.storeTaskResult(task.taskId, 'completed', result);
console.log(`📋 Created client-side task: ${task.taskId}`);
return { task };
}
return result;
};
const schema = request.params.requestedSchema;
const properties = schema.properties;
const required = schema.required || [];
@ -439,7 +375,7 @@ async function connect(url?: string): Promise<void> {
}
if (inputCancelled) {
return returnResult({ action: 'cancel' });
return { action: 'cancel' };
}
// If we didn't complete all fields due to an error, try again
@ -452,7 +388,7 @@ async function connect(url?: string): Promise<void> {
continue;
} else {
console.log('Maximum attempts reached. Declining request.');
return returnResult({ action: 'decline' });
return { action: 'decline' };
}
}
@ -471,7 +407,7 @@ async function connect(url?: string): Promise<void> {
continue;
} else {
console.log('Maximum attempts reached. Declining request.');
return returnResult({ action: 'decline' });
return { action: 'decline' };
}
}
@ -488,14 +424,14 @@ async function connect(url?: string): Promise<void> {
switch (confirmAnswer) {
case 'yes':
case 'y': {
return returnResult({
return {
action: 'accept',
content
});
};
}
case 'cancel':
case 'c': {
return returnResult({ action: 'cancel' });
return { action: 'cancel' };
}
case 'no':
case 'n': {
@ -503,7 +439,7 @@ async function connect(url?: string): Promise<void> {
console.log('Please re-enter the information...');
continue;
} else {
return returnResult({ action: 'decline' });
return { action: 'decline' };
}
break;
@ -513,7 +449,7 @@ async function connect(url?: string): Promise<void> {
}
console.log('Maximum attempts reached. Declining request.');
return returnResult({ action: 'decline' });
return { action: 'decline' };
});
transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
@ -716,12 +652,6 @@ async function callCollectInfoTool(infoType: string): Promise<void> {
await callTool('collect-user-info', { infoType });
}
async function callCollectInfoWithTask(infoType: string): Promise<void> {
console.log(`\n🔄 Testing bidirectional task support with collect-user-info-task tool (${infoType})...`);
console.log('This will create a task on the server, which will elicit input and create a task on the client.\n');
await callToolTask('collect-user-info-task', { infoType });
}
async function startNotifications(interval: number, count: number): Promise<void> {
console.log(`Starting notification stream: interval=${interval}ms, count=${count || 'unlimited'}`);
await callTool('start-notification-stream', { interval, count });
@ -880,70 +810,6 @@ async function readResource(uri: string): Promise<void> {
}
}
async function callToolTask(name: string, args: Record<string, unknown>): Promise<void> {
if (!client) {
console.log('Not connected to server.');
return;
}
console.log(`Calling tool '${name}' with task-based execution...`);
console.log('Arguments:', args);
// Use task-based execution - call now, fetch later
// Using the experimental tasks API - WARNING: may change without notice
console.log('This will return immediately while processing continues in the background...');
try {
// Call the tool with task metadata using streaming API
const stream = client.experimental.tasks.callToolStream(
{
name,
arguments: args
},
{
task: {
ttl: 60_000 // Keep results for 60 seconds
}
}
);
console.log('Waiting for task completion...');
let lastStatus = '';
for await (const message of stream) {
switch (message.type) {
case 'taskCreated': {
console.log('Task created successfully with ID:', message.task.taskId);
break;
}
case 'taskStatus': {
if (lastStatus !== message.task.status) {
console.log(` ${message.task.status}${message.task.statusMessage ? ` - ${message.task.statusMessage}` : ''}`);
}
lastStatus = message.task.status;
break;
}
case 'result': {
console.log('Task completed!');
console.log('Tool result:');
const toolResult = message.result as CallToolResult;
for (const item of toolResult.content) {
if (item.type === 'text') {
console.log(` ${item.text}`);
}
}
break;
}
case 'error': {
throw message.error;
}
}
}
} catch (error) {
console.log(`Error with task-based execution: ${error}`);
}
}
async function cleanup(): Promise<void> {
if (client && transport) {
try {

View file

@ -1,204 +0,0 @@
/**
* Simple interactive task client demonstrating elicitation and sampling responses.
*
* This client connects to simpleTaskInteractive.ts server and demonstrates:
* - Handling elicitation requests (y/n confirmation)
* - Handling sampling requests (returns a hardcoded haiku)
* - Using task-based tool execution with streaming
*/
import { createInterface } from 'node:readline';
import type { CallToolResult, CreateMessageRequest, CreateMessageResult, TextContent } from '@modelcontextprotocol/client';
import { Client, ProtocolError, ProtocolErrorCode, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
// Create readline interface for user input
const readline = createInterface({
input: process.stdin,
output: process.stdout
});
function question(prompt: string): Promise<string> {
return new Promise(resolve => {
readline.question(prompt, answer => {
resolve(answer.trim());
});
});
}
function getTextContent(result: { content: Array<{ type: string; text?: string }> }): string {
const textContent = result.content.find((c): c is TextContent => c.type === 'text');
return textContent?.text ?? '(no text)';
}
async function elicitationCallback(params: {
mode?: string;
message: string;
requestedSchema?: object;
}): Promise<{ action: 'accept' | 'cancel' | 'decline'; content?: Record<string, string | number | boolean | string[]> }> {
console.log(`\n[Elicitation] Server asks: ${params.message}`);
// Simple terminal prompt for y/n
const response = await question('Your response (y/n): ');
const confirmed = ['y', 'yes', 'true', '1'].includes(response.toLowerCase());
console.log(`[Elicitation] Responding with: confirm=${confirmed}`);
return { action: 'accept', content: { confirm: confirmed } };
}
async function samplingCallback(params: CreateMessageRequest['params']): Promise<CreateMessageResult> {
// Get the prompt from the first message
let prompt = 'unknown';
if (params.messages && params.messages.length > 0) {
const firstMessage = params.messages[0]!;
const content = firstMessage.content;
if (typeof content === 'object' && !Array.isArray(content) && content.type === 'text' && 'text' in content) {
prompt = content.text;
} else if (Array.isArray(content)) {
const textPart = content.find(c => c.type === 'text' && 'text' in c);
if (textPart && 'text' in textPart) {
prompt = textPart.text;
}
}
}
console.log(`\n[Sampling] Server requests LLM completion for: ${prompt}`);
// Return a hardcoded haiku (in real use, call your LLM here)
const haiku = `Cherry blossoms fall
Softly on the quiet pond
Spring whispers goodbye`;
console.log('[Sampling] Responding with haiku');
return {
model: 'mock-haiku-model',
role: 'assistant',
content: { type: 'text', text: haiku }
};
}
async function run(url: string): Promise<void> {
console.log('Simple Task Interactive Client');
console.log('==============================');
console.log(`Connecting to ${url}...`);
// Create client with elicitation and sampling capabilities
const client = new Client(
{ name: 'simple-task-interactive-client', version: '1.0.0' },
{
capabilities: {
elicitation: { form: {} },
sampling: {}
}
}
);
// Set up elicitation request handler
client.setRequestHandler('elicitation/create', async request => {
if (request.params.mode && request.params.mode !== 'form') {
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Unsupported elicitation mode: ${request.params.mode}`);
}
return elicitationCallback(request.params);
});
// Set up sampling request handler
client.setRequestHandler('sampling/createMessage', async request => {
return samplingCallback(request.params) as unknown as ReturnType<typeof samplingCallback>;
});
// Connect to server
const transport = new StreamableHTTPClientTransport(new URL(url));
await client.connect(transport);
console.log('Connected!\n');
// List tools
const toolsResult = await client.listTools();
console.log(`Available tools: ${toolsResult.tools.map(t => t.name).join(', ')}`);
// Demo 1: Elicitation (confirm_delete)
console.log('\n--- Demo 1: Elicitation ---');
console.log('Calling confirm_delete tool...');
const confirmStream = client.experimental.tasks.callToolStream(
{ name: 'confirm_delete', arguments: { filename: 'important.txt' } },
{ task: { ttl: 60_000 } }
);
for await (const message of confirmStream) {
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': {
const toolResult = message.result as CallToolResult;
console.log(`Result: ${getTextContent(toolResult)}`);
break;
}
case 'error': {
console.error(`Error: ${message.error}`);
break;
}
}
}
// Demo 2: Sampling (write_haiku)
console.log('\n--- Demo 2: Sampling ---');
console.log('Calling write_haiku tool...');
const haikuStream = client.experimental.tasks.callToolStream(
{ name: 'write_haiku', arguments: { topic: 'autumn leaves' } },
{ task: { ttl: 60_000 } }
);
for await (const message of haikuStream) {
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': {
const toolResult = message.result as CallToolResult;
console.log(`Result:\n${getTextContent(toolResult)}`);
break;
}
case 'error': {
console.error(`Error: ${message.error}`);
break;
}
}
}
// Cleanup
console.log('\nDemo complete. Closing connection...');
await transport.close();
readline.close();
}
// Parse command line arguments
const args = process.argv.slice(2);
let url = 'http://localhost:8000/mcp';
for (let i = 0; i < args.length; i++) {
if (args[i] === '--url' && args[i + 1]) {
url = args[i + 1]!;
i++;
}
}
// Run the client
try {
await run(url);
} catch (error) {
console.error('Error running client:', error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}

View file

@ -65,16 +65,16 @@ async function main(): Promise<void> {
console.log('[Client] Connected successfully');
console.log('');
// Call the long-task tool
console.log('[Client] Calling long-task tool...');
console.log('[Client] Server will disconnect mid-task to demonstrate polling');
// Call the long-operation tool
console.log('[Client] Calling long-operation tool...');
console.log('[Client] Server will disconnect mid-operation to demonstrate polling');
console.log('');
const result = await client.request(
{
method: 'tools/call',
params: {
name: 'long-task',
name: 'long-operation',
arguments: {}
}
},

View file

@ -27,7 +27,7 @@ pnpm tsx src/simpleStreamableHttp.ts
| Scenario | Description | File |
| ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| Streamable HTTP server (stateful) | Feature-rich server with tools/resources/prompts, logging, tasks, sampling, and optional OAuth. | [`src/simpleStreamableHttp.ts`](src/simpleStreamableHttp.ts) |
| Streamable HTTP server (stateful) | Feature-rich server with tools/resources/prompts, logging, sampling, and optional OAuth. | [`src/simpleStreamableHttp.ts`](src/simpleStreamableHttp.ts) |
| Streamable HTTP server (stateless) | No session tracking; good for simple API-style servers. | [`src/simpleStatelessStreamableHttp.ts`](src/simpleStatelessStreamableHttp.ts) |
| Resource-Server-only auth | Minimal OAuth RS using `mcpAuthMetadataRouter` + `requireBearerAuth` from `@modelcontextprotocol/express` (no better-auth). | [`src/resourceServerOnly.ts`](src/resourceServerOnly.ts) |
| JSON response mode (no SSE) | Streamable HTTP with JSON-only responses and limited notifications. | [`src/jsonResponseStreamableHttp.ts`](src/jsonResponseStreamableHttp.ts) |
@ -35,8 +35,7 @@ pnpm tsx src/simpleStreamableHttp.ts
| Output schema server | Demonstrates tool output validation with structured output schemas. | [`src/mcpServerOutputSchema.ts`](src/mcpServerOutputSchema.ts) |
| Form elicitation server | Collects **non-sensitive** user input via schema-driven forms. | [`src/elicitationFormExample.ts`](src/elicitationFormExample.ts) |
| URL elicitation server | Secure browser-based flows for **sensitive** input (API keys, OAuth, payments). | [`src/elicitationUrlExample.ts`](src/elicitationUrlExample.ts) |
| Sampling + tasks server | Demonstrates sampling and experimental task-based execution. | [`src/toolWithSampleServer.ts`](src/toolWithSampleServer.ts) |
| Task interactive server | Task-based execution with interactive server→client requests. | [`src/simpleTaskInteractive.ts`](src/simpleTaskInteractive.ts) |
| Sampling server | Demonstrates server-initiated sampling requests. | [`src/toolWithSampleServer.ts`](src/toolWithSampleServer.ts) |
| Hono Streamable HTTP server | Streamable HTTP server built with Hono instead of Express. | [`src/honoWebStandardStreamableHttp.ts`](src/honoWebStandardStreamableHttp.ts) |
| SSE polling demo server | Legacy SSE server intended for polling demos. | [`src/ssePollingExample.ts`](src/ssePollingExample.ts) |

View file

@ -1,181 +0,0 @@
# Simple Task Interactive Example
This example demonstrates the MCP Tasks message queue pattern with interactive server-to-client requests (elicitation and sampling).
## Overview
The example consists of two components:
1. **Server** (`simpleTaskInteractive.ts`) - Exposes two task-based tools that require client interaction:
- `confirm_delete` - Uses elicitation to ask the user for confirmation before "deleting" a file
- `write_haiku` - Uses sampling to request an LLM to generate a haiku on a topic
2. **Client** (`simpleTaskInteractiveClient.ts`) - Connects to the server and handles:
- Elicitation requests with simple y/n terminal prompts
- Sampling requests with a mock haiku generator
## Key Concepts
### Task-Based Execution
Both tools use `execution.taskSupport: 'required'`, meaning they follow the "call-now, fetch-later" pattern:
1. Client calls tool with `task: { ttl: 60000 }` parameter
2. Server creates a task and returns `CreateTaskResult` immediately
3. Client polls via `tasks/result` to get the final result
4. Server sends elicitation/sampling requests through the task message queue
5. Client handles requests and returns responses
6. Server completes the task with the final result
### Message Queue Pattern
When a tool needs to interact with the client (elicitation or sampling), it:
1. Updates task status to `input_required`
2. Enqueues the request in the task message queue
3. Waits for the response via a Resolver
4. Updates task status back to `working`
5. Continues processing
The `TaskResultHandler` dequeues messages when the client calls `tasks/result` and routes responses back to waiting Resolvers.
## Running the Example
### Start the Server
```bash
# From anywhere in the SDK
pnpm --filter @modelcontextprotocol/examples-server exec tsx src/simpleTaskInteractive.ts
# Or with a custom port
PORT=9000 pnpm --filter @modelcontextprotocol/examples-server exec tsx src/simpleTaskInteractive.ts
```
Or, from within the `examples/server` package:
```bash
cd examples/server
pnpm tsx src/simpleTaskInteractive.ts
# Or with a custom port
PORT=9000 pnpm tsx src/simpleTaskInteractive.ts
```
The server will start on http://localhost:8000/mcp (or your custom port).
### Run the Client
```bash
# From anywhere in the SDK
pnpm --filter @modelcontextprotocol/examples-client exec tsx src/simpleTaskInteractiveClient.ts
# Or connect to a different server
pnpm --filter @modelcontextprotocol/examples-client exec tsx src/simpleTaskInteractiveClient.ts --url http://localhost:9000/mcp
```
Or, from within the `examples/client` package:
```bash
cd examples/client
pnpm tsx src/simpleTaskInteractiveClient.ts
# Or connect to a different server
pnpm tsx src/simpleTaskInteractiveClient.ts --url http://localhost:9000/mcp
```
## Expected Output
### Server Output
```
Starting server on http://localhost:8000/mcp
Available tools:
- confirm_delete: Demonstrates elicitation (asks user y/n)
- write_haiku: Demonstrates sampling (requests LLM completion)
[Server] confirm_delete called, task created: task-abc123
[Server] confirm_delete: asking about 'important.txt'
[Server] Sending elicitation request to client...
[Server] tasks/result called for task task-abc123
[Server] Delivering queued request message for task task-abc123
[Server] Received elicitation response: action=accept, content={"confirm":true}
[Server] Completing task with result: Deleted 'important.txt'
[Server] write_haiku called, task created: task-def456
[Server] write_haiku: topic 'autumn leaves'
[Server] Sending sampling request to client...
[Server] tasks/result called for task task-def456
[Server] Delivering queued request message for task task-def456
[Server] Received sampling response: Cherry blossoms fall...
[Server] Completing task with haiku
```
### Client Output
```
Simple Task Interactive Client
==============================
Connecting to http://localhost:8000/mcp...
Connected!
Available tools: confirm_delete, write_haiku
--- Demo 1: Elicitation ---
Calling confirm_delete tool...
Task created: task-abc123
Task status: working
[Elicitation] Server asks: Are you sure you want to delete 'important.txt'?
Your response (y/n): y
[Elicitation] Responding with: confirm=true
Task status: input_required
Task status: completed
Result: Deleted 'important.txt'
--- Demo 2: Sampling ---
Calling write_haiku tool...
Task created: task-def456
Task status: working
[Sampling] Server requests LLM completion for: Write a haiku about autumn leaves
[Sampling] Responding with haiku
Task status: input_required
Task status: completed
Result:
Haiku:
Cherry blossoms fall
Softly on the quiet pond
Spring whispers goodbye
Demo complete. Closing connection...
```
## Implementation Details
### Server Components
- **Resolver**: Promise-like class for passing results between async operations
- **TaskMessageQueueWithResolvers**: Extended message queue that tracks pending requests with their Resolvers
- **TaskStoreWithNotifications**: Extended task store with notification support for status changes
- **TaskResultHandler**: Handles `tasks/result` requests by dequeuing messages and routing responses
- **TaskSession**: Wraps the server to enqueue requests during task execution
### Client Capabilities
The client declares these capabilities during initialization:
```typescript
capabilities: {
elicitation: { form: {} },
sampling: {}
}
```
This tells the server that the client can handle both form-based elicitation and sampling requests.
## Related Files
- `packages/core/src/experimental/tasks/interfaces.ts` - Core task interfaces (TaskStore, TaskMessageQueue)
- `packages/core/src/experimental/tasks/stores/in-memory.ts` - In-memory task store implementation
- `packages/core/src/types/types.ts` - Task-related types (Task, CreateTaskResult, GetTaskRequestSchema, etc.)

View file

@ -5,13 +5,12 @@ import { createMcpExpressApp, getOAuthProtectedResourceMetadataUrl, requireBeare
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
import type {
CallToolResult,
ElicitResult,
GetPromptResult,
PrimitiveSchemaDefinition,
ReadResourceResult,
ResourceLink
} from '@modelcontextprotocol/server';
import { InMemoryTaskMessageQueue, InMemoryTaskStore, isInitializeRequest, McpServer } from '@modelcontextprotocol/server';
import { isInitializeRequest, McpServer } from '@modelcontextprotocol/server';
import cors from 'cors';
import type { Request, Response } from 'express';
import * as z from 'zod/v4';
@ -22,9 +21,6 @@ import { InMemoryEventStore } from './inMemoryEventStore.js';
const useOAuth = process.argv.includes('--oauth');
const dangerousLoggingEnabled = process.argv.includes('--dangerous-logging-enabled');
// Create shared task store for demonstration
const taskStore = new InMemoryTaskStore();
// Create an MCP server with implementation details
const getServer = () => {
const server = new McpServer(
@ -36,12 +32,7 @@ const getServer = () => {
},
{
capabilities: {
logging: {},
tasks: {
requests: { tools: { call: {} } },
taskStore,
taskMessageQueue: new InMemoryTaskMessageQueue()
}
logging: {}
}
}
);
@ -439,160 +430,6 @@ const getServer = () => {
}
);
// Register a long-running tool that demonstrates task execution
// Using the experimental tasks API - WARNING: may change without notice
server.experimental.tasks.registerToolTask(
'delay',
{
title: 'Delay',
description: 'A simple tool that delays for a specified duration, useful for testing task execution',
inputSchema: z.object({
duration: z.number().describe('Duration in milliseconds').default(5000)
})
},
{
async createTask({ duration }, ctx) {
// Create the task
const task = await ctx.task.store.createTask({
ttl: ctx.task.requestedTtl
});
// Simulate out-of-band work
(async () => {
await new Promise(resolve => setTimeout(resolve, duration));
await ctx.task.store.storeTaskResult(task.taskId, 'completed', {
content: [
{
type: 'text',
text: `Completed ${duration}ms delay`
}
]
});
})();
// Return CreateTaskResult with the created task
return {
task
};
},
async getTask(_args, ctx) {
return await ctx.task.store.getTask(ctx.task.id);
},
async getTaskResult(_args, ctx) {
const result = await ctx.task.store.getTaskResult(ctx.task.id);
return result as CallToolResult;
}
}
);
// Register a tool that demonstrates bidirectional task support:
// Server creates a task, then elicits input from client using elicitInputStream
// Using the experimental tasks API - WARNING: may change without notice
server.experimental.tasks.registerToolTask(
'collect-user-info-task',
{
title: 'Collect Info with Task',
description: 'Collects user info via elicitation with task support using elicitInputStream',
inputSchema: z.object({
infoType: z.enum(['contact', 'preferences']).describe('Type of information to collect').default('contact')
})
},
{
async createTask({ infoType }, ctx) {
// Create the server-side task
const task = await ctx.task.store.createTask({
ttl: ctx.task.requestedTtl
});
// Perform async work that makes a nested elicitation request using elicitInputStream
(async () => {
try {
const message = infoType === 'contact' ? 'Please provide your contact information' : 'Please set your preferences';
// Define schemas with proper typing for PrimitiveSchemaDefinition
const contactSchema: {
type: 'object';
properties: Record<string, PrimitiveSchemaDefinition>;
required: string[];
} = {
type: 'object',
properties: {
name: { type: 'string', title: 'Full Name', description: 'Your full name' },
email: { type: 'string', title: 'Email', description: 'Your email address' }
},
required: ['name', 'email']
};
const preferencesSchema: {
type: 'object';
properties: Record<string, PrimitiveSchemaDefinition>;
required: string[];
} = {
type: 'object',
properties: {
theme: { type: 'string', title: 'Theme', enum: ['light', 'dark', 'auto'] },
notifications: { type: 'boolean', title: 'Enable Notifications', default: true }
},
required: ['theme']
};
const requestedSchema = infoType === 'contact' ? contactSchema : preferencesSchema;
// Use elicitInputStream to elicit input from client
// This demonstrates the streaming elicitation API
// Access via server.server to get the underlying Server instance
const stream = server.server.experimental.tasks.elicitInputStream({
mode: 'form',
message,
requestedSchema
});
let elicitResult: ElicitResult | undefined;
for await (const msg of stream) {
if (msg.type === 'result') {
elicitResult = msg.result as ElicitResult;
} else if (msg.type === 'error') {
throw msg.error;
}
}
if (!elicitResult) {
throw new Error('No result received from elicitation');
}
let resultText: string;
if (elicitResult.action === 'accept') {
resultText = `Collected ${infoType} info: ${JSON.stringify(elicitResult.content, null, 2)}`;
} else if (elicitResult.action === 'decline') {
resultText = `User declined to provide ${infoType} information`;
} else {
resultText = 'User cancelled the request';
}
await taskStore.storeTaskResult(task.taskId, 'completed', {
content: [{ type: 'text', text: resultText }]
});
} catch (error) {
console.error('Error in collect-user-info-task:', error);
await taskStore.storeTaskResult(task.taskId, 'failed', {
content: [{ type: 'text', text: `Error: ${error}` }],
isError: true
});
}
})();
return { task };
},
async getTask(_args, ctx) {
return await ctx.task.store.getTask(ctx.task.id);
},
async getTaskResult(_args, ctx) {
const result = await ctx.task.store.getTaskResult(ctx.task.id);
return result as CallToolResult;
}
}
);
return server;
};

View file

@ -1,758 +0,0 @@
/**
* Simple interactive task server demonstrating elicitation and sampling.
*
* This server demonstrates the task message queue pattern from the MCP Tasks spec:
* - confirm_delete: Uses elicitation to ask the user for confirmation
* - write_haiku: Uses sampling to request an LLM to generate content
*
* Both tools use the "call-now, fetch-later" pattern where the initial call
* creates a task, and the result is fetched via tasks/result endpoint.
*/
import { randomUUID } from 'node:crypto';
import { createMcpExpressApp } from '@modelcontextprotocol/express';
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
import type {
CallToolResult,
CreateMessageRequest,
CreateMessageResult,
CreateTaskOptions,
CreateTaskResult,
ElicitRequestFormParams,
ElicitResult,
GetTaskPayloadResult,
GetTaskResult,
JSONRPCRequest,
PrimitiveSchemaDefinition,
QueuedMessage,
QueuedRequest,
RequestId,
Result,
SamplingMessage,
Task,
TaskMessageQueue,
TextContent,
Tool
} from '@modelcontextprotocol/server';
import { InMemoryTaskStore, isTerminal, RELATED_TASK_META_KEY, Server } from '@modelcontextprotocol/server';
import type { Request, Response } from 'express';
// ============================================================================
// Resolver - Promise-like for passing results between async operations
// ============================================================================
class Resolver<T> {
private _resolve!: (value: T) => void;
private _reject!: (error: Error) => void;
private _promise: Promise<T>;
private _done = false;
constructor() {
this._promise = new Promise<T>((resolve, reject) => {
this._resolve = resolve;
this._reject = reject;
});
}
setResult(value: T): void {
if (this._done) return;
this._done = true;
this._resolve(value);
}
setException(error: Error): void {
if (this._done) return;
this._done = true;
this._reject(error);
}
wait(): Promise<T> {
return this._promise;
}
done(): boolean {
return this._done;
}
}
// ============================================================================
// Extended message queue with resolver support and wait functionality
// ============================================================================
interface QueuedRequestWithResolver extends QueuedRequest {
resolver?: Resolver<Record<string, unknown>>;
originalRequestId?: RequestId;
}
type QueuedMessageWithResolver = QueuedRequestWithResolver | QueuedMessage;
class TaskMessageQueueWithResolvers implements TaskMessageQueue {
private queues = new Map<string, QueuedMessageWithResolver[]>();
private waitResolvers = new Map<string, (() => void)[]>();
private getQueue(taskId: string): QueuedMessageWithResolver[] {
let queue = this.queues.get(taskId);
if (!queue) {
queue = [];
this.queues.set(taskId, queue);
}
return queue;
}
async enqueue(taskId: string, message: QueuedMessage, _sessionId?: string, maxSize?: number): Promise<void> {
const queue = this.getQueue(taskId);
if (maxSize !== undefined && queue.length >= maxSize) {
throw new Error(`Task message queue overflow: queue size (${queue.length}) exceeds maximum (${maxSize})`);
}
queue.push(message);
// Notify any waiters
this.notifyWaiters(taskId);
}
async enqueueWithResolver(
taskId: string,
message: JSONRPCRequest,
resolver: Resolver<Record<string, unknown>>,
originalRequestId: RequestId
): Promise<void> {
const queue = this.getQueue(taskId);
const queuedMessage: QueuedRequestWithResolver = {
type: 'request',
message,
timestamp: Date.now(),
resolver,
originalRequestId
};
queue.push(queuedMessage);
this.notifyWaiters(taskId);
}
async dequeue(taskId: string, _sessionId?: string): Promise<QueuedMessageWithResolver | undefined> {
const queue = this.getQueue(taskId);
return queue.shift();
}
async dequeueAll(taskId: string, _sessionId?: string): Promise<QueuedMessageWithResolver[]> {
const queue = this.queues.get(taskId) ?? [];
this.queues.delete(taskId);
return queue;
}
async waitForMessage(taskId: string): Promise<void> {
// Check if there are already messages
const queue = this.getQueue(taskId);
if (queue.length > 0) return;
// Wait for a message to be added
return new Promise<void>(resolve => {
let waiters = this.waitResolvers.get(taskId);
if (!waiters) {
waiters = [];
this.waitResolvers.set(taskId, waiters);
}
waiters.push(resolve);
});
}
private notifyWaiters(taskId: string): void {
const waiters = this.waitResolvers.get(taskId);
if (waiters) {
this.waitResolvers.delete(taskId);
for (const resolve of waiters) {
resolve();
}
}
}
cleanup(): void {
this.queues.clear();
this.waitResolvers.clear();
}
}
// ============================================================================
// Extended task store with wait functionality
// ============================================================================
class TaskStoreWithNotifications extends InMemoryTaskStore {
private updateResolvers = new Map<string, (() => void)[]>();
override async updateTaskStatus(taskId: string, status: Task['status'], statusMessage?: string, sessionId?: string): Promise<void> {
await super.updateTaskStatus(taskId, status, statusMessage, sessionId);
this.notifyUpdate(taskId);
}
override async storeTaskResult(taskId: string, status: 'completed' | 'failed', result: Result, sessionId?: string): Promise<void> {
await super.storeTaskResult(taskId, status, result, sessionId);
this.notifyUpdate(taskId);
}
async waitForUpdate(taskId: string): Promise<void> {
return new Promise<void>(resolve => {
let waiters = this.updateResolvers.get(taskId);
if (!waiters) {
waiters = [];
this.updateResolvers.set(taskId, waiters);
}
waiters.push(resolve);
});
}
private notifyUpdate(taskId: string): void {
const waiters = this.updateResolvers.get(taskId);
if (waiters) {
this.updateResolvers.delete(taskId);
for (const resolve of waiters) {
resolve();
}
}
}
}
// ============================================================================
// Task Result Handler - delivers queued messages and routes responses
// ============================================================================
class TaskResultHandler {
private pendingRequests = new Map<RequestId, Resolver<Record<string, unknown>>>();
constructor(
private store: TaskStoreWithNotifications,
private queue: TaskMessageQueueWithResolvers
) {}
async handle(taskId: string, server: Server, _sessionId: string): Promise<Result> {
while (true) {
// Get fresh task state
const task = await this.store.getTask(taskId);
if (!task) {
throw new Error(`Task not found: ${taskId}`);
}
// Dequeue and send all pending messages
await this.deliverQueuedMessages(taskId, server, _sessionId);
// If task is terminal, return result
if (isTerminal(task.status)) {
const result = await this.store.getTaskResult(taskId);
// Add related-task metadata per spec
return {
...result,
_meta: {
...result._meta,
[RELATED_TASK_META_KEY]: { taskId }
}
};
}
// Wait for task update or new message
await this.waitForUpdate(taskId);
}
}
private async deliverQueuedMessages(taskId: string, server: Server, _sessionId: string): Promise<void> {
while (true) {
const message = await this.queue.dequeue(taskId);
if (!message) break;
console.log(`[Server] Delivering queued ${message.type} message for task ${taskId}`);
if (message.type === 'request') {
const reqMessage = message as QueuedRequestWithResolver;
// Send the request via the server
// Store the resolver so we can route the response back
if (reqMessage.resolver && reqMessage.originalRequestId) {
this.pendingRequests.set(reqMessage.originalRequestId, reqMessage.resolver);
}
// Send the message - for elicitation/sampling, we use the server's methods
// But since we're in tasks/result context, we need to send via transport
// This is simplified - in production you'd use proper message routing
try {
const request = reqMessage.message;
let response: ElicitResult | CreateMessageResult;
if (request.method === 'elicitation/create') {
// Send elicitation request to client
const params = request.params as ElicitRequestFormParams;
response = await server.elicitInput(params);
} else if (request.method === 'sampling/createMessage') {
// Send sampling request to client
const params = request.params as CreateMessageRequest['params'];
response = await server.createMessage(params);
} else {
throw new Error(`Unknown request method: ${request.method}`);
}
// Route response back to resolver
if (reqMessage.resolver) {
reqMessage.resolver.setResult(response as unknown as Record<string, unknown>);
}
} catch (error) {
if (reqMessage.resolver) {
reqMessage.resolver.setException(error instanceof Error ? error : new Error(String(error)));
}
}
}
// For notifications, we'd send them too but this example focuses on requests
}
}
private async waitForUpdate(taskId: string): Promise<void> {
// Race between store update and queue message
await Promise.race([this.store.waitForUpdate(taskId), this.queue.waitForMessage(taskId)]);
}
routeResponse(requestId: RequestId, response: Record<string, unknown>): boolean {
const resolver = this.pendingRequests.get(requestId);
if (resolver && !resolver.done()) {
this.pendingRequests.delete(requestId);
resolver.setResult(response);
return true;
}
return false;
}
routeError(requestId: RequestId, error: Error): boolean {
const resolver = this.pendingRequests.get(requestId);
if (resolver && !resolver.done()) {
this.pendingRequests.delete(requestId);
resolver.setException(error);
return true;
}
return false;
}
}
// ============================================================================
// Task Session - wraps server to enqueue requests during task execution
// ============================================================================
class TaskSession {
private requestCounter = 0;
constructor(
private server: Server,
private taskId: string,
private store: TaskStoreWithNotifications,
private queue: TaskMessageQueueWithResolvers
) {}
private nextRequestId(): string {
return `task-${this.taskId}-${++this.requestCounter}`;
}
async elicit(
message: string,
requestedSchema: {
type: 'object';
properties: Record<string, PrimitiveSchemaDefinition>;
required?: string[];
}
): Promise<{ action: string; content?: Record<string, unknown> }> {
// Update task status to input_required
await this.store.updateTaskStatus(this.taskId, 'input_required');
const requestId = this.nextRequestId();
// Build the elicitation request with related-task metadata
const params: ElicitRequestFormParams = {
message,
requestedSchema,
mode: 'form',
_meta: {
[RELATED_TASK_META_KEY]: { taskId: this.taskId }
}
};
const jsonrpcRequest: JSONRPCRequest = {
jsonrpc: '2.0',
id: requestId,
method: 'elicitation/create',
params
};
// Create resolver to wait for response
const resolver = new Resolver<Record<string, unknown>>();
// Enqueue the request
await this.queue.enqueueWithResolver(this.taskId, jsonrpcRequest, resolver, requestId);
try {
// Wait for response
const response = await resolver.wait();
// Update status back to working
await this.store.updateTaskStatus(this.taskId, 'working');
return response as { action: string; content?: Record<string, unknown> };
} catch (error) {
await this.store.updateTaskStatus(this.taskId, 'working');
throw error;
}
}
async createMessage(
messages: SamplingMessage[],
maxTokens: number
): Promise<{ role: string; content: TextContent | { type: string } }> {
// Update task status to input_required
await this.store.updateTaskStatus(this.taskId, 'input_required');
const requestId = this.nextRequestId();
// Build the sampling request with related-task metadata
const params = {
messages,
maxTokens,
_meta: {
[RELATED_TASK_META_KEY]: { taskId: this.taskId }
}
};
const jsonrpcRequest: JSONRPCRequest = {
jsonrpc: '2.0',
id: requestId,
method: 'sampling/createMessage',
params
};
// Create resolver to wait for response
const resolver = new Resolver<Record<string, unknown>>();
// Enqueue the request
await this.queue.enqueueWithResolver(this.taskId, jsonrpcRequest, resolver, requestId);
try {
// Wait for response
const response = await resolver.wait();
// Update status back to working
await this.store.updateTaskStatus(this.taskId, 'working');
return response as { role: string; content: TextContent | { type: string } };
} catch (error) {
await this.store.updateTaskStatus(this.taskId, 'working');
throw error;
}
}
}
// ============================================================================
// Server Setup
// ============================================================================
const PORT = process.env.PORT ? Number.parseInt(process.env.PORT, 10) : 8000;
// Create shared stores
const taskStore = new TaskStoreWithNotifications();
const messageQueue = new TaskMessageQueueWithResolvers();
const taskResultHandler = new TaskResultHandler(taskStore, messageQueue);
// Track active task executions
const activeTaskExecutions = new Map<
string,
{
promise: Promise<void>;
server: Server;
sessionId: string;
}
>();
// Create the server
const createServer = (): Server => {
const server = new Server(
{ name: 'simple-task-interactive', version: '1.0.0' },
{
capabilities: {
tools: {},
tasks: {
requests: {
tools: { call: {} }
}
}
}
}
);
// Register tools
server.setRequestHandler('tools/list', async (): Promise<{ tools: Tool[] }> => {
return {
tools: [
{
name: 'confirm_delete',
description: 'Asks for confirmation before deleting (demonstrates elicitation)',
inputSchema: {
type: 'object',
properties: {
filename: { type: 'string' }
}
},
execution: { taskSupport: 'required' }
},
{
name: 'write_haiku',
description: 'Asks LLM to write a haiku (demonstrates sampling)',
inputSchema: {
type: 'object',
properties: {
topic: { type: 'string' }
}
},
execution: { taskSupport: 'required' }
}
]
};
});
// Handle tool calls
server.setRequestHandler('tools/call', async (request, ctx): Promise<CallToolResult | CreateTaskResult> => {
const { name, arguments: args } = request.params;
const taskParams = (request.params._meta?.task || request.params.task) as { ttl?: number; pollInterval?: number } | undefined;
// Validate task mode - these tools require tasks
if (!taskParams) {
throw new Error(`Tool ${name} requires task mode`);
}
// Create task
const taskOptions: CreateTaskOptions = {
ttl: taskParams.ttl,
pollInterval: taskParams.pollInterval ?? 1000
};
const task = await taskStore.createTask(taskOptions, ctx.mcpReq.id, request, ctx.sessionId);
console.log(`\n[Server] ${name} called, task created: ${task.taskId}`);
// Start background task execution
const taskExecution = (async () => {
try {
const taskSession = new TaskSession(server, task.taskId, taskStore, messageQueue);
if (name === 'confirm_delete') {
const filename = args?.filename ?? 'unknown.txt';
console.log(`[Server] confirm_delete: asking about '${filename}'`);
console.log('[Server] Sending elicitation request to client...');
const result = await taskSession.elicit(`Are you sure you want to delete '${filename}'?`, {
type: 'object',
properties: {
confirm: { type: 'boolean' }
},
required: ['confirm']
});
console.log(
`[Server] Received elicitation response: action=${result.action}, content=${JSON.stringify(result.content)}`
);
let text: string;
if (result.action === 'accept' && result.content) {
const confirmed = result.content.confirm;
text = confirmed ? `Deleted '${filename}'` : 'Deletion cancelled';
} else {
text = 'Deletion cancelled';
}
console.log(`[Server] Completing task with result: ${text}`);
await taskStore.storeTaskResult(task.taskId, 'completed', {
content: [{ type: 'text', text }]
});
} else if (name === 'write_haiku') {
const topic = args?.topic ?? 'nature';
console.log(`[Server] write_haiku: topic '${topic}'`);
console.log('[Server] Sending sampling request to client...');
const result = await taskSession.createMessage(
[
{
role: 'user',
content: { type: 'text', text: `Write a haiku about ${topic}` }
}
],
50
);
let haiku = 'No response';
if (result.content && 'text' in result.content) {
haiku = (result.content as TextContent).text;
}
console.log(`[Server] Received sampling response: ${haiku.slice(0, 50)}...`);
console.log('[Server] Completing task with haiku');
await taskStore.storeTaskResult(task.taskId, 'completed', {
content: [{ type: 'text', text: `Haiku:\n${haiku}` }]
});
}
} catch (error) {
console.error(`[Server] Task ${task.taskId} failed:`, error);
await taskStore.storeTaskResult(task.taskId, 'failed', {
content: [{ type: 'text', text: `Error: ${error}` }],
isError: true
});
} finally {
activeTaskExecutions.delete(task.taskId);
}
})();
activeTaskExecutions.set(task.taskId, {
promise: taskExecution,
server,
sessionId: ctx.sessionId ?? ''
});
return { task };
});
// Handle tasks/get
server.setRequestHandler('tasks/get', async (request): Promise<GetTaskResult> => {
const { taskId } = request.params;
const task = await taskStore.getTask(taskId);
if (!task) {
throw new Error(`Task ${taskId} not found`);
}
return task;
});
// Handle tasks/result
server.setRequestHandler('tasks/result', async (request, ctx): Promise<GetTaskPayloadResult> => {
const { taskId } = request.params;
console.log(`[Server] tasks/result called for task ${taskId}`);
return taskResultHandler.handle(taskId, server, ctx.sessionId ?? '');
});
return server;
};
// ============================================================================
// Express App Setup
// ============================================================================
const app = createMcpExpressApp();
// Map to store transports by session ID
const transports: { [sessionId: string]: NodeStreamableHTTPServerTransport } = {};
// Helper to check if request is initialize
const isInitializeRequest = (body: unknown): boolean => {
return typeof body === 'object' && body !== null && 'method' in body && (body as { method: string }).method === 'initialize';
};
// MCP POST endpoint
app.post('/mcp', async (req: Request, res: Response) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
try {
let transport: NodeStreamableHTTPServerTransport;
if (sessionId && transports[sessionId]) {
transport = transports[sessionId];
} else if (!sessionId && isInitializeRequest(req.body)) {
transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: sid => {
console.log(`Session initialized: ${sid}`);
transports[sid] = transport;
}
});
transport.onclose = () => {
const sid = transport.sessionId;
if (sid && transports[sid]) {
console.log(`Transport closed for session ${sid}`);
delete transports[sid];
}
};
const server = createServer();
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
return;
} else if (sessionId) {
res.status(404).json({
jsonrpc: '2.0',
error: { code: -32_001, message: 'Session not found' },
id: null
});
return;
} else {
res.status(400).json({
jsonrpc: '2.0',
error: { code: -32_000, message: 'Bad Request: Session ID required' },
id: null
});
return;
}
await transport.handleRequest(req, res, req.body);
} catch (error) {
console.error('Error handling MCP request:', error);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: '2.0',
error: { code: -32_603, message: 'Internal server error' },
id: null
});
}
}
});
// Handle GET requests for SSE streams
app.get('/mcp', async (req: Request, res: Response) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
if (!sessionId) {
res.status(400).send('Missing session ID');
return;
}
if (!transports[sessionId]) {
res.status(404).send('Session not found');
return;
}
const transport = transports[sessionId];
await transport.handleRequest(req, res);
});
// Handle DELETE requests for session termination
app.delete('/mcp', async (req: Request, res: Response) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
if (!sessionId) {
res.status(400).send('Missing session ID');
return;
}
if (!transports[sessionId]) {
res.status(404).send('Session not found');
return;
}
console.log(`Session termination request: ${sessionId}`);
const transport = transports[sessionId];
await transport.handleRequest(req, res);
});
// Start server
app.listen(PORT, () => {
console.log(`Starting server on http://localhost:${PORT}/mcp`);
console.log('\nAvailable tools:');
console.log(' - confirm_delete: Demonstrates elicitation (asks user y/n)');
console.log(' - write_haiku: Demonstrates sampling (requests LLM completion)');
});
// Handle shutdown
process.on('SIGINT', async () => {
console.log('\nShutting down server...');
for (const sessionId of Object.keys(transports)) {
try {
await transports[sessionId]!.close();
delete transports[sessionId];
} catch (error) {
console.error(`Error closing session ${sessionId}:`, error);
}
}
taskStore.cleanup();
messageQueue.cleanup();
console.log('Server shutdown complete');
process.exit(0);
});

View file

@ -37,14 +37,14 @@ const getServer = () => {
// Register a long-running tool that demonstrates server-initiated disconnect
server.registerTool(
'long-task',
'long-operation',
{
description: 'A long-running task that sends progress updates. Server will disconnect mid-task to demonstrate polling.'
description: 'A long-running operation that sends progress updates. Server will disconnect mid-stream to demonstrate polling.'
},
async (ctx): Promise<CallToolResult> => {
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
console.log(`[${ctx.sessionId}] Starting long-task...`);
console.log(`[${ctx.sessionId}] Starting long-operation...`);
// Send first progress notification
await ctx.mcpReq.log('info', 'Progress: 25% - Starting work...');
@ -70,13 +70,13 @@ const getServer = () => {
await sleep(500);
await ctx.mcpReq.log('info', 'Progress: 100% - Complete!');
console.log(`[${ctx.sessionId}] Task complete`);
console.log(`[${ctx.sessionId}] Operation complete`);
return {
content: [
{
type: 'text',
text: 'Long task completed successfully!'
text: 'Long operation completed successfully!'
}
]
};
@ -131,5 +131,5 @@ app.listen(PORT, () => {
console.log('- retryInterval: 2000ms (client waits 2s before reconnecting)');
console.log('- eventStore: InMemoryEventStore (events are persisted for replay)');
console.log('');
console.log('Try calling the "long-task" tool to see server-initiated disconnect in action.');
console.log('Try calling the "long-operation" tool to see server-initiated disconnect in action.');
});

View file

@ -29,24 +29,19 @@ import type {
Result,
ServerCapabilities,
SubscribeRequest,
TaskManagerOptions,
Tool,
Transport,
UnsubscribeRequest
} from '@modelcontextprotocol/core';
import {
assertClientRequestTaskCapability,
assertToolsCallTaskCapability,
CallToolResultSchema,
CompleteResultSchema,
CreateMessageRequestSchema,
CreateMessageResultSchema,
CreateMessageResultWithToolsSchema,
CreateTaskResultSchema,
ElicitRequestSchema,
ElicitResultSchema,
EmptyResultSchema,
extractTaskManagerOptions,
GetPromptResultSchema,
InitializeResultSchema,
LATEST_PROTOCOL_VERSION,
@ -65,8 +60,6 @@ import {
SdkErrorCode
} from '@modelcontextprotocol/core';
import { ExperimentalClientTasks } from '../experimental/tasks/client.js';
/**
* Elicitation default application helper. Applies defaults to the `data` based on the `schema`.
*
@ -141,19 +134,11 @@ export function getSupportedElicitationModes(capabilities: ClientCapabilities['e
return { supportsFormMode, supportsUrlMode };
}
/**
* Extended tasks capability that includes runtime configuration (store, messageQueue).
* The runtime-only fields are stripped before advertising capabilities to servers.
*/
export type ClientTasksCapabilityWithRuntime = NonNullable<ClientCapabilities['tasks']> & TaskManagerOptions;
export type ClientOptions = ProtocolOptions & {
/**
* Capabilities to advertise as being supported by this client.
*/
capabilities?: Omit<ClientCapabilities, 'tasks'> & {
tasks?: ClientTasksCapabilityWithRuntime;
};
capabilities?: ClientCapabilities;
/**
* JSON Schema validator for tool output validation.
@ -230,9 +215,6 @@ export class Client extends Protocol<ClientContext> {
private _instructions?: string;
private _jsonSchemaValidator: jsonSchemaValidator;
private _cachedToolOutputValidators: Map<string, JsonSchemaValidator<unknown>> = new Map();
private _cachedKnownTaskTools: Set<string> = new Set();
private _cachedRequiredTaskTools: Set<string> = new Set();
private _experimental?: { tasks: ExperimentalClientTasks };
private _listChangedDebounceTimers: Map<string, ReturnType<typeof setTimeout>> = new Map();
private _pendingListChangedConfig?: ListChangedHandlers;
private _enforceStrictCapabilities: boolean;
@ -244,22 +226,11 @@ export class Client extends Protocol<ClientContext> {
private _clientInfo: Implementation,
options?: ClientOptions
) {
super({
...options,
tasks: extractTaskManagerOptions(options?.capabilities?.tasks)
});
super(options);
this._capabilities = options?.capabilities ? { ...options.capabilities } : {};
this._jsonSchemaValidator = options?.jsonSchemaValidator ?? new DefaultJsonSchemaValidator();
this._enforceStrictCapabilities = options?.enforceStrictCapabilities ?? false;
// Strip runtime-only fields from advertised capabilities
if (options?.capabilities?.tasks) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { taskStore, taskMessageQueue, defaultTaskPollInterval, maxTaskQueueSize, ...wireCapabilities } =
options.capabilities.tasks;
this._capabilities.tasks = wireCapabilities;
}
// Store list changed config for setup after connection (when we know server capabilities)
if (options?.listChanged) {
this._pendingListChangedConfig = options.listChanged;
@ -299,22 +270,6 @@ export class Client extends Protocol<ClientContext> {
}
}
/**
* Access experimental features.
*
* WARNING: These APIs are experimental and may change without notice.
*
* @experimental
*/
get experimental(): { tasks: ExperimentalClientTasks } {
if (!this._experimental) {
this._experimental = {
tasks: new ExperimentalClientTasks(this)
};
}
return this._experimental;
}
/**
* Registers new capabilities. This can only be called before connecting to a transport.
*
@ -360,20 +315,6 @@ export class Client extends Protocol<ClientContext> {
const result = await handler(request, ctx);
// When task creation is requested, validate and return CreateTaskResult
if (params.task) {
const taskValidationResult = parseSchema(CreateTaskResultSchema, result);
if (!taskValidationResult.success) {
const errorMessage =
taskValidationResult.error instanceof Error
? taskValidationResult.error.message
: String(taskValidationResult.error);
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid task creation result: ${errorMessage}`);
}
return taskValidationResult.data;
}
// For non-task requests, validate against ElicitResultSchema
const validationResult = parseSchema(ElicitResultSchema, result);
if (!validationResult.success) {
// Type guard: if success is false, error is guaranteed to exist
@ -416,20 +357,6 @@ export class Client extends Protocol<ClientContext> {
const result = await handler(request, ctx);
// When task creation is requested, validate and return CreateTaskResult
if (params.task) {
const taskValidationResult = parseSchema(CreateTaskResultSchema, result);
if (!taskValidationResult.success) {
const errorMessage =
taskValidationResult.error instanceof Error
? taskValidationResult.error.message
: String(taskValidationResult.error);
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid task creation result: ${errorMessage}`);
}
return taskValidationResult.data;
}
// For non-task requests, validate against appropriate schema based on tools presence
const hasTools = params.tools || params.toolChoice;
const resultSchema = hasTools ? CreateMessageResultWithToolsSchema : CreateMessageResultSchema;
const validationResult = parseSchema(resultSchema, result);
@ -701,14 +628,6 @@ export class Client extends Protocol<ClientContext> {
}
}
protected assertTaskCapability(method: string): void {
assertToolsCallTaskCapability(this._serverCapabilities?.tasks?.requests, method, 'Server');
}
protected assertTaskHandlerCapability(method: string): void {
assertClientRequestTaskCapability(this._capabilities?.tasks?.requests, method, 'Client');
}
async ping(options?: RequestOptions) {
return this._requestWithSchema({ method: 'ping' }, EmptyResultSchema, options);
}
@ -828,8 +747,6 @@ export class Client extends Protocol<ClientContext> {
* a problem), and thrown {@linkcode ProtocolError} for protocol-level failures or {@linkcode SdkError} for
* SDK-level issues (timeouts, missing capabilities).
*
* For task-based execution with streaming behavior, use {@linkcode ExperimentalClientTasks.callToolStream | client.experimental.tasks.callToolStream()} instead.
*
* @example Basic usage
* ```ts source="./client.examples.ts#Client_callTool_basic"
* const result = await client.callTool({
@ -860,14 +777,6 @@ export class Client extends Protocol<ClientContext> {
* ```
*/
async callTool(params: CallToolRequest['params'], options?: RequestOptions) {
// Guard: required-task tools need experimental API
if (this.isToolTaskRequired(params.name)) {
throw new ProtocolError(
ProtocolErrorCode.InvalidRequest,
`Tool "${params.name}" requires task-based execution. Use client.experimental.tasks.callToolStream() instead.`
);
}
const result = await this._requestWithSchema({ method: 'tools/call', params }, CallToolResultSchema, options);
// Check if the tool has an outputSchema
@ -908,30 +817,12 @@ export class Client extends Protocol<ClientContext> {
return result;
}
private isToolTask(toolName: string): boolean {
if (!this._serverCapabilities?.tasks?.requests?.tools?.call) {
return false;
}
return this._cachedKnownTaskTools.has(toolName);
}
/**
* Check if a tool requires task-based execution.
* Unlike {@linkcode isToolTask} which includes `'optional'` tools, this only checks for `'required'`.
*/
private isToolTaskRequired(toolName: string): boolean {
return this._cachedRequiredTaskTools.has(toolName);
}
/**
* Cache validators for tool output schemas.
* Called after {@linkcode listTools | listTools()} to pre-compile validators for better performance.
*/
private cacheToolMetadata(tools: Tool[]): void {
this._cachedToolOutputValidators.clear();
this._cachedKnownTaskTools.clear();
this._cachedRequiredTaskTools.clear();
for (const tool of tools) {
// If the tool has an outputSchema, create and cache the validator
@ -939,15 +830,6 @@ export class Client extends Protocol<ClientContext> {
const toolValidator = this._jsonSchemaValidator.getValidator(tool.outputSchema as JsonSchemaType);
this._cachedToolOutputValidators.set(tool.name, toolValidator);
}
// If the tool supports task-based execution, cache that information
const taskSupport = tool.execution?.taskSupport;
if (taskSupport === 'required' || taskSupport === 'optional') {
this._cachedKnownTaskTools.add(tool.name);
}
if (taskSupport === 'required') {
this._cachedRequiredTaskTools.add(tool.name);
}
}
}

View file

@ -18,7 +18,7 @@ declare const platformBackgroundTask: {
};
/**
* Example: Using a platform background-task API to schedule reconnections.
* Example: Using a platform background-scheduler API to schedule reconnections.
*/
function ReconnectionScheduler_basicUsage() {
//#region ReconnectionScheduler_basicUsage

Some files were not shown because too many files have changed in this diff Show more