Restore the MCP tracer config, and trace the api → maple-ai hop - #868
Merged
Conversation
…acer The move out of apps/api dropped both options the api passed to `WorkerTelemetry`, and nothing tied the two files together. `anticipatedErrorIdentifiers` is what keeps an expected 400/401 — a tool call that does not decode, a missing or invalid credential — exporting with an `Ok` status and no exception event, per the rule that only 5xx is an `Error` span; `resolve-tenant.ts` says so in a comment and then relies on a config that no longer set it. Without it every anticipated MCP rejection lands in Maple's own error tracking as an unexpected error. `dropSpanNames` is the same story for the MCP server's notification spans, and it stayed behind on api, where nothing serves MCP any more. The guard in `expected-failures.test.ts` could not see this: it only inspects tracers built through `MapleCloudflareSDK.make`, and the request-facing one is built by `WorkerTelemetry` in the init. It now covers that too, and fails on a `WorkerTelemetry` call in this app that omits the identifiers. `AiObservabilityLive` carries the `TracerDisabledWhen` filter the bridge's tracer reads, so a liveness probe does not span. Header redaction stays at Effect's defaults, which already cover every credential reaching this Worker — the provider webhook signatures on api's list are received on api's routes and never forwarded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`fromCloudflareFetcher` calls the binding through a raw `fetch`, and the server SDK instruments no outbound calls, so the forwarded request left with whatever `traceparent` the client sent — usually none. maple-ai then opened a new root trace and one `/mcp` call read as two unrelated traces, with no edge between the Workers on the service map. The forward now replaces `traceparent` with api's own server span, which is what parents ai's span to it; a client that sent its own is already that span's parent, so the trace stays whole either way. Everything else still crosses byte for byte, streams included. Moved into its own module for two reasons. The path predicate is the contract between the two Workers — a path it misses 404s from api's router, one it over-matches never reaches api's routes at all, and both failures are invisible to a typecheck — and it is now covered by a table in both directions, including `/mcp-something` and `/.well-known/oauth-protected-resource/mcp`. And the binding arrives as an unparsed `env` value, so it is narrowed by a guard rather than asserted into the fetcher type: a binding that is present but wrong is a logged 503 instead of a defect inside alchemy's adapter. A binding call that rejects stays a defect the bridge renders and reports, exactly as before the split. The only expected failure that is absorbed is a request that cannot be rendered to the web shape, which on workerd cannot happen — the bridge's request already is one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The fan-out Workflow moved to maple-ai still yielded `MapleDb("api")`, which is the
consumer that picks the Hyperdrive config `MAPLE_DB` binds on the HOST script — so
maple-ai bound `MAPLE_DB` twice, once per consumer. Harmless only while both resolve to
the same config, which is exactly the state the `maple-ai-prd` TODO exists to end.
Also moves `@maple-dev/effect-sdk` to dependencies, where `chat/turn-runner.ts` imports
it from production code, and corrects two comments that still name apps/api as the
Workflow's host.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Follow-up to #861. Four things the split changed by omission, plus the tests that would have caught two of them.
The MCP tracer config did not move with the surfaces
apps/apipassedWorkerTelemetrytwo options that maple-ai did not:anticipatedErrorIdentifiers: MCP_ANTICIPATED_ERROR_IDENTIFIERS— what keeps an expected 400/401 (a tool call that does not decode, a missing or invalid credential) exporting with anOkstatus and no exception event.resolve-tenant.tsdocuments the mechanism and then relied on a config that no longer set it, so every anticipated MCP rejection was landing in Maple's own error tracking as an unexpected error.dropSpanNames: ["McpServer/Notifications."]— which stayed behind on api, where nothing serves MCP any more.The existing guard could not see this: it only inspects tracers built through
MapleCloudflareSDK.make, and the request-facing one is built byWorkerTelemetryin the Worker init. It now covers that too, and I checked it fails when the line is removed.maple-ai also gets the
TracerDisabledWhenfilter the bridge's tracer reads, so a liveness probe does not span. Header redaction stays at Effect's defaults, which already cover every credential reaching this Worker.The forward carried no trace context
fromCloudflareFetchercalls the binding through a rawfetchand nothing instruments outbound calls, so maple-ai opened a new root trace and one/mcpcall read as two unrelated traces with no edge between the Workers on the service map. The forward now replacestraceparentwith api's own server span. Everything else still crosses byte for byte, streams included.The forward had no test, and an inline cast
The path predicate is the contract between the two Workers — a path it misses 404s from api's router, one it over-matches never reaches api's routes at all — and both failures are invisible to a typecheck. It moves to
worker/ai-forward.tswith a table in both directions, plus coverage of header, body and host preservation and the injectedtraceparent. The binding is narrowed by a guard rather than asserted into the fetcher type, so a present-but-wrong binding is a logged 503 instead of a defect inside alchemy's adapter.The Workflow bound api's database consumer
MapleDb(consumer)picks the Hyperdrive configMAPLE_DBbinds on the host script, and the fan-out Workflow still named"api"while its host yields"ai"— twoMAPLE_DBbinds on maple-ai, harmless only while both resolve to the same config. That is exactly the state themaple-ai-prdTODO exists to end.Also moves
@maple-dev/effect-sdkto dependencies, wherechat/turn-runner.tsimports it from production code, and corrects two comments that still name apps/api as the Workflow's host.Checks
@maple/aitypecheck,apps/apitsc, oxfmt and oxlint on every touched file, and the forward, graph-boundary and expected-failure suites. All green. The repo-wide typecheck and full suite are CI's.Still not code
Two prd gates from the review remain open: the dedicated
maple-ai-prdHyperdrive config does not exist, so maple-ai serves prd traffic off api's pool, and the "Prod revision skew" alert rule (its SQL lives in the production database) has to listmaple-ai.🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.