v2: Errors refactor (ProtocolError, SdkError, OAuthError) (#1454)
This commit is contained in:
parent
160902eb2e
commit
0f0a4eb7c8
2 changed files with 1086 additions and 705 deletions
|
|
@ -36,14 +36,14 @@ import type {
|
|||
import {
|
||||
assertCompleteRequestPrompt,
|
||||
assertCompleteRequestResourceTemplate,
|
||||
ErrorCode,
|
||||
getObjectShape,
|
||||
getParseErrorMessage,
|
||||
getSchemaDescription,
|
||||
isSchemaOptional,
|
||||
McpError,
|
||||
normalizeObjectSchema,
|
||||
objectFromShape,
|
||||
ProtocolError,
|
||||
ProtocolErrorCode,
|
||||
safeParseAsync,
|
||||
toJsonSchemaCompat,
|
||||
UriTemplate,
|
||||
|
|
@ -171,10 +171,10 @@ export class McpServer {
|
|||
try {
|
||||
const tool = this._registeredTools[request.params.name];
|
||||
if (!tool) {
|
||||
throw new McpError(ErrorCode.InvalidParams, `Tool ${request.params.name} not found`);
|
||||
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Tool ${request.params.name} not found`);
|
||||
}
|
||||
if (!tool.enabled) {
|
||||
throw new McpError(ErrorCode.InvalidParams, `Tool ${request.params.name} disabled`);
|
||||
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Tool ${request.params.name} disabled`);
|
||||
}
|
||||
|
||||
const isTaskRequest = !!request.params.task;
|
||||
|
|
@ -183,16 +183,16 @@ export class McpServer {
|
|||
|
||||
// Validate task hint configuration
|
||||
if ((taskSupport === 'required' || taskSupport === 'optional') && !isTaskHandler) {
|
||||
throw new McpError(
|
||||
ErrorCode.InternalError,
|
||||
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 McpError(
|
||||
ErrorCode.MethodNotFound,
|
||||
throw new ProtocolError(
|
||||
ProtocolErrorCode.MethodNotFound,
|
||||
`Tool ${request.params.name} requires task augmentation (taskSupport: 'required')`
|
||||
);
|
||||
}
|
||||
|
|
@ -215,7 +215,7 @@ export class McpServer {
|
|||
await this.validateToolOutput(tool, result, request.params.name);
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (error instanceof McpError && error.code === ErrorCode.UrlElicitationRequired) {
|
||||
if (error instanceof ProtocolError && error.code === ProtocolErrorCode.UrlElicitationRequired) {
|
||||
throw error; // Return the error to the caller without wrapping in CallToolResult
|
||||
}
|
||||
return this.createToolError(error instanceof Error ? error.message : String(error));
|
||||
|
|
@ -266,7 +266,10 @@ export class McpServer {
|
|||
if (!parseResult.success) {
|
||||
const error = 'error' in parseResult ? parseResult.error : 'Unknown error';
|
||||
const errorMessage = getParseErrorMessage(error);
|
||||
throw new McpError(ErrorCode.InvalidParams, `Input validation error: Invalid arguments for tool ${toolName}: ${errorMessage}`);
|
||||
throw new ProtocolError(
|
||||
ProtocolErrorCode.InvalidParams,
|
||||
`Input validation error: Invalid arguments for tool ${toolName}: ${errorMessage}`
|
||||
);
|
||||
}
|
||||
|
||||
return parseResult.data as unknown as Args;
|
||||
|
|
@ -290,8 +293,8 @@ export class McpServer {
|
|||
}
|
||||
|
||||
if (!result.structuredContent) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
throw new ProtocolError(
|
||||
ProtocolErrorCode.InvalidParams,
|
||||
`Output validation error: Tool ${toolName} has an output schema but no structured content was provided`
|
||||
);
|
||||
}
|
||||
|
|
@ -302,8 +305,8 @@ export class McpServer {
|
|||
if (!parseResult.success) {
|
||||
const error = 'error' in parseResult ? parseResult.error : 'Unknown error';
|
||||
const errorMessage = getParseErrorMessage(error);
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
throw new ProtocolError(
|
||||
ProtocolErrorCode.InvalidParams,
|
||||
`Output validation error: Invalid structured content for tool ${toolName}: ${errorMessage}`
|
||||
);
|
||||
}
|
||||
|
|
@ -379,7 +382,7 @@ export class McpServer {
|
|||
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
||||
const updatedTask = await extra.taskStore.getTask(taskId);
|
||||
if (!updatedTask) {
|
||||
throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
|
||||
throw new ProtocolError(ProtocolErrorCode.InternalError, `Task ${taskId} not found during polling`);
|
||||
}
|
||||
task = updatedTask;
|
||||
}
|
||||
|
|
@ -414,7 +417,7 @@ export class McpServer {
|
|||
}
|
||||
|
||||
default: {
|
||||
throw new McpError(ErrorCode.InvalidParams, `Invalid completion reference: ${request.params.ref}`);
|
||||
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid completion reference: ${request.params.ref}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -425,11 +428,11 @@ export class McpServer {
|
|||
private async handlePromptCompletion(request: CompleteRequestPrompt, ref: PromptReference): Promise<CompleteResult> {
|
||||
const prompt = this._registeredPrompts[ref.name];
|
||||
if (!prompt) {
|
||||
throw new McpError(ErrorCode.InvalidParams, `Prompt ${ref.name} not found`);
|
||||
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Prompt ${ref.name} not found`);
|
||||
}
|
||||
|
||||
if (!prompt.enabled) {
|
||||
throw new McpError(ErrorCode.InvalidParams, `Prompt ${ref.name} disabled`);
|
||||
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Prompt ${ref.name} disabled`);
|
||||
}
|
||||
|
||||
if (!prompt.argsSchema) {
|
||||
|
|
@ -462,7 +465,7 @@ export class McpServer {
|
|||
return EMPTY_COMPLETION_RESULT;
|
||||
}
|
||||
|
||||
throw new McpError(ErrorCode.InvalidParams, `Resource template ${request.params.ref.uri} not found`);
|
||||
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Resource template ${request.params.ref.uri} not found`);
|
||||
}
|
||||
|
||||
const completer = template.resourceTemplate.completeCallback(request.params.argument.name);
|
||||
|
|
@ -536,7 +539,7 @@ export class McpServer {
|
|||
const resource = this._registeredResources[uri.toString()];
|
||||
if (resource) {
|
||||
if (!resource.enabled) {
|
||||
throw new McpError(ErrorCode.InvalidParams, `Resource ${uri} disabled`);
|
||||
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Resource ${uri} disabled`);
|
||||
}
|
||||
return resource.readCallback(uri, extra);
|
||||
}
|
||||
|
|
@ -549,7 +552,7 @@ export class McpServer {
|
|||
}
|
||||
}
|
||||
|
||||
throw new McpError(ErrorCode.InvalidParams, `Resource ${uri} not found`);
|
||||
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Resource ${uri} not found`);
|
||||
});
|
||||
|
||||
this._resourceHandlersInitialized = true;
|
||||
|
|
@ -590,11 +593,11 @@ export class McpServer {
|
|||
this.server.setRequestHandler('prompts/get', async (request, extra): Promise<GetPromptResult> => {
|
||||
const prompt = this._registeredPrompts[request.params.name];
|
||||
if (!prompt) {
|
||||
throw new McpError(ErrorCode.InvalidParams, `Prompt ${request.params.name} not found`);
|
||||
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Prompt ${request.params.name} not found`);
|
||||
}
|
||||
|
||||
if (!prompt.enabled) {
|
||||
throw new McpError(ErrorCode.InvalidParams, `Prompt ${request.params.name} disabled`);
|
||||
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Prompt ${request.params.name} disabled`);
|
||||
}
|
||||
|
||||
if (prompt.argsSchema) {
|
||||
|
|
@ -603,7 +606,10 @@ export class McpServer {
|
|||
if (!parseResult.success) {
|
||||
const error = 'error' in parseResult ? parseResult.error : 'Unknown error';
|
||||
const errorMessage = getParseErrorMessage(error);
|
||||
throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for prompt ${request.params.name}: ${errorMessage}`);
|
||||
throw new ProtocolError(
|
||||
ProtocolErrorCode.InvalidParams,
|
||||
`Invalid arguments for prompt ${request.params.name}: ${errorMessage}`
|
||||
);
|
||||
}
|
||||
|
||||
const args = parseResult.data;
|
||||
|
|
|
|||
|
|
@ -44,14 +44,16 @@ import {
|
|||
CreateTaskResultSchema,
|
||||
ElicitResultSchema,
|
||||
EmptyResultSchema,
|
||||
ErrorCode,
|
||||
LATEST_PROTOCOL_VERSION,
|
||||
ListRootsResultSchema,
|
||||
LoggingLevelSchema,
|
||||
McpError,
|
||||
mergeCapabilities,
|
||||
Protocol,
|
||||
safeParse
|
||||
ProtocolError,
|
||||
ProtocolErrorCode,
|
||||
safeParse,
|
||||
SdkError,
|
||||
SdkErrorCode
|
||||
} from '@modelcontextprotocol/core';
|
||||
|
||||
import { ExperimentalServerTasks } from '../experimental/tasks/server.js';
|
||||
|
|
@ -206,7 +208,7 @@ export class Server<
|
|||
*/
|
||||
public registerCapabilities(capabilities: ServerCapabilities): void {
|
||||
if (this.transport) {
|
||||
throw new Error('Cannot register capabilities after connecting to transport');
|
||||
throw new SdkError(SdkErrorCode.AlreadyConnected, 'Cannot register capabilities after connecting to transport');
|
||||
}
|
||||
this._capabilities = mergeCapabilities(this._capabilities, capabilities);
|
||||
}
|
||||
|
|
@ -230,7 +232,7 @@ export class Server<
|
|||
if (!validatedRequest.success) {
|
||||
const errorMessage =
|
||||
validatedRequest.error instanceof Error ? validatedRequest.error.message : String(validatedRequest.error);
|
||||
throw new McpError(ErrorCode.InvalidParams, `Invalid tools/call request: ${errorMessage}`);
|
||||
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid tools/call request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
const { params } = validatedRequest.data;
|
||||
|
|
@ -245,7 +247,7 @@ export class Server<
|
|||
taskValidationResult.error instanceof Error
|
||||
? taskValidationResult.error.message
|
||||
: String(taskValidationResult.error);
|
||||
throw new McpError(ErrorCode.InvalidParams, `Invalid task creation result: ${errorMessage}`);
|
||||
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid task creation result: ${errorMessage}`);
|
||||
}
|
||||
return taskValidationResult.data;
|
||||
}
|
||||
|
|
@ -255,7 +257,7 @@ export class Server<
|
|||
if (!validationResult.success) {
|
||||
const errorMessage =
|
||||
validationResult.error instanceof Error ? validationResult.error.message : String(validationResult.error);
|
||||
throw new McpError(ErrorCode.InvalidParams, `Invalid tools/call result: ${errorMessage}`);
|
||||
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid tools/call result: ${errorMessage}`);
|
||||
}
|
||||
|
||||
return validationResult.data;
|
||||
|
|
@ -273,21 +275,24 @@ export class Server<
|
|||
switch (method as ServerRequest['method']) {
|
||||
case 'sampling/createMessage': {
|
||||
if (!this._clientCapabilities?.sampling) {
|
||||
throw new Error(`Client does not support sampling (required for ${method})`);
|
||||
throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Client does not support sampling (required for ${method})`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'elicitation/create': {
|
||||
if (!this._clientCapabilities?.elicitation) {
|
||||
throw new Error(`Client does not support elicitation (required for ${method})`);
|
||||
throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Client does not support elicitation (required for ${method})`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'roots/list': {
|
||||
if (!this._clientCapabilities?.roots) {
|
||||
throw new Error(`Client does not support listing roots (required for ${method})`);
|
||||
throw new SdkError(
|
||||
SdkErrorCode.CapabilityNotSupported,
|
||||
`Client does not support listing roots (required for ${method})`
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -303,7 +308,7 @@ export class Server<
|
|||
switch (method as ServerNotification['method']) {
|
||||
case 'notifications/message': {
|
||||
if (!this._capabilities.logging) {
|
||||
throw new Error(`Server does not support logging (required for ${method})`);
|
||||
throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Server does not support logging (required for ${method})`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -311,28 +316,40 @@ export class Server<
|
|||
case 'notifications/resources/updated':
|
||||
case 'notifications/resources/list_changed': {
|
||||
if (!this._capabilities.resources) {
|
||||
throw new Error(`Server does not support notifying about resources (required for ${method})`);
|
||||
throw new SdkError(
|
||||
SdkErrorCode.CapabilityNotSupported,
|
||||
`Server does not support notifying about resources (required for ${method})`
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'notifications/tools/list_changed': {
|
||||
if (!this._capabilities.tools) {
|
||||
throw new Error(`Server does not support notifying of tool list changes (required for ${method})`);
|
||||
throw new SdkError(
|
||||
SdkErrorCode.CapabilityNotSupported,
|
||||
`Server does not support notifying of tool list changes (required for ${method})`
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'notifications/prompts/list_changed': {
|
||||
if (!this._capabilities.prompts) {
|
||||
throw new Error(`Server does not support notifying of prompt list changes (required for ${method})`);
|
||||
throw new SdkError(
|
||||
SdkErrorCode.CapabilityNotSupported,
|
||||
`Server does not support notifying of prompt list changes (required for ${method})`
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'notifications/elicitation/complete': {
|
||||
if (!this._clientCapabilities?.elicitation?.url) {
|
||||
throw new Error(`Client does not support URL elicitation (required for ${method})`);
|
||||
throw new SdkError(
|
||||
SdkErrorCode.CapabilityNotSupported,
|
||||
`Client does not support URL elicitation (required for ${method})`
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -359,14 +376,14 @@ export class Server<
|
|||
switch (method) {
|
||||
case 'completion/complete': {
|
||||
if (!this._capabilities.completions) {
|
||||
throw new Error(`Server does not support completions (required for ${method})`);
|
||||
throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Server does not support completions (required for ${method})`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'logging/setLevel': {
|
||||
if (!this._capabilities.logging) {
|
||||
throw new Error(`Server does not support logging (required for ${method})`);
|
||||
throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Server does not support logging (required for ${method})`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -374,7 +391,7 @@ export class Server<
|
|||
case 'prompts/get':
|
||||
case 'prompts/list': {
|
||||
if (!this._capabilities.prompts) {
|
||||
throw new Error(`Server does not support prompts (required for ${method})`);
|
||||
throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Server does not support prompts (required for ${method})`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -383,7 +400,7 @@ export class Server<
|
|||
case 'resources/templates/list':
|
||||
case 'resources/read': {
|
||||
if (!this._capabilities.resources) {
|
||||
throw new Error(`Server does not support resources (required for ${method})`);
|
||||
throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Server does not support resources (required for ${method})`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -391,7 +408,7 @@ export class Server<
|
|||
case 'tools/call':
|
||||
case 'tools/list': {
|
||||
if (!this._capabilities.tools) {
|
||||
throw new Error(`Server does not support tools (required for ${method})`);
|
||||
throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Server does not support tools (required for ${method})`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -401,7 +418,10 @@ export class Server<
|
|||
case 'tasks/result':
|
||||
case 'tasks/cancel': {
|
||||
if (!this._capabilities.tasks) {
|
||||
throw new Error(`Server does not support tasks capability (required for ${method})`);
|
||||
throw new SdkError(
|
||||
SdkErrorCode.CapabilityNotSupported,
|
||||
`Server does not support tasks capability (required for ${method})`
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -496,7 +516,7 @@ export class Server<
|
|||
): Promise<CreateMessageResult | CreateMessageResultWithTools> {
|
||||
// Capability check - only required when tools/toolChoice are provided
|
||||
if ((params.tools || params.toolChoice) && !this._clientCapabilities?.sampling?.tools) {
|
||||
throw new Error('Client does not support sampling tools capability.');
|
||||
throw new SdkError(SdkErrorCode.CapabilityNotSupported, 'Client does not support sampling tools capability.');
|
||||
}
|
||||
|
||||
// Message structure validation - always validate tool_use/tool_result pairs.
|
||||
|
|
@ -517,10 +537,16 @@ export class Server<
|
|||
|
||||
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');
|
||||
throw new ProtocolError(
|
||||
ProtocolErrorCode.InvalidParams,
|
||||
'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');
|
||||
throw new ProtocolError(
|
||||
ProtocolErrorCode.InvalidParams,
|
||||
'tool_result blocks are not matching any tool_use from the previous message'
|
||||
);
|
||||
}
|
||||
}
|
||||
if (hasPreviousToolUse) {
|
||||
|
|
@ -529,7 +555,10 @@ export class Server<
|
|||
lastContent.filter(c => c.type === 'tool_result').map(c => (c as ToolResultContent).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');
|
||||
throw new ProtocolError(
|
||||
ProtocolErrorCode.InvalidParams,
|
||||
'ids of tool_result blocks and tool_use blocks from previous message do not match'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -554,7 +583,7 @@ export class Server<
|
|||
switch (mode) {
|
||||
case 'url': {
|
||||
if (!this._clientCapabilities?.elicitation?.url) {
|
||||
throw new Error('Client does not support url elicitation.');
|
||||
throw new SdkError(SdkErrorCode.CapabilityNotSupported, 'Client does not support url elicitation.');
|
||||
}
|
||||
|
||||
const urlParams = params as ElicitRequestURLParams;
|
||||
|
|
@ -562,7 +591,7 @@ export class Server<
|
|||
}
|
||||
case 'form': {
|
||||
if (!this._clientCapabilities?.elicitation?.form) {
|
||||
throw new Error('Client does not support form elicitation.');
|
||||
throw new SdkError(SdkErrorCode.CapabilityNotSupported, 'Client does not support form elicitation.');
|
||||
}
|
||||
|
||||
const formParams: ElicitRequestFormParams =
|
||||
|
|
@ -576,17 +605,17 @@ export class Server<
|
|||
const validationResult = validator(result.content);
|
||||
|
||||
if (!validationResult.valid) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
throw new ProtocolError(
|
||||
ProtocolErrorCode.InvalidParams,
|
||||
`Elicitation response content does not match requested schema: ${validationResult.errorMessage}`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof McpError) {
|
||||
if (error instanceof ProtocolError) {
|
||||
throw error;
|
||||
}
|
||||
throw new McpError(
|
||||
ErrorCode.InternalError,
|
||||
throw new ProtocolError(
|
||||
ProtocolErrorCode.InternalError,
|
||||
`Error validating elicitation response: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
|
|
@ -606,7 +635,10 @@ export class Server<
|
|||
*/
|
||||
createElicitationCompletionNotifier(elicitationId: string, options?: NotificationOptions): () => Promise<void> {
|
||||
if (!this._clientCapabilities?.elicitation?.url) {
|
||||
throw new Error('Client does not support URL elicitation (required for notifications/elicitation/complete)');
|
||||
throw new SdkError(
|
||||
SdkErrorCode.CapabilityNotSupported,
|
||||
'Client does not support URL elicitation (required for notifications/elicitation/complete)'
|
||||
);
|
||||
}
|
||||
|
||||
return () =>
|
||||
|
|
|
|||
Loading…
Reference in a new issue