feat(mcp): Connectors — catalog + global vs per-user runtimes (§7/§14/§15)
Re-architects MCP from one owner table + agent-written registration into an admin-curated catalog with two runtimes unioned per session, surfaced in the UI as "Connectors" (mcp/schema stays neutral, §0.1). Two runtimes behind one seam (§7): - Global runtime: shared, stateless connectors (web-search, Tavily…) on the HOST, connected at boot from mcp_global_servers, access-filtered per user via mcp_global_access. - Per-user runtime: a user's activated connectors run INSIDE their container, started at first login from mcp_user_servers and living until restart (§9); docker exec -i children die via kill_on_drop when the UserContext drops. - McpProvider trait (mcp/provider.rs): the session round-loop never learns which runtime owns a server. McpManager implements it directly (inert ownerless bundle); UserMcpView implements global ∪ user with an accessible_global snapshot. Both share McpManager::connect_all; McpServerSpec + global_row_spec/user_row_spec turn a DB row into a connectable spec. - mcp-client: McpServerConfig.launch_in runs a stdio command inside a container via docker exec -i (set at runtime, never parsed from config). Authorization is a capability on the role, not `if role==admin` (§0.1/§14): role_capabilities table + db/role_capabilities.rs — register_remote and register_local_from_catalog are self-service (seeded on every new role), while register_local_script and manage_catalog are admin-only. admin holds every capability by construction. This removes the agent-facing register_mcp/delete_mcp tools and the mcp kinds of list_items/toggle_item, closing the §14 RCE vector. Schema: - Registry: mcp_catalog (vetted templates — schema only, no live creds), mcp_global_servers + mcp_global_access, role_capabilities. - Owner: mcp_user_servers (per-user activations; api_key encrypted at rest, catalog_name a bare TEXT snapshot, never an owner→registry FK). - Drops the old owner table mcp_servers. API + UI: src/frontend/api/mcp.rs (admin catalog/global/access + user available/activate/activated, all capability-gated via require_cap); web/components/connectors.js (<connectors-page>) renders the user view always and the admin view for role_id === 'admin'. Deferred: interactive per-user auth (OAuth callback / QR / SSH elicitation, §15) — only none/api_key wired; no boot seed of catalog presets; per-(user, session) MCP grant model still open.
This commit is contained in:
+16
-1
@@ -128,8 +128,19 @@ pub fn router() -> Router<Arc<Skald>> {
|
||||
.route("/tool-permission-groups/{id}/duplicate", post(run_context::duplicate_group))
|
||||
// Session tool_group assignment (runtime)
|
||||
.route("/sessions/{session_id}/run-context", put(run_context::set_session_run_context))
|
||||
// MCP
|
||||
// MCP / Connectors (blueprint §14/§15)
|
||||
.route("/mcp/servers", get(mcp::list_servers))
|
||||
// admin: catalog + globally-active connectors
|
||||
.route("/mcp/catalog", get(mcp::catalog_list).post(mcp::catalog_upsert))
|
||||
.route("/mcp/catalog/{id}", delete(mcp::catalog_delete))
|
||||
.route("/mcp/global", get(mcp::global_list).post(mcp::global_enable))
|
||||
.route("/mcp/global/{id}", delete(mcp::global_delete))
|
||||
.route("/mcp/global/{id}/access", get(mcp::global_get_access).put(mcp::global_set_access))
|
||||
// user: available catalog + per-user activation
|
||||
.route("/mcp/available", get(mcp::available))
|
||||
.route("/mcp/activate", post(mcp::activate))
|
||||
.route("/mcp/activated", get(mcp::activated_list))
|
||||
.route("/mcp/activated/{id}", delete(mcp::deactivate))
|
||||
// Dev / debug
|
||||
.route("/dev/debug_mode", get(dev::get_debug_mode).post(dev::set_debug_mode).put(dev::set_debug_mode))
|
||||
.route("/dev/llm-requests", get(dev::list_llm_requests))
|
||||
@@ -179,6 +190,10 @@ impl ApiError {
|
||||
pub fn unauthorized(msg: impl Into<String>) -> Self {
|
||||
Self { status: StatusCode::UNAUTHORIZED, message: msg.into() }
|
||||
}
|
||||
|
||||
pub fn forbidden(msg: impl Into<String>) -> Self {
|
||||
Self { status: StatusCode::FORBIDDEN, message: msg.into() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves the authenticated caller's per-user runtime context, or `401` when the
|
||||
|
||||
Reference in New Issue
Block a user