Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ca726c5
tasks extraction - execute on capability declaration, accept task sto…
KKonstantinov Mar 12, 2026
9f62d77
fix check
KKonstantinov Mar 12, 2026
893b484
lint fix
KKonstantinov Mar 12, 2026
f98101a
Merge branch 'main' into feature/extract-tasks-in-task-manager-trigge…
KKonstantinov Mar 12, 2026
d6fa944
add tests, bug fixes
KKonstantinov Mar 13, 2026
a3f240e
Merge branch 'feature/extract-tasks-in-task-manager-trigger-on-capabi…
KKonstantinov Mar 13, 2026
556b819
Merge branch 'main' into feature/extract-tasks-in-task-manager-trigge…
KKonstantinov Mar 13, 2026
3b7c0fd
lint fix
KKonstantinov Mar 13, 2026
daf6c55
Merge branch 'main' into feature/extract-tasks-in-task-manager-trigge…
KKonstantinov Mar 17, 2026
9c00777
revert module indirection
KKonstantinov Mar 18, 2026
bcda271
Merge branch 'feature/extract-tasks-in-task-manager-trigger-on-capabi…
KKonstantinov Mar 18, 2026
135969f
clean up
KKonstantinov Mar 19, 2026
a2e589d
check fix
KKonstantinov Mar 19, 2026
ca49882
Fix changeset: assertTaskCapability methods are not removed
felixweinberger Mar 19, 2026
cdb8b1f
Fix NullTaskManager: detect task-augmented requests and assert capabi…
felixweinberger Mar 25, 2026
2e3c91d
Merge branch 'main' into feature/extract-tasks-in-task-manager-trigge…
felixweinberger Mar 25, 2026
2a6e9ce
Fix changeset: correct breaking change description
felixweinberger Mar 25, 2026
3061350
Fix NullTaskManager task param detection
felixweinberger Mar 25, 2026
044c160
Format with prettier
felixweinberger Mar 25, 2026
0e9dd77
Sort imports and format
felixweinberger Mar 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/extract-task-manager.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@modelcontextprotocol/core": minor
"@modelcontextprotocol/client": minor
"@modelcontextprotocol/server": minor
---

refactor: extract task orchestration from Protocol into TaskManager

**Breaking changes:**
- `taskStore`, `taskMessageQueue`, `defaultTaskPollInterval`, and `maxTaskQueueSize` moved from `ProtocolOptions` to `capabilities.tasks` on `ClientOptions`/`ServerOptions`
4 changes: 2 additions & 2 deletions examples/client/src/simpleStreamableHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,14 +265,14 @@ async function connect(url?: string): Promise<void> {
form: {}
},
tasks: {
taskStore: clientTaskStore,
requests: {
elicitation: {
create: {}
}
}
}
},
taskStore: clientTaskStore
}
}
);
client.onerror = error => {
Expand Down
11 changes: 8 additions & 3 deletions examples/server/src/simpleStreamableHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,14 @@ const getServer = () => {
websiteUrl: 'https://github.com/modelcontextprotocol/typescript-sdk'
},
{
capabilities: { logging: {}, tasks: { requests: { tools: { call: {} } } } },
taskStore, // Enable task support
taskMessageQueue: new InMemoryTaskMessageQueue()
capabilities: {
logging: {},
tasks: {
requests: { tools: { call: {} } },
taskStore,
taskMessageQueue: new InMemoryTaskMessageQueue()
}
}
}
);

Expand Down
55 changes: 25 additions & 30 deletions packages/client/src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import type {
ResultTypeMap,
ServerCapabilities,
SubscribeRequest,
TaskManagerOptions,
Tool,
Transport,
UnsubscribeRequest
Expand All @@ -46,6 +47,7 @@ import {
ElicitRequestSchema,
ElicitResultSchema,
EmptyResultSchema,
extractTaskManagerOptions,
GetPromptResultSchema,
InitializeResultSchema,
LATEST_PROTOCOL_VERSION,
Expand Down Expand Up @@ -140,11 +142,19 @@ 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?: ClientCapabilities;
capabilities?: Omit<ClientCapabilities, 'tasks'> & {
tasks?: ClientTasksCapabilityWithRuntime;
};

/**
* JSON Schema validator for tool output validation.
Expand Down Expand Up @@ -213,11 +223,22 @@ export class Client extends Protocol<ClientContext> {
private _clientInfo: Implementation,
options?: ClientOptions
) {
super(options);
this._capabilities = options?.capabilities ?? {};
super({
...options,
tasks: extractTaskManagerOptions(options?.capabilities?.tasks)
});
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;
Expand Down Expand Up @@ -650,12 +671,6 @@ export class Client extends Protocol<ClientContext> {
}

protected assertRequestHandlerCapability(method: string): void {
// Task handlers are registered in Protocol constructor before _capabilities is initialized
// Skip capability check for task methods during initialization
if (!this._capabilities) {
return;
}

switch (method) {
case 'sampling/createMessage': {
if (!this._capabilities.sampling) {
Expand Down Expand Up @@ -687,19 +702,6 @@ export class Client extends Protocol<ClientContext> {
break;
}

case 'tasks/get':
case 'tasks/list':
case 'tasks/result':
case 'tasks/cancel': {
if (!this._capabilities.tasks) {
throw new SdkError(
SdkErrorCode.CapabilityNotSupported,
`Client does not support tasks capability (required for ${method})`
);
}
break;
}

case 'ping': {
// No specific capability required for ping
break;
Expand All @@ -712,16 +714,9 @@ export class Client extends Protocol<ClientContext> {
}

protected assertTaskHandlerCapability(method: string): void {
// Task handlers are registered in Protocol constructor before _capabilities is initialized
// Skip capability check for task methods during initialization
if (!this._capabilities) {
return;
}

assertClientRequestTaskCapability(this._capabilities.tasks?.requests, method, 'Client');
assertClientRequestTaskCapability(this._capabilities?.tasks?.requests, method, 'Client');
}

/** Sends a ping to the server to check connectivity. */
async ping(options?: RequestOptions) {
return this._requestWithSchema({ method: 'ping' }, EmptyResultSchema, options);
}
Expand Down
56 changes: 24 additions & 32 deletions packages/client/src/experimental/tasks/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,27 @@
*/

import type {
AnyObjectSchema,
CallToolRequest,
CallToolResult,
CancelTaskResult,
CreateTaskResult,
GetTaskPayloadResult,
GetTaskResult,
ListTasksResult,
Request,
RequestMethod,
RequestOptions,
ResponseMessage,
ResultTypeMap
} from '@modelcontextprotocol/core';
import { GetTaskPayloadResultSchema, ProtocolError, ProtocolErrorCode } from '@modelcontextprotocol/core';
import {
CallToolResultSchema,
getResultSchema,
GetTaskPayloadResultSchema,
ProtocolError,
ProtocolErrorCode
} from '@modelcontextprotocol/core';

import type { Client } from '../../client/client.js';

Expand All @@ -27,10 +35,6 @@ import type { Client } from '../../client/client.js';
* @internal
*/
interface ClientInternal {
requestStream<M extends RequestMethod>(
request: { method: M; params?: Record<string, unknown> },
options?: RequestOptions
): AsyncGenerator<ResponseMessage<ResultTypeMap[M]>, void, void>;
isToolTask(toolName: string): boolean;
getToolOutputValidator(toolName: string): ((data: unknown) => { valid: boolean; errorMessage?: string }) | undefined;
}
Expand All @@ -49,6 +53,10 @@ interface ClientInternal {
export class ExperimentalClientTasks {
constructor(private readonly _client: Client) {}

private get _module() {
return this._client.taskManager;
}

/**
* Calls a tool and returns an AsyncGenerator that yields response messages.
* The generator is guaranteed to end with either a `'result'` or `'error'` message.
Expand Down Expand Up @@ -103,7 +111,7 @@ export class ExperimentalClientTasks {
task: options?.task ?? (clientInternal.isToolTask(params.name) ? {} : undefined)
};

const stream = clientInternal.requestStream({ method: 'tools/call', params }, optionsWithTask);
const stream = this._module.requestStream({ method: 'tools/call', params }, CallToolResultSchema, optionsWithTask);

// Get the validator for this tool (if it has an output schema)
const validator = clientInternal.getToolOutputValidator(params.name);
Expand Down Expand Up @@ -175,9 +183,7 @@ export class ExperimentalClientTasks {
* @experimental
*/
async getTask(taskId: string, options?: RequestOptions): Promise<GetTaskResult> {
// Delegate to the client's underlying Protocol method
type ClientWithGetTask = { getTask(params: { taskId: string }, options?: RequestOptions): Promise<GetTaskResult> };
return (this._client as unknown as ClientWithGetTask).getTask({ taskId }, options);
return this._module.getTask({ taskId }, options);
}

/**
Expand All @@ -191,15 +197,7 @@ export class ExperimentalClientTasks {
* @experimental
*/
async getTaskResult(taskId: string, options?: RequestOptions): Promise<GetTaskPayloadResult> {
return (
this._client as unknown as {
getTaskResult: (
params: { taskId: string },
resultSchema: typeof GetTaskPayloadResultSchema,
options?: RequestOptions
) => Promise<GetTaskPayloadResult>;
}
).getTaskResult({ taskId }, GetTaskPayloadResultSchema, options);
return this._module.getTaskResult({ taskId }, GetTaskPayloadResultSchema, options);
}

/**
Expand All @@ -212,12 +210,7 @@ export class ExperimentalClientTasks {
* @experimental
*/
async listTasks(cursor?: string, options?: RequestOptions): Promise<ListTasksResult> {
// Delegate to the client's underlying Protocol method
return (
this._client as unknown as {
listTasks: (params?: { cursor?: string }, options?: RequestOptions) => Promise<ListTasksResult>;
}
).listTasks(cursor ? { cursor } : undefined, options);
return this._module.listTasks(cursor ? { cursor } : undefined, options);
}

/**
Expand All @@ -229,12 +222,7 @@ export class ExperimentalClientTasks {
* @experimental
*/
async cancelTask(taskId: string, options?: RequestOptions): Promise<CancelTaskResult> {
// Delegate to the client's underlying Protocol method
return (
this._client as unknown as {
cancelTask: (params: { taskId: string }, options?: RequestOptions) => Promise<CancelTaskResult>;
}
).cancelTask({ taskId }, options);
return this._module.cancelTask({ taskId }, options);
}

/**
Expand Down Expand Up @@ -279,7 +267,11 @@ export class ExperimentalClientTasks {
request: { method: M; params?: Record<string, unknown> },
options?: RequestOptions
): AsyncGenerator<ResponseMessage<ResultTypeMap[M]>, void, void> {
// Delegate to the client's underlying Protocol method
return (this._client as unknown as ClientInternal).requestStream(request, options);
const resultSchema = getResultSchema(request.method) as unknown as AnyObjectSchema;
return this._module.requestStream(request as Request, resultSchema, options) as AsyncGenerator<
ResponseMessage<ResultTypeMap[M]>,
void,
void
>;
}
}
4 changes: 2 additions & 2 deletions packages/core/src/experimental/tasks/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ interface TaskRequestsCapability {

/**
* Asserts that task creation is supported for `tools/call`.
* Used by {@linkcode @modelcontextprotocol/client!client/client.Client.assertTaskCapability | Client.assertTaskCapability} and {@linkcode @modelcontextprotocol/server!server/server.Server.assertTaskHandlerCapability | Server.assertTaskHandlerCapability}.
* Used to implement the `assertTaskCapability` or `assertTaskHandlerCapability` abstract methods on Protocol.
*
* @param requests - The task requests capability object
* @param method - The method being checked
Expand Down Expand Up @@ -52,7 +52,7 @@ export function assertToolsCallTaskCapability(

/**
* Asserts that task creation is supported for `sampling/createMessage` or `elicitation/create`.
* Used by {@linkcode @modelcontextprotocol/server!server/server.Server.assertTaskCapability | Server.assertTaskCapability} and {@linkcode @modelcontextprotocol/client!client/client.Client.assertTaskHandlerCapability | Client.assertTaskHandlerCapability}.
* Used to implement the `assertTaskCapability` or `assertTaskHandlerCapability` abstract methods on Protocol.
*
* @param requests - The task requests capability object
* @param method - The method being checked
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/experimental/tasks/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
* WARNING: These APIs are experimental and may change without notice.
*/

import type { RequestTaskStore, ServerContext } from '../../shared/protocol.js';
import type { ServerContext } from '../../shared/protocol.js';
import type { RequestTaskStore } from '../../shared/taskManager.js';
import type {
JSONRPCErrorResponse,
JSONRPCNotification,
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export * from './shared/metadataUtils.js';
export * from './shared/protocol.js';
export * from './shared/responseMessage.js';
export * from './shared/stdio.js';
export type { RequestTaskStore, TaskContext, TaskManagerOptions, TaskRequestOptions } from './shared/taskManager.js';
export { extractTaskManagerOptions, NullTaskManager, TaskManager } from './shared/taskManager.js';
export * from './shared/toolNameValidation.js';
export * from './shared/transport.js';
export * from './shared/uriTemplate.js';
Expand Down
Loading
Loading