Skip to content

feat(mcp): surface which agents can reach each MCP server (#568) - #631

Merged
oxoxDev merged 4 commits into
tinyhumansai:mainfrom
YellowSnnowmann:feat/568-mcp-reachability
Aug 11, 2026
Merged

feat(mcp): surface which agents can reach each MCP server (#568)#631
oxoxDev merged 4 commits into
tinyhumansai:mainfrom
YellowSnnowmann:feat/568-mcp-reachability

Conversation

@YellowSnnowmann

@YellowSnnowmann YellowSnnowmann commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes #568 (finding #3 of the MCP/Skills console↔harness epic #565).

The MCP console showed a server's health and tool count but never who could actually call it. An agent reaches mcp:<slug> only when its effective grants cover it, so the moment a company narrows grants per agent, a server can be live, healthy, and reachable by nobody — silently, with no error anywhere in the chain (such an agent does not even get mcp_list_servers).

GET .../mcp/servers (and every mutating response) now carries reachableBy: the ids of the agents whose effective grants cover the server. The console renders it per row and flags the empty case loudly, because a healthy server no teammate can reach is almost always a misconfiguration rather than an intent.

The design decision worth challenging

Reachability is computed over the same roster the harness builds — manifest agents plus promoted overlay teammates — using the exact machinery the harness registry uses, so the console cannot disagree with what an agent is actually granted (the thesis of #565):

  • agent_effective_grants for each agent's effective grants (already shared with the team-agent console route).
  • grants_cover_server for the mcp:<slug> match.

grants_cover_server moves from harness::mcp (which is #[cfg(feature = "openhuman")]) to runtime::tools, beside grant_matches. The console route ships in the default build and cannot import a harness symbol; relocating the primitive — rather than reimplementing the two-line check — keeps one matcher read by both the registry and the console (the "one validator" rule the codebase already follows). The registry path is unchanged.

Overlay teammates are included, deliberately

build_roster grants tools to manifest agents and overlay teammates (an overlay has no tools row, so it inherits the company allow). Iterating only manifest.agents would let the loud zero-state falsely fire when an overlay agent actually reaches the server — the exact console/harness disagreement #565 is about. roster_grants mirrors build_roster exactly, including skipping an overlay id already claimed by a manifest agent.

One note on the issue text

The issue says agent_effective_grants is "already shared between the harness and the console capability route." Accurate, but that console reader is the team-agent detail route, not an MCP route — this PR makes the MCP list route a new reader. No change to the approach; recording it for the next reader.

API / behavior changes

  • McpServerDto gains reachableBy: string[]always serialized, even when empty (the empty array is the load-bearing signal). Additive; existing fields unchanged.
  • A disabled server is reachable by nobody, whatever the grants say: registry_for_agent filters on decl.enabled && grants_cover_server(..), so reachers_of mirrors both halves of that filter. The console scopes its loud zero-state to enabled servers — a disabled server is empty by construction, and flagging it would cry wolf on intent.
  • No route or signature changes. list_servers and mutation_response load the company record once and derive both the manifest servers and the roster from it — not an extra load.

Tests

  • mcp_reachability_lists_reaching_agents_including_overlay — with allow = ["*"], a narrowed agent (tools = ["mcp:notion"]) reaches only that server, while a no-tools agent and an overlay teammate inherit the wildcard and reach everything.
  • mcp_reachability_flags_a_server_no_agent_can_reach — a narrow company allow leaves a manifest server with an empty reachableBy (the flagged zero case).
  • Both run in the default build, exercising the relocation: the route reads grants_cover_server with no openhuman feature.

Verified locally: cargo test --lib (default and --features openhuman,tinycortex), cargo clippy --no-deps --features openhuman,tinycortex --all-targets -- -D warnings, tsc -b --noEmit, cargo fmt --all -- --check — all clean.

Depends on #554 — merge it first

#554 (feat(mcp): install-wide default MCP servers) also edits McpServerDto, list_servers, and the frontend McpServer type, and changes resolve_effective from 3 to 4 args. This PR is written against main; the agreed order (see #554/#598) is #554 → this. Once #554 merges I will follow up — adopting the 4-arg resolve_effective and the source: "default" servers, which flow through the reachability loop for free.

main has since been merged in (not rebased, to keep the review history readable). The one conflict was in src/server/ops/mcp.rs: #598 renamed REBUILD_NOTE to NEXT_TURN_NOTE while this branch added the reachable_by argument to dto_from_decl — both kept.

Closes #568

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • MCP server listings now show which agents can access each server.
    • Displays a clear warning when no agents have permission to reach a server.
    • Reachability information is included in server creation and update responses.
  • Bug Fixes

    • Reachability now accurately reflects narrowed, wildcard, and overlay-agent tool permissions.
  • Tests

    • Added coverage for reachable, unreachable, and permission-specific MCP server scenarios.

…ai#568)

The MCP console showed a server's health and tool count but never who
could actually call it. An agent reaches `mcp:<slug>` only when its
effective grants cover it, so the moment a company narrows grants per
agent a server can be live, healthy, and reachable by nobody — silently,
with no error anywhere in the chain (such an agent doesn't even get
`mcp_list_servers`).

GET .../mcp/servers (and every mutating response) now carries
`reachableBy`: the ids of the agents whose effective grants cover the
server. The console renders it per row and flags the empty case loudly,
since a healthy server no teammate can reach is almost always a
misconfiguration rather than intent.

Reachability is computed over the same roster the harness builds —
manifest agents plus promoted overlay teammates — using the exact
machinery the harness registry uses, so the console can't disagree with
what an agent is actually granted: `agent_effective_grants` for each
agent's grants, `grants_cover_server` for the `mcp:<slug>` match.
`grants_cover_server` moves from `harness::mcp` (openhuman-gated) to
`runtime::tools` beside `grant_matches` so the always-compiled console
route reads the one primitive instead of reimplementing it; the registry
path is unchanged. Overlay teammates are included deliberately — omitting
them would let the zero-state falsely fire when an overlay agent reaches
the server.

Closes tinyhumansai#568

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

MCP server DTOs now include agent reachability derived from effective grants. Listing and mutation responses calculate reachability for manifest and overlay agents. The frontend displays authorized agents or a warning when no agent can access a server.

Changes

MCP reachability

Layer / File(s) Summary
Shared grant matching
src/runtime/tools.rs, src/harness/mcp.rs
The shared grants_cover_server helper matches MCP server names against grants. The harness reuses this helper.
Server response reachability
src/server/ops/mcp.rs, src/server/ops/write_test.rs
MCP responses calculate reachableBy for manifest and overlay agents. Tests cover wildcard grants, narrowed grants, mutation responses, and unreachable servers.
Reachability display
frontend/src/api/types.ts, frontend/src/views/connections/McpServersSection.tsx
McpServer exposes optional reachableBy data. Server rows display reachable agents or an access warning.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MCPServerOps
  participant CompanyRecord
  participant EffectiveAgentGrants
  MCPServerOps->>CompanyRecord: Load company and roster
  CompanyRecord->>EffectiveAgentGrants: Derive manifest and overlay grants
  EffectiveAgentGrants-->>MCPServerOps: Return effective grants
  MCPServerOps->>MCPServerOps: Calculate reachable agent IDs
  MCPServerOps-->>MCPServerOps: Return server DTO with reachableBy
Loading

Possibly related PRs

Suggested reviewers: oxoxdev, m3ga-mind

Poem

A rabbit checks the server gate,
Grants decide who may pass through.
Wildcards stretch and narrow bounds,
Empty lists tell the truth.
MCP rows now show the way.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: exposing which agents can reach each MCP server.
✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch feat/568-mcp-reachability

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…nsai#568)

list_servers had reachability coverage but mutation_response did not —
add a shape assertion on the PUT response in the delete-guard test so the
field stays wired on the mutating path, not just on GET.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@YellowSnnowmann
YellowSnnowmann marked this pull request as ready for review August 11, 2026 10:19
Conflict in src/server/ops/mcp.rs: main (tinyhumansai#598) renamed REBUILD_NOTE to
NEXT_TURN_NOTE; this branch (tinyhumansai#568) added the reachable_by argument to
dto_from_decl. Kept both.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/server/ops/mcp.rs`:
- Around line 294-300: Update reachers_of in src/server/ops/mcp.rs (lines
294-300) to accept the server declaration or enabled value and return an empty
list when the server is disabled, while preserving grant-based filtering for
enabled servers. Add or update the focused test in src/server/ops/write_test.rs
(lines 2545-2550) so a disabled server with matching grants asserts reachableBy:
[].
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5f2543d1-f822-45c2-9ec1-c4a659d119a0

📥 Commits

Reviewing files that changed from the base of the PR and between 9721664 and 9e221ab.

📒 Files selected for processing (6)
  • frontend/src/api/types.ts
  • frontend/src/views/connections/McpServersSection.tsx
  • src/harness/mcp.rs
  • src/runtime/tools.rs
  • src/server/ops/mcp.rs
  • src/server/ops/write_test.rs

Comment thread src/server/ops/mcp.rs Outdated
`registry_for_agent` filters on `decl.enabled && grants_cover_server(..)`, so
an agent holding `mcp:<slug>` is handed no such tool while the server is off.
`reachers_of` mirrored only the grant half, so a disabled server came back
listing agents that cannot call it — the console/harness disagreement this
feature exists to remove.

It now takes the declaration and returns an empty list when the server is
disabled. The console scopes its loud zero-state to enabled servers: a disabled
server is empty by construction, so flagging it would cry wolf on intent.

Test: a disabled server with an otherwise matching grant reports
`reachableBy: []` in both readers — the mutating response that turns it off,
and the later list.

@oxoxDev oxoxDev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Relocating grants_cover_server from harness::mcp to runtime::tools rather than reimplementing a two-line check in the console route is the right call, and the reason is the one that matters: the console route ships in the default build and cannot import a #[cfg(feature = "openhuman")] symbol, so the alternative was a second matcher that could drift from the registry's. One matcher read by both is the whole point of #565, and a reachability display that can disagree with actual reachability would be worse than no display.

Including overlay teammates is also right, and the reasoning is the sharper half: iterating only manifest.agents would let the loud zero-state fire falsely when an overlay agent does reach the server — a console/harness disagreement announcing itself as a misconfiguration warning. Mirroring build_roster exactly, including the skip for an overlay id already claimed by a manifest agent, is what makes that hold.

Major — this collides with #554, on the two files where #554 owes frontend work.

This PR edits frontend/src/api/types.ts and frontend/src/views/connections/McpServersSection.tsx. Those are the exact two files #554 (feat(mcp): ship install-wide default MCP servers, @M3gA-Mind) has to touch: it adds a third McpSource::Default value to the DTO while types.ts:747 still declares source: "manifest" | "runtime", and McpServersSection.tsx:386 picks its badge variant with server.source === "manifest" ? … : …, so a default currently renders identically to a console-added server. I raised that on #554 as a blocker for its own stated rationale.

So both PRs are adding a field to the same interface and a new element to the same row. Neither is aware of the other. Concretely:

  • If this lands first, #554's frontend fix-up rebases onto your reachableBy changes — fine, but nobody has told @M3gA-Mind that.
  • If #554 lands first with the badge work, this rebases onto it — also fine, also untold.
  • If #554 lands without the frontend work (its current state), you both end up editing types.ts for different reasons and the source union stays wrong, with two PRs' worth of history making it look considered.

Worth a note on #554 agreeing an order, the way you did for #598.

src/server/ops/mcp.rs and src/server/ops/write_test.rs now have three claimants. #598 (approved, agreed to land first), #554, and this. #598 is small and green so it should still go first; after that these two need a sequence between themselves. Not a problem yet — it becomes one the moment two of them merge on the same afternoon.

Question — is the loud zero-state ever legitimate? "A healthy server no teammate can reach is almost always a misconfiguration" is right as a default. But a company mid-rollout — server added, grants not yet widened — hits it on purpose, and a warning that fires during a normal sequence teaches operators to ignore warnings. Is there a state where it reads as informational rather than wrong, or is the answer simply that the window is short enough not to matter? Worth a sentence either way, since the loudness is the feature.

Minor: reachableBy is computed by evaluating every agent's effective grants against every server on each list call. Almost certainly fine at company scale — flagging only so the cost is a known one rather than a discovered one, if a roster ever gets large.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

mcp: nothing shows which agents can actually reach a server

2 participants