v2: Errors refactor (ProtocolError, SdkError, OAuthError) (#1454)

This commit is contained in:
Konstantin Konstantinov 2026-02-03 18:41:22 +02:00 • committed by GitHub
commit 0f0a4eb7c8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 1086 additions and 705 deletions

View file

@ -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;

View file

@ -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 () =>