Skip to content

An MCP server needs 116 per-tool selectors to bind its own published surface; the two shorter spellings both dead-end #432

Description

@pengfei-threemoonslab

Summary

Binding an MCP server's own published surface requires naming every tool individually in agent_bindings.declarations[].tools. For github/github-mcp-server that is 116 selector rows to state a fact that is structurally true of the source: a server publishes its tools, and every published tool is callable by any client connected to it.

The two shorter spellings a reader would reach for both fail, and neither failure explains what to do instead. Until one of them is written, reachable_tools: 0, nothing is evaluated, and the verdict is insufficient_evidence regardless of what the PR did.

This is the same shape as #410 increment 3"authority follows credentials, not functions" — applied to the binding dimension. Asking per-tool for something that is one fact about the source is what breeds copy-paste, and 116 hand-written rows is where an adopter stops.

Reproduced, three spellings

github/github-mcp-server at #3020, 116 tools merged from the repository's checked-in __toolsnaps__/*.snap into one type: mcp source. CLI 0.16.0b7 at 9233c6b8.

1. No agent_bindings at all — the manifest states source-level authority (increment 3) and a control_pack, nothing else:

"binding_coverage": {"total_catalog_tools": 116, "reachable_tools": 0, "unbound_tools": 116,
                     "pass_eligible": false, "gap_count": 2,
                     "reason_counts": {"ambiguous_root_agent": 1, "missing_binding_evidence": 1}}

2. agent_bindings.root naming the source:

agent_bindings:
  root:
    object: github_mcp
    source_id: github_mcp
ambiguous_root_agent | agent binding graph | "No root agent matched the configured selector."
reachable_tools: 0

root.object is documented as "Exact selector for the statically reviewed root agent" — a code object the scanner observed. A JSON tool export produces no code objects, so this field cannot be satisfied for a pure MCP source by construction.

3. A wildcard selector:

tools:
  - {tool: "*", source_id: github_mcp}
unresolved_bound_tool: 1
reachable_tools: 0

"*" is matched as a literal tool name. BoundToolSelectorConfig requires a tool string and offers no wildcard or source-only form.

The only route that works is the one samples/ai_generated_refund_pr demonstrates — agent: root, complete: true with an explicit tools: list. That fixture has two tools. This server has 116.

Why "unbound" is not a meaningful state here

For an agent, binding is real information: a catalog may hold 63 OpenAPI operations of which the agent wires 5, and #385 drew the boundary there deliberately. For a tool server, there is no such gap. The repository under review is the tool surface; anything in its published tools/list is callable by any client that connects. There is no root agent, and there is nothing to select between.

That reasoning is already accepted elsewhere in the codebase — schemas/exclusions.py records it as the motivating case for the exclusion ledger:

github/github-mcp-server#3076 adds delete_repository (destructiveHint: true) to a 117-tool MCP server. The scan computed unbound_tools: 1 and reported gap_count: 0 […]

The ledger fixed the accounting. The declaration required to make those tools reachable in the first place is still per-tool.

What is working, for the record

This run also validated the #403 discriminator on a repository it had never seen. All 116 tools were excluded (nothing bound), and the ledger gated exactly one:

surface_exclusions: {"total": 116, "gated": 1, "gap_backed": 1}
"1 of 2 evidence gap(s) are new in this diff"

find_duplicate, the tool this PR adds — with the 115 pre-existing correctly spared, blockers: 0, and control.state: review_publishable rather than a stop. Proportionate on a benign change. The problem is only that nothing downstream could run.

Options

A. A source-level binding statement. tool_sources[].binding: complete (or equivalent) — one reviewed human declaration that this source's published surface is the surface under review. Mirrors tool_sources[].authority from increment 3 exactly: same place, same shape, same argument.

B. A source selector in BoundToolSelectorConfig. Allow {source_id: github_mcp} with tool omitted, meaning every tool this source contributes. Smaller schema change; keeps the declaration inside agent_bindings where a reader already looks.

C. Make root.object satisfiable for artifact sources, or fail with a message that says it cannot be — currently it reports ambiguous_root_agent as though a better selector existed.

Recommend A, for the symmetry with authority. Either way, C's error text is worth fixing independently: two separate walks reached for root.object first, and the message sent both of them looking for a selector that does not exist.

Whichever is chosen, it must stay a human declaration — inferring "the source binds everything" from source content is the #268 attack, and the point here is to make the statement one line rather than to remove it.

Acceptance


Found 2026-08-24 walking github-mcp-server#3020; the same wall was hit on #3076 in an earlier walk, where it was worked around by generating the 116-row list programmatically.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Next after P0; blocks other work or ships a misleading resultarea:verifyverify / preview orchestration and SHIP-VERIFY-* checksenhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions