Skip to content

fix(mcp): surface parameterized custom resources as MCP tools#1602

Merged
kriszyp merged 7 commits into
mainfrom
kris/mcp-paramroute-tools
Jul 6, 2026
Merged

fix(mcp): surface parameterized custom resources as MCP tools#1602
kriszyp merged 7 commits into
mainfrom
kris/mcp-paramroute-tools

Conversation

@kriszyp

@kriszyp kriszyp commented Jul 4, 2026

Copy link
Copy Markdown
Member

Summary

Custom resources on a parameterised path (static path = '/widget/:id') are stored by Resources.setParamRoute in a paramRoutes side-array and never inserted into the base resources Map. MCP's buildApplicationTools iterated only the Map, so these resources produced zero MCP tools — even though OpenAPI (which already iterates paramRoutes) exposed them. This enumerates paramRoutes for MCP tool building so the tool surface matches the REST surface.

What to look at

The enumeration is deliberately restricted to what the generated verb handlers can actually bind, so we never advertise a tool that would dispatch to the wrong target:

  • Single-:id routesget/update/patch/delete tools. These handlers bind target.id = args.id correctly. create/search are suppressed: makeCreateHandler forces target.isCollection = true and never binds id, and makeSearchHandler is collection-scoped — neither matches "the record named by :id".
  • Multi-segment / named-wildcard routes (/files/*rest) → no generated verb tools yet (nothing binds the extra segments), but author-defined mcpTools/mcpPrompts still register, since they carry their own schemas and handler methods.
  • liveResource() (live-registry dispatch, added in Consolidate row-level allowRead enforcement — apply it on all read paths, not just the single-record point-read #1487) now also consults paramRoutes, so param-route tools don't silently fall back to the registration-time class.

Regression coverage in unitTests/components/mcp/tools/application-paramroutes.test.js (get present; create/search absent on :id; custom tools present + generated verbs absent on wildcard). Full MCP tools suite green (94 passing).

Provenance

The core enumeration fix was authored by @dawsontoth as part of the discussion on #1503 (RFC 0001) — extracted here so it can land independently of the RFC. The binding-mode restrictions (single-:id scoping, create/search suppression, custom-tool preservation on wildcards) were added during extraction in response to a Codex cross-model review that converged over four rounds.

Cross-model review: Codex — clean (no actionable correctness issues) after the changes above. Gemini/agy timed out (twice) and did not produce a review.

Prepared by an AI agent (Claude, Opus 4.8). Not the original commit author — see per-commit Co-Authored-By trailers.

🤖 Generated with Claude Code

dawsontoth and others added 4 commits July 4, 2026 14:21
A custom resource on a parameterised path (e.g. `static path = '/widget/:id'`)
is stored by Resources.setParamRoute in the `paramRoutes` side-array and never
inserted into the base Map. buildApplicationTools iterated only the Map, so such
resources produced ZERO MCP tools — even though OpenAPI (which iterates
paramRoutes) exposed them.

Enumerate `resources.paramRoutes` too, so the MCP tool surface matches the REST
surface. The common single-`:id` case binds via `target.id` in the existing verb
handlers; richer multi-segment/named-wildcard binding can layer on later.

Also fix `liveResource()` (added by #1487 for live-registry dispatch so a
component's row-level `allow*` overrides stay in effect) to check `paramRoutes`
as well as the base Map — otherwise every param-route tool silently fell back to
its registration-time class reference, since a plain Map lookup always misses for
paths that live in the side array. Caught by cross-model review (Codex + Gemini
independently flagged it); added a regression test that swaps the live registry
entry after registration and asserts the tool dispatches on the replacement.

Regression tests: unitTests/components/mcp/tools/application-paramroutes.test.js.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e handlers bind

The verb handlers only bind `target.id = args.id`. A route with a
differently-named param (:widgetId), more than one param, or a *wildcard
segment would advertise an id-only tool that silently drops the other
segment(s) the Resource actually needs. Gate enumeration on isSimpleIdRoute
so those shapes are skipped until richer path-param binding lands, instead
of shipping a broken tool.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…s can't bind the record id

makeCreateHandler forces target.isCollection = true and never sets target.id;
makeSearchHandler is collection-scoped by design. Neither matches a :id-scoped
record, so an MCP client calling create_*/search_* on a param route would
silently run against the wrong target (a fresh collection insert, or a
collection scan) instead of the selected record. Restrict param-route
enumeration to get/update/patch/delete, which all correctly bind
target.id = args.id.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…rd param routes

The simple-:id gate suppressed considerEntry entirely for richer route shapes,
hiding author-defined mcpTools/mcpPrompts that carry their own schemas and
handler methods and don't depend on generated verb binding. Suppress only the
generated verbs ('none' binding mode); custom surfaces register on every route
shape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds support for parameterised routes (e.g., /widget/:id) in the MCP application tools builder, ensuring that parameterised resources are correctly enumerated and registered. The feedback identifies a critical issue in the isSimpleIdRoute helper function, which incorrectly returns true for routes where :id is not the final segment (such as widget/:id/action). This would lead to incorrect behavior in the generated verb handlers. The reviewer has provided a code suggestion to ensure :id is the final segment and recommended adding a corresponding test case to verify this behavior.

Comment thread components/mcp/tools/application.ts
Comment thread unitTests/components/mcp/tools/application-paramroutes.test.js Outdated
@claude

claude Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Reviewed; no blockers found. Prior blocker (banned node:assert/strict import) resolved in 85c7ee8.

…routes

isSimpleIdRoute treated `widget/:id/action` as a bindable single-:id route
(one :id, no wildcard), but the verb handlers bind `target.id = args.id`
treating :id as the record itself — for a sub-resource route that silently
drops the trailing `/action` segment and dispatches to the wrong target.
Require :id to be the final segment so such routes fall through to the
custom-tool-only path instead of advertising bogus get/update/patch/delete.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@kriszyp kriszyp marked this pull request as ready for review July 6, 2026 16:45
Comment thread unitTests/components/mcp/tools/application-paramroutes.test.js Outdated
@dawsontoth

Copy link
Copy Markdown
Contributor

try rebasing

kriszyp and others added 2 commits July 6, 2026 13:23
@kriszyp kriszyp merged commit 8de78e5 into main Jul 6, 2026
13 of 16 checks passed
@kriszyp kriszyp deleted the kris/mcp-paramroute-tools branch July 6, 2026 21:02
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.

2 participants