A can grant cannot carry an authored description, so tool text is derived and near-identical across servers #58

Closed
opened 2026-08-13 16:21:18 +00:00 by coilyco-ops · 1 comment
Member

What this is

describe is accepted inside a proxy block but not inside a can grant. applyInlineGrantChild fails closed on anything outside path | query | body | set | fail-when, so a wrap-mode tool's description is always the derived string from describe():

"Use this when the user wants to <leaf> <group> through the configured upstream service."

Descriptor.Describe exists and describe() honours it — the field is simply unreachable from the wrap grammar.

Why this is being raised now

coilyco-bridge/deploy#465 builds five media servers on this runtime and names the tool surface as the whole craft of the work:

Each must say what it covers and what it does not, so the model can rule tools out rather than guessing.

That is not expressible. Two of the five are book servers (deploy#468 Open Library, deploy#469 Gutendex), and the epic calls their overlap "the sharpest tool-naming risk in the batch." What ships is:

  • search_book → "Use this when the user wants to search book through the configured upstream service."
  • get_readable_text → "Use this when the user wants to get readable-text through the configured upstream service."

Neither sentence rules the other tool out. The distinction survives only because Kai routed on the verb — lookup versus retrieval — which the names carry. That was a good call independently, and it is currently the only thing standing between those two servers and a model coin-flip.

The derived text is also visibly generic in practice: every Forgejo MCP tool in a live agent's context reads "Use this when the user wants to X through the configured upstream service," which tells a model nothing about scope, bounds, or when not to reach for it.

What would be enough

Accept describe on a can grant, exactly as proxy already does:

can search book {
    path "/search.json"
    describe "Look up bibliographic detail for any book, in or out of print. Metadata and availability only - does NOT return readable text; use get_readable_text for that."
    query { ... }
}

Falling back to the derived string when absent keeps every existing guardfile working.

Note on scope

This is the more valuable of the two gaps I found — the other is #57, rate limiting. Rate limiting bounds what these servers do to someone else's infrastructure; this one bounds whether the model picks the right tool at all, which is the difference between five servers and five servers that get used correctly.

What I verified

Read against mcp-beaver main and cli-guard v0.131.0 on 2026-08-13, and confirmed empirically against derived descriptions in a live agent's loaded tool list. No deployed guardfile in coilyco-bridge/deploy uses describe on a grant, so nothing depends on the current behaviour.

## What this is `describe` is accepted inside a `proxy` block but not inside a `can` grant. `applyInlineGrantChild` fails closed on anything outside `path | query | body | set | fail-when`, so a wrap-mode tool's description is always the derived string from `describe()`: ``` "Use this when the user wants to <leaf> <group> through the configured upstream service." ``` `Descriptor.Describe` exists and `describe()` honours it — the field is simply unreachable from the wrap grammar. ## Why this is being raised now `coilyco-bridge/deploy#465` builds five media servers on this runtime and names the tool surface as the whole craft of the work: > Each must say what it covers **and what it does not**, so the model can rule tools out rather than guessing. That is not expressible. Two of the five are book servers (`deploy#468` Open Library, `deploy#469` Gutendex), and the epic calls their overlap "the sharpest tool-naming risk in the batch." What ships is: - `search_book` → "Use this when the user wants to search book through the configured upstream service." - `get_readable_text` → "Use this when the user wants to get readable-text through the configured upstream service." Neither sentence rules the other tool out. **The distinction survives only because Kai routed on the verb** — lookup versus retrieval — which the *names* carry. That was a good call independently, and it is currently the only thing standing between those two servers and a model coin-flip. The derived text is also visibly generic in practice: every Forgejo MCP tool in a live agent's context reads "Use this when the user wants to X through the configured upstream service," which tells a model nothing about scope, bounds, or when not to reach for it. ## What would be enough Accept `describe` on a `can` grant, exactly as `proxy` already does: ```kdl can search book { path "/search.json" describe "Look up bibliographic detail for any book, in or out of print. Metadata and availability only - does NOT return readable text; use get_readable_text for that." query { ... } } ``` Falling back to the derived string when absent keeps every existing guardfile working. ## Note on scope This is the more valuable of the two gaps I found — the other is `#57`, rate limiting. Rate limiting bounds what these servers do to someone else's infrastructure; this one bounds whether the model picks the right tool at all, which is the difference between five servers and five servers that get used correctly. ## What I verified Read against `mcp-beaver` `main` and `cli-guard v0.131.0` on 2026-08-13, and confirmed empirically against derived descriptions in a live agent's loaded tool list. No deployed guardfile in `coilyco-bridge/deploy` uses `describe` on a grant, so nothing depends on the current behaviour.
Author
Member

This already works. Verified end to end against main and umbra v0.139.0, and pinned with tests in 89f5a02.

The issue was filed against cli-guard v0.131.0. The dependency is now umbra v0.139.0, and describe is accepted on a can grant there:

  • http/opcore/inline.go - applyInlineGrantControlChild has a case "describe", and the fail-closed message reads want path | query | body | set | fail-when | describe.
  • It lands on Descriptor.Describe (http/opcore/descriptor.go:25).
  • describe() in internal/mcpserver/server.go returns it in preference to the derived string, which the issue correctly said was already true.

So the field is reachable from the wrap grammar now. Three tests in internal/mcpserver/describe_test.go cover the authored note reaching the served description, the derived fallback when absent, and fail-closed on an empty or duplicated note.

Why no guardfile uses it is worth separating from whether it works. It is documented in DESIGN.md and FEATURES.md, but no example showed the shape, which is a good way for a feature to be invisible. Added a worked one to examples/guardfile-siblings.mcp.kdl, written deliberately as the ruling-out sentence deploy#465 asks for:

can get thing {
    path "/owners/{owner}/things/{id}"
    describe "Fetch one thing by id, with its metadata. Returns the record only - does NOT create or modify anything; use create_thing for that."
}

The two book servers can now say what they do not cover, so the distinction stops resting entirely on the verb routing.

Closing as already-delivered rather than as newly-built.

**This already works.** Verified end to end against `main` and umbra v0.139.0, and pinned with tests in 89f5a02. The issue was filed against cli-guard v0.131.0. The dependency is now umbra v0.139.0, and `describe` is accepted on a `can` grant there: * `http/opcore/inline.go` - `applyInlineGrantControlChild` has a `case "describe"`, and the fail-closed message reads `want path | query | body | set | fail-when | describe`. * It lands on `Descriptor.Describe` (`http/opcore/descriptor.go:25`). * `describe()` in `internal/mcpserver/server.go` returns it in preference to the derived string, which the issue correctly said was already true. So the field is reachable from the wrap grammar now. Three tests in `internal/mcpserver/describe_test.go` cover the authored note reaching the served description, the derived fallback when absent, and fail-closed on an empty or duplicated note. **Why no guardfile uses it is worth separating from whether it works.** It is documented in `DESIGN.md` and `FEATURES.md`, but no example showed the shape, which is a good way for a feature to be invisible. Added a worked one to `examples/guardfile-siblings.mcp.kdl`, written deliberately as the ruling-out sentence deploy#465 asks for: ```kdl can get thing { path "/owners/{owner}/things/{id}" describe "Fetch one thing by id, with its metadata. Returns the record only - does NOT create or modify anything; use create_thing for that." } ``` The two book servers can now say what they do not cover, so the distinction stops resting entirely on the verb routing. Closing as already-delivered rather than as newly-built.
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#58
No description provided.