Skip to content

fix: stop asserting fleet tool registration through MCP SDK internals - #296

Open
saucam wants to merge 1 commit into
mainfrom
fix/fleet-tool-registration-assert
Open

fix: stop asserting fleet tool registration through MCP SDK internals#296
saucam wants to merge 1 commit into
mainfrom
fix/fleet-tool-registration-assert

Conversation

@saucam

@saucam saucam commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

main is red right now, and this unblocks it

Every open PR currently fails daemon-check on one test:

ORCHESTRATOR_FLEET_TOOLS > the BUILT server registers exactly those tools

TypeError: undefined is not an object
  (evaluating 'Object.keys(server.instance._registeredTools)')

No code caused this. The workflow pins bun-version: latest:

Run Tree Bun Result
#294 @ 00:35 main 1.3.13 pass
#295 @ 14:46 main + an unrelated feature 1.3.14 fail
#295 re-run, identical code same 1.3.14 fail (not flaky)
#294 re-run @ 14:49 main's own tree, unchanged 1.3.14 fail

That last row is the proof: main's own commit fails the same test today having passed this morning. Dependencies are identical either side — --frozen-lockfile, claude-agent-sdk@0.3.220 both runs. Only the runtime moved.

Why the test was fragile

It read server.instance._registeredTools, a private field on a minified bundled copy of the MCP SDK — @anthropic-ai/claude-agent-sdk inlines it, and the class shows up at runtime as Ck. Minified internals are not a surface anything should assert on; a patch-level bump was always going to be able to break it.

The fix

buildFleetMcpServer now materialises the picked tool array once and returns its names as registeredToolNames, derived from the exact array handed to createSdkMcpServer.

That preserves what the test was actually protecting — its own comment says it best, "Asserting the constant alone would pass while pick() silently ignored it — the filter is what actually reaches the model" — because the assertion still runs against what went into the SDK, not against a restatement of the constant. It just no longer depends on anything minified.

A second test cross-checks registeredToolNames against the live server's internals so the new accessor can't drift from reality. It treats the private field as a corroborating signal and skips when absent, so the next SDK move degrades it to a no-op instead of re-breaking the build.

Why not just pin bun

bun-version: 1.3.13 would go green today and re-break on the next bump, with the same debugging cost. Worth doing separately if you want CI reproducibility, but it isn't a fix for this test.

Verification

  • Fleet tests pass on both bun 1.3.13 and 1.3.14 (downloaded 1.3.14 locally and ran them side by side)
  • Full suite: 2311 pass / 0 fail, typecheck and biome clean

Note

Found while opening #295 (repeat-tool guard). That PR is blocked on this one; it should go green once this lands and it rebases.

🤖 Generated with Claude Code

`ORCHESTRATOR_FLEET_TOOLS > the BUILT server registers exactly those tools`
read `server.instance._registeredTools` — a private field on the MINIFIED
copy of the MCP SDK that @anthropic-ai/claude-agent-sdk bundles. That is not
a stable surface, and it moved: the field now reads back undefined, so the
test dies with

  TypeError: undefined is not an object
    (evaluating 'Object.keys(server.instance._registeredTools)')

No code changed to cause this. The workflow pins `bun-version: latest`; #294
went green on bun 1.3.13 at 00:35 and the same job re-run at 14:49 on bun
1.3.14 fails on main's own tree. Dependencies are identical either side
(--frozen-lockfile, claude-agent-sdk@0.3.220) — only the runtime moved. Every
open PR is red until this lands.

buildFleetMcpServer now materialises the picked tool array once and returns
its names as `registeredToolNames`, derived from the exact array handed to
`createSdkMcpServer`. That keeps what the test was actually protecting — that
`pick()` is applied on the way in, rather than a constant quietly diverging
from the real surface — while depending on nothing minified.

A second test cross-checks `registeredToolNames` against the live server so
the accessor cannot drift from reality. It reads the private field only as a
corroborating signal and skips when absent, so an SDK move degrades it to a
no-op instead of re-breaking the build.

Pinning bun would also go green today and re-break on the next bump.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant