feat: let an agent ask what its connectors are, instead of guessing
Nightly Build / build (push) Successful in 7m44s

An agent that wanted to know which MCP servers it had called
`list_mcp_servers` — a tool that has never existed anywhere in this
repo — and got "unknown tool". It was not a random hallucination: the
prompt block says "the system prompt shows available servers", and
`render_mcp_list` returned an empty string when nothing was connected.
The model read a promise, found no table, and invented the discovery
tool the text implied. The `mcp` kinds of `list_items`/`toggle_item`
had been removed to close the §14 RCE vector, which was right for the
write half and left no read half at all.

So `list_items` gains `type: "mcp"` and returns the whole picture in
one call, split into four buckets that each answer a different
question: what is already loaded (call its tools directly), what is
ready for `activate_tools`, what is installed but unusable and why,
and what the user could still activate. Conflating the first two is
what produced the original failure, so they stay apart. Every entry
carries a derived note and a next step; when the step is a human one,
it says so and names the UI page, because there is no tool for it.

Read-only, and structurally so: `toggle_item` deliberately gains
nothing, and the new `McpDirectory` trait exposes exactly one method.
Enabling a connector from a tool is the thing §14 removed, and a wider
seam here is how it would come back. Deny-by-default survives the
report — an ungranted connector is not named at all, since a listing
of what to ask for is itself a leak — except for a catalogue manager,
who cannot administer what they cannot see.

Three sources answer three questions and none is redundant: the
registry says what exists and who may have it, the owner database says
what was activated, and the live runtimes say what is connected right
now — a row can read `ready` while its process is dead. The live half
reaches the tool through the turn's extension map, alongside the pool
and the fs view; with no live view the durable picture still renders,
so freshness is an improvement and never a precondition.

The static `__MCP_LIST__` table stays as it was, because it is frozen
per conversation for prompt-cache stability. Its empty case now says
so out loud and points at the tool.
This commit is contained in:
2026-08-04 19:41:18 +01:00
parent daaceff6ba
commit efb5b1dc33
14 changed files with 693 additions and 22 deletions
+32
View File
@@ -58,6 +58,38 @@ pub struct ToolContext {
/// the container they resolve into. `execute_cmd` execs into `fs.container_name`
/// and the disk fs-tools resolve physical paths against `fs`'s host bases.
pub fs: Arc<crate::user_fs::UserFs>,
/// The caller's live MCP runtimes, read-only (blueprint §7). `None` outside a
/// turn that has one — a tool must degrade to whatever the database says
/// rather than fail.
pub mcp: Option<Arc<dyn McpDirectory>>,
}
// ── McpDirectory ──────────────────────────────────────────────────────────────
/// One connected MCP server, as the tool layer sees it.
#[derive(Debug, Clone)]
pub struct McpServerView {
/// Runtime name — the id `activate_tools` takes and the `mcp__<name>__` prefix.
pub name: String,
pub description: Option<String>,
/// Bare tool names, without the `mcp__<server>__` prefix the model calls.
pub tools: Vec<String>,
}
/// Read-only window onto the caller's live MCP runtimes, threaded into
/// [`ToolContext`] so a tool can report what is **actually connected right now**
/// — the one thing no query can answer, since a connector row can read `ready`
/// while its process is dead, and a per-user server appears only once its
/// container has started it.
///
/// Deliberately read-only and deliberately narrow. Enabling, activating or
/// configuring a connector is not an agent-reachable operation (blueprint §14 —
/// the whole reason the old `register_mcp` tool was removed), and a wider trait
/// here is precisely the seam through which it would become one again.
pub trait McpDirectory: Send + Sync {
/// Every server this caller's session can currently reach, in whatever order
/// the runtimes report them.
fn connected(&self) -> Vec<McpServerView>;
}
// ── Tool trait ────────────────────────────────────────────────────────────────