-
Notifications
You must be signed in to change notification settings - Fork 1.7k
fix(server): sanitize internal error details in tool error responses #1830
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@modelcontextprotocol/server': minor | ||
| --- | ||
|
|
||
| Add ToolError class for secure-by-default tool error handling. Unhandled errors from tool handlers are now sanitized to "Internal error" instead of exposing raw messages. Use `throw new ToolError('message')` when you want clients to see a specific error message. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -45,6 +45,21 @@ import { getCompleter, isCompletable } from './completable.js'; | |
| import type { ServerOptions } from './server.js'; | ||
| import { Server } from './server.js'; | ||
|
|
||
| /** | ||
| * Error class for tool handlers to throw when they want to send a | ||
| * user-visible error message to the client. Unlike regular errors, | ||
| * ToolError messages are passed through to the client as-is. | ||
| * | ||
| * Regular errors thrown from tool handlers are sanitized to "Internal | ||
| * error" to prevent leaking sensitive server internals. | ||
| */ | ||
| export class ToolError extends Error { | ||
| constructor(message: string) { | ||
| super(message); | ||
| this.name = 'ToolError'; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * High-level MCP server that provides a simpler API for working with resources, tools, and prompts. | ||
| * For advanced usage (like sending notifications or setting custom request handlers), use the underlying | ||
|
|
@@ -209,7 +224,16 @@ export class McpServer { | |
| 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)); | ||
| if (error instanceof ProtocolError) { | ||
| // SDK-generated validation errors are safe to expose | ||
| return this.createToolError(error.message); | ||
| } | ||
| if (error instanceof ToolError) { | ||
|
Comment on lines
225
to
+231
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 The Extended reasoning...Bug: ProtocolError bypass of sanitizationWhat the bug is and how it manifests The error sanitization logic in The specific code path that triggers it In the catch block at lines 226-232 of Why existing code does not prevent it There is no private symbol, internal flag, or sealed constructor that distinguishes an SDK-generated Impact The PR's stated security goal — sanitizing all unexpected errors to 'Internal error' — is weakened. Any sensitive information placed into a Step-by-step proof
How to fix it Use a private symbol or internal subclass to mark SDK-generated Addressing the refutation The refutation argues this requires deliberately non-idiomatic code and no third-party library throws |
||
| // Developer intentionally wants this message shown to client | ||
| return this.createToolError(error.message); | ||
| } | ||
| // All other errors: sanitize to prevent leaking internals | ||
| return this.createToolError('Internal error'); | ||
| } | ||
| }); | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 This PR introduces a breaking behavioral change: generic Error messages from tool handlers are now sanitized to Internal error instead of being forwarded to clients. No changeset was added and neither docs/migration.md nor docs/migration-SKILL.md were updated as required by CLAUDE.md. Existing tool authors who relied on throw new Error messages reaching clients will be silently broken without migration guidance; they need to update their code to use the new ToolError class.
Extended reasoning...
What the bug is: This PR changes how tool handler errors are surfaced to MCP clients. Previously, any Error thrown from a tool handler had its .message forwarded to the client verbatim. After this PR, only ToolError and ProtocolError messages pass through; all other errors are sanitized to Internal error. This is a meaningful security improvement, but it is a breaking behavioral change for any existing tool author who relied on the previous behavior.
The specific code path: In packages/server/src/server/mcp.ts, the catch block of the tools/call handler was changed from returning error.message for any Error, to a conditional that only passes through ProtocolError and ToolError messages, and maps everything else to Internal error.
Why existing safeguards do not prevent the break: CLAUDE.md explicitly states When making breaking changes, document them in both docs/migration.md and docs/migration-SKILL.md. Neither file was updated. Grep for ToolError, sanitize, or Internal error returns zero matches in both files. Additionally, the changeset bot flagged this PR as having no changeset, confirming the versioning and documentation process was not followed.
Impact: Any tool author who currently does throw new Error with a specific message expecting that message to reach their client will now receive Internal error instead. This breakage is silent. There is no compile-time or runtime warning, and without migration notes, tool authors have no way of knowing they need to switch to ToolError.
How to fix: Add a .changeset file with at minimum a patch bump for the server package describing the sanitization behavior. Update docs/migration.md and docs/migration-SKILL.md with a section explaining the old behavior, the new behavior, the security rationale, and the migration path using ToolError for developer-controlled messages.
Step-by-step proof: Before this PR, calling server.registerTool and throwing new Error with a database connection string causes the client to receive that full string as the error message. After this PR, the same code causes the client to receive Internal error. The integration test in mcp.test.ts was itself updated from checking for the specific message text to checking for Internal error, directly confirming the observable behavioral difference.