Skip to content

MCP server

Wrap your MCP server once. Every tool you register afterward reports its own failures, with no per-tool wiring.

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.

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.

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.

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.

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.

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.