server-info should be on by default — the disclosure argument for opt-in does not survive reading the payload #61

Closed
opened 2026-08-14 04:33:39 +00:00 by coilyco-ops · 1 comment
Member

🤖 Filed by Claude Code on Kai's behalf.

Filed by Olaf (OPS). Kai's ask, 2026-08-13: no boilerplate — every guardfile wanting the same line is a default in the wrong place.

The current justification

server-info is opt-in, and README.md gives the reason:

It is opt-in rather than always-on because deny-by-absence is the rule here, and a locked-down deployment has a fair reason to refuse to describe its own shape.

That reads well. It does not hold once you read what the tool actually returns.

The payload discloses nothing a caller cannot already get

From serverInfoPayload() in internal/mcpserver/serverinfo.go, field by field:

Field Already obtainable by any caller who can reach the server
status constant "ok"
server initializeImplementation{Name: name} (server.go:338)
tools, toolCount tools/list
resourceCount resources/list
promptCount prompts/list
mode inferable from the surface
spec specName() — basename with .kdl stripped (server.go:433-441), no directory component, and it falls back to the server name

There is no field here that a client cannot already obtain by asking the protocol. A caller who can call ward_mcp_info can call tools/list, which is strictly more detail — full schemas rather than names.

So "a locked-down deployment refusing to describe its own shape" describes a property this runtime does not have and cannot have: the shape is discoverable through the standard MCP surface by anyone who can reach the endpoint at all. The gate is inbound authentication, which this runtime correctly delegates to the consuming deployment.

The code already knows this. registerServerInfo's comment is the tight version of the actual boundary:

The payload reports the server's own shape and nothing about the upstream beyond whether one is proxied: it must not become a way to read configuration the grants themselves do not expose.

That invariant is what matters, it is already enforced, and it is unaffected by whether the tool is on by default.

Why on-by-default is worth more than the line it saves

  1. Liveness. MCP 2026-07-28 removed protocol-level ping. Every server without this node has no cheap liveness probe. Relevant to #49, where a request hung 180s inside healthy-looking pods and nothing cheap could distinguish "serving" from "wedged."
  2. Capability grounding. sirens-echo has three open issues on the agent misdescribing its own capabilities (coilyco-gaming/sirens-echo#199, coilyco-gaming/sirens-echo#211, coilyco-gaming/sirens-echo#200 — the last says whatever enumerates real capabilities must keep up or the claim check has nothing to check). A tool reporting the served inventory from the server is the grounded answer, and it only helps if it is reliably present.
  3. Fleet consistency. Present-on-some-servers is worse than either extreme, because an agent cannot infer from absence whether a server is unhealthy or simply did not opt in — the same reasoning as #54.

Requested

Default on. Keep server-info name="..." for the rename. Add an explicit opt-out for a deployment that genuinely wants it gone:

server-info disabled       // or `no-server-info`, naming is yours

Fail-closed parsing stays as it is.

Two consequences to design against — neither is a blocker

1. Name collision becomes a startup failure rather than an opt-in error. serverInfoTool errors when its name collides with a granted tool. Today that is only reachable if you opted in. Default-on means an existing guardfile granting a tool named ward_mcp_info stops building. Unlikely given the name, but it is a silent-until-deploy break and should be checked across coilyco-bridge/deploy before this lands. If any exist, the opt-out or a rename is the migration.

2. It makes one sentence in the README untrue. The pitch is:

the served surface is exactly the guardfile's grants: an unwritten delete issue grant means no delete_issue tool or HTTP endpoint can ever be served (deny-by-absence)

A default-on tool nobody granted is a real exception to that. The security property is intact — the tool reaches no upstream and cannot widen the grant surface — but the sentence needs to become "exactly the grants, plus one read-only tool that reports the server's own shape." Worth getting right, because that sentence is the product's core claim and it is load-bearing in the rename decision on #48, which already caught one over-strong claim in the same paragraph.

Copy is Content's call. Flagging it so it does not ship as an unnoticed weakening of the pitch.

Acceptance

  • A guardfile with no server-info node mints the info tool.
  • An explicit opt-out suppresses it, and is tested.
  • name= override still works.
  • lint output includes the tool, so lint and tools/list still report the same surface.
  • Existing guardfiles across coilyco-bridge/deploy checked for a ward_mcp_info collision before merge.
  • README's deny-by-absence sentence updated in the same change.

What I verified

Read internal/mcpserver/serverinfo.go, server.go:338, and server.go:433-441 at mcp-beaver main on 2026-08-13. I did not run the server. The claim that every payload field is otherwise obtainable is from reading the payload builder against the SDK surface, not from an empirical diff of ward_mcp_info versus tools/list — worth confirming empirically if this is contentious.

> 🤖 Filed by Claude Code on Kai's behalf. **Filed by Olaf (OPS).** Kai's ask, 2026-08-13: no boilerplate — every guardfile wanting the same line is a default in the wrong place. ## The current justification `server-info` is opt-in, and `README.md` gives the reason: > It is opt-in rather than always-on because deny-by-absence is the rule here, and a locked-down deployment has a fair reason to refuse to describe its own shape. That reads well. It does not hold once you read what the tool actually returns. ## The payload discloses nothing a caller cannot already get From `serverInfoPayload()` in `internal/mcpserver/serverinfo.go`, field by field: | Field | Already obtainable by any caller who can reach the server | | --- | --- | | `status` | constant `"ok"` | | `server` | `initialize` → `Implementation{Name: name}` (`server.go:338`) | | `tools`, `toolCount` | `tools/list` | | `resourceCount` | `resources/list` | | `promptCount` | `prompts/list` | | `mode` | inferable from the surface | | `spec` | `specName()` — basename with `.kdl` stripped (`server.go:433-441`), no directory component, and it falls back to the server name | **There is no field here that a client cannot already obtain by asking the protocol.** A caller who can call `ward_mcp_info` can call `tools/list`, which is strictly more detail — full schemas rather than names. So "a locked-down deployment refusing to describe its own shape" describes a property this runtime does not have and cannot have: the shape is discoverable through the standard MCP surface by anyone who can reach the endpoint at all. The gate is inbound authentication, which this runtime correctly delegates to the consuming deployment. The code already knows this. `registerServerInfo`'s comment is the tight version of the actual boundary: > The payload reports the server's own shape and nothing about the upstream beyond whether one is proxied: it must not become a way to read configuration the grants themselves do not expose. That invariant is what matters, it is already enforced, and it is unaffected by whether the tool is on by default. ## Why on-by-default is worth more than the line it saves 1. **Liveness.** MCP 2026-07-28 removed protocol-level `ping`. Every server without this node has no cheap liveness probe. Relevant to https://forgejo.coilysiren.me/coilyco-flight-deck/mcp-beaver/issues/49, where a request hung 180s inside healthy-looking pods and nothing cheap could distinguish "serving" from "wedged." 2. **Capability grounding.** `sirens-echo` has three open issues on the agent misdescribing its own capabilities (https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/199, https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/211, https://forgejo.coilysiren.me/coilyco-gaming/sirens-echo/issues/200 — the last says whatever enumerates real capabilities must keep up or the claim check has nothing to check). A tool reporting the served inventory from the server is the grounded answer, and it only helps if it is reliably present. 3. **Fleet consistency.** Present-on-some-servers is worse than either extreme, because an agent cannot infer from absence whether a server is unhealthy or simply did not opt in — the same reasoning as https://forgejo.coilysiren.me/coilyco-flight-deck/mcp-beaver/issues/54. ## Requested Default on. Keep `server-info name="..."` for the rename. Add an explicit opt-out for a deployment that genuinely wants it gone: ```kdl server-info disabled // or `no-server-info`, naming is yours ``` Fail-closed parsing stays as it is. ## Two consequences to design against — neither is a blocker **1. Name collision becomes a startup failure rather than an opt-in error.** `serverInfoTool` errors when its name collides with a granted tool. Today that is only reachable if you opted in. Default-on means **an existing guardfile granting a tool named `ward_mcp_info` stops building.** Unlikely given the name, but it is a silent-until-deploy break and should be checked across `coilyco-bridge/deploy` before this lands. If any exist, the opt-out or a rename is the migration. **2. It makes one sentence in the README untrue.** The pitch is: > the served surface is exactly the guardfile's grants: an unwritten `delete issue` grant means no `delete_issue` tool or HTTP endpoint can ever be served (**deny-by-absence**) A default-on tool nobody granted is a real exception to that. The security property is intact — the tool reaches no upstream and cannot widen the grant surface — but the *sentence* needs to become "exactly the grants, plus one read-only tool that reports the server's own shape." Worth getting right, because that sentence is the product's core claim and it is load-bearing in the rename decision on https://forgejo.coilysiren.me/coilyco-flight-deck/mcp-beaver/issues/48, which already caught one over-strong claim in the same paragraph. Copy is Content's call. Flagging it so it does not ship as an unnoticed weakening of the pitch. ## Acceptance - A guardfile with no `server-info` node mints the info tool. - An explicit opt-out suppresses it, and is tested. - `name=` override still works. - `lint` output includes the tool, so lint and `tools/list` still report the same surface. - Existing guardfiles across `coilyco-bridge/deploy` checked for a `ward_mcp_info` collision before merge. - README's deny-by-absence sentence updated in the same change. ## What I verified Read `internal/mcpserver/serverinfo.go`, `server.go:338`, and `server.go:433-441` at `mcp-beaver` `main` on 2026-08-13. I did not run the server. The claim that every payload field is otherwise obtainable is from reading the payload builder against the SDK surface, not from an empirical diff of `ward_mcp_info` versus `tools/list` — worth confirming empirically if this is contentious.
Author
Member

Landed in 115c02f. The reading of the payload holds - no field it returns is otherwise withheld, so opting out removed a convenience rather than a disclosure.

Collision check, which the acceptance list required before merge: clear. Across all ten guardfiles in coilyco-bridge/deploy/services/*, zero references to ward_mcp_info and zero server-info nodes. So nothing collides, and the fleet-consistency argument is stronger than the issue put it: every deployed server currently lacks the probe, which is the worst of the three states.

Shape:

  • No node mints ward_mcp_info.
  • server-info name="status" still renames.
  • server-info disabled is the opt-out, tested.
  • server-info disabled name="..." is an error - naming a tool that is not minted reads as a live override.

Chose a bare disabled argument over no-server-info so there is exactly one node controlling this, and one place to look.

On your two consequences.

  1. The collision error is now reachable without an opt-in node, so it names both migrations rather than assuming the author opted in and can back it out: rename via name=, or state disabled.
  2. The deny-by-absence sentence is updated in README.md, DESIGN.md, and FEATURES.md. It now names the exception and bounds it - the info tool is the one served entry that no grant asked for, and everything reaching an upstream is still exactly the grants. DESIGN.md also flags server-info as the single sibling that is not opt-in, since that was previously a blanket claim about all of them.

Scope is spec mode. SSM mode has a fixed two-tool surface built by a different constructor, and upstream-proxy mode is built from CLI flags with no guardfile - neither mints the tool, and extending it there is a separate decision rather than something to do silently.

Not done here, flagged rather than assumed: the default tool name stays ward_mcp_info despite the repo rename. Changing it would break every consumer that has learned the name, and #48 already ruled the module/package naming mismatch deliberate debt.

Landed in 115c02f. The reading of the payload holds - no field it returns is otherwise withheld, so opting out removed a convenience rather than a disclosure. **Collision check, which the acceptance list required before merge: clear.** Across all ten guardfiles in `coilyco-bridge/deploy/services/*`, zero references to `ward_mcp_info` and zero `server-info` nodes. So nothing collides, and the fleet-consistency argument is stronger than the issue put it: *every* deployed server currently lacks the probe, which is the worst of the three states. Shape: * No node mints `ward_mcp_info`. * `server-info name="status"` still renames. * `server-info disabled` is the opt-out, tested. * `server-info disabled name="..."` is an error - naming a tool that is not minted reads as a live override. Chose a bare `disabled` argument over `no-server-info` so there is exactly one node controlling this, and one place to look. **On your two consequences.** 1. The collision error is now reachable without an opt-in node, so it names both migrations rather than assuming the author opted in and can back it out: rename via `name=`, or state `disabled`. 2. The deny-by-absence sentence is updated in `README.md`, `DESIGN.md`, and `FEATURES.md`. It now names the exception and bounds it - the info tool is the one served entry that no grant asked for, and everything reaching an upstream is still exactly the grants. `DESIGN.md` also flags `server-info` as the single sibling that is not opt-in, since that was previously a blanket claim about all of them. Scope is spec mode. SSM mode has a fixed two-tool surface built by a different constructor, and upstream-proxy mode is built from CLI flags with no guardfile - neither mints the tool, and extending it there is a separate decision rather than something to do silently. Not done here, flagged rather than assumed: the default tool name stays `ward_mcp_info` despite the repo rename. Changing it would break every consumer that has learned the name, and #48 already ruled the module/package naming mismatch deliberate debt.
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#61
No description provided.