Skip to content

fix(core): instrument MCP transports before start - #23978

Draft
betegon wants to merge 1 commit into
developfrom
bt/fix-mcp-transport-start
Draft

fix(core): instrument MCP transports before start#23978
betegon wants to merge 1 commit into
developfrom
bt/fix-mcp-transport-start

Conversation

@betegon

@betegon betegon commented Sep 2, 2026

Copy link
Copy Markdown
Member

MCP server instrumentation now observes messages a transport delivers while
server.connect() is still in progress. This closes the first-message gap
without changing the wrapper API or leaving a permanent layer around the
transport lifecycle.

The point immediately before transport.start() is the only generic lifecycle
boundary after the upstream SDK has installed its callbacks but before a
transport may synchronously drain queued input. The interceptor is therefore
one-shot per connection attempt and restores the transport before invoking its
original method. A post-connect fallback retains support for structural
transports which do not expose an interceptable start().

The boundary preserves the original receiver, Promise, synchronous failure and
property shape, including inherited and configurable own methods. Regression
coverage uses the official MCP v1 1.30.0 and v2 2.0.0 in-memory transports
with initialize queued before direct connect (v2's legacy-compatible mode),
and asserts exactly one MCP span. The same causal scenario runs in ESM and CJS
in v11's default Sentry-only mode and in its optional Sentry-managed
OpenTelemetry-compatible mode
(enableOpenTelemetrySetup: true). The latter is v11 setup 2: OpenTelemetry
API spans become native Sentry spans through Sentry's minimal provider. It is
not the user-owned-provider setup using openTelemetryIntegration(), which
keeps both span pipelines separate. This gate checks the MCP timing under setup
2's alternate context strategy; it does not claim coverage of user-created
OpenTelemetry spans, manual attributes or OTLP. This change does not alter
providers, exporters, semantic conventions or W3C propagation.

Packaged Node and Cloudflare fixtures also pass for MCP v1, v2 and the modern
2026-07-28 protocol. A causal Worker A/B compared the PR's direct parent
7fe2c7cfdde7 with candidate 2c46be688ac0 using byte-identical Worker source
and dependency inputs. With requests provably queued before server connect,
the baseline produced zero spans for both v1 and v2 direct connect; the
candidate produced exactly one for each. Both builds completed the same MCP
connections successfully.

The same deployment included a genuine v2 2026-07-28 serveStdio exchange
as a no-regression control: it produced one server/discover span before and
after. That standard modern entry path was not affected because it connects
its internal channel before delivering the opening request. Sentry ingested
the exact Worker-observed span IDs with the expected HTTP parent, status,
method, name and transport. Each evidence version served 100% of traffic, and
both disposable Workers were deleted and independently confirmed absent.
The corresponding Sentry traces are
fa89a48f1d59460ab89af7f6852da81d (baseline) and
52aa25c2afcc4eaeb8db42a00ca98b4d (candidate).

Root cause

The MCP SDK assigns onmessage, onclose and onerror during connect() and
then awaits transport.start(). Some transports synchronously deliver already
queued messages from start(). Sentry previously waited for connect() to
finish before wrapping those callbacks, so the first request could complete
without instrumentation.

Fixes #23977

Co-Authored-By: OpenAI Codex <codex@openai.com>
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 28.57 kB - -
@sentry/browser - with treeshaking flags 26.94 kB - -
@sentry/browser - with treeshaking flags tracing without tracing 26.83 kB - -
@sentry/browser (incl. Tracing) 48.87 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 48.88 kB - -
@sentry/browser (incl. Tracing, Profiling) 51.8 kB - -
@sentry/browser (incl. Tracing, Replay) 88.36 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 77.76 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 93.05 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 106.02 kB - -
@sentry/browser (incl. Feedback) 46.06 kB - -
@sentry/browser (incl. sendFeedback) 33.64 kB - -
@sentry/browser (incl. FeedbackAsync) 38.74 kB - -
@sentry/browser (incl. Metrics) 29.52 kB - -
@sentry/browser (incl. Logs) 29.81 kB - -
@sentry/browser (incl. Metrics & Logs) 30.45 kB - -
@sentry/react 30.32 kB - -
@sentry/react (incl. Tracing) 51.08 kB - -
@sentry/vue 35.74 kB - -
@sentry/vue (incl. Tracing) 51.14 kB - -
@sentry/svelte 28.6 kB - -
CDN Bundle 30.36 kB - -
CDN Bundle (incl. Tracing) 49.52 kB - -
CDN Bundle (incl. Logs, Metrics) 32.59 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 51.43 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 73.19 kB - -
CDN Bundle (incl. Tracing, Replay) 87 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 88.88 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 92.93 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 94.83 kB - -
CDN Bundle - uncompressed 89.97 kB - -
CDN Bundle (incl. Tracing) - uncompressed 147.57 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 96.27 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 153.26 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 225.43 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 267.07 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 272.74 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 280.77 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 286.43 kB - -
@sentry/nextjs (client) 53.7 kB - -
@sentry/sveltekit (client) 49.3 kB - -
@sentry/core/server 40.97 kB +0.56% +225 B 🔺
@sentry/core/browser 13.42 kB - -
@sentry/node 124.48 kB +0.02% +24 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 81.51 kB - -
@sentry/node - without tracing 88.3 kB +0.03% +23 B 🔺
@sentry/node - without channel injection 103.77 kB +0.02% +20 B 🔺
@sentry/aws-serverless 96.66 kB +0.03% +25 B 🔺
@sentry/cloudflare (withSentry) - minified 200.81 kB - -
@sentry/cloudflare (withSentry) 499.76 kB - -

View base workflow run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MCP server wrapper misses messages delivered during transport start

1 participant