feat(flows): NF-9 — synthesise mcp.<server>.<tool> nodes from MCP catalogs #396
No reviewers
Labels
No labels
area:agents
area:dashboard
area:database
area:design
area:design-review
area:flows
area:infra
area:meta
area:security
area:sessions
area:webhook
area:workdir
security
type:bug
type:chore
type:meta
type:user-story
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/claude-hooks!396
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "boss/330"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
mcp.<server>.<tool>flow nodes are synthesised at startup from each registered MCP server's introspected catalog. Input ports come from the tool's JSON-schemaproperties(plus a catch-allargsport that overrides per-property inputs). Outputs areresult+err. Implementation inapps/server/src/domain/flows/mcp-nodes.ts.McpClientper server (apps/server/src/infrastructure/mcp/mcp-{client,registry}.ts) — hand-rolled JSON-RPC 2.0 over stdio, transport-agnostic for testability. OneBun.spawnper boot, not per call.errport (never thrown) so flow authors can branch viarouter.try-style patterns without wrapping every MCP call. The envelope mirrorsrouter.try's_errorshape ({name, message, server, tool}).POST /flows/mcp/reload(auth-gated) re-introspects every server and rebuilds the catalog. SubsequentdefaultRegistry()builds see the refreshed node set, so brand-new servers added post-startup are picked up without a service restart.Test plan
bun x turbo run typecheck— green across all 4 packages.bun x biome check .— green (415 files).bun x turbo run test— 1989 / 1989 pass.apps/server/src/infrastructure/mcp/mcp-client.test.ts— initialize handshake, idempotency, JSON-RPC error rejection,listToolsparsing,callToolcontent + isError, transport-close rejection (8 tests).apps/server/src/infrastructure/mcp/mcp-registry.test.ts—setServersdiffing, error survival, drop-and-add, reload picking up new tools,callToolforwarding, unknown server, isError throw, lazy init,close(10 tests).apps/server/src/domain/flows/mcp-nodes.test.ts— schema synthesis, zero-input tool, payload forwarding,args-port override, statictool_args, errors-on-err-port, rate-limit charging, register-per-tool, call delegation, isError surfacing, empty registry, reload picking up new servers (12 tests).mcp.forge.list_repo_issues, confirmresultcarries the issue list and that the rate-limit counter increments.POST /flows/mcp/reload, confirm the newmcp.<name>.*nodes appear in/flows/<id>/dry-runvalidation.Closes #330
🤖 Generated with Claude Code
behavior
apps/server/src/infrastructure/mcp/mcp-registry.ts—callToollazy-init has a subprocess-leak race. Two concurrent flow calls for the same uninitialised server both see!entry.clientbefore either'sclientFactoryresolves, so both spawn a subprocess. Whichever resolves second overwritesentry.clientwithout closing the first — the orphaned process runs until the service restarts. Fix: add a per-entrylazyInitPromise: Promise<void> | nullfield (same pattern asMcpClient.readingPump) and coalesce concurrent callers onto the same promise:doc-gap
apps/server/src/domain/flows/mcp-nodes.ts— two comments misstate collision precedence. The module docblock (line 12) says "args port… overrides per-property inputs on key collision" and the inline handler comment says "Prefer the fully-populatedargsport if the author provided one". Both are wrong — the code (and the test named "overridden by per-property inputs") shows per-property inputs WIN over theargsport on overlapping keys. Update both comments to read "per-property inputs override theargsport on key collision."Both findings landed in
0bc4a74:InternalEntry.lazyInitPromisefield added;callToolcoalesces concurrent uninitialised callers onto a single IIFE, withfinally { lazyInitPromise = null }so a failed init doesn't strand the entry. Two new tests inmcp-registry.test.ts: one asserts a single factory call across two concurrent callers (gated promise), the other asserts a transient failure clears the promise so the next caller retries.description, and the inline handler comment inmcp-nodes.tsnow all read "per-property inputs override theargsport on key collision".bun x turbo run typecheck+bun x biome check .+bun x turbo run test(1991 / 1991) green.Both round-1 findings addressed and CI green.
lazyInitPromisefield +finally-cleared promise incallToolcorrectly prevents the subprocess-leak race.args.No new issues on the delta.