fix(mcp): strip server-internal toolTelemetry at the wire boundary#1083
Draft
vjsai wants to merge 1 commit into
Draft
fix(mcp): strip server-internal toolTelemetry at the wire boundary#1083vjsai wants to merge 1 commit into
vjsai wants to merge 1 commit into
Conversation
INTERNAL tools without their own try/catch let errors propagate to the framework's outer catch in the CallToolRequestSchema handler. That path returns via `captureResult` and never passes through `extractToolTelemetry()` — the only place that strips `toolTelemetry` — so the server-internal field reached the client, because the MCP SDK's `CallToolResultSchema` is a `z.looseObject` (passthrough). Fix it at the framework boundary (option 1 in apify#1052): `captureResult` is the single point every tool-call response leaves the handler through, so strip `toolTelemetry` there. This closes the leak on every current and future return path, not just the ones that read the field. The outer catch no longer attaches `toolTelemetry` at all: the ABORTED-aware `toolStatus` it carried is already reported to telemetry from the handler's local state, so classification accuracy is unchanged. Adds a wire-level test that drives a real MCP client over an in-memory transport and asserts the client-visible result carries no `toolTelemetry`; it fails on master and passes here. The two existing outer-catch tests, which pinned the leaked shape, now assert the field is absent while the ABORTED/FAILED status still reaches telemetry.
Collaborator
|
@vjsai I'm sorry for the delayed response, it looks good, can you please fix the conflicts? |
jirispilka
self-requested a review
July 16, 2026 07:36
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.
Closes #1052
Problem
INTERNAL tools with no
try/catchof their own let a thrown error propagate to the framework's outercatchin theCallToolRequestSchemahandler. That path returns viacaptureResultand never goes throughextractToolTelemetry()— the only placetoolTelemetryis stripped. Because the MCP SDK'sCallToolResultSchemais az.looseObject(passthrough), the server-internaltoolTelemetry: { toolStatus }key is forwarded verbatim to the client.Confirmed end-to-end, not just by reading the code: driving a real MCP
Clientover an in-memory transport against a throwing INTERNAL tool, the client-visible result carriestoolTelemetryonmaster.Fix — option 1 from the issue (single framework boundary)
captureResultis the one point every tool-call response leaves the handler through, so the strip goes there. That closes the leak on every return path — including future ones — rather than only the paths that happen to read the field.src/utils/tool_status.ts— newstripToolTelemetry();extractToolTelemetry()now delegates itsdeleteto it, so there is a single definition of "remove the internal field".src/mcp/server.ts—captureResult()stripstoolTelemetrybefore the response is captured and returned.src/mcp/server.ts— the outer catch no longer attachestoolTelemetryat all. The ABORTED-awaretoolStatusit carried is already reported to telemetry from the handler's local state, so no classification accuracy is lost. (The comment there previously explained the field was kept for byte-identity during refactor: Adopt respond* response constructors across tools #937; that rationale is what this PR retires.)~10 lines of production code.
I scoped this to the leak only — the issue's second effect (generic
getToolCallErrorUserTextinstead of tool-specific messages, i.e. option 2's per-tooltry/catch) is a separate, larger change across several tools and is better as its own PR. Happy to follow up if you want it.Wire-shape change (please read)
This changes client-facing wire output: the leaked
toolTelemetrykey is gone from error responses. As you flagged in the issue,apify-mcp-server-internalconsumes the stripped wire shape — it should need no change (it now simply always sees the shape it already expected), but it's worth a matching assertion in its contract suite. I can't touch that repo, so flagging it here.Tests
tests/unit/mcp.server.error_handling.test.ts): realClient+InMemoryTransportlinked pair against a throwing INTERNAL tool, asserting the client-visible result has notoolTelemetry. Fails onmaster, passes here — this is the before/after wire snapshot the issue asked for.expect(result.toolTelemetry).toEqual({ toolStatus })). They now assert the field is absent from the response while the ABORTED / FAILED status still reaches telemetry (spying onlogToolCallAndTelemetry) — so the strip is proven not to cost classification.stripToolTelemetry()(tests/unit/utils.tool_status.test.ts): strips in place, leaves clean responses untouched, passes through non-objects.Local gates, all green:
CHANGELOG is git-cliff-generated from the conventional commit, so it is not hand-edited.
Prepared with AI assistance (Claude Code); every line reviewed and the behaviour verified locally against the repo's own harness. Opened as a draft — happy to adjust the approach (e.g. option 2 instead) before you spend review time on it.