[SEP-2663] refactor!: remove 2025-11 experimental tasks (#2128)
This commit is contained in:
parent
600ba75f34
commit
c8d7401b46
8 changed files with 423 additions and 18280 deletions
|
|
@ -1,13 +0,0 @@
|
|||
/**
|
||||
* Experimental MCP SDK features.
|
||||
* WARNING: These APIs are experimental and may change without notice.
|
||||
*
|
||||
* Import experimental features from this module:
|
||||
* ```typescript
|
||||
* import { TaskStore, InMemoryTaskStore } from '@modelcontextprotocol/sdk/experimental';
|
||||
* ```
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
|
||||
export * from './tasks/index.js';
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
/**
|
||||
* Experimental task features for MCP SDK.
|
||||
* WARNING: These APIs are experimental and may change without notice.
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
|
||||
export * from './interfaces.js';
|
||||
export * from './mcpServer.js';
|
||||
export * from './server.js';
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
/**
|
||||
* Experimental task interfaces for MCP SDK.
|
||||
* WARNING: These APIs are experimental and may change without notice.
|
||||
*/
|
||||
|
||||
import type {
|
||||
CallToolResult,
|
||||
CreateTaskResult,
|
||||
CreateTaskServerContext,
|
||||
GetTaskResult,
|
||||
Result,
|
||||
StandardSchemaWithJSON,
|
||||
TaskServerContext
|
||||
} from '@modelcontextprotocol/core';
|
||||
|
||||
import type { BaseToolCallback } from '../../server/mcp.js';
|
||||
|
||||
// ============================================================================
|
||||
// Task Handler Types (for registerToolTask)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Handler for creating a task.
|
||||
* @experimental
|
||||
*/
|
||||
export type CreateTaskRequestHandler<
|
||||
SendResultT extends Result,
|
||||
Args extends StandardSchemaWithJSON | undefined = undefined
|
||||
> = BaseToolCallback<SendResultT, CreateTaskServerContext, Args>;
|
||||
|
||||
/**
|
||||
* Handler for task operations (`get`, `getResult`).
|
||||
* @experimental
|
||||
*/
|
||||
export type TaskRequestHandler<SendResultT extends Result, Args extends StandardSchemaWithJSON | undefined = undefined> = BaseToolCallback<
|
||||
SendResultT,
|
||||
TaskServerContext,
|
||||
Args
|
||||
>;
|
||||
|
||||
/**
|
||||
* Interface for task-based tool handlers.
|
||||
*
|
||||
* Task-based tools split a long-running operation into three phases:
|
||||
* `createTask`, `getTask`, and `getTaskResult`.
|
||||
*
|
||||
* @see {@linkcode @modelcontextprotocol/server!experimental/tasks/mcpServer.ExperimentalMcpServerTasks#registerToolTask | registerToolTask} for registration.
|
||||
* @experimental
|
||||
*/
|
||||
export interface ToolTaskHandler<Args extends StandardSchemaWithJSON | undefined = undefined> {
|
||||
/**
|
||||
* Called on the initial `tools/call` request.
|
||||
*
|
||||
* Creates a task via `ctx.task.store.createTask(...)`, starts any
|
||||
* background work, and returns the task object.
|
||||
*/
|
||||
createTask: CreateTaskRequestHandler<CreateTaskResult, Args>;
|
||||
/**
|
||||
* Handler for `tasks/get` requests.
|
||||
*/
|
||||
getTask: TaskRequestHandler<GetTaskResult, Args>;
|
||||
/**
|
||||
* Handler for `tasks/result` requests.
|
||||
*/
|
||||
getTaskResult: TaskRequestHandler<CallToolResult, Args>;
|
||||
}
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
/**
|
||||
* Experimental {@linkcode McpServer} task features for MCP SDK.
|
||||
* WARNING: These APIs are experimental and may change without notice.
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
|
||||
import type { StandardSchemaWithJSON, TaskToolExecution, ToolAnnotations, ToolExecution } from '@modelcontextprotocol/core';
|
||||
|
||||
import type { AnyToolHandler, McpServer, RegisteredTool } from '../../server/mcp.js';
|
||||
import type { ToolTaskHandler } from './interfaces.js';
|
||||
|
||||
/**
|
||||
* Internal interface for accessing {@linkcode McpServer}'s private _createRegisteredTool method.
|
||||
* @internal
|
||||
*/
|
||||
interface McpServerInternal {
|
||||
_createRegisteredTool(
|
||||
name: string,
|
||||
title: string | undefined,
|
||||
description: string | undefined,
|
||||
inputSchema: StandardSchemaWithJSON | undefined,
|
||||
outputSchema: StandardSchemaWithJSON | undefined,
|
||||
annotations: ToolAnnotations | undefined,
|
||||
execution: ToolExecution | undefined,
|
||||
_meta: Record<string, unknown> | undefined,
|
||||
handler: AnyToolHandler<StandardSchemaWithJSON | undefined>
|
||||
): RegisteredTool;
|
||||
}
|
||||
|
||||
/**
|
||||
* Experimental task features for {@linkcode McpServer}.
|
||||
*
|
||||
* Access via `server.experimental.tasks`:
|
||||
* ```typescript
|
||||
* server.experimental.tasks.registerToolTask('long-running', config, handler);
|
||||
* ```
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export class ExperimentalMcpServerTasks {
|
||||
constructor(private readonly _mcpServer: McpServer) {}
|
||||
|
||||
/**
|
||||
* Registers a task-based tool with a config object and handler.
|
||||
*
|
||||
* Task-based tools support long-running operations that can be polled for status
|
||||
* and results. The handler must implement {@linkcode ToolTaskHandler.createTask | createTask}, {@linkcode ToolTaskHandler.getTask | getTask}, and {@linkcode ToolTaskHandler.getTaskResult | getTaskResult}
|
||||
* methods.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* server.experimental.tasks.registerToolTask('long-computation', {
|
||||
* description: 'Performs a long computation',
|
||||
* inputSchema: z.object({ input: z.string() }),
|
||||
* execution: { taskSupport: 'required' }
|
||||
* }, {
|
||||
* createTask: async (args, ctx) => {
|
||||
* const task = await ctx.task.store.createTask({ ttl: 300000 });
|
||||
* startBackgroundWork(task.taskId, args);
|
||||
* return { task };
|
||||
* },
|
||||
* getTask: async (args, ctx) => {
|
||||
* return ctx.task.store.getTask(ctx.task.id);
|
||||
* },
|
||||
* getTaskResult: async (args, ctx) => {
|
||||
* return ctx.task.store.getTaskResult(ctx.task.id);
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param name - The tool name
|
||||
* @param config - Tool configuration (description, schemas, etc.)
|
||||
* @param handler - Task handler with {@linkcode ToolTaskHandler.createTask | createTask}, {@linkcode ToolTaskHandler.getTask | getTask}, {@linkcode ToolTaskHandler.getTaskResult | getTaskResult} methods
|
||||
* @returns {@linkcode server/mcp.RegisteredTool | RegisteredTool} for managing the tool's lifecycle
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
registerToolTask<OutputArgs extends StandardSchemaWithJSON | undefined>(
|
||||
name: string,
|
||||
config: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
outputSchema?: OutputArgs;
|
||||
annotations?: ToolAnnotations;
|
||||
execution?: TaskToolExecution;
|
||||
_meta?: Record<string, unknown>;
|
||||
},
|
||||
handler: ToolTaskHandler<undefined>
|
||||
): RegisteredTool;
|
||||
|
||||
registerToolTask<InputArgs extends StandardSchemaWithJSON, OutputArgs extends StandardSchemaWithJSON | undefined>(
|
||||
name: string,
|
||||
config: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
inputSchema: InputArgs;
|
||||
outputSchema?: OutputArgs;
|
||||
annotations?: ToolAnnotations;
|
||||
execution?: TaskToolExecution;
|
||||
_meta?: Record<string, unknown>;
|
||||
},
|
||||
handler: ToolTaskHandler<InputArgs>
|
||||
): RegisteredTool;
|
||||
|
||||
registerToolTask<InputArgs extends StandardSchemaWithJSON | undefined, OutputArgs extends StandardSchemaWithJSON | undefined>(
|
||||
name: string,
|
||||
config: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
inputSchema?: InputArgs;
|
||||
outputSchema?: OutputArgs;
|
||||
annotations?: ToolAnnotations;
|
||||
execution?: TaskToolExecution;
|
||||
_meta?: Record<string, unknown>;
|
||||
},
|
||||
handler: ToolTaskHandler<InputArgs>
|
||||
): RegisteredTool {
|
||||
// Validate that taskSupport is not 'forbidden' for task-based tools
|
||||
const execution: ToolExecution = { taskSupport: 'required', ...config.execution };
|
||||
if (execution.taskSupport === 'forbidden') {
|
||||
throw new Error(`Cannot register task-based tool '${name}' with taskSupport 'forbidden'. Use registerTool() instead.`);
|
||||
}
|
||||
|
||||
// Access McpServer's internal _createRegisteredTool method
|
||||
const mcpServerInternal = this._mcpServer as unknown as McpServerInternal;
|
||||
return mcpServerInternal._createRegisteredTool(
|
||||
name,
|
||||
config.title,
|
||||
config.description,
|
||||
config.inputSchema,
|
||||
config.outputSchema,
|
||||
config.annotations,
|
||||
execution,
|
||||
config._meta,
|
||||
handler as AnyToolHandler<StandardSchemaWithJSON | undefined>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,298 +0,0 @@
|
|||
/**
|
||||
* Experimental server task features for MCP SDK.
|
||||
* WARNING: These APIs are experimental and may change without notice.
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
|
||||
import type {
|
||||
AnyObjectSchema,
|
||||
CancelTaskResult,
|
||||
CreateMessageRequestParams,
|
||||
CreateMessageResult,
|
||||
ElicitRequestFormParams,
|
||||
ElicitRequestURLParams,
|
||||
ElicitResult,
|
||||
GetTaskPayloadResult,
|
||||
GetTaskResult,
|
||||
ListTasksResult,
|
||||
Request,
|
||||
RequestMethod,
|
||||
RequestOptions,
|
||||
ResponseMessage,
|
||||
ResultTypeMap
|
||||
} from '@modelcontextprotocol/core';
|
||||
import { getResultSchema, GetTaskPayloadResultSchema, SdkError, SdkErrorCode } from '@modelcontextprotocol/core';
|
||||
|
||||
import type { Server } from '../../server/server.js';
|
||||
|
||||
/**
|
||||
* Experimental task features for low-level MCP servers.
|
||||
*
|
||||
* Access via `server.experimental.tasks`:
|
||||
* ```typescript
|
||||
* const stream = server.experimental.tasks.requestStream(request, options);
|
||||
* ```
|
||||
*
|
||||
* For high-level server usage with task-based tools, use {@linkcode index.McpServer | McpServer}.experimental.tasks instead.
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export class ExperimentalServerTasks {
|
||||
constructor(private readonly _server: Server) {}
|
||||
|
||||
private get _module() {
|
||||
return this._server.taskManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request and returns an AsyncGenerator that yields response messages.
|
||||
* The generator is guaranteed to end with either a `'result'` or `'error'` message.
|
||||
*
|
||||
* This method provides streaming access to request processing, allowing you to
|
||||
* observe intermediate task status updates for task-augmented requests.
|
||||
*
|
||||
* @param request - The request to send (method name determines the result schema)
|
||||
* @param options - Optional request options (timeout, signal, task creation params, etc.)
|
||||
* @returns AsyncGenerator that yields {@linkcode ResponseMessage} objects
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
requestStream<M extends RequestMethod>(
|
||||
request: { method: M; params?: Record<string, unknown> },
|
||||
options?: RequestOptions
|
||||
): AsyncGenerator<ResponseMessage<ResultTypeMap[M]>, void, void> {
|
||||
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
|
||||
>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a sampling request and returns an AsyncGenerator that yields response messages.
|
||||
* The generator is guaranteed to end with either a 'result' or 'error' message.
|
||||
*
|
||||
* For task-augmented requests, yields 'taskCreated' and 'taskStatus' messages
|
||||
* before the final result.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const stream = server.experimental.tasks.createMessageStream({
|
||||
* messages: [{ role: 'user', content: { type: 'text', text: 'Hello' } }],
|
||||
* maxTokens: 100
|
||||
* }, {
|
||||
* onprogress: (progress) => {
|
||||
* // Handle streaming tokens via progress notifications
|
||||
* console.log('Progress:', progress.message);
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* for await (const message of stream) {
|
||||
* 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':
|
||||
* console.log('Final result:', message.result);
|
||||
* break;
|
||||
* case 'error':
|
||||
* console.error('Error:', message.error);
|
||||
* break;
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param params - The sampling request parameters
|
||||
* @param options - Optional request options (timeout, signal, task creation params, onprogress, etc.)
|
||||
* @returns AsyncGenerator that yields ResponseMessage objects
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
createMessageStream(
|
||||
params: CreateMessageRequestParams,
|
||||
options?: RequestOptions
|
||||
): AsyncGenerator<ResponseMessage<CreateMessageResult>, void, void> {
|
||||
// Access client capabilities via the server
|
||||
const clientCapabilities = this._server.getClientCapabilities();
|
||||
|
||||
// Capability check - only required when tools/toolChoice are provided
|
||||
if ((params.tools || params.toolChoice) && !clientCapabilities?.sampling?.tools) {
|
||||
throw new SdkError(SdkErrorCode.CapabilityNotSupported, 'Client does not support sampling tools capability.');
|
||||
}
|
||||
|
||||
// Message structure validation - always validate tool_use/tool_result pairs.
|
||||
// These may appear even without tools/toolChoice in the current request when
|
||||
// a previous sampling request returned tool_use and this is a follow-up with results.
|
||||
if (params.messages.length > 0) {
|
||||
const lastMessage = params.messages.at(-1)!;
|
||||
const lastContent = Array.isArray(lastMessage.content) ? lastMessage.content : [lastMessage.content];
|
||||
const hasToolResults = lastContent.some(c => c.type === 'tool_result');
|
||||
|
||||
const previousMessage = params.messages.length > 1 ? params.messages.at(-2) : undefined;
|
||||
const previousContent = previousMessage
|
||||
? Array.isArray(previousMessage.content)
|
||||
? previousMessage.content
|
||||
: [previousMessage.content]
|
||||
: [];
|
||||
const hasPreviousToolUse = previousContent.some(c => c.type === 'tool_use');
|
||||
|
||||
if (hasToolResults) {
|
||||
if (lastContent.some(c => c.type !== 'tool_result')) {
|
||||
throw new Error('The last message must contain only tool_result content if any is present');
|
||||
}
|
||||
if (!hasPreviousToolUse) {
|
||||
throw new Error('tool_result blocks are not matching any tool_use from the previous message');
|
||||
}
|
||||
}
|
||||
if (hasPreviousToolUse) {
|
||||
const toolUseIds = new Set(previousContent.filter(c => c.type === 'tool_use').map(c => c.id));
|
||||
const toolResultIds = new Set(lastContent.filter(c => c.type === 'tool_result').map(c => c.toolUseId));
|
||||
if (toolUseIds.size !== toolResultIds.size || ![...toolUseIds].every(id => toolResultIds.has(id))) {
|
||||
throw new Error('ids of tool_result blocks and tool_use blocks from previous message do not match');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.requestStream(
|
||||
{
|
||||
method: 'sampling/createMessage',
|
||||
params
|
||||
},
|
||||
options
|
||||
) as AsyncGenerator<ResponseMessage<CreateMessageResult>, void, void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an elicitation request and returns an AsyncGenerator that yields response messages.
|
||||
* The generator is guaranteed to end with either a 'result' or 'error' message.
|
||||
*
|
||||
* For task-augmented requests (especially URL-based elicitation), yields 'taskCreated'
|
||||
* and 'taskStatus' messages before the final result.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const stream = server.experimental.tasks.elicitInputStream({
|
||||
* mode: 'url',
|
||||
* message: 'Please authenticate',
|
||||
* elicitationId: 'auth-123',
|
||||
* url: 'https://example.com/auth'
|
||||
* }, {
|
||||
* task: { ttl: 300000 } // Task-augmented for long-running auth flow
|
||||
* });
|
||||
*
|
||||
* for await (const message of stream) {
|
||||
* 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':
|
||||
* console.log('User action:', message.result.action);
|
||||
* break;
|
||||
* case 'error':
|
||||
* console.error('Error:', message.error);
|
||||
* break;
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param params - The elicitation request parameters
|
||||
* @param options - Optional request options (timeout, signal, task creation params, etc.)
|
||||
* @returns AsyncGenerator that yields ResponseMessage objects
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
elicitInputStream(
|
||||
params: ElicitRequestFormParams | ElicitRequestURLParams,
|
||||
options?: RequestOptions
|
||||
): AsyncGenerator<ResponseMessage<ElicitResult>, void, void> {
|
||||
// Access client capabilities via the server
|
||||
const clientCapabilities = this._server.getClientCapabilities();
|
||||
const mode = params.mode ?? 'form';
|
||||
|
||||
// Capability check based on mode
|
||||
switch (mode) {
|
||||
case 'url': {
|
||||
if (!clientCapabilities?.elicitation?.url) {
|
||||
throw new SdkError(SdkErrorCode.CapabilityNotSupported, 'Client does not support url elicitation.');
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'form': {
|
||||
if (!clientCapabilities?.elicitation?.form) {
|
||||
throw new SdkError(SdkErrorCode.CapabilityNotSupported, 'Client does not support form elicitation.');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize params to ensure mode is set
|
||||
const normalizedParams = mode === 'form' && params.mode !== 'form' ? { ...params, mode: 'form' } : params;
|
||||
return this.requestStream(
|
||||
{
|
||||
method: 'elicitation/create',
|
||||
params: normalizedParams
|
||||
},
|
||||
options
|
||||
) as AsyncGenerator<ResponseMessage<ElicitResult>, void, void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current status of a task.
|
||||
*
|
||||
* @param taskId - The task identifier
|
||||
* @param options - Optional request options
|
||||
* @returns The task status
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
async getTask(taskId: string, options?: RequestOptions): Promise<GetTaskResult> {
|
||||
return this._module.getTask({ taskId }, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the result of a completed task.
|
||||
*
|
||||
* @param taskId - The task identifier
|
||||
* @param options - Optional request options
|
||||
* @returns The task result. The payload structure matches the result type of the
|
||||
* original request (e.g., a `tools/call` task returns a `CallToolResult`).
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
async getTaskResult(taskId: string, options?: RequestOptions): Promise<GetTaskPayloadResult> {
|
||||
return this._module.getTaskResult({ taskId }, GetTaskPayloadResultSchema, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists tasks with optional pagination.
|
||||
*
|
||||
* @param cursor - Optional pagination cursor
|
||||
* @param options - Optional request options
|
||||
* @returns List of tasks with optional next cursor
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
async listTasks(cursor?: string, options?: RequestOptions): Promise<ListTasksResult> {
|
||||
return this._module.listTasks(cursor ? { cursor } : undefined, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels a running task.
|
||||
*
|
||||
* @param taskId - The task identifier
|
||||
* @param options - Optional request options
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
async cancelTask(taskId: string, options?: RequestOptions): Promise<CancelTaskResult> {
|
||||
return this._module.cancelTask({ taskId }, options);
|
||||
}
|
||||
}
|
||||
|
|
@ -40,11 +40,6 @@ export type {
|
|||
} from './server/streamableHttp.js';
|
||||
export { WebStandardStreamableHTTPServerTransport } from './server/streamableHttp.js';
|
||||
|
||||
// experimental exports
|
||||
export type { CreateTaskRequestHandler, TaskRequestHandler, ToolTaskHandler } from './experimental/tasks/interfaces.js';
|
||||
export { ExperimentalMcpServerTasks } from './experimental/tasks/mcpServer.js';
|
||||
export { ExperimentalServerTasks } from './experimental/tasks/server.js';
|
||||
|
||||
// runtime-aware wrapper (shadows core/public's fromJsonSchema with optional validator)
|
||||
export { fromJsonSchema } from './fromJsonSchema.js';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
import type {
|
||||
BaseMetadata,
|
||||
CallToolRequest,
|
||||
CallToolResult,
|
||||
CompleteRequestPrompt,
|
||||
CompleteRequestResourceTemplate,
|
||||
CompleteResult,
|
||||
CreateTaskResult,
|
||||
CreateTaskServerContext,
|
||||
GetPromptResult,
|
||||
Implementation,
|
||||
ListPromptsResult,
|
||||
|
|
@ -41,8 +38,6 @@ import {
|
|||
} from '@modelcontextprotocol/core';
|
||||
import type * as z from 'zod/v4';
|
||||
|
||||
import type { ToolTaskHandler } from '../experimental/tasks/interfaces.js';
|
||||
import { ExperimentalMcpServerTasks } from '../experimental/tasks/mcpServer.js';
|
||||
import { getCompleter, isCompletable } from './completable.js';
|
||||
import type { ServerOptions } from './server.js';
|
||||
import { Server } from './server.js';
|
||||
|
|
@ -72,28 +67,11 @@ export class McpServer {
|
|||
} = {};
|
||||
private _registeredTools: { [name: string]: RegisteredTool } = {};
|
||||
private _registeredPrompts: { [name: string]: RegisteredPrompt } = {};
|
||||
private _experimental?: { tasks: ExperimentalMcpServerTasks };
|
||||
|
||||
constructor(serverInfo: Implementation, options?: ServerOptions) {
|
||||
this.server = new Server(serverInfo, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Access experimental features.
|
||||
*
|
||||
* WARNING: These APIs are experimental and may change without notice.
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
get experimental(): { tasks: ExperimentalMcpServerTasks } {
|
||||
if (!this._experimental) {
|
||||
this._experimental = {
|
||||
tasks: new ExperimentalMcpServerTasks(this)
|
||||
};
|
||||
}
|
||||
return this._experimental;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches to the given transport, starts it, and starts listening for messages.
|
||||
*
|
||||
|
|
@ -160,7 +138,7 @@ export class McpServer {
|
|||
})
|
||||
);
|
||||
|
||||
this.server.setRequestHandler('tools/call', async (request, ctx): Promise<CallToolResult | CreateTaskResult> => {
|
||||
this.server.setRequestHandler('tools/call', async (request, ctx): Promise<CallToolResult> => {
|
||||
const tool = this._registeredTools[request.params.name];
|
||||
if (!tool) {
|
||||
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Tool ${request.params.name} not found`);
|
||||
|
|
@ -170,41 +148,8 @@ export class McpServer {
|
|||
}
|
||||
|
||||
try {
|
||||
const isTaskRequest = !!request.params.task;
|
||||
const taskSupport = tool.execution?.taskSupport;
|
||||
const isTaskHandler = 'createTask' in (tool.handler as AnyToolHandler<StandardSchemaWithJSON>);
|
||||
|
||||
// Validate task hint configuration
|
||||
if ((taskSupport === 'required' || taskSupport === 'optional') && !isTaskHandler) {
|
||||
throw new ProtocolError(
|
||||
ProtocolErrorCode.InternalError,
|
||||
`Tool ${request.params.name} has taskSupport '${taskSupport}' but was not registered with registerToolTask`
|
||||
);
|
||||
}
|
||||
|
||||
// Handle taskSupport 'required' without task augmentation
|
||||
if (taskSupport === 'required' && !isTaskRequest) {
|
||||
throw new ProtocolError(
|
||||
ProtocolErrorCode.MethodNotFound,
|
||||
`Tool ${request.params.name} requires task augmentation (taskSupport: 'required')`
|
||||
);
|
||||
}
|
||||
|
||||
// Handle taskSupport 'optional' without task augmentation - automatic polling
|
||||
if (taskSupport === 'optional' && !isTaskRequest && isTaskHandler) {
|
||||
return await this.handleAutomaticTaskPolling(tool, request, ctx);
|
||||
}
|
||||
|
||||
// Normal execution path
|
||||
const args = await this.validateToolInput(tool, request.params.arguments, request.params.name);
|
||||
const result = await this.executeToolHandler(tool, args, ctx);
|
||||
|
||||
// Return CreateTaskResult immediately for task requests
|
||||
if (isTaskRequest) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Validate output schema for non-task requests
|
||||
await this.validateToolOutput(tool, result, request.params.name);
|
||||
return result;
|
||||
} catch (error) {
|
||||
|
|
@ -265,16 +210,11 @@ export class McpServer {
|
|||
/**
|
||||
* Validates tool output against the tool's output schema.
|
||||
*/
|
||||
private async validateToolOutput(tool: RegisteredTool, result: CallToolResult | CreateTaskResult, toolName: string): Promise<void> {
|
||||
private async validateToolOutput(tool: RegisteredTool, result: CallToolResult, toolName: string): Promise<void> {
|
||||
if (!tool.outputSchema) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only validate CallToolResult, not CreateTaskResult
|
||||
if (!('content' in result)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.isError) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -297,47 +237,13 @@ export class McpServer {
|
|||
}
|
||||
|
||||
/**
|
||||
* Executes a tool handler (either regular or task-based).
|
||||
* Executes a tool handler.
|
||||
*/
|
||||
private async executeToolHandler(tool: RegisteredTool, args: unknown, ctx: ServerContext): Promise<CallToolResult | CreateTaskResult> {
|
||||
private async executeToolHandler(tool: RegisteredTool, args: unknown, ctx: ServerContext): Promise<CallToolResult> {
|
||||
// Executor encapsulates handler invocation with proper types
|
||||
return tool.executor(args, ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles automatic task polling for tools with `taskSupport` `'optional'`.
|
||||
*/
|
||||
private async handleAutomaticTaskPolling<RequestT extends CallToolRequest>(
|
||||
tool: RegisteredTool,
|
||||
request: RequestT,
|
||||
ctx: ServerContext
|
||||
): Promise<CallToolResult> {
|
||||
if (!ctx.task?.store) {
|
||||
throw new Error('No task store provided for task-capable tool.');
|
||||
}
|
||||
|
||||
// Validate input and create task using the executor
|
||||
const args = await this.validateToolInput(tool, request.params.arguments, request.params.name);
|
||||
const createTaskResult = (await tool.executor(args, ctx)) as CreateTaskResult;
|
||||
|
||||
// Poll until completion
|
||||
const taskId = createTaskResult.task.taskId;
|
||||
let task = createTaskResult.task;
|
||||
const pollInterval = task.pollInterval ?? 5000;
|
||||
|
||||
while (task.status !== 'completed' && task.status !== 'failed' && task.status !== 'cancelled') {
|
||||
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
||||
const updatedTask = await ctx.task.store.getTask(taskId);
|
||||
if (!updatedTask) {
|
||||
throw new ProtocolError(ProtocolErrorCode.InternalError, `Task ${taskId} not found during polling`);
|
||||
}
|
||||
task = updatedTask;
|
||||
}
|
||||
|
||||
// Return the final result
|
||||
return (await ctx.task.store.getTaskResult(taskId)) as CallToolResult;
|
||||
}
|
||||
|
||||
private _completionHandlerInitialized = false;
|
||||
|
||||
private setCompletionRequestHandler() {
|
||||
|
|
@ -914,7 +820,7 @@ export class McpServer {
|
|||
normalizeRawShapeSchema(inputSchema),
|
||||
normalizeRawShapeSchema(outputSchema),
|
||||
annotations,
|
||||
{ taskSupport: 'forbidden' },
|
||||
undefined,
|
||||
_meta,
|
||||
cb as ToolCallback<StandardSchemaWithJSON | undefined>
|
||||
);
|
||||
|
|
@ -1148,14 +1054,14 @@ export type ToolCallback<Args extends StandardSchemaWithJSON | undefined = undef
|
|||
>;
|
||||
|
||||
/**
|
||||
* Supertype that can handle both regular tools (simple callback) and task-based tools (task handler object).
|
||||
* Tool handler callback type.
|
||||
*/
|
||||
export type AnyToolHandler<Args extends StandardSchemaWithJSON | undefined = undefined> = ToolCallback<Args> | ToolTaskHandler<Args>;
|
||||
export type AnyToolHandler<Args extends StandardSchemaWithJSON | undefined = undefined> = ToolCallback<Args>;
|
||||
|
||||
/**
|
||||
* Internal executor type that encapsulates handler invocation with proper types.
|
||||
*/
|
||||
type ToolExecutor = (args: unknown, ctx: ServerContext) => Promise<CallToolResult | CreateTaskResult>;
|
||||
type ToolExecutor = (args: unknown, ctx: ServerContext) => Promise<CallToolResult>;
|
||||
|
||||
export type RegisteredTool = {
|
||||
title?: string;
|
||||
|
|
@ -1194,23 +1100,6 @@ function createToolExecutor(
|
|||
inputSchema: StandardSchemaWithJSON | undefined,
|
||||
handler: AnyToolHandler<StandardSchemaWithJSON | undefined>
|
||||
): ToolExecutor {
|
||||
const isTaskHandler = 'createTask' in handler;
|
||||
|
||||
if (isTaskHandler) {
|
||||
const taskHandler = handler as TaskHandlerInternal;
|
||||
return async (args, ctx) => {
|
||||
if (!ctx.task?.store) {
|
||||
throw new Error('No task store provided.');
|
||||
}
|
||||
const taskCtx: CreateTaskServerContext = { ...ctx, task: { store: ctx.task.store, requestedTtl: ctx.task?.requestedTtl } };
|
||||
if (inputSchema) {
|
||||
return taskHandler.createTask(args, taskCtx);
|
||||
}
|
||||
// When no inputSchema, call with just ctx (the handler expects (ctx) signature)
|
||||
return (taskHandler.createTask as (ctx: CreateTaskServerContext) => CreateTaskResult | Promise<CreateTaskResult>)(taskCtx);
|
||||
};
|
||||
}
|
||||
|
||||
if (inputSchema) {
|
||||
const callback = handler as ToolCallbackInternal;
|
||||
return async (args, ctx) => callback(args, ctx);
|
||||
|
|
@ -1300,10 +1189,6 @@ type PromptHandler = (args: Record<string, unknown> | undefined, ctx: ServerCont
|
|||
|
||||
type ToolCallbackInternal = (args: unknown, ctx: ServerContext) => CallToolResult | Promise<CallToolResult>;
|
||||
|
||||
type TaskHandlerInternal = {
|
||||
createTask: (args: unknown, ctx: CreateTaskServerContext) => CreateTaskResult | Promise<CreateTaskResult>;
|
||||
};
|
||||
|
||||
export type RegisteredPrompt = {
|
||||
title?: string;
|
||||
description?: string;
|
||||
|
|
|
|||
|
|
@ -28,21 +28,16 @@ import type {
|
|||
Result,
|
||||
ServerCapabilities,
|
||||
ServerContext,
|
||||
TaskManagerOptions,
|
||||
ToolResultContent,
|
||||
ToolUseContent
|
||||
} from '@modelcontextprotocol/core';
|
||||
import {
|
||||
assertClientRequestTaskCapability,
|
||||
assertToolsCallTaskCapability,
|
||||
CallToolRequestSchema,
|
||||
CallToolResultSchema,
|
||||
CreateMessageResultSchema,
|
||||
CreateMessageResultWithToolsSchema,
|
||||
CreateTaskResultSchema,
|
||||
ElicitResultSchema,
|
||||
EmptyResultSchema,
|
||||
extractTaskManagerOptions,
|
||||
LATEST_PROTOCOL_VERSION,
|
||||
ListRootsResultSchema,
|
||||
LoggingLevelSchema,
|
||||
|
|
@ -56,21 +51,11 @@ import {
|
|||
} from '@modelcontextprotocol/core';
|
||||
import { DefaultJsonSchemaValidator } from '@modelcontextprotocol/server/_shims';
|
||||
|
||||
import { ExperimentalServerTasks } from '../experimental/tasks/server.js';
|
||||
|
||||
/**
|
||||
* Extended tasks capability that includes runtime configuration (store, messageQueue).
|
||||
* The runtime-only fields are stripped before advertising capabilities to clients.
|
||||
*/
|
||||
export type ServerTasksCapabilityWithRuntime = NonNullable<ServerCapabilities['tasks']> & TaskManagerOptions;
|
||||
|
||||
export type ServerOptions = ProtocolOptions & {
|
||||
/**
|
||||
* Capabilities to advertise as being supported by this server.
|
||||
*/
|
||||
capabilities?: Omit<ServerCapabilities, 'tasks'> & {
|
||||
tasks?: ServerTasksCapabilityWithRuntime;
|
||||
};
|
||||
capabilities?: ServerCapabilities;
|
||||
|
||||
/**
|
||||
* Optional instructions describing how to use the server and its features.
|
||||
|
|
@ -102,7 +87,6 @@ export class Server extends Protocol<ServerContext> {
|
|||
private _capabilities: ServerCapabilities;
|
||||
private _instructions?: string;
|
||||
private _jsonSchemaValidator: jsonSchemaValidator;
|
||||
private _experimental?: { tasks: ExperimentalServerTasks };
|
||||
|
||||
/**
|
||||
* Callback for when initialization has fully completed (i.e., the client has sent an `notifications/initialized` notification).
|
||||
|
|
@ -116,22 +100,11 @@ export class Server extends Protocol<ServerContext> {
|
|||
private _serverInfo: Implementation,
|
||||
options?: ServerOptions
|
||||
) {
|
||||
super({
|
||||
...options,
|
||||
tasks: extractTaskManagerOptions(options?.capabilities?.tasks)
|
||||
});
|
||||
super(options);
|
||||
this._capabilities = options?.capabilities ? { ...options.capabilities } : {};
|
||||
this._instructions = options?.instructions;
|
||||
this._jsonSchemaValidator = options?.jsonSchemaValidator ?? new DefaultJsonSchemaValidator();
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
this.setRequestHandler('initialize', request => this._oninitialize(request));
|
||||
this.setNotificationHandler('notifications/initialized', () => this.oninitialized?.());
|
||||
|
||||
|
|
@ -175,22 +148,6 @@ export class Server extends Protocol<ServerContext> {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Access experimental features.
|
||||
*
|
||||
* WARNING: These APIs are experimental and may change without notice.
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
get experimental(): { tasks: ExperimentalServerTasks } {
|
||||
if (!this._experimental) {
|
||||
this._experimental = {
|
||||
tasks: new ExperimentalServerTasks(this)
|
||||
};
|
||||
}
|
||||
return this._experimental;
|
||||
}
|
||||
|
||||
// Map log levels by session id
|
||||
private _loggingLevels = new Map<string | undefined, LoggingLevel>();
|
||||
|
||||
|
|
@ -238,24 +195,8 @@ export class Server extends Protocol<ServerContext> {
|
|||
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid tools/call request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
const { params } = validatedRequest.data;
|
||||
|
||||
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 CallToolResultSchema
|
||||
const validationResult = parseSchema(CallToolResultSchema, result);
|
||||
if (!validationResult.success) {
|
||||
const errorMessage =
|
||||
|
|
@ -411,14 +352,6 @@ export class Server extends Protocol<ServerContext> {
|
|||
}
|
||||
}
|
||||
|
||||
protected assertTaskCapability(method: string): void {
|
||||
assertClientRequestTaskCapability(this._clientCapabilities?.tasks?.requests, method, 'Client');
|
||||
}
|
||||
|
||||
protected assertTaskHandlerCapability(method: string): void {
|
||||
assertToolsCallTaskCapability(this._capabilities?.tasks?.requests, method, 'Server');
|
||||
}
|
||||
|
||||
private async _oninitialize(request: InitializeRequest): Promise<InitializeResult> {
|
||||
const requestedVersion = request.params.protocolVersion;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue