v2 ESlint fixes (#1403)
Co-authored-by: Matt <77928207+mattzcarey@users.noreply.github.com>
This commit is contained in:
parent
9296459ac0
commit
9fc7e28f48
6 changed files with 1672 additions and 1346 deletions
|
|
@ -9,5 +9,5 @@
|
|||
export * from './interfaces.js';
|
||||
|
||||
// Wrapper classes
|
||||
export * from './mcp-server.js';
|
||||
export * from './mcpServer.js';
|
||||
export * from './server.js';
|
||||
|
|
|
|||
142
packages/server/src/experimental/tasks/mcpServer.ts
Normal file
142
packages/server/src/experimental/tasks/mcpServer.ts
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
/**
|
||||
* Experimental McpServer task features for MCP SDK.
|
||||
* WARNING: These APIs are experimental and may change without notice.
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
|
||||
import type { AnySchema, TaskToolExecution, ToolAnnotations, ToolExecution, ZodRawShapeCompat } from '@modelcontextprotocol/core';
|
||||
|
||||
import type { AnyToolHandler, McpServer, RegisteredTool } from '../../server/mcp.js';
|
||||
import type { ToolTaskHandler } from './interfaces.js';
|
||||
|
||||
/**
|
||||
* Internal interface for accessing McpServer's private _createRegisteredTool method.
|
||||
* @internal
|
||||
*/
|
||||
interface McpServerInternal {
|
||||
_createRegisteredTool(
|
||||
name: string,
|
||||
title: string | undefined,
|
||||
description: string | undefined,
|
||||
inputSchema: ZodRawShapeCompat | AnySchema | undefined,
|
||||
outputSchema: ZodRawShapeCompat | AnySchema | undefined,
|
||||
annotations: ToolAnnotations | undefined,
|
||||
execution: ToolExecution | undefined,
|
||||
_meta: Record<string, unknown> | undefined,
|
||||
handler: AnyToolHandler<ZodRawShapeCompat | undefined>
|
||||
): RegisteredTool;
|
||||
}
|
||||
|
||||
/**
|
||||
* Experimental task features for 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 `createTask`, `getTask`, and `getTaskResult`
|
||||
* methods.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* server.experimental.tasks.registerToolTask('long-computation', {
|
||||
* description: 'Performs a long computation',
|
||||
* inputSchema: { input: z.string() },
|
||||
* execution: { taskSupport: 'required' }
|
||||
* }, {
|
||||
* createTask: async (args, extra) => {
|
||||
* const task = await extra.taskStore.createTask({ ttl: 300000 });
|
||||
* startBackgroundWork(task.taskId, args);
|
||||
* return { task };
|
||||
* },
|
||||
* getTask: async (args, extra) => {
|
||||
* return extra.taskStore.getTask(extra.taskId);
|
||||
* },
|
||||
* getTaskResult: async (args, extra) => {
|
||||
* return extra.taskStore.getTaskResult(extra.taskId);
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param name - The tool name
|
||||
* @param config - Tool configuration (description, schemas, etc.)
|
||||
* @param handler - Task handler with createTask, getTask, getTaskResult methods
|
||||
* @returns RegisteredTool for managing the tool's lifecycle
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
registerToolTask<OutputArgs extends undefined | ZodRawShapeCompat | AnySchema>(
|
||||
name: string,
|
||||
config: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
outputSchema?: OutputArgs;
|
||||
annotations?: ToolAnnotations;
|
||||
execution?: TaskToolExecution;
|
||||
_meta?: Record<string, unknown>;
|
||||
},
|
||||
handler: ToolTaskHandler<undefined>
|
||||
): RegisteredTool;
|
||||
|
||||
registerToolTask<InputArgs extends ZodRawShapeCompat | AnySchema, OutputArgs extends undefined | ZodRawShapeCompat | AnySchema>(
|
||||
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 undefined | ZodRawShapeCompat | AnySchema,
|
||||
OutputArgs extends undefined | ZodRawShapeCompat | AnySchema
|
||||
>(
|
||||
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<ZodRawShapeCompat | undefined>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -61,7 +61,7 @@ import {
|
|||
import { ZodOptional } from 'zod';
|
||||
|
||||
import type { ToolTaskHandler } from '../experimental/tasks/interfaces.js';
|
||||
import { ExperimentalMcpServerTasks } from '../experimental/tasks/mcp-server.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';
|
||||
|
|
@ -224,10 +224,8 @@ export class McpServer {
|
|||
await this.validateToolOutput(tool, result, request.params.name);
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (error instanceof McpError) {
|
||||
if (error.code === ErrorCode.UrlElicitationRequired) {
|
||||
throw error; // Return the error to the caller without wrapping in CallToolResult
|
||||
}
|
||||
if (error instanceof McpError && error.code === ErrorCode.UrlElicitationRequired) {
|
||||
throw error; // Return the error to the caller without wrapping in CallToolResult
|
||||
}
|
||||
return this.createToolError(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
|
|
@ -414,16 +412,19 @@ export class McpServer {
|
|||
|
||||
this.server.setRequestHandler(CompleteRequestSchema, async (request): Promise<CompleteResult> => {
|
||||
switch (request.params.ref.type) {
|
||||
case 'ref/prompt':
|
||||
case 'ref/prompt': {
|
||||
assertCompleteRequestPrompt(request);
|
||||
return this.handlePromptCompletion(request, request.params.ref);
|
||||
}
|
||||
|
||||
case 'ref/resource':
|
||||
case 'ref/resource': {
|
||||
assertCompleteRequestResourceTemplate(request);
|
||||
return this.handleResourceCompletion(request, request.params.ref);
|
||||
}
|
||||
|
||||
default:
|
||||
default: {
|
||||
throw new McpError(ErrorCode.InvalidParams, `Invalid completion reference: ${request.params.ref}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -768,15 +769,15 @@ export class McpServer {
|
|||
enable: () => registeredResource.update({ enabled: true }),
|
||||
remove: () => registeredResource.update({ uri: null }),
|
||||
update: updates => {
|
||||
if (typeof updates.uri !== 'undefined' && updates.uri !== uri) {
|
||||
if (updates.uri !== undefined && updates.uri !== uri) {
|
||||
delete this._registeredResources[uri];
|
||||
if (updates.uri) this._registeredResources[updates.uri] = registeredResource;
|
||||
}
|
||||
if (typeof updates.name !== 'undefined') registeredResource.name = updates.name;
|
||||
if (typeof updates.title !== 'undefined') registeredResource.title = updates.title;
|
||||
if (typeof updates.metadata !== 'undefined') registeredResource.metadata = updates.metadata;
|
||||
if (typeof updates.callback !== 'undefined') registeredResource.readCallback = updates.callback;
|
||||
if (typeof updates.enabled !== 'undefined') registeredResource.enabled = updates.enabled;
|
||||
if (updates.name !== undefined) registeredResource.name = updates.name;
|
||||
if (updates.title !== undefined) registeredResource.title = updates.title;
|
||||
if (updates.metadata !== undefined) registeredResource.metadata = updates.metadata;
|
||||
if (updates.callback !== undefined) registeredResource.readCallback = updates.callback;
|
||||
if (updates.enabled !== undefined) registeredResource.enabled = updates.enabled;
|
||||
this.sendResourceListChanged();
|
||||
}
|
||||
};
|
||||
|
|
@ -801,15 +802,15 @@ export class McpServer {
|
|||
enable: () => registeredResourceTemplate.update({ enabled: true }),
|
||||
remove: () => registeredResourceTemplate.update({ name: null }),
|
||||
update: updates => {
|
||||
if (typeof updates.name !== 'undefined' && updates.name !== name) {
|
||||
if (updates.name !== undefined && updates.name !== name) {
|
||||
delete this._registeredResourceTemplates[name];
|
||||
if (updates.name) this._registeredResourceTemplates[updates.name] = registeredResourceTemplate;
|
||||
}
|
||||
if (typeof updates.title !== 'undefined') registeredResourceTemplate.title = updates.title;
|
||||
if (typeof updates.template !== 'undefined') registeredResourceTemplate.resourceTemplate = updates.template;
|
||||
if (typeof updates.metadata !== 'undefined') registeredResourceTemplate.metadata = updates.metadata;
|
||||
if (typeof updates.callback !== 'undefined') registeredResourceTemplate.readCallback = updates.callback;
|
||||
if (typeof updates.enabled !== 'undefined') registeredResourceTemplate.enabled = updates.enabled;
|
||||
if (updates.title !== undefined) registeredResourceTemplate.title = updates.title;
|
||||
if (updates.template !== undefined) registeredResourceTemplate.resourceTemplate = updates.template;
|
||||
if (updates.metadata !== undefined) registeredResourceTemplate.metadata = updates.metadata;
|
||||
if (updates.callback !== undefined) registeredResourceTemplate.readCallback = updates.callback;
|
||||
if (updates.enabled !== undefined) registeredResourceTemplate.enabled = updates.enabled;
|
||||
this.sendResourceListChanged();
|
||||
}
|
||||
};
|
||||
|
|
@ -842,15 +843,15 @@ export class McpServer {
|
|||
enable: () => registeredPrompt.update({ enabled: true }),
|
||||
remove: () => registeredPrompt.update({ name: null }),
|
||||
update: updates => {
|
||||
if (typeof updates.name !== 'undefined' && updates.name !== name) {
|
||||
if (updates.name !== undefined && updates.name !== name) {
|
||||
delete this._registeredPrompts[name];
|
||||
if (updates.name) this._registeredPrompts[updates.name] = registeredPrompt;
|
||||
}
|
||||
if (typeof updates.title !== 'undefined') registeredPrompt.title = updates.title;
|
||||
if (typeof updates.description !== 'undefined') registeredPrompt.description = updates.description;
|
||||
if (typeof updates.argsSchema !== 'undefined') registeredPrompt.argsSchema = objectFromShape(updates.argsSchema);
|
||||
if (typeof updates.callback !== 'undefined') registeredPrompt.callback = updates.callback;
|
||||
if (typeof updates.enabled !== 'undefined') registeredPrompt.enabled = updates.enabled;
|
||||
if (updates.title !== undefined) registeredPrompt.title = updates.title;
|
||||
if (updates.description !== undefined) registeredPrompt.description = updates.description;
|
||||
if (updates.argsSchema !== undefined) registeredPrompt.argsSchema = objectFromShape(updates.argsSchema);
|
||||
if (updates.callback !== undefined) registeredPrompt.callback = updates.callback;
|
||||
if (updates.enabled !== undefined) registeredPrompt.enabled = updates.enabled;
|
||||
this.sendPromptListChanged();
|
||||
}
|
||||
};
|
||||
|
|
@ -898,21 +899,21 @@ export class McpServer {
|
|||
enable: () => registeredTool.update({ enabled: true }),
|
||||
remove: () => registeredTool.update({ name: null }),
|
||||
update: updates => {
|
||||
if (typeof updates.name !== 'undefined' && updates.name !== name) {
|
||||
if (updates.name !== undefined && updates.name !== name) {
|
||||
if (typeof updates.name === 'string') {
|
||||
validateAndWarnToolName(updates.name);
|
||||
}
|
||||
delete this._registeredTools[name];
|
||||
if (updates.name) this._registeredTools[updates.name] = registeredTool;
|
||||
}
|
||||
if (typeof updates.title !== 'undefined') registeredTool.title = updates.title;
|
||||
if (typeof updates.description !== 'undefined') registeredTool.description = updates.description;
|
||||
if (typeof updates.paramsSchema !== 'undefined') registeredTool.inputSchema = objectFromShape(updates.paramsSchema);
|
||||
if (typeof updates.outputSchema !== 'undefined') registeredTool.outputSchema = objectFromShape(updates.outputSchema);
|
||||
if (typeof updates.callback !== 'undefined') registeredTool.handler = updates.callback;
|
||||
if (typeof updates.annotations !== 'undefined') registeredTool.annotations = updates.annotations;
|
||||
if (typeof updates._meta !== 'undefined') registeredTool._meta = updates._meta;
|
||||
if (typeof updates.enabled !== 'undefined') registeredTool.enabled = updates.enabled;
|
||||
if (updates.title !== undefined) registeredTool.title = updates.title;
|
||||
if (updates.description !== undefined) registeredTool.description = updates.description;
|
||||
if (updates.paramsSchema !== undefined) registeredTool.inputSchema = objectFromShape(updates.paramsSchema);
|
||||
if (updates.outputSchema !== undefined) registeredTool.outputSchema = objectFromShape(updates.outputSchema);
|
||||
if (updates.callback !== undefined) registeredTool.handler = updates.callback;
|
||||
if (updates.annotations !== undefined) registeredTool.annotations = updates.annotations;
|
||||
if (updates._meta !== undefined) registeredTool._meta = updates._meta;
|
||||
if (updates.enabled !== undefined) registeredTool.enabled = updates.enabled;
|
||||
this.sendToolListChanged();
|
||||
}
|
||||
};
|
||||
|
|
@ -1383,7 +1384,7 @@ function isZodRawShapeCompat(obj: unknown): obj is ZodRawShapeCompat {
|
|||
}
|
||||
|
||||
// A raw shape has at least one property that is a Zod schema
|
||||
return Object.values(obj).some(isZodTypeLike);
|
||||
return Object.values(obj).some(element => isZodTypeLike(element));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -50,15 +50,15 @@ export function hostHeaderValidationResponse(req: Request, allowedHostnames: str
|
|||
const result = validateHostHeader(req.headers.get('host'), allowedHostnames);
|
||||
if (result.ok) return undefined;
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
return Response.json(
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
error: {
|
||||
code: -32000,
|
||||
code: -32_000,
|
||||
message: result.message
|
||||
},
|
||||
id: null
|
||||
}),
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
|
|
|
|||
|
|
@ -248,7 +248,7 @@ export class Server<
|
|||
}
|
||||
|
||||
if (typeof methodValue !== 'string') {
|
||||
throw new Error('Schema method literal must be a string');
|
||||
throw new TypeError('Schema method literal must be a string');
|
||||
}
|
||||
const method = methodValue;
|
||||
|
||||
|
|
@ -302,70 +302,81 @@ export class Server<
|
|||
|
||||
protected assertCapabilityForMethod(method: RequestT['method']): void {
|
||||
switch (method as ServerRequest['method']) {
|
||||
case 'sampling/createMessage':
|
||||
case 'sampling/createMessage': {
|
||||
if (!this._clientCapabilities?.sampling) {
|
||||
throw new Error(`Client does not support sampling (required for ${method})`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'elicitation/create':
|
||||
case 'elicitation/create': {
|
||||
if (!this._clientCapabilities?.elicitation) {
|
||||
throw new Error(`Client does not support elicitation (required for ${method})`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'roots/list':
|
||||
case 'roots/list': {
|
||||
if (!this._clientCapabilities?.roots) {
|
||||
throw new Error(`Client does not support listing roots (required for ${method})`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'ping':
|
||||
case 'ping': {
|
||||
// No specific capability required for ping
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected assertNotificationCapability(method: (ServerNotification | NotificationT)['method']): void {
|
||||
switch (method as ServerNotification['method']) {
|
||||
case 'notifications/message':
|
||||
case 'notifications/message': {
|
||||
if (!this._capabilities.logging) {
|
||||
throw new Error(`Server does not support logging (required for ${method})`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'notifications/resources/updated':
|
||||
case 'notifications/resources/list_changed':
|
||||
case 'notifications/resources/list_changed': {
|
||||
if (!this._capabilities.resources) {
|
||||
throw new Error(`Server does not support notifying about resources (required for ${method})`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'notifications/tools/list_changed':
|
||||
case 'notifications/tools/list_changed': {
|
||||
if (!this._capabilities.tools) {
|
||||
throw new Error(`Server does not support notifying of tool list changes (required for ${method})`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'notifications/prompts/list_changed':
|
||||
case 'notifications/prompts/list_changed': {
|
||||
if (!this._capabilities.prompts) {
|
||||
throw new Error(`Server does not support notifying of prompt list changes (required for ${method})`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'notifications/elicitation/complete':
|
||||
case 'notifications/elicitation/complete': {
|
||||
if (!this._clientCapabilities?.elicitation?.url) {
|
||||
throw new Error(`Client does not support URL elicitation (required for ${method})`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'notifications/cancelled':
|
||||
case 'notifications/cancelled': {
|
||||
// Cancellation notifications are always allowed
|
||||
break;
|
||||
}
|
||||
|
||||
case 'notifications/progress':
|
||||
case 'notifications/progress': {
|
||||
// Progress notifications are always allowed
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -377,53 +388,60 @@ export class Server<
|
|||
}
|
||||
|
||||
switch (method) {
|
||||
case 'completion/complete':
|
||||
case 'completion/complete': {
|
||||
if (!this._capabilities.completions) {
|
||||
throw new Error(`Server does not support completions (required for ${method})`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'logging/setLevel':
|
||||
case 'logging/setLevel': {
|
||||
if (!this._capabilities.logging) {
|
||||
throw new Error(`Server does not support logging (required for ${method})`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'prompts/get':
|
||||
case 'prompts/list':
|
||||
case 'prompts/list': {
|
||||
if (!this._capabilities.prompts) {
|
||||
throw new Error(`Server does not support prompts (required for ${method})`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'resources/list':
|
||||
case 'resources/templates/list':
|
||||
case 'resources/read':
|
||||
case 'resources/read': {
|
||||
if (!this._capabilities.resources) {
|
||||
throw new Error(`Server does not support resources (required for ${method})`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'tools/call':
|
||||
case 'tools/list':
|
||||
case 'tools/list': {
|
||||
if (!this._capabilities.tools) {
|
||||
throw new Error(`Server does not support tools (required for ${method})`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'tasks/get':
|
||||
case 'tasks/list':
|
||||
case 'tasks/result':
|
||||
case 'tasks/cancel':
|
||||
case 'tasks/cancel': {
|
||||
if (!this._capabilities.tasks) {
|
||||
throw new Error(`Server does not support tasks capability (required for ${method})`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'ping':
|
||||
case 'initialize':
|
||||
case 'initialize': {
|
||||
// No specific capability required for these methods
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -506,21 +524,19 @@ export class Server<
|
|||
options?: RequestOptions
|
||||
): Promise<CreateMessageResult | CreateMessageResultWithTools> {
|
||||
// Capability check - only required when tools/toolChoice are provided
|
||||
if (params.tools || params.toolChoice) {
|
||||
if (!this._clientCapabilities?.sampling?.tools) {
|
||||
throw new Error('Client does not support sampling tools capability.');
|
||||
}
|
||||
if ((params.tools || params.toolChoice) && !this._clientCapabilities?.sampling?.tools) {
|
||||
throw new Error('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[params.messages.length - 1]!;
|
||||
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[params.messages.length - 2] : undefined;
|
||||
const previousMessage = params.messages.length > 1 ? params.messages.at(-2) : undefined;
|
||||
const previousContent = previousMessage
|
||||
? Array.isArray(previousMessage.content)
|
||||
? previousMessage.content
|
||||
|
|
@ -646,10 +662,8 @@ export class Server<
|
|||
* @param sessionId optional for stateless and backward compatibility
|
||||
*/
|
||||
async sendLoggingMessage(params: LoggingMessageNotification['params'], sessionId?: string) {
|
||||
if (this._capabilities.logging) {
|
||||
if (!this.isMessageIgnored(params.level, sessionId)) {
|
||||
return this.notification({ method: 'notifications/message', params });
|
||||
}
|
||||
if (this._capabilities.logging && !this.isMessageIgnored(params.level, sessionId)) {
|
||||
return this.notification({ method: 'notifications/message', params });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -262,12 +262,12 @@ export class WebStandardStreamableHTTPServerTransport implements Transport {
|
|||
if (options?.data !== undefined) {
|
||||
error.data = options.data;
|
||||
}
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
return Response.json(
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
error,
|
||||
id: null
|
||||
}),
|
||||
},
|
||||
{
|
||||
status,
|
||||
headers: {
|
||||
|
|
@ -294,7 +294,7 @@ export class WebStandardStreamableHTTPServerTransport implements Transport {
|
|||
if (!hostHeader || !this._allowedHosts.includes(hostHeader)) {
|
||||
const error = `Invalid Host header: ${hostHeader}`;
|
||||
this.onerror?.(new Error(error));
|
||||
return this.createJsonErrorResponse(403, -32000, error);
|
||||
return this.createJsonErrorResponse(403, -32_000, error);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -304,7 +304,7 @@ export class WebStandardStreamableHTTPServerTransport implements Transport {
|
|||
if (originHeader && !this._allowedOrigins.includes(originHeader)) {
|
||||
const error = `Invalid Origin header: ${originHeader}`;
|
||||
this.onerror?.(new Error(error));
|
||||
return this.createJsonErrorResponse(403, -32000, error);
|
||||
return this.createJsonErrorResponse(403, -32_000, error);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -323,14 +323,18 @@ export class WebStandardStreamableHTTPServerTransport implements Transport {
|
|||
}
|
||||
|
||||
switch (req.method) {
|
||||
case 'POST':
|
||||
case 'POST': {
|
||||
return this.handlePostRequest(req, options);
|
||||
case 'GET':
|
||||
}
|
||||
case 'GET': {
|
||||
return this.handleGetRequest(req);
|
||||
case 'DELETE':
|
||||
}
|
||||
case 'DELETE': {
|
||||
return this.handleDeleteRequest(req);
|
||||
default:
|
||||
}
|
||||
default: {
|
||||
return this.handleUnsupportedRequest();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -372,7 +376,7 @@ export class WebStandardStreamableHTTPServerTransport implements Transport {
|
|||
// The client MUST include an Accept header, listing text/event-stream as a supported content type.
|
||||
const acceptHeader = req.headers.get('accept');
|
||||
if (!acceptHeader?.includes('text/event-stream')) {
|
||||
return this.createJsonErrorResponse(406, -32000, 'Not Acceptable: Client must accept text/event-stream');
|
||||
return this.createJsonErrorResponse(406, -32_000, 'Not Acceptable: Client must accept text/event-stream');
|
||||
}
|
||||
|
||||
// If an Mcp-Session-Id is returned by the server during initialization,
|
||||
|
|
@ -398,7 +402,7 @@ export class WebStandardStreamableHTTPServerTransport implements Transport {
|
|||
// Check if there's already an active standalone SSE stream for this session
|
||||
if (this._streamMapping.get(this._standaloneSseStreamId) !== undefined) {
|
||||
// Only one GET SSE stream is allowed per session
|
||||
return this.createJsonErrorResponse(409, -32000, 'Conflict: Only one SSE stream is allowed per session');
|
||||
return this.createJsonErrorResponse(409, -32_000, 'Conflict: Only one SSE stream is allowed per session');
|
||||
}
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
|
|
@ -449,7 +453,7 @@ export class WebStandardStreamableHTTPServerTransport implements Transport {
|
|||
*/
|
||||
private async replayEvents(lastEventId: string): Promise<Response> {
|
||||
if (!this._eventStore) {
|
||||
return this.createJsonErrorResponse(400, -32000, 'Event store not configured');
|
||||
return this.createJsonErrorResponse(400, -32_000, 'Event store not configured');
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -459,12 +463,12 @@ export class WebStandardStreamableHTTPServerTransport implements Transport {
|
|||
streamId = await this._eventStore.getStreamIdForEventId(lastEventId);
|
||||
|
||||
if (!streamId) {
|
||||
return this.createJsonErrorResponse(400, -32000, 'Invalid event ID format');
|
||||
return this.createJsonErrorResponse(400, -32_000, 'Invalid event ID format');
|
||||
}
|
||||
|
||||
// Check conflict with the SAME streamId we'll use for mapping
|
||||
if (this._streamMapping.get(streamId) !== undefined) {
|
||||
return this.createJsonErrorResponse(409, -32000, 'Conflict: Stream already has an active connection');
|
||||
return this.createJsonErrorResponse(409, -32_000, 'Conflict: Stream already has an active connection');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -523,7 +527,7 @@ export class WebStandardStreamableHTTPServerTransport implements Transport {
|
|||
return new Response(readable, { headers });
|
||||
} catch (error) {
|
||||
this.onerror?.(error as Error);
|
||||
return this.createJsonErrorResponse(500, -32000, 'Error replaying events');
|
||||
return this.createJsonErrorResponse(500, -32_000, 'Error replaying events');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -554,15 +558,15 @@ export class WebStandardStreamableHTTPServerTransport implements Transport {
|
|||
* Handles unsupported requests (PUT, PATCH, etc.)
|
||||
*/
|
||||
private handleUnsupportedRequest(): Response {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
return Response.json(
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
error: {
|
||||
code: -32000,
|
||||
code: -32_000,
|
||||
message: 'Method not allowed.'
|
||||
},
|
||||
id: null
|
||||
}),
|
||||
},
|
||||
{
|
||||
status: 405,
|
||||
headers: {
|
||||
|
|
@ -584,14 +588,14 @@ export class WebStandardStreamableHTTPServerTransport implements Transport {
|
|||
if (!acceptHeader?.includes('application/json') || !acceptHeader.includes('text/event-stream')) {
|
||||
return this.createJsonErrorResponse(
|
||||
406,
|
||||
-32000,
|
||||
-32_000,
|
||||
'Not Acceptable: Client must accept both application/json and text/event-stream'
|
||||
);
|
||||
}
|
||||
|
||||
const ct = req.headers.get('content-type');
|
||||
if (!ct || !ct.includes('application/json')) {
|
||||
return this.createJsonErrorResponse(415, -32000, 'Unsupported Media Type: Content-Type must be application/json');
|
||||
return this.createJsonErrorResponse(415, -32_000, 'Unsupported Media Type: Content-Type must be application/json');
|
||||
}
|
||||
|
||||
// Build request info from headers
|
||||
|
|
@ -600,40 +604,38 @@ export class WebStandardStreamableHTTPServerTransport implements Transport {
|
|||
};
|
||||
|
||||
let rawMessage;
|
||||
if (options?.parsedBody !== undefined) {
|
||||
rawMessage = options.parsedBody;
|
||||
} else {
|
||||
if (options?.parsedBody === undefined) {
|
||||
try {
|
||||
rawMessage = await req.json();
|
||||
} catch {
|
||||
return this.createJsonErrorResponse(400, -32700, 'Parse error: Invalid JSON');
|
||||
return this.createJsonErrorResponse(400, -32_700, 'Parse error: Invalid JSON');
|
||||
}
|
||||
} else {
|
||||
rawMessage = options.parsedBody;
|
||||
}
|
||||
|
||||
let messages: JSONRPCMessage[];
|
||||
|
||||
// handle batch and single messages
|
||||
try {
|
||||
if (Array.isArray(rawMessage)) {
|
||||
messages = rawMessage.map(msg => JSONRPCMessageSchema.parse(msg));
|
||||
} else {
|
||||
messages = [JSONRPCMessageSchema.parse(rawMessage)];
|
||||
}
|
||||
messages = Array.isArray(rawMessage)
|
||||
? rawMessage.map(msg => JSONRPCMessageSchema.parse(msg))
|
||||
: [JSONRPCMessageSchema.parse(rawMessage)];
|
||||
} catch {
|
||||
return this.createJsonErrorResponse(400, -32700, 'Parse error: Invalid JSON-RPC message');
|
||||
return this.createJsonErrorResponse(400, -32_700, 'Parse error: Invalid JSON-RPC message');
|
||||
}
|
||||
|
||||
// Check if this is an initialization request
|
||||
// https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/lifecycle/
|
||||
const isInitializationRequest = messages.some(isInitializeRequest);
|
||||
const isInitializationRequest = messages.some(element => isInitializeRequest(element));
|
||||
if (isInitializationRequest) {
|
||||
// If it's a server with session management and the session ID is already set we should reject the request
|
||||
// to avoid re-initialization.
|
||||
if (this._initialized && this.sessionId !== undefined) {
|
||||
return this.createJsonErrorResponse(400, -32600, 'Invalid Request: Server already initialized');
|
||||
return this.createJsonErrorResponse(400, -32_600, 'Invalid Request: Server already initialized');
|
||||
}
|
||||
if (messages.length > 1) {
|
||||
return this.createJsonErrorResponse(400, -32600, 'Invalid Request: Only one initialization request is allowed');
|
||||
return this.createJsonErrorResponse(400, -32_600, 'Invalid Request: Only one initialization request is allowed');
|
||||
}
|
||||
this.sessionId = this.sessionIdGenerator?.();
|
||||
this._initialized = true;
|
||||
|
|
@ -660,7 +662,7 @@ export class WebStandardStreamableHTTPServerTransport implements Transport {
|
|||
}
|
||||
|
||||
// check if it contains requests
|
||||
const hasRequests = messages.some(isJSONRPCRequest);
|
||||
const hasRequests = messages.some(element => isJSONRPCRequest(element));
|
||||
|
||||
if (!hasRequests) {
|
||||
// if it only contains notifications or responses, return 202
|
||||
|
|
@ -778,7 +780,7 @@ export class WebStandardStreamableHTTPServerTransport implements Transport {
|
|||
} catch (error) {
|
||||
// return JSON-RPC formatted error
|
||||
this.onerror?.(error as Error);
|
||||
return this.createJsonErrorResponse(400, -32700, 'Parse error', { data: String(error) });
|
||||
return this.createJsonErrorResponse(400, -32_700, 'Parse error', { data: String(error) });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -812,19 +814,19 @@ export class WebStandardStreamableHTTPServerTransport implements Transport {
|
|||
}
|
||||
if (!this._initialized) {
|
||||
// If the server has not been initialized yet, reject all requests
|
||||
return this.createJsonErrorResponse(400, -32000, 'Bad Request: Server not initialized');
|
||||
return this.createJsonErrorResponse(400, -32_000, 'Bad Request: Server not initialized');
|
||||
}
|
||||
|
||||
const sessionId = req.headers.get('mcp-session-id');
|
||||
|
||||
if (!sessionId) {
|
||||
// Non-initialization requests without a session ID should return 400 Bad Request
|
||||
return this.createJsonErrorResponse(400, -32000, 'Bad Request: Mcp-Session-Id header is required');
|
||||
return this.createJsonErrorResponse(400, -32_000, 'Bad Request: Mcp-Session-Id header is required');
|
||||
}
|
||||
|
||||
if (sessionId !== this.sessionId) {
|
||||
// Reject requests with invalid session ID with 404 Not Found
|
||||
return this.createJsonErrorResponse(404, -32001, 'Session not found');
|
||||
return this.createJsonErrorResponse(404, -32_001, 'Session not found');
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
|
@ -849,7 +851,7 @@ export class WebStandardStreamableHTTPServerTransport implements Transport {
|
|||
if (protocolVersion !== null && !SUPPORTED_PROTOCOL_VERSIONS.includes(protocolVersion)) {
|
||||
return this.createJsonErrorResponse(
|
||||
400,
|
||||
-32000,
|
||||
-32_000,
|
||||
`Bad Request: Unsupported protocol version: ${protocolVersion} (supported versions: ${SUPPORTED_PROTOCOL_VERSIONS.join(', ')})`
|
||||
);
|
||||
}
|
||||
|
|
@ -858,9 +860,9 @@ export class WebStandardStreamableHTTPServerTransport implements Transport {
|
|||
|
||||
async close(): Promise<void> {
|
||||
// Close all SSE connections
|
||||
this._streamMapping.forEach(({ cleanup }) => {
|
||||
for (const { cleanup } of this._streamMapping.values()) {
|
||||
cleanup();
|
||||
});
|
||||
}
|
||||
this._streamMapping.clear();
|
||||
|
||||
// Clear any pending responses
|
||||
|
|
@ -952,9 +954,7 @@ export class WebStandardStreamableHTTPServerTransport implements Transport {
|
|||
|
||||
if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) {
|
||||
this._requestResponseMap.set(requestId, message);
|
||||
const relatedIds = Array.from(this._requestToStreamMapping.entries())
|
||||
.filter(([_, sid]) => sid === streamId)
|
||||
.map(([id]) => id);
|
||||
const relatedIds = [...this._requestToStreamMapping.entries()].filter(([_, sid]) => sid === streamId).map(([id]) => id);
|
||||
|
||||
// Check if we have responses for all requests using this connection
|
||||
const allResponsesReady = relatedIds.every(id => this._requestResponseMap.has(id));
|
||||
|
|
@ -975,9 +975,9 @@ export class WebStandardStreamableHTTPServerTransport implements Transport {
|
|||
const responses = relatedIds.map(id => this._requestResponseMap.get(id)!);
|
||||
|
||||
if (responses.length === 1) {
|
||||
stream.resolveJson(new Response(JSON.stringify(responses[0]), { status: 200, headers }));
|
||||
stream.resolveJson(Response.json(responses[0], { status: 200, headers }));
|
||||
} else {
|
||||
stream.resolveJson(new Response(JSON.stringify(responses), { status: 200, headers }));
|
||||
stream.resolveJson(Response.json(responses, { status: 200, headers }));
|
||||
}
|
||||
} else {
|
||||
// End the SSE stream
|
||||
|
|
|
|||
Loading…
Reference in a new issue