From e87d015b13170752047f80a0745ea862dff91234 Mon Sep 17 00:00:00 2001 From: Radesh Govind Date: Sat, 28 Mar 2026 09:19:38 +0000 Subject: [PATCH 1/4] docs: document best practice for error handling in MCP tool implementations Resolves #356 Clarify the two-tier error model: - Recoverable tool errors: use CallToolResult with isError(true) - Protocol-level errors: throw McpError / let exceptions propagate as JSON-RPC errors --- docs/server.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/docs/server.md b/docs/server.md index f9f3aa683..57fc462e5 100644 --- a/docs/server.md +++ b/docs/server.md @@ -795,3 +795,43 @@ Supported logging levels (in order of increasing severity): DEBUG (0), INFO (1), ## Error Handling The SDK provides comprehensive error handling through the McpError class, covering protocol compatibility, transport communication, JSON-RPC messaging, tool execution, resource management, prompt handling, timeouts, and connection issues. This unified error handling approach ensures consistent and reliable error management across both synchronous and asynchronous operations. + +### Error Handling in Tool Implementations + +#### Two Tiers of Errors + +MCP distinguishes between two categories of errors in tool execution: + +**1. Tool-Level Errors (Recoverable by the LLM)** + +Use `CallToolResult` with `isError(true)` for validation failures, missing arguments, or domain errors the LLM can act on and retry. + +```java +// Example: missing required argument +if (lastName == null || lastName.isBlank()) { + return CallToolResult.builder() + .content(List.of(new McpSchema.TextContent("Missing required argument: last_name"))) + .isError(true) + .build(); +} +``` + +The LLM receives this as part of the normal tool response and can self-correct in a subsequent interaction. + +**2. Protocol-Level Errors (Unrecoverable)** + +Uncaught exceptions from a tool handler are mapped to a JSON-RPC error response. Use this only for truly unexpected failures (e.g., infrastructure errors), not for input validation. + +```java +// This propagates as a JSON-RPC error — use sparingly +throw new McpError(McpSchema.ErrorCodes.INTERNAL_ERROR, "Unexpected failure"); +``` + +#### Decision Guide + +| Situation | Approach | +|------------------------------------|---------------------------------------| +| Missing / invalid argument | `CallToolResult` with `isError=true` | +| Domain validation failure | `CallToolResult` with `isError=true` | +| Infrastructure / unexpected error | Throw `McpError` or let it propagate | +| Partial success with a warning | `CallToolResult` with warning in text | From 84ea9ed888b008970fdcd7d0886f63aa01ad7cf6 Mon Sep 17 00:00:00 2001 From: Radesh Govind Date: Wed, 1 Apr 2026 16:22:53 +0100 Subject: [PATCH 2/4] docs: clarify infrastructure errors Adds "DB timeout" as an explicit example of an infrastructure error to distinguish from input validation. Co-authored-by: Daniel Garnier-Moiroux --- docs/server.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/server.md b/docs/server.md index 57fc462e5..04c28be85 100644 --- a/docs/server.md +++ b/docs/server.md @@ -820,7 +820,7 @@ The LLM receives this as part of the normal tool response and can self-correct i **2. Protocol-Level Errors (Unrecoverable)** -Uncaught exceptions from a tool handler are mapped to a JSON-RPC error response. Use this only for truly unexpected failures (e.g., infrastructure errors), not for input validation. +Uncaught exceptions from a tool handler are mapped to a JSON-RPC error response. Use this only for truly unexpected failures (e.g., infrastructure errors such as DB timeout), not for input validation. ```java // This propagates as a JSON-RPC error — use sparingly From aaa8f817a347d7c6c685f976359050ac6878f973 Mon Sep 17 00:00:00 2001 From: Radesh Govind Date: Wed, 1 Apr 2026 16:23:41 +0100 Subject: [PATCH 3/4] docs: consolidate summary table Folds duplicate domain validation rows in the summary table to improve readability. Co-authored-by: Daniel Garnier-Moiroux --- docs/server.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/server.md b/docs/server.md index 04c28be85..51871257f 100644 --- a/docs/server.md +++ b/docs/server.md @@ -831,7 +831,6 @@ throw new McpError(McpSchema.ErrorCodes.INTERNAL_ERROR, "Unexpected failure"); | Situation | Approach | |------------------------------------|---------------------------------------| -| Missing / invalid argument | `CallToolResult` with `isError=true` | | Domain validation failure | `CallToolResult` with `isError=true` | | Infrastructure / unexpected error | Throw `McpError` or let it propagate | | Partial success with a warning | `CallToolResult` with warning in text | From 1616f28caf3d1f6f76dae70967879657473058be Mon Sep 17 00:00:00 2001 From: Radesh Govind Date: Wed, 1 Apr 2026 16:30:22 +0100 Subject: [PATCH 4/4] docs: update tool error handling example to use domain validation Replaces the contrived missing argument check with a realistic email validation example per maintainer review. This clarifies that CallToolResult(isError=true) should be used for business logic/domain validation, since MCP natively handles missing required arguments via JSON Schema. --- docs/server.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/server.md b/docs/server.md index 51871257f..378de6975 100644 --- a/docs/server.md +++ b/docs/server.md @@ -807,10 +807,10 @@ MCP distinguishes between two categories of errors in tool execution: Use `CallToolResult` with `isError(true)` for validation failures, missing arguments, or domain errors the LLM can act on and retry. ```java -// Example: missing required argument -if (lastName == null || lastName.isBlank()) { - return CallToolResult.builder() - .content(List.of(new McpSchema.TextContent("Missing required argument: last_name"))) +// Example: Domain validation failure (e.g., invalid email format) +if (!emailAddress.matches("^[A-Za-z0-9+_.-]+@(.+)$")) { + return CallToolResult.builder() + .content(List.of(new McpSchema.TextContent("Invalid argument: 'email' must be a valid email address."))) .isError(true) .build(); }