Improve operation discovery and token efficiency#7
Conversation
Keep the MCP tool surface constant while making registered APIs more discoverable and search/execute cheaper in always-on context: - search: emit compact JSON instead of pretty-printed and accept an optional limit (default 25, max 50) - ~32% fewer tokens per search. - execute: assemble its description from only the API kinds actually registered, so an OpenAPI-only setup drops the GraphQL/SOAP blurbs. - Capture each API's description (OpenAPI info.description; GraphQL Query-root doc, best-effort) through the adapter seam into the registry, surfaced by list_apis. - list_apis: an optional api id returns that API plus a capability map (operation tags -> counts, top 40) so an agent sees an API's areas before searching - a navigable overview of thousands of operations. - Add a one-line discovery-strategy hint to the server instructions.
There was a problem hiding this comment.
Pull request overview
This PR improves MCP capability discovery and reduces token overhead in always-on context by (a) propagating a short API description from protocol adapters into the registry and surfacing it via list_apis, and (b) making search results more compact while tailoring execute’s tool description to only the API kinds present.
Changes:
- Add optional
descriptionmetadata toPreparedApi/RegistryEntry, populated from OpenAPIinfo.descriptionand (best-effort) GraphQL Query-root docs. - Enhance
list_apiswith an optionalapifilter that returns a per-tag “capability map” (tag counts) for that API. - Make
searchreturn compact JSON and accept an optionallimit(default 25, max 50); generate a slimmerexecutetool description based on registered kinds.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/registry.ts | Adds optional description field to persisted registry entries. |
| src/adapter.ts | Extends PreparedApi to carry an optional source-derived description. |
| src/register.ts | Persists adapter-provided descriptions into registry entries during registration. |
| src/openapi.ts | Extracts and clamps OpenAPI info.description, returning it from prepare(). |
| src/graphql.ts | Captures a best-effort description from the schema’s Query root type. |
| src/commands/serve.ts | Adds search.limit, compact search output, per-kind execute description assembly, and list_apis drill-in capability map. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| async ({ api }: ListApisArgs) => { | ||
| const state = await loadState(); | ||
| return { | ||
| content: [{ type: "text" as const, text: await listApisResult(state) }], | ||
| content: [{ | ||
| type: "text" as const, |
| } | ||
| apis.push(api); | ||
| } | ||
| return JSON.stringify(apis, null, 2); |
| const tail = | ||
| `console.log anything you want back. Chain multiple calls in one execution: intermediate results ` + | ||
| `stay in the sandbox instead of round-tripping through you. Set check:false to skip type-checking ` + | ||
| `for one run (use when a stale spec enum rejects a value the live API accepts; you lose type ` + | ||
| `feedback that run). Set timeoutMs to raise the 30s default (max 120000) for long or paginated ` + | ||
| `runs. Input: { api: <registered id>, code: <typescript>, check?: boolean, timeoutMs?: number }.`; |
… description
- list_apis returns { text, isError }: an unknown api id is now reported
with isError set, rather than a bare string that broke the JSON contract.
- list_apis emits compact JSON like search (was pretty-printed), trimming
per-call tokens, especially with the capability map.
- The execute description hot-reloads: every handler reads the registry via
loadStateSynced, which re-derives the description (and pushes
tools/list_changed) whenever the registered kind-set changes, so a
mid-session add_api/remove_api or external add keeps it accurate.
Verified end-to-end over stdio.
|
Addressed all three review comments in 99da401:
Verified end-to-end over stdio: with an OpenAPI + GraphQL registry the description lists both; after a mid-session |
What
Keeps the MCP tool surface constant (still 7 tools) while making registered APIs more discoverable and
search/executecheaper in always-on context. Two themes: token efficiency and capability discovery.Changes
Token efficiency
searchemits compact JSON instead of pretty-printed, and accepts an optionallimit(default 25, max 50) — ~32% fewer tokens per search on a typical result set.execute's description is assembled from only the API kinds actually registered, so an OpenAPI-only setup no longer carries the GraphQL/SOAP client-shape blurbs in always-on context.Capability discovery
info.description; GraphQL Query-root doc, best-effort) through theProtocolAdapterseam into the registry, surfaced bylist_apisso an agent can see what an API is for. SOAP is left best-effort/unset — itsnameis already the service name.list_apisgains an optionalapiid that returns just that API plus a capability map: operation tags with per-tag counts, capped at the 40 busiest with a truncation note, so an agent can see an API's areas before searching. Turns thousands of opaque operations into a navigable overview (Cloudflare's 3,068 ops → a ~309-token map).Interaction with codegen versioning (#6)
descriptionis registration-time metadata derived inprepare()and stored in the registry — exactly likename, whichregenerateApipreserves verbatim. It does not change the cached artifacts (types / ops index), so it deliberately does not bumpCODEGEN_VERSION(which gates artifact staleness; bumping would re-fetch every spec for a non-artifact change). Existing entries pick up a description on re-registration (add --force), the same way they'd pick up a changed spec title.Verification
deno task check && lint && fmt && testall pass (including the newopenapi-sanitizetests). The ~32% search compaction and the capability-map size were measured against the real cached ops indexes.