Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions TELEMETRY.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,13 @@ Rates:
that failure either as a wrapped `Parse error` message or, since MCP SDK 2.1, as the raw
`SyntaxError` from `JSON.parse`, and both forms are dropped. Both HTTP responses
are still counted by `app.server.response`. Other error events are retained.
- **`createMcpHandler` reports through its own `onerror`**, separate from the transport
hook above. It passes client rejections (unsupported protocol revision, failed header or
envelope checks, wrong content type) together with real faults. `isMcpClientRejection`
skips the rejections, which already answer 4xx. Everything else is captured: faults the
handler answers with a 500, and `subscriptions/listen` calls refused at the subscription
limit, which answer HTTP 200 with a JSON-RPC error and so are not visible in the
response metric.

## Privacy

Expand Down
21 changes: 21 additions & 0 deletions __tests__/telemetry.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { describe, it, expect } from "vitest";
import { UnsupportedProtocolVersionError } from "@modelcontextprotocol/server";
import {
classifyMcpMethod,
classifyMcpRequest,
Expand All @@ -7,6 +8,7 @@ import {
rootSampleRate,
statusClass,
errorDropReason,
isMcpClientRejection,
HEARTBEAT_SPAN_KEEP_RATE,
type ErrorEventLike,
} from "../src/telemetry.js";
Expand Down Expand Up @@ -131,6 +133,25 @@ describe("resolveClientFamily", () => {
});
});

describe("isMcpClientRejection", () => {
it.each([
new UnsupportedProtocolVersionError({ supported: ["2026-07-28"], requested: "1999-01-01" }),
new Error("Rejected inbound request (H3): MCP-Method header does not match the body"),
new Error("Rejected 2025-era request on a modern-only endpoint (L1): unsupported"),
new Error("Unsupported Media Type: Content-Type must be application/json"),
])("skips the client rejection %s", (error) => {
expect(isMcpClientRejection(error)).toBe(true);
});

it.each([
new Error("subscriptions/listen refused: subscription limit reached (1024)"),
new Error("Response for request ID 7 is undeliverable: per-request stream is disconnected"),
new TypeError("Cannot read properties of undefined"),
])("captures the server fault %s", (error) => {
expect(isMcpClientRejection(error)).toBe(false);
});
});

describe("errorDropReason", () => {
it("drops the expected MCP 406 raised for a GET without SSE support", () => {
const event: ErrorEventLike = {
Expand Down
17 changes: 17 additions & 0 deletions src/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
* unit-tested like redaction.ts; worker.ts owns the Sentry-specific wiring.
*/

import { UnsupportedProtocolVersionError } from "@modelcontextprotocol/server";

export type RouteGroup = "mcp" | "internal";
export type McpRequestKind = "heartbeat" | "tool_call" | "control" | "unknown";

Expand Down Expand Up @@ -244,6 +246,21 @@ export function errorDropReason(event: ErrorEventLike): string | null {
return null;
}

/**
* True for the client rejections that `createMcpHandler` passes to its `onerror` next to
* real faults: an unsupported protocol revision, a failed header or envelope check, or a
* wrong content type. The handler already answers each with a 4xx that
* `app.server.response` counts. Faults it answers with a 500, and refused
* `subscriptions/listen` calls, which it answers with HTTP 200, fall through to capture.
*/
export function isMcpClientRejection(error: Error): boolean {
return (
error instanceof UnsupportedProtocolVersionError ||
error.message.startsWith("Rejected ") ||
error.message.startsWith("Unsupported Media Type")
);
}

// --- Root span sampling (tracesSampler) ---------------------------------------

/**
Expand Down
8 changes: 7 additions & 1 deletion src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
classifyRoute,
errorDropReason,
isKnownMcpMethod,
isMcpClientRejection,
resolveClientFamily,
rootSampleRate,
statusClass,
Expand Down Expand Up @@ -151,7 +152,12 @@ const workerMcpHandler = createMcpHandler(
enableFeedbackTool: true,
});
},
{ legacy: "stateless" },
{
legacy: "stateless",
onerror: (error) => {
if (!isMcpClientRejection(error)) Sentry.captureException(error);
},
},
);

/** Best-effort pathname for a request URL, or null if it doesn't parse. */
Expand Down
Loading