Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/mcp/common/mcpTaskManager.ts
4780 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { disposableTimeout } from '../../../../base/common/async.js';
7
import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
8
import { CancellationError } from '../../../../base/common/errors.js';
9
import { Emitter } from '../../../../base/common/event.js';
10
import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
11
import { generateUuid } from '../../../../base/common/uuid.js';
12
import type { McpServerRequestHandler } from './mcpServerRequestHandler.js';
13
import { McpError } from './mcpTypes.js';
14
import { MCP } from './modelContextProtocol.js';
15
16
export interface IMcpTaskInternal extends IDisposable {
17
readonly id: string;
18
onDidUpdateState(task: MCP.Task): void;
19
setHandler(handler: McpServerRequestHandler | undefined): void;
20
}
21
22
interface TaskEntry extends IDisposable {
23
task: MCP.Task;
24
result?: MCP.Result;
25
error?: MCP.Error;
26
cts: CancellationTokenSource;
27
/** Time when the task was created (client time), used to calculate TTL expiration */
28
createdAtTime: number;
29
/** Promise that resolves when the task execution completes */
30
executionPromise: Promise<void>;
31
}
32
33
/**
34
* Manages in-memory task state for server-side MCP tasks (sampling and elicitation).
35
* Also tracks client-side tasks to survive handler reconnections.
36
* Lifecycle is tied to the McpServer instance.
37
*/
38
export class McpTaskManager extends Disposable {
39
private readonly _serverTasks = this._register(new DisposableMap<string, TaskEntry>());
40
private readonly _clientTasks = this._register(new DisposableMap<string, IMcpTaskInternal>());
41
private readonly _onDidUpdateTask = this._register(new Emitter<MCP.Task>());
42
public readonly onDidUpdateTask = this._onDidUpdateTask.event;
43
44
/**
45
* Attach a new handler to this task manager.
46
* Updates all client tasks to use the new handler.
47
*/
48
setHandler(handler: McpServerRequestHandler | undefined): void {
49
for (const task of this._clientTasks.values()) {
50
task.setHandler(handler);
51
}
52
}
53
54
/**
55
* Get a client task by ID for status notification handling.
56
*/
57
getClientTask(taskId: string): IMcpTaskInternal | undefined {
58
return this._clientTasks.get(taskId);
59
}
60
61
/**
62
* Track a new client task.
63
*/
64
adoptClientTask(task: IMcpTaskInternal): void {
65
this._clientTasks.set(task.id, task);
66
}
67
68
/**
69
* Untracks a client task.
70
*/
71
abandonClientTask(taskId: string): void {
72
this._clientTasks.deleteAndDispose(taskId);
73
}
74
75
/**
76
* Create a new task and execute it asynchronously.
77
* Returns the task immediately while execution continues in the background.
78
*/
79
public createTask<TResult extends MCP.Result>(
80
ttl: number | null,
81
executor: (token: CancellationToken) => Promise<TResult>
82
): MCP.CreateTaskResult {
83
const taskId = generateUuid();
84
const createdAt = new Date().toISOString();
85
const createdAtTime = Date.now();
86
87
const task: MCP.Task = {
88
taskId,
89
status: 'working',
90
createdAt,
91
ttl,
92
pollInterval: 1000, // Suggest 1 second polling interval
93
};
94
95
const store = new DisposableStore();
96
const cts = new CancellationTokenSource();
97
store.add(toDisposable(() => cts.dispose(true)));
98
99
const executionPromise = this._executeTask(taskId, executor, cts.token);
100
101
// Delete the task after its TTL. Or, if no TTL is given, delete it shortly after the task completes.
102
if (ttl) {
103
store.add(disposableTimeout(() => this._serverTasks.deleteAndDispose(taskId), ttl));
104
} else {
105
executionPromise.finally(() => {
106
const timeout = this._register(disposableTimeout(() => {
107
this._serverTasks.deleteAndDispose(taskId);
108
this._store.delete(timeout);
109
}, 60_000));
110
});
111
}
112
113
this._serverTasks.set(taskId, {
114
task,
115
cts,
116
dispose: () => store.dispose(),
117
createdAtTime,
118
executionPromise,
119
});
120
121
return { task };
122
}
123
124
/**
125
* Execute a task asynchronously and update its state.
126
*/
127
private async _executeTask<TResult extends MCP.Result>(
128
taskId: string,
129
executor: (token: CancellationToken) => Promise<TResult>,
130
token: CancellationToken
131
): Promise<void> {
132
try {
133
const result = await executor(token);
134
this._updateTaskStatus(taskId, 'completed', undefined, result);
135
} catch (error) {
136
if (error instanceof CancellationError) {
137
this._updateTaskStatus(taskId, 'cancelled', 'Task was cancelled by the client');
138
} else if (error instanceof McpError) {
139
this._updateTaskStatus(taskId, 'failed', error.message, undefined, {
140
code: error.code,
141
message: error.message,
142
data: error.data,
143
});
144
} else if (error instanceof Error) {
145
this._updateTaskStatus(taskId, 'failed', error.message, undefined, {
146
code: MCP.INTERNAL_ERROR,
147
message: error.message,
148
});
149
} else {
150
this._updateTaskStatus(taskId, 'failed', 'Unknown error', undefined, {
151
code: MCP.INTERNAL_ERROR,
152
message: 'Unknown error',
153
});
154
}
155
}
156
}
157
158
/**
159
* Update task status and optionally store result or error.
160
*/
161
private _updateTaskStatus(
162
taskId: string,
163
status: MCP.TaskStatus,
164
statusMessage?: string,
165
result?: MCP.Result,
166
error?: MCP.Error
167
): void {
168
const entry = this._serverTasks.get(taskId);
169
if (!entry) {
170
return;
171
}
172
173
entry.task.status = status;
174
if (statusMessage !== undefined) {
175
entry.task.statusMessage = statusMessage;
176
}
177
if (result !== undefined) {
178
entry.result = result;
179
}
180
if (error !== undefined) {
181
entry.error = error;
182
}
183
184
this._onDidUpdateTask.fire({ ...entry.task });
185
}
186
187
/**
188
* Get the current state of a task.
189
* Returns an error if the task doesn't exist or has expired.
190
*/
191
public getTask(taskId: string): MCP.GetTaskResult {
192
const entry = this._serverTasks.get(taskId);
193
if (!entry) {
194
throw new McpError(MCP.INVALID_PARAMS, `Task not found: ${taskId}`);
195
}
196
197
return { ...entry.task };
198
}
199
200
/**
201
* Get the result of a completed task.
202
* Blocks until the task completes if it's still in progress.
203
*/
204
public async getTaskResult(taskId: string): Promise<MCP.GetTaskPayloadResult> {
205
const entry = this._serverTasks.get(taskId);
206
if (!entry) {
207
throw new McpError(MCP.INVALID_PARAMS, `Task not found: ${taskId}`);
208
}
209
210
if (entry.task.status === 'working' || entry.task.status === 'input_required') {
211
await entry.executionPromise;
212
}
213
214
// Refresh entry after waiting
215
const updatedEntry = this._serverTasks.get(taskId);
216
if (!updatedEntry) {
217
throw new McpError(MCP.INVALID_PARAMS, `Task not found: ${taskId}`);
218
}
219
220
if (updatedEntry.error) {
221
throw new McpError(updatedEntry.error.code, updatedEntry.error.message, updatedEntry.error.data);
222
}
223
224
if (!updatedEntry.result) {
225
throw new McpError(MCP.INTERNAL_ERROR, 'Task completed but no result available');
226
}
227
228
return updatedEntry.result;
229
}
230
231
/**
232
* Cancel a task.
233
*/
234
public cancelTask(taskId: string): MCP.CancelTaskResult {
235
const entry = this._serverTasks.get(taskId);
236
if (!entry) {
237
throw new McpError(MCP.INVALID_PARAMS, `Task not found: ${taskId}`);
238
}
239
240
// Check if already in terminal status
241
if (entry.task.status === 'completed' || entry.task.status === 'failed' || entry.task.status === 'cancelled') {
242
throw new McpError(MCP.INVALID_PARAMS, `Cannot cancel task in ${entry.task.status} status`);
243
}
244
245
entry.task.status = 'cancelled';
246
entry.task.statusMessage = 'Task was cancelled by the client';
247
entry.cts.cancel();
248
249
return { ...entry.task };
250
}
251
252
/**
253
* List all tasks.
254
*/
255
public listTasks(): MCP.ListTasksResult {
256
const tasks: MCP.Task[] = [];
257
258
for (const entry of this._serverTasks.values()) {
259
tasks.push({ ...entry.task });
260
}
261
262
return { tasks };
263
}
264
}
265
266