Skip to content

Commit c3104d6

Browse files
fix: address review feedback on TaskManager extraction
- Move assertTaskCapability/assertTaskHandlerCapability from mutable properties on TaskManager to TaskManagerOptions constructor params, eliminating fragile post-construction wiring - Add _requireHost getter with clear error message instead of _host! non-null assertions throughout TaskManager - Extract duplicated relatedTask metadata injection in Protocol.notification() into _buildJsonRpcNotification() helper - Add @internal JSDoc to TaskManagerHost (will be removed from public exports in PR #1447) - Add changeset for the breaking changes - Revert unrelated pnpm-workspace.yaml change
1 parent 34423d9 commit c3104d6

File tree

6 files changed

+82
-66
lines changed

6 files changed

+82
-66
lines changed

.changeset/extract-task-manager.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@modelcontextprotocol/core": minor
3+
"@modelcontextprotocol/client": minor
4+
"@modelcontextprotocol/server": minor
5+
---
6+
7+
refactor: extract task orchestration from Protocol into TaskManager
8+
9+
**Breaking changes:**
10+
- `extra.taskId``extra.task?.taskId`
11+
- `extra.taskStore``extra.task?.taskStore`
12+
- `extra.taskRequestedTtl``extra.task?.requestedTtl`
13+
- `ProtocolOptions` no longer accepts `taskStore`/`taskMessageQueue` — pass via `TaskManagerOptions` in `ClientOptions`/`ServerOptions`
14+
- Abstract methods `assertTaskCapability`/`assertTaskHandlerCapability` removed from Protocol

packages/client/src/client/client.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -271,23 +271,21 @@ export class Client<
271271
taskStore: options?.taskStore,
272272
taskMessageQueue: options?.taskMessageQueue,
273273
defaultTaskPollInterval: options?.defaultTaskPollInterval,
274-
maxTaskQueueSize: options?.maxTaskQueueSize
274+
maxTaskQueueSize: options?.maxTaskQueueSize,
275+
assertTaskCapability: (method: string) => {
276+
assertToolsCallTaskCapability(this._serverCapabilities?.tasks?.requests, method, 'Server');
277+
},
278+
assertTaskHandlerCapability: (method: string) => {
279+
if (!this._capabilities) return;
280+
assertClientRequestTaskCapability(this._capabilities.tasks?.requests, method, 'Client');
281+
}
275282
});
276283

277284
super(options, taskManager);
278285
this._capabilities = options?.capabilities ?? {};
279286
this._jsonSchemaValidator = options?.jsonSchemaValidator ?? new AjvJsonSchemaValidator();
280287
this._enforceStrictCapabilities = options?.enforceStrictCapabilities ?? false;
281288

282-
// Wire task capability assertions
283-
taskManager.assertTaskCapability = (method: string) => {
284-
assertToolsCallTaskCapability(this._serverCapabilities?.tasks?.requests, method, 'Server');
285-
};
286-
taskManager.assertTaskHandlerCapability = (method: string) => {
287-
if (!this._capabilities) return;
288-
assertClientRequestTaskCapability(this._capabilities.tasks?.requests, method, 'Client');
289-
};
290-
291289
// Store list changed config for setup after connection (when we know server capabilities)
292290
if (options?.listChanged) {
293291
this._pendingListChangedConfig = options.listChanged;

packages/core/src/shared/protocol.ts

Lines changed: 19 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -840,49 +840,37 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
840840
return;
841841
}
842842

843-
let jsonrpcNotification: JSONRPCNotification = {
844-
...notification,
845-
jsonrpc: '2.0'
846-
};
847-
848-
if (options?.relatedTask) {
849-
jsonrpcNotification = {
850-
...jsonrpcNotification,
851-
params: {
852-
...jsonrpcNotification.params,
853-
_meta: {
854-
...jsonrpcNotification.params?._meta,
855-
[RELATED_TASK_META_KEY]: options.relatedTask
856-
}
857-
}
858-
};
859-
}
860-
843+
const jsonrpcNotification = this._buildJsonRpcNotification(notification, options);
861844
this._transport?.send(jsonrpcNotification, options).catch(error => this._onerror(error));
862845
});
863846

864847
return;
865848
}
866849

867-
let jsonrpcNotification: JSONRPCNotification = {
850+
const jsonrpcNotification = this._buildJsonRpcNotification(notification, options);
851+
await this._transport.send(jsonrpcNotification, options);
852+
}
853+
854+
private _buildJsonRpcNotification(notification: SendNotificationT, options?: NotificationOptions): JSONRPCNotification {
855+
const jsonrpcNotification: JSONRPCNotification = {
868856
...notification,
869857
jsonrpc: '2.0'
870858
};
871859

872-
if (options?.relatedTask) {
873-
jsonrpcNotification = {
874-
...jsonrpcNotification,
875-
params: {
876-
...jsonrpcNotification.params,
877-
_meta: {
878-
...jsonrpcNotification.params?._meta,
879-
[RELATED_TASK_META_KEY]: options.relatedTask
880-
}
881-
}
882-
};
860+
if (!options?.relatedTask) {
861+
return jsonrpcNotification;
883862
}
884863

885-
await this._transport.send(jsonrpcNotification, options);
864+
return {
865+
...jsonrpcNotification,
866+
params: {
867+
...jsonrpcNotification.params,
868+
_meta: {
869+
...jsonrpcNotification.params?._meta,
870+
[RELATED_TASK_META_KEY]: options.relatedTask
871+
}
872+
}
873+
};
886874
}
887875

888876
/**

packages/core/src/shared/taskManager.ts

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,16 @@ export type TaskManagerOptions = {
6363
* If undefined, the queue size is unbounded.
6464
*/
6565
maxTaskQueueSize?: number;
66+
/**
67+
* Checks if task creation is supported for a given request method.
68+
* Wired by Client/Server to enforce capability negotiation.
69+
*/
70+
assertTaskCapability?: (method: string) => void;
71+
/**
72+
* Checks if task handling is supported for a given request method.
73+
* Wired by Client/Server to enforce capability negotiation.
74+
*/
75+
assertTaskHandlerCapability?: (method: string) => void;
6676
};
6777

6878
/**
@@ -94,6 +104,8 @@ export interface TaskContext {
94104
/**
95105
* Internal interface that TaskManager needs from the Protocol host
96106
* for sending outbound requests and notifications.
107+
*
108+
* @internal Not intended for external use. Will be removed from public exports.
97109
*/
98110
export interface TaskManagerHost {
99111
request<T extends AnySchema>(request: Request, resultSchema: T, options?: RequestOptions): Promise<SchemaOutput<T>>;
@@ -136,6 +148,13 @@ export class TaskManager {
136148
this._host = host;
137149
}
138150

151+
private get _requireHost(): TaskManagerHost {
152+
if (!this._host) {
153+
throw new McpError(ErrorCode.InternalError, 'TaskManager is not bound to a Protocol host — call bind() first');
154+
}
155+
return this._host;
156+
}
157+
139158
get taskStore(): TaskStore | undefined {
140159
return this._taskStore;
141160
}
@@ -151,17 +170,13 @@ export class TaskManager {
151170
return this._taskMessageQueue;
152171
}
153172

154-
/**
155-
* Checks if task creation is supported for a given request method.
156-
* Set by Client/Server to wire capability checking.
157-
*/
158-
assertTaskCapability?: (method: string) => void;
173+
get assertTaskCapability(): ((method: string) => void) | undefined {
174+
return this._options.assertTaskCapability;
175+
}
159176

160-
/**
161-
* Checks if task handling is supported for a given request method.
162-
* Set by Client/Server to wire capability checking.
163-
*/
164-
assertTaskHandlerCapability?: (method: string) => void;
177+
get assertTaskHandlerCapability(): ((method: string) => void) | undefined {
178+
return this._options.assertTaskHandlerCapability;
179+
}
165180

166181
// =========================================================================
167182
// Public API methods (previously protected on Protocol)
@@ -176,7 +191,7 @@ export class TaskManager {
176191
resultSchema: T,
177192
options?: RequestOptions
178193
): AsyncGenerator<ResponseMessage<SchemaOutput<T>>, void, void> {
179-
const host = this._host!;
194+
const host = this._requireHost;
180195
const { task } = options ?? {};
181196

182197
// For non-task requests, just yield the result
@@ -247,23 +262,23 @@ export class TaskManager {
247262
}
248263

249264
async getTask(params: GetTaskRequest['params'], options?: RequestOptions): Promise<GetTaskResult> {
250-
return this._host!.request({ method: 'tasks/get', params }, GetTaskResultSchema, options);
265+
return this._requireHost.request({ method: 'tasks/get', params }, GetTaskResultSchema, options);
251266
}
252267

253268
async getTaskResult<T extends AnySchema>(
254269
params: GetTaskPayloadRequest['params'],
255270
resultSchema: T,
256271
options?: RequestOptions
257272
): Promise<SchemaOutput<T>> {
258-
return this._host!.request({ method: 'tasks/result', params }, resultSchema, options);
273+
return this._requireHost.request({ method: 'tasks/result', params }, resultSchema, options);
259274
}
260275

261276
async listTasks(params?: { cursor?: string }, options?: RequestOptions): Promise<SchemaOutput<typeof ListTasksResultSchema>> {
262-
return this._host!.request({ method: 'tasks/list', params }, ListTasksResultSchema, options);
277+
return this._requireHost.request({ method: 'tasks/list', params }, ListTasksResultSchema, options);
263278
}
264279

265280
async cancelTask(params: { taskId: string }, options?: RequestOptions): Promise<SchemaOutput<typeof CancelTaskResultSchema>> {
266-
return this._host!.request({ method: 'tasks/cancel', params }, CancelTaskResultSchema, options);
281+
return this._requireHost.request({ method: 'tasks/cancel', params }, CancelTaskResultSchema, options);
267282
}
268283

269284
// =========================================================================

packages/server/src/server/server.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -166,23 +166,21 @@ export class Server<
166166
taskStore: options?.taskStore,
167167
taskMessageQueue: options?.taskMessageQueue,
168168
defaultTaskPollInterval: options?.defaultTaskPollInterval,
169-
maxTaskQueueSize: options?.maxTaskQueueSize
169+
maxTaskQueueSize: options?.maxTaskQueueSize,
170+
assertTaskCapability: (method: string) => {
171+
assertClientRequestTaskCapability(this._clientCapabilities?.tasks?.requests, method, 'Client');
172+
},
173+
assertTaskHandlerCapability: (method: string) => {
174+
if (!this._capabilities) return;
175+
assertToolsCallTaskCapability(this._capabilities.tasks?.requests, method, 'Server');
176+
}
170177
});
171178

172179
super(options, taskManager);
173180
this._capabilities = options?.capabilities ?? {};
174181
this._instructions = options?.instructions;
175182
this._jsonSchemaValidator = options?.jsonSchemaValidator ?? new AjvJsonSchemaValidator();
176183

177-
// Wire task capability assertions
178-
taskManager.assertTaskCapability = (method: string) => {
179-
assertClientRequestTaskCapability(this._clientCapabilities?.tasks?.requests, method, 'Client');
180-
};
181-
taskManager.assertTaskHandlerCapability = (method: string) => {
182-
if (!this._capabilities) return;
183-
assertToolsCallTaskCapability(this._capabilities.tasks?.requests, method, 'Server');
184-
};
185-
186184
this.setRequestHandler(InitializeRequestSchema, request => this._oninitialize(request));
187185
this.setNotificationHandler(InitializedNotificationSchema, () => this.oninitialized?.());
188186

pnpm-workspace.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,6 @@ minimumReleaseAgeExclude:
6363
onlyBuiltDependencies:
6464
- better-sqlite3
6565
- esbuild
66+
67+
ignoredBuiltDependencies:
68+
- unrs-resolver

0 commit comments

Comments
 (0)