mcp: live-refresh global connector access without restart; add MCP list to kid agent
Nightly Build / build (push) Successful in 6m36s

A user's session sees global MCP connectors through UserMcpView, filtered by
accessible_global — a snapshot of mcp_global_access taken when the user's
UserContext is built at login. That context is cached until restart, so an admin
enabling/deleting a global connector or changing its access set was invisible in
MCP_LIST (and in the tool surface) until the whole process restarted.

Make accessible_global a swappable cell (SharedGlobalAccess, the MCP twin of
SharedFs for §6 fs remount): UserContext::refresh_global_access re-reads the
registry and stores it in place, and Skald::refresh_global_mcp_access broadcasts
that to every live context. Wire it into global_enable, global_delete,
global_set_access and user_connectors_set so a grant/enable is reflected in
running sessions immediately.

Also add the shared common/mcp.md include (the <!-- MCP_LIST --> sentinel) to the
kid agent, aligning it with the other agents.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 00:22:35 +01:00
co-authored by Claude Opus 4.8
parent c11702c3d3
commit 5d5c3ff2ff
6 changed files with 107 additions and 16 deletions
+15 -3
View File
@@ -499,7 +499,11 @@ pub async fn global_enable(
let row = mcp_global_servers::get(skald.db(), id).await?
.ok_or_else(|| ApiError::bad_request("global server vanished after upsert"))?;
let spec = skald_core::mcp::global_row_spec(&row);
match skald.mcp().start_server(spec).await {
let started = skald.mcp().start_server(spec).await;
// Now that the server runs in the global runtime, refresh every live user's
// access snapshot so the connector shows up in `MCP_LIST` without a restart.
skald.refresh_global_mcp_access().await;
match started {
Ok(tools) => Ok(Json(json!({ "id": id, "tools": tools, "verify": verify }))),
Err(e) => Ok(Json(json!({ "id": id, "error": e.to_string(), "verify": verify }))),
}
@@ -515,6 +519,8 @@ pub async fn global_delete(
skald.mcp().stop_server(&row.name);
}
mcp_global_servers::delete(skald.db(), id).await?;
// Drop the now-deleted server from every live user's access snapshot in place.
skald.refresh_global_mcp_access().await;
Ok(Json(json!({ "ok": true })))
}
@@ -633,6 +639,8 @@ pub async fn global_set_access(
) -> Result<Json<Value>, ApiError> {
require_cap(&skald, &auth.user_id, role_capabilities::MANAGE_CATALOG).await?;
mcp_global_access::set_access(skald.db(), id, &body.user_ids).await?;
// Reflect the new grant set in every live user's access snapshot at once.
skald.refresh_global_mcp_access().await;
Ok(Json(json!({ "ok": true })))
}
@@ -716,8 +724,8 @@ pub async fn user_connectors_set(
skald_core::db::users::get(skald.db(), &target).await?
.ok_or_else(|| ApiError::not_found("no such user"))?;
// Globals settle at the target user's next login (their `accessible_global`
// snapshot is captured then) — same as the existing per-server access flow.
// Apply the target user's global grants; the live-access snapshot is refreshed
// in place below (the §7 MCP remount), so a grant/revoke shows without a restart.
mcp_global_access::set_for_user(skald.db(), &target, &body.global_ids).await?;
// Catalog: apply the grant set; `set_for_user` returns the names this revoked.
@@ -738,6 +746,10 @@ pub async fn user_connectors_set(
}
}
}
// Refresh live access snapshots so the target user's global grants take effect
// without a restart (the catalog side is handled by the live-revoke block above).
skald.refresh_global_mcp_access().await;
Ok(Json(json!({ "ok": true })))
}