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.
Summary
Binding an MCP server's own published surface requires naming every tool individually in
agent_bindings.declarations[].tools. Forgithub/github-mcp-serverthat 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 isinsufficient_evidenceregardless 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-serverat #3020, 116 tools merged from the repository's checked-in__toolsnaps__/*.snapinto onetype: mcpsource. CLI0.16.0b7at9233c6b8.1. No
agent_bindingsat all — the manifest states source-levelauthority(increment 3) and acontrol_pack, nothing else:2.
agent_bindings.rootnaming the source:root.objectis 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:
"*"is matched as a literal tool name.BoundToolSelectorConfigrequires atoolstring and offers no wildcard or source-only form.The only route that works is the one
samples/ai_generated_refund_prdemonstrates —agent: root, complete: truewith an explicittools: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/listis 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.pyrecords it as the motivating case for the exclusion ledger: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:
find_duplicate, the tool this PR adds — with the 115 pre-existing correctly spared,blockers: 0, andcontrol.state: review_publishablerather 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. Mirrorstool_sources[].authorityfrom increment 3 exactly: same place, same shape, same argument.B. A source selector in
BoundToolSelectorConfig. Allow{source_id: github_mcp}withtoolomitted, meaning every tool this source contributes. Smaller schema change; keeps the declaration insideagent_bindingswhere a reader already looks.C. Make
root.objectsatisfiable for artifact sources, or fail with a message that says it cannot be — currently it reportsambiguous_root_agentas 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.objectfirst, 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
reachable_tools: 116from one reviewed declaration.root.objectagainst an artifact-only source either resolves or explains that it cannot, rather than reporting an ambiguous root.Found 2026-08-24 walking
github-mcp-server#3020; the same wall was hit on#3076in an earlier walk, where it was worked around by generating the 116-row list programmatically.