mcp: live-refresh global connector access without restart; add MCP list to kid agent
Nightly Build / build (push) Successful in 6m36s
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:
@@ -32,7 +32,7 @@ pub mod verify;
|
||||
|
||||
pub use install::{CONNECTORS_DIR, MANIFEST_FILE, connector_dir, ensure_installed_host, install_into_home, split_script_path};
|
||||
pub use oauth::DeliverSpec;
|
||||
pub use provider::{McpProvider, UserMcpView};
|
||||
pub use provider::{McpProvider, SharedGlobalAccess, UserMcpView};
|
||||
pub use verify::{VerifyReport, VerifyTarget, apply_placeholders, run_verify};
|
||||
|
||||
const SERVER_START_TIMEOUT_SECS: u64 = 120;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
//! the union.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
@@ -46,20 +46,43 @@ impl McpProvider for McpManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// The set of global-connector names one user may see, behind a swappable cell so
|
||||
/// an admin enabling/granting a global connector refreshes every live session in
|
||||
/// place (the MCP twin of `SharedFs` for fs membership) — no restart needed. The
|
||||
/// inner `Arc<HashSet>` lets a reader hold a cheap snapshot while a writer swaps.
|
||||
#[derive(Clone)]
|
||||
pub struct SharedGlobalAccess(Arc<RwLock<Arc<HashSet<String>>>>);
|
||||
|
||||
impl SharedGlobalAccess {
|
||||
pub fn new(names: HashSet<String>) -> Self {
|
||||
Self(Arc::new(RwLock::new(Arc::new(names))))
|
||||
}
|
||||
/// Cheap snapshot of the current set (clones an `Arc`, not the set).
|
||||
pub fn load(&self) -> Arc<HashSet<String>> {
|
||||
Arc::clone(&self.0.read().expect("SharedGlobalAccess lock poisoned"))
|
||||
}
|
||||
/// Replace the set in place — every `UserMcpView` sharing this cell sees it.
|
||||
pub fn store(&self, names: HashSet<String>) {
|
||||
*self.0.write().expect("SharedGlobalAccess lock poisoned") = Arc::new(names);
|
||||
}
|
||||
}
|
||||
|
||||
/// One logged-in user's MCP view: the access-filtered global runtime unioned with
|
||||
/// their per-user container runtime. A per-user server wins on a name collision
|
||||
/// (which activation prevents anyway — see the uniqueness check at activation).
|
||||
pub struct UserMcpView {
|
||||
pub global: Arc<McpManager>,
|
||||
pub user: Arc<McpManager>,
|
||||
/// Names of the global servers this user may use — a snapshot of
|
||||
/// `mcp_global_access`, captured when the user's context is built.
|
||||
pub accessible_global: HashSet<String>,
|
||||
/// Names of the global servers this user may use — read from `mcp_global_access`
|
||||
/// when the user's context is built, then held in a swappable cell so an admin
|
||||
/// enabling/granting a global connector refreshes it in place (§7 — the MCP twin
|
||||
/// of the §6 fs remount) rather than settling only at the next restart.
|
||||
pub accessible_global: SharedGlobalAccess,
|
||||
}
|
||||
|
||||
impl UserMcpView {
|
||||
fn accessible_names(&self) -> Vec<String> {
|
||||
self.accessible_global.iter().cloned().collect()
|
||||
self.accessible_global.load().iter().cloned().collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,8 +98,9 @@ impl McpProvider for UserMcpView {
|
||||
// A granted name belongs to exactly one runtime (unique per user); route
|
||||
// the accessible-global ones to the global runtime and the rest to the
|
||||
// per-user one, which filters to its own server map.
|
||||
let accessible = self.accessible_global.load();
|
||||
let global_names: Vec<String> = names.iter()
|
||||
.filter(|n| self.accessible_global.contains(*n))
|
||||
.filter(|n| accessible.contains(*n))
|
||||
.cloned()
|
||||
.collect();
|
||||
let mut out = self.global.tools_for(&global_names);
|
||||
@@ -85,19 +109,21 @@ impl McpProvider for UserMcpView {
|
||||
}
|
||||
|
||||
fn server_descriptions(&self) -> HashMap<String, Option<String>> {
|
||||
let accessible = self.accessible_global.load();
|
||||
let mut m: HashMap<String, Option<String>> = self.global.server_descriptions()
|
||||
.into_iter()
|
||||
.filter(|(name, _)| self.accessible_global.contains(name))
|
||||
.filter(|(name, _)| accessible.contains(name))
|
||||
.collect();
|
||||
m.extend(self.user.server_descriptions());
|
||||
m
|
||||
}
|
||||
|
||||
fn server_infos(&self) -> Vec<Value> {
|
||||
let accessible = self.accessible_global.load();
|
||||
let mut v: Vec<Value> = self.global.server_infos()
|
||||
.into_iter()
|
||||
.filter(|info| info["name"].as_str()
|
||||
.map(|n| self.accessible_global.contains(n))
|
||||
.map(|n| accessible.contains(n))
|
||||
.unwrap_or(false))
|
||||
.collect();
|
||||
v.extend(self.user.server_infos());
|
||||
@@ -105,7 +131,7 @@ impl McpProvider for UserMcpView {
|
||||
}
|
||||
|
||||
fn tool_display_name(&self, server: &str, tool: &str) -> Option<String> {
|
||||
if self.accessible_global.contains(server) {
|
||||
if self.accessible_global.load().contains(server) {
|
||||
self.global.tool_display_name(server, tool)
|
||||
} else {
|
||||
self.user.tool_display_name(server, tool)
|
||||
@@ -113,7 +139,7 @@ impl McpProvider for UserMcpView {
|
||||
}
|
||||
|
||||
async fn call(&self, server: &str, tool: &str, args: Value) -> Result<ToolResult> {
|
||||
if self.accessible_global.contains(server) {
|
||||
if self.accessible_global.load().contains(server) {
|
||||
self.global.call(server, tool, args).await
|
||||
} else {
|
||||
// A per-user server, or an unknown/forbidden one — the per-user
|
||||
|
||||
Reference in New Issue
Block a user