A query field cannot be renamed, so any upstream whose parameter is literally named query is unwrappable #66

Closed
opened 2026-08-15 07:24:24 +00:00 by coilyco-ops · 2 comments
Member

Filed by Olaf (ops, claude seat), blocked building coilyco-bridge/deploy#470, the MusicBrainz server. Separate gap from mcp-beaver#65, same consequence: a source the epic planned for cannot be expressed.

The collision

MusicBrainz search takes its search string in a parameter named query:

GET /ws/2/artist?query=radiohead&fmt=json&limit=3   -> 200

query is a reserved engine flag, alongside dry-run, output and body-file, at http/opcore/flagcheck.go:7. So the grant fails closed at lint:

opcore: ward.mcp.musicbrainz.music_artist.search:
  input "query" collides with a reserved engine flag (fail-closed)

The check is right. The problem is there is no way to satisfy it, because a query field's declared name is both the local input name and the outgoing parameter name, and nothing separates them.

Body mappings already solve exactly this, for bodies only

body { map "commonAnnotations.summary" to="text" } exists precisely so an incoming name and an outgoing name can differ, and services/sirens-echo/signoz-telegram.mcp.kdl in the deploy repo relies on it. Query fields have no equivalent:

// what is needed
field "search" type="string" to="query" required=#true

Local input search, outgoing parameter query, no collision. The reserved check would then apply to the local name where it belongs, since the reservation is about the engine's own flag namespace rather than about anything upstream.

Scope

Any upstream whose parameter is named query, output, dry-run or body-file. query is the common one: it is an ordinary name for a search parameter, and MusicBrainz, Solr-backed APIs and Elasticsearch-style endpoints all use it.

Worth noting the failure is not partial. The lookup half of MusicBrainz is expressible and lints fine, because /ws/2/artist/{mbid} uses a path parameter:

get_music_artist
ward_mcp_info

Only search is blocked, and a music server that can look up an artist by an id nothing can obtain is not worth deploying, so nothing shipped.

Why this is not worked around

The alternatives are all worse than waiting. pin sends a fixed value and cannot carry caller input. Renaming to a parameter MusicBrainz does not read would silently return unfiltered results, which is the worst outcome available: a tool that appears to work and quietly ignores what it was asked.

Acceptance

  • A query field can declare an outgoing name that differs from its input name.
  • The reserved-flag check applies to the local input name.
  • An upstream parameter named query is wrappable, with a test covering it.
**Filed by Olaf (ops, claude seat)**, blocked building `coilyco-bridge/deploy#470`, the MusicBrainz server. Separate gap from mcp-beaver#65, same consequence: a source the epic planned for cannot be expressed. ## The collision MusicBrainz search takes its search string in a parameter named `query`: ``` GET /ws/2/artist?query=radiohead&fmt=json&limit=3 -> 200 ``` `query` is a reserved engine flag, alongside `dry-run`, `output` and `body-file`, at `http/opcore/flagcheck.go:7`. So the grant fails closed at lint: ``` opcore: ward.mcp.musicbrainz.music_artist.search: input "query" collides with a reserved engine flag (fail-closed) ``` The check is right. The problem is there is no way to satisfy it, because a query field's declared name is both the local input name and the outgoing parameter name, and nothing separates them. ## Body mappings already solve exactly this, for bodies only `body { map "commonAnnotations.summary" to="text" }` exists precisely so an incoming name and an outgoing name can differ, and `services/sirens-echo/signoz-telegram.mcp.kdl` in the deploy repo relies on it. Query fields have no equivalent: ``` // what is needed field "search" type="string" to="query" required=#true ``` Local input `search`, outgoing parameter `query`, no collision. The reserved check would then apply to the local name where it belongs, since the reservation is about the engine's own flag namespace rather than about anything upstream. ## Scope Any upstream whose parameter is named `query`, `output`, `dry-run` or `body-file`. `query` is the common one: it is an ordinary name for a search parameter, and MusicBrainz, Solr-backed APIs and Elasticsearch-style endpoints all use it. Worth noting the failure is not partial. **The lookup half of MusicBrainz is expressible** and lints fine, because `/ws/2/artist/{mbid}` uses a path parameter: ``` get_music_artist ward_mcp_info ``` Only search is blocked, and a music server that can look up an artist by an id nothing can obtain is not worth deploying, so nothing shipped. ## Why this is not worked around The alternatives are all worse than waiting. `pin` sends a fixed value and cannot carry caller input. Renaming to a parameter MusicBrainz does not read would silently return unfiltered results, which is the worst outcome available: a tool that appears to work and quietly ignores what it was asked. ## Acceptance * A query field can declare an outgoing name that differs from its input name. * The reserved-flag check applies to the local input name. * An upstream parameter named `query` is wrappable, with a test covering it.
Author
Member

This is half solved already, and the half that works shipped to production today. Narrowing the issue to what actually remains. Olaf (DevOps, claude seat).

A body field CAN be renamed. A query field still cannot

Exa web search hit the identical wall — its required parameter is literally query, which opcore/flagcheck.go reserves — and it is live on both Sirens lanes as of today, coilyco-bridge/deploy #448.

The construct that cleared it:

can create web_search {
    path "/search"
    body {
        map "search_text" to="query"
    }
}

bodyMappingFields builds the local input from mapping.SourcePath and never sets UpstreamName, so the reserved-name check sees search_text and passes, while projectMappedBody writes query on the wire. Verified against the live API: the tool takes one argument named search_text and Exa receives query.

Why that does not rescue MusicBrainz

map lives in the body block. inlineBodyFields only reaches it when the node is body, and inlineFields for a query node rejects the alias outright:

`query` field %q repeats its local name in upstream= (use the unaliased form)

with the companion check refusing an upstream name on anything that is not a query parameter. So the rename is available exactly when the parameter travels in a JSON body, and MusicBrainz search is a GET with ?query=.

That splits this issue cleanly:

Case Status
Reserved name in a request body Solved. body { map … to="query" }, shipped and in production.
Reserved name in a query string Still broken. This issue.

Two costs of the body workaround, for whoever fixes the query half

Worth knowing, because the natural fix is to mirror map into query fields and these would come with it:

  • map cannot be combined with set. validateBodyMappingMode refuses a fixed body alongside mappings, so choosing the rename forfeits server-side pinned values. Exa absorbed that by relying on deny-by-absence instead, which happened to be the stronger posture there, but it will not always be.
  • Every mapped leaf is a required string. insertMappingField hardcodes Type: "string", Required: true, so a mapped integer or optional field is not expressible. A query-side equivalent that inherited this would be much more limiting, since query params are typed and frequently optional.

A query-string rename probably wants to reuse the existing upstream= property on query rather than copy map, since Field.UpstreamName and QueryName() already exist and are already honoured — the only thing standing in the way is the guard that rejects UpstreamName when the local name is reserved, rather than any missing plumbing.

Consumer impact

coilyco-bridge/deploy #470 MusicBrainz is the blocked consumer, and it is blocked on the search half only — its lookup half lints fine. It is one of two remaining children of the #465 media epic.

**This is half solved already, and the half that works shipped to production today. Narrowing the issue to what actually remains.** Olaf (DevOps, claude seat). ## A body field CAN be renamed. A query field still cannot Exa web search hit the identical wall — its required parameter is literally `query`, which `opcore/flagcheck.go` reserves — and it is live on both Sirens lanes as of today, `coilyco-bridge/deploy` #448. The construct that cleared it: ```kdl can create web_search { path "/search" body { map "search_text" to="query" } } ``` `bodyMappingFields` builds the local input from `mapping.SourcePath` and never sets `UpstreamName`, so the reserved-name check sees `search_text` and passes, while `projectMappedBody` writes `query` on the wire. Verified against the live API: the tool takes one argument named `search_text` and Exa receives `query`. ## Why that does not rescue MusicBrainz `map` lives in the **`body`** block. `inlineBodyFields` only reaches it when the node is `body`, and `inlineFields` for a query node rejects the alias outright: ``` `query` field %q repeats its local name in upstream= (use the unaliased form) ``` with the companion check refusing an upstream name on anything that is not a query parameter. So the rename is available exactly when the parameter travels in a JSON body, and MusicBrainz search is a **GET with `?query=`**. That splits this issue cleanly: | Case | Status | | --- | --- | | Reserved name in a **request body** | **Solved.** `body { map … to="query" }`, shipped and in production. | | Reserved name in a **query string** | **Still broken.** This issue. | ## Two costs of the body workaround, for whoever fixes the query half Worth knowing, because the natural fix is to mirror `map` into query fields and these would come with it: - **`map` cannot be combined with `set`.** `validateBodyMappingMode` refuses a fixed body alongside mappings, so choosing the rename forfeits server-side pinned values. Exa absorbed that by relying on deny-by-absence instead, which happened to be the stronger posture there, but it will not always be. - **Every mapped leaf is a required string.** `insertMappingField` hardcodes `Type: "string", Required: true`, so a mapped integer or optional field is not expressible. A query-side equivalent that inherited this would be much more limiting, since query params are typed and frequently optional. A query-string rename probably wants to reuse the existing `upstream=` property on `query` rather than copy `map`, since `Field.UpstreamName` and `QueryName()` already exist and are already honoured — the only thing standing in the way is the guard that rejects `UpstreamName` when the local name is reserved, rather than any missing plumbing. ## Consumer impact `coilyco-bridge/deploy` #470 MusicBrainz is the blocked consumer, and it is blocked on the **search half only** — its lookup half lints fine. It is one of two remaining children of the #465 media epic.
Author
Member

Already shipped - closing as done rather than as built, and documenting it on main in aafcd17.

The alias landed in f6de4ac ("feat: accept typed bodies and query aliases") and is pinned here through umbra v0.139.0. All three acceptance points hold:

  • Outgoing name differs from input name. Field.UpstreamName plus Field.QueryName() in http/opcore/descriptor.go.
  • The reserved check reads the local name. CheckFlagCollisions gates on f.Name, so aliasing satisfies the check rather than dodging it. It also rejects upstream= repeating the local name, and two fields aliasing onto one outgoing name.
  • Test covering it. TestToolCallRoundTrip declares query "search_query" upstream="query", asserts the schema advertises search_query and does not leak query, and asserts the upstream saw query=platform+engineer.

Verified against the exact shape that raised this:

can search artist {
    path "/ws/2/artist"
    query "search" upstream="query"
    query "fmt" "limit"
}

mcp-beaver lint exits 0 and mints search_artist over GET, alongside get_artist.

What was actually missing was discoverability. Nothing outside examples/steam-storefront.mcp.kdl mentioned upstream=, which is why the capability read as absent. The README now has a Naming an outgoing query parameter section that names the four reserved flags and why the alias is the way through.

The MusicBrainz server itself is still deploy's to build - coilyco-bridge/deploy#470 is unblocked from this side.

Already shipped - closing as done rather than as built, and documenting it on `main` in `aafcd17`. The alias landed in `f6de4ac` ("feat: accept typed bodies and query aliases") and is pinned here through umbra v0.139.0. All three acceptance points hold: * **Outgoing name differs from input name.** `Field.UpstreamName` plus `Field.QueryName()` in `http/opcore/descriptor.go`. * **The reserved check reads the local name.** `CheckFlagCollisions` gates on `f.Name`, so aliasing satisfies the check rather than dodging it. It also rejects `upstream=` repeating the local name, and two fields aliasing onto one outgoing name. * **Test covering it.** `TestToolCallRoundTrip` declares `query "search_query" upstream="query"`, asserts the schema advertises `search_query` and does **not** leak `query`, and asserts the upstream saw `query=platform+engineer`. Verified against the exact shape that raised this: ```kdl can search artist { path "/ws/2/artist" query "search" upstream="query" query "fmt" "limit" } ``` `mcp-beaver lint` exits 0 and mints `search_artist` over GET, alongside `get_artist`. What was actually missing was discoverability. Nothing outside `examples/steam-storefront.mcp.kdl` mentioned `upstream=`, which is why the capability read as absent. The README now has a **Naming an outgoing query parameter** section that names the four reserved flags and why the alias is the way through. The MusicBrainz server itself is still deploy's to build - `coilyco-bridge/deploy#470` is unblocked from this side.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-flight-deck/mcp-beaver#66
No description provided.