MCP server
Instrument the MCP server that executes your tools. Installing Problems in an underlying HTTP client SDK does not automatically capture MCP calls.
Create one Problems client per process and choose the integration that matches your server:
| Your server | Use |
|---|---|
McpServer with registerTool() or tool() |
wrapMcpServer(server) before registering tools |
| Custom dispatcher, another framework, or individual tool handlers | wrapToolHandler(name, handler) |
Standard MCP server
Section titled “Standard MCP server”Wrap the server before registering tools. This example uses
McpServer from @modelcontextprotocol/sdk:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { createProblems } from "@problemsdev/sdk";
const server = new McpServer({ name: "example", version: "1.0.0" });const problems = createProblems({ publicKey: "YOUR_PUBLIC_KEY" });problems.wrapMcpServer(server);
server.registerTool("search", { inputSchema: {} }, async () => { return { content: [{ type: "text", text: "Found it" }] };});Both registerTool and tool registrations are supported. Connect your server
to its usual transport after registering the tools.
Custom servers and individual handlers
Section titled “Custom servers and individual handlers”Wrap the function that executes a named tool and returns its MCP result. Use the wrapped function in your existing tool dispatcher:
import { createProblems } from "@problemsdev/sdk";
const problems = createProblems({ publicKey: "YOUR_PUBLIC_KEY" });
const search = problems.wrapToolHandler( "search", async (input: { query: string }) => { if (!input.query.trim()) { return { isError: true, content: [ { type: "text" as const, text: "A search query is required" }, ], }; }
return { content: [ { type: "text" as const, text: `Searching for ${input.query}` }, ], }; },);
// In your existing dispatcher, call the wrapped handler with the tool arguments.const result = await search({ query: "billing" });// Use result as the result field of your existing JSON-RPC response.Replace the example search body with your tool’s implementation. The wrapper
returns a promise and preserves the handler’s arguments, this, return value,
and thrown errors. Create the wrapped handler once per tool and use a stable tool
name, without arguments or customer identifiers.
The wrapped function must return the tool result itself, including isError
when applicable. Wrap before constructing the JSON-RPC envelope or HTTP
Response; Problems does not inspect nested results or HTTP status codes.
If your dispatcher catches exceptions and converts them into tool errors, the
wrapper can surround that conversion and count the returned isError: true.
Choose one wrapping boundary per tool so each call is recorded once. Calls that fail authentication, routing, or validation before reaching that boundary are not counted. Keep those checks in your existing server.
What gets recorded
Section titled “What gets recorded”Both integrations count successful results, returned isError: true results,
and thrown errors. Original return values and thrown errors are preserved.
Tool arguments, successful content, and outputs are never sent. Error text and
stacks may be sent after redaction.
Delivery and shutdown
Section titled “Delivery and shutdown”The SDK sends batches every two seconds or at 50 events. Calls never wait for
telemetry delivery. In your server’s existing graceful shutdown handler, call
await problems.flush(). The SDK adds no global process handlers.
Optional agent status context
Section titled “Optional agent status context”With SDK 0.0.2 or later, an error can include context from the project’s public
agent status page. Enable that page in Problems settings first. checkStatus
returns data; your integration decides whether to append it to the tool result.
Use an inner recording wrapper and append context afterward. This preserves the original error for grouping and counts each call once. This example’s search body returns a timeout to show the flow; replace it with your tool implementation:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";import { createProblems } from "@problemsdev/sdk";
const server = new McpServer({ name: "example", version: "1.0.0" });const problems = createProblems({ publicKey: "YOUR_PUBLIC_KEY" });
const search = problems.wrapToolHandler( "search", async (): Promise<CallToolResult> => ({ isError: true, content: [{ type: "text", text: "Search request timed out" }], }),);
server.registerTool("search", { inputSchema: {} }, async () => { const result = await search(); // Records the original result once. if (!result.isError) return result;
const status = await problems.checkStatus({ surface: "mcp", operation: "search", error: result, }); if (status.status !== "matched") return result;
const { reports, url } = status.problem; return { ...result, content: [ ...result.content, { type: "text", text: `This error has been reported ${reports} times in the last 5 minutes. ${url}`, }, ], };});Use this in place of wrapMcpServer for the tool being enriched; wrapping it a
second time would double-count the call and record the added context as error
text. Pass the original direct isError result, not a JSON-RPC envelope.
checkStatus also accepts the same thrown Error that a wrapper recorded. If
your integration catches one to check status, preserve its existing error handling.
Checks return matched, no_match, or unavailable and never throw on lookup
failure. They do not record or flush an event. Network reads time out after 500 ms
and use a cache lasting at most 15 seconds, bounded by the problem’s expiry. The
newly recorded report may still be processing; the helper only matches problems
already published. Successes skip the check. Reporting opt-outs and beforeSend
filtering apply to both paths; see Privacy and delivery.