fix(mcp): a nameless tool is not a tool named after its server #589

Merged
coilyco-ops merged 1 commit from fix/a-nameless-tool-is-not-the-server into main 2026-08-13 16:37:38 +00:00
Member

closes #587

The nine empty tool names that motivated #587 are old pods, not a live defect. I established that first, and then found a real hole underneath it.

The empty spans

The scratchpad shipped without Original at 15:51 on 12 August (02b2d07), and 2cc7ddb fixed it at 17:21 the same day — "fix(scratchpad): name the tool in telemetry". All nine empty spans are scratchpad on sirens-deep, from an image built in that 90-minute window.

Same explanation Lucia established for failure_cause on #292: image age, not breakage. I checked the attribute's own history before concluding, because "the field is new" and "the field is broken" look identical in a query.

The hole underneath

name := invalidProxyToolName.ReplaceAllString(server+"__"+tool, "_")
name = strings.Trim(name, "_")
if name == "" { ... }

proxyToolName("forgejo", "") composes forgejo__, trims to forgejo, and passes. A nameless tool therefore registers as a tool named after its own server, with Original: "".

Two consequences, both the shape settled repeatedly today:

  • mcp.tool.name reads empty, which is indistinguishable from an absent attribute rather than from a real name
  • dispatch carries an empty tool name to the server

The emptiness check could not see the missing half because trimming leaves the other half standing. Checked before composing now, because after composition the information is already gone.

Blast radius

Unexercised by our own tools — the scratchpad, fetch, refresh and fixture definitions all name themselves. Reachable by any external MCP server that publishes a nameless tool.

The error is fatal to the turn, which is consistent rather than new: proxyToolName already errors on an unusable name, and the register path already treats a roster mistake as fatal on the stated grounds that degrading past it silently drops a tool.

A test pins that ordinary names still compose unchanged, and one pins the composition itself, so if forgejo__ ever stops trimming to forgejo the test says the cause moved rather than silently passing.

ward exec gate green.

closes #587 **The nine empty tool names that motivated #587 are old pods, not a live defect.** I established that first, and then found a real hole underneath it. ## The empty spans The scratchpad shipped without `Original` at 15:51 on 12 August (`02b2d07`), and `2cc7ddb` fixed it at 17:21 the same day — *"fix(scratchpad): name the tool in telemetry"*. All nine empty spans are `scratchpad` on `sirens-deep`, from an image built in that 90-minute window. Same explanation Lucia established for `failure_cause` on #292: **image age, not breakage.** I checked the attribute's own history before concluding, because "the field is new" and "the field is broken" look identical in a query. ## The hole underneath ```go name := invalidProxyToolName.ReplaceAllString(server+"__"+tool, "_") name = strings.Trim(name, "_") if name == "" { ... } ``` `proxyToolName("forgejo", "")` composes `forgejo__`, trims to **`forgejo`**, and passes. A nameless tool therefore registers as **a tool named after its own server**, with `Original: ""`. Two consequences, both the shape settled repeatedly today: - `mcp.tool.name` reads empty, which is indistinguishable from an absent attribute rather than from a real name - dispatch carries an empty tool name to the server The emptiness check could not see the missing half because trimming leaves the other half standing. **Checked before composing now, because after composition the information is already gone.** ## Blast radius Unexercised by our own tools — the scratchpad, fetch, refresh and fixture definitions all name themselves. Reachable by any external MCP server that publishes a nameless tool. The error is fatal to the turn, which is consistent rather than new: `proxyToolName` already errors on an unusable name, and the register path already treats a roster mistake as fatal on the stated grounds that degrading past it silently drops a tool. A test pins that ordinary names still compose unchanged, and one pins the composition itself, so if `forgejo__` ever stops trimming to `forgejo` the test says the cause moved rather than silently passing. `ward exec gate` green.
fix(mcp): a nameless tool is not a tool named after its server
All checks were successful
ci / test (pull_request) Successful in 31s
ci / publish-echo-image (pull_request) Has been skipped
ci / publish-observed (pull_request) Has been skipped
ci / image-build (pull_request) Successful in 18s
4786cff41a
proxyToolName composed server plus separator plus tool, then trimmed
underscores and rejected an empty result. An empty tool name survives that:
forgejo plus __ plus nothing trims to forgejo, which is not empty, so it
registered as a tool named after the server with an empty Original.

The consequence is the shape settled repeatedly today. The span attribute
mcp.tool.name reads empty, which is indistinguishable from an absent attribute
rather than from a real name, and dispatch carries an empty tool name to the
server.

Checked before composing rather than after, because after is where the
information is already gone.

Found while establishing that the nine empty tool names in telemetry are old
pods rather than a live defect. They are: the scratchpad shipped without
Original at 15:51 on 12 August and 2cc7ddb fixed it at 17:21 the same day.
This hole is unexercised by our own tools, which all name themselves, and
reachable by any MCP server that publishes a nameless one.

Co-authored-by: Kai Siren <coilysiren@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
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-gaming/sirens-echo!589
No description provided.