tool card UI redesign: semantic icons, inline diff persistence, tool detail page, MCP-friendly titles
Nightly Build / build (push) Successful in 6m38s

This commit is contained in:
2026-07-21 23:39:41 +01:00
parent 8e891fbced
commit c11702c3d3
44 changed files with 1103 additions and 50 deletions
+39
View File
@@ -44,6 +44,10 @@ pub struct McpManager {
servers: RwLock<HashMap<String, Arc<dyn McpServerClient>>>,
errors: RwLock<HashMap<String, String>>,
descriptions: RwLock<HashMap<String, Option<String>>>,
/// Per-server manifest-declared friendly tool names (`server → tool → title`),
/// the authoritative override for a tool's UI display name (`tool_display_name`).
/// Populated from each spec's `tool_titles` at connect, forgotten on stop.
titles: RwLock<HashMap<String, HashMap<String, String>>>,
notification_tx: mpsc::UnboundedSender<McpNotification>,
/// Feeds per-server diagnostic lines (stderr, `notifications/message`,
/// lifecycle) to the `logs::log_consumer`, which writes `logs/mcp/<name>.log`.
@@ -69,6 +73,7 @@ impl McpManager {
servers: RwLock::new(HashMap::new()),
errors: RwLock::new(HashMap::new()),
descriptions: RwLock::new(HashMap::new()),
titles: RwLock::new(HashMap::new()),
notification_tx,
log_tx,
elicitation_handler: RwLock::new(None),
@@ -179,8 +184,10 @@ impl McpManager {
}
{
let mut descs = self.descriptions.write().unwrap();
let mut titles = self.titles.write().unwrap();
for spec in &specs {
descs.insert(spec.config.name.clone(), spec.description.clone());
titles.insert(spec.config.name.clone(), spec.tool_titles.clone());
}
}
if boot {
@@ -256,6 +263,7 @@ impl McpManager {
self.log_lifecycle(&name, format!("connected — {} tool(s)", tool_names.len()));
self.errors.write().unwrap().remove(&name);
self.descriptions.write().unwrap().insert(name.clone(), spec.description);
self.titles.write().unwrap().insert(name.clone(), spec.tool_titles);
self.servers.write().unwrap().insert(name, client);
Ok(tool_names)
}
@@ -266,6 +274,7 @@ impl McpManager {
self.servers.write().unwrap().remove(name);
self.errors.write().unwrap().remove(name);
self.descriptions.write().unwrap().remove(name);
self.titles.write().unwrap().remove(name);
}
/// Stops **every** running server (each dropped client → `kill_on_drop` kills
@@ -277,6 +286,7 @@ impl McpManager {
self.servers.write().unwrap().clear();
self.errors.write().unwrap().clear();
self.descriptions.write().unwrap().clear();
self.titles.write().unwrap().clear();
}
/// Whether a server by this name currently has a live connection in the
@@ -299,6 +309,18 @@ impl McpManager {
.collect()
}
/// Best friendly name for a tool for UI display: the manifest-declared override
/// (`tool_titles`) wins, else the server's live MCP `title` (2025-06-18+), else
/// `None` — the caller falls back to a prettified raw name. Cheap: an O(tools)
/// scan per call, run once per tool-call event.
pub fn tool_display_name(&self, server: &str, tool: &str) -> Option<String> {
if let Some(t) = self.titles.read().unwrap().get(server).and_then(|m| m.get(tool)) {
return Some(t.clone());
}
self.servers.read().unwrap().get(server)
.and_then(|s| s.tools().iter().find(|t| t.name == tool).and_then(|t| t.title.clone()))
}
pub fn server_descriptions(&self) -> HashMap<String, Option<String>> {
self.descriptions.read().unwrap().clone()
}
@@ -407,6 +429,11 @@ impl McpManager {
pub struct McpServerSpec {
pub config: McpServerConfig,
pub description: Option<String>,
/// Manifest-declared friendly tool names (`tool name → display title`), the
/// authoritative override for a connector's UI display names. Empty for globals
/// and for per-user rows with no catalog `tool_meta_json`; the runtime then
/// falls back to the server's live MCP `title` and finally a prettified name.
pub tool_titles: HashMap<String, String>,
}
fn transport_of(s: &str) -> McpTransport {
@@ -532,6 +559,7 @@ pub fn global_row_spec(row: &crate::db::mcp_global_servers::McpGlobalServerRow)
launch_in: None,
},
description: row.description.clone(),
tool_titles: HashMap::new(),
}
}
@@ -616,6 +644,9 @@ pub fn user_row_spec(
// A per-user connector's description falls back to its catalog name; the
// catalog's friendly description can be injected by the caller if richer.
description: row.catalog_name.clone(),
// Manifest tool titles are loaded from the catalog by `user_row_spec_resolved`,
// which has registry access; the bare sync builder leaves them empty.
tool_titles: HashMap::new(),
}
}
@@ -632,6 +663,14 @@ pub async fn user_row_spec_resolved(
registry: &SqlitePool,
) -> McpServerSpec {
let mut spec = user_row_spec(row, container);
// Manifest-declared friendly tool names (UI card titles): snapshot the catalog's
// `tool_meta_json` for this activation so `tool_display_name` can override the raw
// name. Best-effort — a missing/failed lookup just leaves the live `title` path.
if let Some(catalog_name) = row.catalog_name.as_deref() {
if let Ok(Some(entry)) = crate::db::mcp_catalog::get_by_name(registry, catalog_name).await {
spec.tool_titles = crate::db::mcp_catalog::parse_tool_titles(entry.tool_meta_json.as_deref());
}
}
if let (Some(provider), Some(deliver), Some(refresh)) =
(row.oauth_provider.as_deref(), row.deliver(), row.api_key.as_deref())
{
+15
View File
@@ -25,6 +25,10 @@ pub trait McpProvider: Send + Sync {
fn tools_for(&self, names: &[String]) -> Vec<McpTool>;
fn server_descriptions(&self) -> HashMap<String, Option<String>>;
fn server_infos(&self) -> Vec<Value>;
/// Best friendly name for a `server`/`tool` pair for the chat card (manifest
/// override > live MCP `title` > `None`, the caller then prettifies the raw
/// name). Routed to whichever runtime owns the server.
fn tool_display_name(&self, server: &str, tool: &str) -> Option<String>;
async fn call(&self, server: &str, tool: &str, args: Value) -> Result<ToolResult>;
}
@@ -34,6 +38,9 @@ impl McpProvider for McpManager {
fn tools_for(&self, names: &[String]) -> Vec<McpTool> { McpManager::tools_for(self, names) }
fn server_descriptions(&self) -> HashMap<String, Option<String>> { McpManager::server_descriptions(self) }
fn server_infos(&self) -> Vec<Value> { McpManager::server_infos(self) }
fn tool_display_name(&self, server: &str, tool: &str) -> Option<String> {
McpManager::tool_display_name(self, server, tool)
}
async fn call(&self, server: &str, tool: &str, args: Value) -> Result<ToolResult> {
McpManager::call(self, server, tool, args).await
}
@@ -97,6 +104,14 @@ impl McpProvider for UserMcpView {
v
}
fn tool_display_name(&self, server: &str, tool: &str) -> Option<String> {
if self.accessible_global.contains(server) {
self.global.tool_display_name(server, tool)
} else {
self.user.tool_display_name(server, tool)
}
}
async fn call(&self, server: &str, tool: &str, args: Value) -> Result<ToolResult> {
if self.accessible_global.contains(server) {
self.global.call(server, tool, args).await