fix(core): instrument MCP transports before start - #23978
Draft
betegon wants to merge 1 commit into
Draft
Conversation
Co-Authored-By: OpenAI Codex <codex@openai.com>
Contributor
size-limit report 📦
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
MCP server instrumentation now observes messages a transport delivers while
server.connect()is still in progress. This closes the first-message gapwithout changing the wrapper API or leaving a permanent layer around the
transport lifecycle.
The point immediately before
transport.start()is the only generic lifecycleboundary 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.0and v22.0.0in-memory transportswith
initializequeued 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: OpenTelemetryAPI spans become native Sentry spans through Sentry's minimal provider. It is
not the user-owned-provider setup using
openTelemetryIntegration(), whichkeeps 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-28protocol. A causal Worker A/B compared the PR's direct parent7fe2c7cfdde7with candidate2c46be688ac0using byte-identical Worker sourceand 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-28serveStdioexchangeas a no-regression control: it produced one
server/discoverspan before andafter. 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) and52aa25c2afcc4eaeb8db42a00ca98b4d(candidate).Root cause
The MCP SDK assigns
onmessage,oncloseandonerrorduringconnect()andthen awaits
transport.start(). Some transports synchronously deliver alreadyqueued messages from
start(). Sentry previously waited forconnect()tofinish before wrapping those callbacks, so the first request could complete
without instrumentation.
Fixes #23977