Skip to content

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)

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.

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.

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.

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.

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.