MCP server
Wrap your MCP server once. Every tool you register afterward reports its own failures, with no per-tool wiring.
Wrap your server
Section titled “Wrap your server”Call wrapMcpServer on your server instance before you register tools:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { createProblems } from "@problemsdev/sdk";
const problems = createProblems({ slug: "acme" });
const server = problems.wrapMcpServer( new McpServer({ name: "acme", version: "1.4.2" }),);
// Every tool registered after the wrap is covered:server.registerTool("create_widget", schema, async (args) => { // ...your handler. If it throws or returns { isError: true }, it's reported.});It returns the same server, patched in place, so the rest of your setup is
unchanged. A tool is reported when its handler throws or returns a result with
isError: true.
Fixes come back in-band
Section titled “Fixes come back in-band”When a reported failure matches a known answer you’ve published, the SDK appends
a { type: "text" } block with the fix to the tool result, so the agent sees it
on the failing call and can retry. Set injectAnswers: false to leave tool
output untouched.
Answers for thrown exceptions
Section titled “Answers for thrown exceptions”By default, thrown exceptions are reported and rethrown unchanged. To return
fixes for selected exceptions, supply mapToolError. Return a safe message for
an application error, or undefined to preserve the original throw. Leave
protocol errors and cancellation unmapped.
class QuotaError extends Error {}
const problems = createProblems({ slug: "acme", mapToolError: (error) => error instanceof QuotaError ? "Workspace quota exceeded" : undefined,});A mapped exception becomes an isError: true result containing your message,
with a known fix appended when available. If no answer is available, the mapped
error result is still returned. The original exception is reported once,
including its stack when present. Avoid secrets in both exception text and
mapped messages. A mapper that throws preserves the original exception.
Mapping is disabled when reporting or injectAnswers is disabled. Existing
returned isError: true results continue to support answer injection without
a mapper.
Escape hatch: wrap a single handler
Section titled “Escape hatch: wrap a single handler”If wrapMcpServer can’t reach your registration, such as when using a custom
framework, wrap the handler directly:
const handler = problems.wrapToolHandler("create_widget", async (args) => { // ...});
server.registerTool("create_widget", schema, handler);wrapToolHandler wraps the function you pass it without patching the server’s
registration methods. Use it when your framework owns tool registration. Choose
one wrapping method for each handler; don’t wrap the same handler twice.
What the wrapper captures
Section titled “What the wrapper captures”The wrapper observes execution of tool handlers, not every failure in an MCP connection. Reporting is still subject to opt-out, filtering, limits, and network availability.
| Failure | Automatic capture | Answer behavior |
|---|---|---|
Wrapped handler returns isError: true |
Yes | A matched answer can be appended to the error result |
| Wrapped handler throws | Yes | Original exception is rethrown unless mapToolError maps it |
| Input validation rejects the call before the handler runs | No | Framework’s original validation response |
| Connection, authentication, protocol, or transport failure outside the handler | No | Framework’s original failure behavior |
| Successful result that is incorrect or incomplete | No | Successful result is unchanged |
| Work started by the handler but not returned or awaited | No, if it fails after the handler has returned | No tool-result answer injection |
wrapMcpServer intercepts subsequent registerTool() and tool() calls. It
does not retrofit tools registered earlier, and a later handler replacement must
also be wrapped. Install it once before your registration code, or pass a
wrapToolHandler result to your framework’s registration/update API.
Cancellation is not automatically classified as an expected error. If it reaches
the handler as an exception, the wrapper attempts to report it and preserves the
throw unless mapped. Keep cancellation out of mapToolError; optional filtering
belongs in the advanced capture controls.
For failures outside a handler, use report() at the boundary your application
owns when that failure is useful to the provider. Keep the original protocol or
transport response intact. Don’t report the same error manually and through a
wrapped handler. These boundaries don’t require extra setup for normal tool
failures.
Add context
Section titled “Add context”The wrap captures the tool name and argument names automatically. For explicit
reporting outside wrapped handlers, report() accepts additional context such as
goal and versions, as shown on the CLI page. Use it for errors that
aren’t already captured by the wrapper.