src/vs/workbench/contrib/mcp/common/mcpTaskManager.ts
267 LOC · 104 covered · 163 uncovered · 16 ranges · 165 concepts · 3 introducers · 98 tests
File neighbourhood
The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.
Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file
In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.
Graph controls are ready.
Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.
/*---------------------------------------------------------------------------------------------
mcpTaskManager.ts ×13
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { disposableTimeout } from '../../../../base/common/async.js';
import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
import { CancellationError } from '../../../../base/common/errors.js';
import { Emitter } from '../../../../base/common/event.js';
import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
import { generateUuid } from '../../../../base/common/uuid.js';
import type { McpServerRequestHandler } from './mcpServerRequestHandler.js';
import { McpError } from './mcpTypes.js';
import { MCP } from './modelContextProtocol.js';
export interface IMcpTaskInternal extends IDisposable {
readonly id: string;
onDidUpdateState(task: MCP.Task): void;
setHandler(handler: McpServerRequestHandler | undefined): void;
}
interface TaskEntry extends IDisposable {
task: MCP.Task;
result?: MCP.Result;
error?: MCP.Error;
cts: CancellationTokenSource;
/** Time when the task was created (client time), used to calculate TTL expiration */
createdAtTime: number;
/** Promise that resolves when the task execution completes */
executionPromise: Promise<void>;
}
/**
* Manages in-memory task state for server-side MCP tasks (sampling and elicitation).
* Also tracks client-side tasks to survive handler reconnections.
* Lifecycle is tied to the McpServer instance.
*/
export class McpTaskManager extends Disposable {
private readonly _serverTasks = this._register(new DisposableMap<string, TaskEntry>());
mcpServerRequestHandler.ts ×42
private readonly _clientTasks = this._register(new DisposableMap<string, IMcpTaskInternal>());
private readonly _onDidUpdateTask = this._register(new Emitter<MCP.Task>());
public readonly onDidUpdateTask = this._onDidUpdateTask.event;
/**
* Attach a new handler to this task manager.
* Updates all client tasks to use the new handler.
*/
setHandler(handler: McpServerRequestHandler | undefined): void {
task.setHandler(handler);
}
/**
* Get a client task by ID for status notification handling.
*/
getClientTask(taskId: string): IMcpTaskInternal | undefined {
return this._clientTasks.get(taskId);
}
/**
* Track a new client task.
*/
adoptClientTask(task: IMcpTaskInternal): void {
this._clientTasks.set(task.id, task);
}
/**
* Untracks a client task.
*/
abandonClientTask(taskId: string): void {
this._clientTasks.deleteAndDispose(taskId);
}
/**
* Create a new task and execute it asynchronously.
* Returns the task immediately while execution continues in the background.
*/
public createTask<TResult extends MCP.Result>(
ttl: number | null,
executor: (token: CancellationToken) => Promise<TResult>
): MCP.CreateTaskResult {
const taskId = generateUuid();
const createdAt = new Date().toISOString();
const createdAtTime = Date.now();
const task: MCP.Task = {
taskId,
status: 'working',
createdAt,
ttl,
lastUpdatedAt: new Date().toISOString(),
pollInterval: 1000, // Suggest 1 second polling interval
};
const store = new DisposableStore();
const cts = new CancellationTokenSource();
store.add(toDisposable(() => cts.dispose(true)));
const executionPromise = this._executeTask(taskId, executor, cts.token);
// Delete the task after its TTL. Or, if no TTL is given, delete it shortly after the task completes.
if (ttl) {
store.add(disposableTimeout(() => this._serverTasks.deleteAndDispose(taskId), ttl));
} else {
executionPromise.finally(() => {
const timeout = this._register(disposableTimeout(() => {
this._serverTasks.deleteAndDispose(taskId);
this._store.delete(timeout);
}, 60_000));
});
}
this._serverTasks.set(taskId, {
task,
cts,
dispose: () => store.dispose(),
createdAtTime,
executionPromise,
});
return { task };
}
/**
* Execute a task asynchronously and update its state.
*/
private async _executeTask<TResult extends MCP.Result>(
taskId: string,
executor: (token: CancellationToken) => Promise<TResult>,
token: CancellationToken
): Promise<void> {
try {
const result = await executor(token);
this._updateTaskStatus(taskId, 'completed', undefined, result);
} catch (error) {
if (error instanceof CancellationError) {
this._updateTaskStatus(taskId, 'cancelled', 'Task was cancelled by the client');
} else if (error instanceof McpError) {
this._updateTaskStatus(taskId, 'failed', error.message, undefined, {
code: error.code,
message: error.message,
data: error.data,
});
} else if (error instanceof Error) {
this._updateTaskStatus(taskId, 'failed', error.message, undefined, {
code: MCP.INTERNAL_ERROR,
message: error.message,
});
} else {
this._updateTaskStatus(taskId, 'failed', 'Unknown error', undefined, {
code: MCP.INTERNAL_ERROR,
message: 'Unknown error',
});
}
}
}
/**
* Update task status and optionally store result or error.
*/
private _updateTaskStatus(
taskId: string,
status: MCP.TaskStatus,
statusMessage?: string,
result?: MCP.Result,
error?: MCP.Error
): void {
const entry = this._serverTasks.get(taskId);
if (!entry) {
return;
}
entry.task.status = status;
entry.task.lastUpdatedAt = new Date().toISOString();
if (statusMessage !== undefined) {
entry.task.statusMessage = statusMessage;
}
if (result !== undefined) {
entry.result = result;
}
if (error !== undefined) {
entry.error = error;
}
this._onDidUpdateTask.fire({ ...entry.task });
}
/**
* Get the current state of a task.
* Returns an error if the task doesn't exist or has expired.
*/
public getTask(taskId: string): MCP.GetTaskResult {
const entry = this._serverTasks.get(taskId);
if (!entry) {
throw new McpError(MCP.INVALID_PARAMS, `Task not found: ${taskId}`);
}
return { ...entry.task };
}
/**
* Get the result of a completed task.
* Blocks until the task completes if it's still in progress.
*/
public async getTaskResult(taskId: string): Promise<MCP.GetTaskPayloadResult> {
const entry = this._serverTasks.get(taskId);
if (!entry) {
throw new McpError(MCP.INVALID_PARAMS, `Task not found: ${taskId}`);
}
if (entry.task.status === 'working' || entry.task.status === 'input_required') {
await entry.executionPromise;
}
// Refresh entry after waiting
const updatedEntry = this._serverTasks.get(taskId);
if (!updatedEntry) {
throw new McpError(MCP.INVALID_PARAMS, `Task not found: ${taskId}`);
}
if (updatedEntry.error) {
throw new McpError(updatedEntry.error.code, updatedEntry.error.message, updatedEntry.error.data);
}
if (!updatedEntry.result) {
throw new McpError(MCP.INTERNAL_ERROR, 'Task completed but no result available');
}
return updatedEntry.result;
}
/**
* Cancel a task.
*/
public cancelTask(taskId: string): MCP.CancelTaskResult {
const entry = this._serverTasks.get(taskId);
if (!entry) {
throw new McpError(MCP.INVALID_PARAMS, `Task not found: ${taskId}`);
}
// Check if already in terminal status
if (entry.task.status === 'completed' || entry.task.status === 'failed' || entry.task.status === 'cancelled') {
throw new McpError(MCP.INVALID_PARAMS, `Cannot cancel task in ${entry.task.status} status`);
}
entry.task.status = 'cancelled';
entry.task.statusMessage = 'Task was cancelled by the client';
entry.cts.cancel();
return { ...entry.task };
}
/**
* List all tasks.
*/
public listTasks(): MCP.ListTasksResult {
const tasks: MCP.Task[] = [];
for (const entry of this._serverTasks.values()) {
tasks.push({ ...entry.task });
}
return { tasks };
}