fix(runtime-api): report MCP servers' last connect error; scope Fleet member-start replay to its session

`GET /v1/apps/mcp/servers` now carries `last_error` for a configured server
that is in connection backoff, truncated to 2,048 characters, so clients can
say why a server is not connected instead of only showing it as disconnected.
`McpPool::last_connect_error` returns the error only while the same
configuration is the one in backoff (runtime-added flag, reviewed plugin
source and runtime config hash unchanged); runtime-added servers report none.
docs/RUNTIME_API.md documents the field.

Fleet member starts are keyed by (state namespace, operation key), so a
replayed start in one session can no longer resolve to a member that another
session started under the same operation key.

Recovered from stranded drafts (the readiness MCP last-error patch, the MCP
pool accessor and release-engine 9cb914b617) and rewritten against the current
has_connection_backoff.

Verified: the five runtime_api MCP management tests pass (5 passed, 0 failed:
crud, redacts_credentials, project_ownership_blocks_global_shadow_mutations,
blocks_credential_retargeting, revision_precondition_prevents_stale_mutation),
run on the shared tree with other sessions' uncommitted edits present;
clippy on codewhale-engine and codewhale-app-server libs was clean earlier
today; the patch applies to HEAD cd7f5b32b1 and every item it references
exists there. No new tests.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
CodeWhale BotandClaude Opus 5.5 committed 2026-11-07 14:33:41 -08:00
1 parent cd7f5b32b1
commit 4c5426f9bb
5 files changed
+52 -4

No files matched your search

+7
View File
@@ -70,6 +70,10 @@ until the matching tag, packages, checksums, and release assets exist.
### Added
- **MCP servers say why they are not connected:** the Runtime API's
`GET /v1/apps/mcp/servers` now includes a configured server's last
connection error while it is retrying, so the desktop and other clients can
show the reason instead of only "disconnected".
- **Inspect and repair plugin snapshots:** `codew plugins status [name]
[--json]` reports bundle state and diagnostics without changing plugin
stores. Runtime loading restores a missing snapshot only from bytes matching
@@ -285,6 +289,9 @@ until the matching tag, packages, checksums, and release assets exist.
### Fixed
- **Fleet members:** a replayed member start now matches only within its own
session, so two sessions that reuse an operation key no longer receive each
other's agent.
- **Plugin stages created during a registry load are checked:** a plugin whose staged copy appears while the registry is loading now goes through the same restage lock, receipt and policy checks as any other stage instead of being left unstaged; invalid bytes are still refused and stay in the failure cache.
- **Host-provided model clients:** keep configured session isolation and
approval deadlines when optional credential preflight fails.
+7
View File
@@ -1640,6 +1640,8 @@ struct McpServerEntry {
url: Option<String>,
connected: bool,
has_connection_backoff: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
last_error: Option<String>,
enabled_tools: Vec<String>,
disabled_tools: Vec<String>,
always_allow_tools: Vec<String>,
@@ -7380,6 +7382,10 @@ async fn list_mcp_servers(
has_connection_backoff: pool
.as_ref()
.and_then(|pool| pool.has_connection_backoff(&name, config.timeouts, &server_cfg)),
last_error: pool
.as_ref()
.and_then(|pool| pool.last_connect_error(&name, config.timeouts, &server_cfg))
.map(|error| truncate_text(error, 2048)),
enabled_tools: server_cfg.enabled_tools.clone(),
disabled_tools: server_cfg.disabled_tools.clone(),
always_allow_tools: server_cfg.always_allow_tools.clone(),
@@ -7405,6 +7411,7 @@ async fn list_mcp_servers(
.as_ref()
.is_some_and(|pool| pool.connected_servers().contains(&name.as_str())),
has_connection_backoff: None,
last_error: None,
enabled_tools: server_cfg.enabled_tools.clone(),
disabled_tools: server_cfg.disabled_tools.clone(),
always_allow_tools: server_cfg.always_allow_tools.clone(),
+25
View File
@@ -4547,6 +4547,31 @@ impl McpPool {
Some(self.connect_backoff.contains_key(name))
}
#[must_use]
pub fn last_connect_error(
&self,
name: &str,
timeouts: McpTimeouts,
server: &McpServerConfig,
) -> Option<&str> {
if self.has_connection_backoff(name, timeouts, server) != Some(true) {
return None;
}
let current = self.config.servers.get(name)?;
if current.runtime_added != server.runtime_added
|| (current.reviewed_plugin.is_some() && current.reviewed_plugin_source().is_none())
|| (server.reviewed_plugin.is_some() && server.reviewed_plugin_source().is_none())
|| server
.reviewed_plugin_source()
.is_some_and(|source| source.validate_before_use(name, "inspect").is_err())
|| runtime_servers_hash(&self.dynamic_servers.read()) != self.runtime_config_hash
{
return None;
}
let error = self.connect_backoff.get(name)?.last_error.as_str();
(!error.is_empty()).then_some(error)
}
/// The needs-auth server that owns a model tool name (`mcp_<server>_…`),
/// if any: the server the model is trying to reach with a real tool name
/// from a catalog built before its login lapsed. Longest configured name
+7 -3
View File
@@ -4196,7 +4196,7 @@ pub struct SubAgentManager {
/// agent id, so the task's failure text blames the route that failed.
/// Taken when the task settles.
rerouted_requests: HashMap<String, ReroutedRequest>,
member_starts: HashMap<String, (String, String)>,
member_starts: HashMap<(String, String), (String, String)>,
}
/// One child request waiting on a person (approvals C2). The store is the
@@ -23669,7 +23669,11 @@ pub async fn start_fleet_member(
"prompt": &start.prompt,
}))
.map_err(|error| error.to_string())?;
if let Some((accepted, agent_id)) = manager.read().await.member_starts.get(&start.operation_key)
let operation_identity = (
runtime.context.state_namespace.clone(),
start.operation_key.clone(),
);
if let Some((accepted, agent_id)) = manager.read().await.member_starts.get(&operation_identity)
{
return if *accepted == request {
Ok(agent_id.clone())
@@ -23703,6 +23707,6 @@ pub async fn start_fleet_member(
.write()
.await
.member_starts
.insert(start.operation_key, (request, result.agent_id.clone()));
.insert(operation_identity, (request, result.agent_id.clone()));
Ok(result.agent_id)
}
+6 -1
View File
@@ -2139,7 +2139,12 @@ This persisted project decision is separate from a thread’s `trust_mode` / Ful
`capabilities.skill_detail` on `GET /v1/runtime/info`, which is the source a
client should use rather than probing this path: a `404` here means "no such
skill" and is indistinguishable from "no such route".
- `GET /v1/apps/mcp/servers`
- `GET /v1/apps/mcp/servers` — each row reports `connected` and
`has_connection_backoff`; while a server is cooling down after a failed
connect it also carries `last_error`, that attempt's diagnosis (at most 2048
characters). `last_error` is omitted when the server is ready or connecting,
when its configuration changed since the failure, and when its reviewed
plugin authority no longer holds.
- `GET /v1/apps/mcp/tools?server=<optional>`
Skill activation toggles are persisted under a cross-process transaction lock.