Skip to content

fix(mcp): compute operations-profile tools lazily so late-registered ops surface (#1562)#1579

Merged
kriszyp merged 2 commits into
mainfrom
kris/mcp-ops-lazy-1562
Jul 8, 2026
Merged

fix(mcp): compute operations-profile tools lazily so late-registered ops surface (#1562)#1579
kriszyp merged 2 commits into
mainfrom
kris/mcp-ops-lazy-1562

Conversation

@kriszyp

@kriszyp kriszyp commented Jul 2, 2026

Copy link
Copy Markdown
Member

Problem

The MCP operations profile (components/mcp/tools/operations.tsregisterOperationsTools) walked OPERATION_FUNCTION_MAP once at registration time and registered one tool per allowed op. But components register their operations via server.registerOperation during startOnMainThread, which runs after the profile is built. Those late-registered ops (e.g. the built-in agent's agent_prompt) never became MCP tools, so listing them in mcp.operations.allow silently had no effect and tools/call returned -32601 Unknown tool.

Closes #1562.

Fix

Adopts the issue's Option 1 (lazy) — the most robust, order-independent choice.

  • Lazy per-profile tool provider. Replaces the one-time snapshot with a ProfileToolProvider the registry consults on every tools/list / tools/call. The operations provider walks the live OPERATION_FUNCTION_MAP and applies the allow/deny filter at request time, so component-registered ops surface as soon as they exist. No boot-ordering dependence.
  • Generic mechanism (setProfileToolProvider) merges provider tools with statically-registered tools, deduping by name; static registrations win on collision — so the built-in agent's curated MCP tools (feat(agent): expose the built-in agent over MCP (curated tools) (#626) #1561) remain a deliberate curated surface and are unaffected.
  • Defs are rebuilt per call (a def is a pure function of the op name). tools/list isn't a hot path, so no module-level cache to leak or stale across tests/boots.

Cross-model review fix (2nd commit)

Gemini + Codex both flagged that the flat name→def registry is a single namespace shared by both profiles, so an unscoped getTool let a static tool in one profile shadow a same-named provider tool in another — the shadowed tool listed but was uncallable (transport rejected the wrong-profile def). getTool now takes the caller's profile and resolves profile-scoped; dispatchToolsCall passes request.profile. Also dropped the module-level toolDefCache (not a hot path; avoids cross-test/boot module state).

Tests

  • unitTests/components/mcp/toolRegistry.test.js — provider merge, static-wins-on-collision, profile-scoped getTool, cache invalidation.
  • unitTests/components/mcp/tools/operations.test.js — late-registered op surfaces after provider install; allow/deny applied per request.

🤖 Generated with Claude Code

Kris Zyp and others added 2 commits July 2, 2026 09:45
…ops surface

The MCP operations profile walked OPERATION_FUNCTION_MAP once at registration
time (registerMcpProfile), before components register their operations via
server.registerOperation in startOnMainThread. Those late-registered ops (e.g.
the built-in agent's agent_prompt) never became MCP tools, so listing them in
mcp.operations.allow silently had no effect and tools/call returned -32601.

Replace the one-time snapshot with a lazy per-profile tool provider: the
operations profile installs a provider that the registry consults on every
tools/list / tools/call, walking the live OPERATION_FUNCTION_MAP and applying
the allow/deny filter at request time. This removes the boot-ordering
dependence and stays consistent as components load.

The generic provider mechanism (setProfileToolProvider) merges with
statically-registered tools, with static registrations winning on a name
collision — so the built-in agent's curated MCP tools (PR #1561) remain a
deliberate curated surface and are unaffected.

Closes #1562

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cross-model review (Gemini + Codex both flagged) — the flat name->def registry
is a single namespace shared by both MCP profiles, so an unscoped getTool let a
static tool in one profile shadow a same-named provider tool in another: the
shadowed tool listed but was uncallable (transport rejected the wrong-profile
def as Unknown tool). getTool now takes the caller's profile and resolves
profile-scoped (static-of-profile wins over its provider, then the provider,
then any static by name); dispatchToolsCall passes request.profile.

Also drop the module-level toolDefCache — tools/list isn't a hot path, so the
provider rebuilds the (cheap, stateless) defs per call rather than carrying
cross-test/boot-lived module state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

Gemini encountered an error creating the review. You can try again by commenting /gemini review.

@kriszyp kriszyp requested a review from kylebernhardy July 2, 2026 19:53
@claude

claude Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Reviewed; no blockers found.

@kriszyp kriszyp merged commit d263beb into main Jul 8, 2026
47 checks passed
@kriszyp kriszyp deleted the kris/mcp-ops-lazy-1562 branch July 8, 2026 16:58
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.

MCP operations profile misses operations registered after component load (mcp.operations.allow silently ineffective)

1 participant