Repository navigation
Conversation
enableToolsets() reports failures in its result instead of throwing, so ServerOrchestrator.isReady() was true even when a startup toolset failed to enable, and FastifyTransport ignored the result anyway. A client could connect with only part of its tools. initializeToolsets() now turns a failed result into initError, and the transport answers initialize with a JSON-RPC -32603 error (HTTP 500) and drops the bundle so the next initialize retries. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UJfs8sWB5EK8dXXjDU7Mv3
Reviewer findings: exposure-policy refusals and duplicate startup toolset names were made fatal by the previous commit. Only failures that happen while registering a toolset's tools (carried as a code on the enable result) now fail readiness. Changelog files the behaviour change as Breaking. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UJfs8sWB5EK8dXXjDU7Mv3
TS review:
|
| # | Sev | Location | Finding |
|---|---|---|---|
| 1 | MEDIUM | src/core/ServerOrchestrator.ts:86-101 |
Duplicate names in startup.toolsets (for example ["core","core"]) are now fatal. The second enable returns "already enabled", which used to be ignored. Dedupe the names or ignore that case, and add a test. |
| 2 | MEDIUM | ServerOrchestrator.ts:94-101, createMcpServer.ts:34-36, CHANGELOG.md:7-13 |
toolsets: "ALL" with an exposure policy (allowlist, denylist, maxActiveToolsets) used to serve only the permitted toolsets. It now makes createMcpServer() reject at startup. The changelog files this under Fixed without saying so. |
| 3 | LOW | src/http/FastifyTransport.ts:243-259 |
No negative caching: each initialize with a failing config rebuilds the bundle and re-runs the module loaders. Intended retry design; document it. |
| 4 | LOW | tests/serverOrchestrator.test.ts:285-299 |
console.error spy is restored after the assertions, so it leaks if one fails. Use try/finally. |
| 5 | LOW | tests/fastifyTransport.test.ts:220-275 |
The identity guard (get(cacheKey) === bundle) is untested, and the retry's status code is not asserted. |
| 6 | LOW | tests/e2e/staticMode.e2e.test.ts:140 |
ctx: any. Prefer unknown with a guard. |
| 7 | LOW | ServerOrchestrator.ts:86-91 |
Nested ternary for names; a small helper would read better. Style only. |
{
"recommendation": "COMMENT",
"summary": "The fix is correct and well scoped, the identity-guarded eviction is sound, and the tests cover the failure and retry paths. Two MEDIUM items remain: duplicate startup toolset names now fail fatally, and ALL combined with an exposure policy now fails server startup without an explicit CHANGELOG callout.",
"findings": [
{ "severity": "MEDIUM", "file": "src/core/ServerOrchestrator.ts", "line": 86, "issue": "Duplicate startup toolset names are fatal now" },
{ "severity": "MEDIUM", "file": "CHANGELOG.md", "line": 7, "issue": "ALL plus exposure policy now rejects createMcpServer at startup; entry is under Fixed and does not say so" },
{ "severity": "LOW", "file": "src/http/FastifyTransport.ts", "line": 243, "issue": "No negative caching; document" },
{ "severity": "LOW", "file": "tests/serverOrchestrator.test.ts", "line": 285, "issue": "spy restored after assertions" },
{ "severity": "LOW", "file": "tests/fastifyTransport.test.ts", "line": 270, "issue": "identity guard untested; retry status not asserted" },
{ "severity": "LOW", "file": "tests/e2e/staticMode.e2e.test.ts", "line": 140, "issue": "ctx typed any" },
{ "severity": "LOW", "file": "src/core/ServerOrchestrator.ts", "line": 86, "issue": "nested ternary" }
]
}Generated by Claude Code
Fixes for the round 1 TS reviewPushed in eb3411b.
Found while verifying against the FMP server ( Generated by Claude Code |
…g entry Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UJfs8sWB5EK8dXXjDU7Mv3
TS review round 2:
|
| # | Sev | Location | Finding | Disposition |
|---|---|---|---|---|
| 1 | LOW | CHANGELOG.md Breaking entry |
"a tool name that another toolset already registered" is imprecise: with default namespacing, collisions come from duplicates inside one toolset, or from namespacing being off. | Fixed in the commit below: reworded to "a duplicate tool name, within a toolset or across toolsets when namespaceToolsWithSetKey is off". |
| 2 | LOW | DynamicToolManager.ts enableToolset |
The return type gains an optional code; the @returns doc is stale and the changelog doesn't mention it (the enable_toolset meta-tool output now shows it on failure). |
Fixed: @returns updated, and the Breaking entry notes the new code. |
| 3 | LOW | ServerOrchestrator.ts:97 |
"Has a code" is an implicit signal for "failed while registering". |
Fixed with a comment at the catch-path code in DynamicToolManager. Invariant 8 and the tests already pin the behaviour. |
| 4 | LOW | ServerOrchestrator.ts:86-91 |
The nested ternary reduces to initial ?? []. Optional. |
Left unchanged, as in round 1: the reviewer marked it optional. |
The follow-up commit (docs, comment and TSDoc only, no logic change) is cbdd1a3. typecheck and 339/339 tests pass.
Generated by Claude Code
Requested by Ben · project thread
Before: when a startup toolset fails while registering its tools for a client (for example a tool name collision), the client still connects with whatever tools did register.
ServerOrchestrator.isReady()reportstruein that case, andFastifyTransportignores the result anyway. This is follow-up 1 from the review on #27 (#27 (comment)).After: the client's
initializegets a JSON-RPC-32603error (HTTP 500, "Failed to load toolsets for this client.") and no session is created. The failed bundle is dropped from the cache, so the client's nextinitializeretries the load instead of hitting the cached failure for up to an hour.isReady()now returnsfalse, andensureReady()throws, when registering a startup toolset's tools fails.Two findings that shape the fix:
DynamicToolManager.enableToolsets()catches per-toolset errors and returns{ success: false, results }. It never throws, so thecatchininitializeToolsets()was effectively unreachable andisReady()was alwaystrue. The review comment assumed it already returnedfalse. It only does now.ModuleResolverlogs a throwing module loader and carries on (there's a test for that), so a loader that throws still yields a toolset with zero tools and is not treated as a failure. I left that alone because changing it also changesenable_toolsetin DYNAMIC mode. If you want loader errors to fail the client too, that is a separate decision.Breaking, and needs an FMP server fix first. The base orchestrator uses the same code, so
createMcpServerin STATIC mode now fails at startup (the existing fail-fastensureReady()) when registering a startup toolset's tools fails, where it used to start with the partial set. Toolsets refused before registration (exposure policy, duplicate names instartup.toolsets) don't count.npm run verify:toolceptionon this branch packed gives 19/21 for the FMP server:ALL_TOOLS, the default mode, fails to start. The server'searningsandcalendartoolsets both include theearnings-transcriptmodule, sogetLatestEarningsTranscriptscollides. Today that collision is swallowed andearningsis left inactive. The server's catalog has to be fixed and merged before it takes a toolception release with this change. Whether to ship it strict is on a decision card in the project thread.How: enable results now carry a
codewhen registering tools fails;initializeToolsets()recordsinitErroronly for failed results that have one.FastifyTransportchecks theisReady()result beforeconnect(). Intent nodes (core,http,server) and an Unreleased changelog entry (Fixed and Breaking) are updated.Tests (the new ones fail on
main, pass here):serverOrchestrator: not ready on a tool name collision; ready when the exposure policy refuses a toolset, when a startup toolset is listed twice, and when all enable.fastifyTransport:initializereturns the JSON-RPC error, never callsconnect, and a secondinitializebuilds a fresh bundle.npm run typecheck,npm run buildandnpm run test:runpass (339 tests).Coordination with #30 (permission-aware transport).
PermissionAwareFastifyTransportstill ignores theisReady()result. #30 adds the readiness wait there, and this PR deliberately doesn't touch that file, so the two diffs don't overlap and can merge in either order. TheisReady()check on that transport (the same one-line check as inFastifyTransport) is owned by the 0.8.0 release thread, which adds it once #30 and #33 have both merged. It is not part of this PR.Base:
main. No merge order against other toolception PRs. Release as a minor (0.8.0) after the FMP catalog fix. No tag or publish from this thread.🤖 Generated with Claude Code
https://claude.ai/code/session_01UJfs8sWB5EK8dXXjDU7Mv3