From edf4697caa12d997b37a7af5df92121f65ceb46d Mon Sep 17 00:00:00 2001 From: MaximEdogawa Date: Sat, 11 Apr 2026 22:15:00 +0200 Subject: [PATCH] refactor: update terminology from "server" to "tool" in MCP components - Changed references in `AddServerForm`, `McpServerCard`, and `McpToolsPanel` to use "tool" instead of "server" for consistency. - Updated error messages and UI labels to reflect the new terminology. - Enhanced comments and documentation to clarify the purpose of the components related to tools. - Introduced a new module for resolving executable paths for subprocesses, improving command handling in the MCP context. --- .../src/infrastructure/executable_resolve.rs | 114 ++++++++++++++++++ src-tauri/src/infrastructure/mod.rs | 1 + src-tauri/src/modules/mcp/client.rs | 1 + src-tauri/src/modules/mcp/registry.rs | 35 +++--- src-tauri/src/modules/mcp/service.rs | 89 +++++++++----- src-tauri/src/modules/mcp/transport.rs | 14 ++- src-tauri/src/modules/mcp/types.rs | 4 +- src-tauri/src/modules/tool_engine/runtime.rs | 49 +------- src/modules/mcp/components/AddServerForm.tsx | 28 ++--- src/modules/mcp/components/McpServerCard.tsx | 8 +- src/modules/mcp/components/McpToolsPanel.tsx | 24 ++-- src/pages/DashboardPage.tsx | 2 +- 12 files changed, 238 insertions(+), 131 deletions(-) create mode 100644 src-tauri/src/infrastructure/executable_resolve.rs diff --git a/src-tauri/src/infrastructure/executable_resolve.rs b/src-tauri/src/infrastructure/executable_resolve.rs new file mode 100644 index 0000000..9e58ddf --- /dev/null +++ b/src-tauri/src/infrastructure/executable_resolve.rs @@ -0,0 +1,114 @@ +//! Resolve CLI executable paths for subprocess spawn. +//! +//! Tauri GUI apps on macOS (and some Linux desktop sessions) start with a minimal `PATH` that +//! omits Homebrew and other common install locations. [`resolve_command_for_spawn`] expands bare +//! names like `podman` / `docker` to an absolute path when possible so MCP stdio servers can start. + +use std::path::{Path, PathBuf}; + +fn push_candidate(out: &mut Vec, p: PathBuf) { + if p.as_os_str().is_empty() { + return; + } + if !out.iter().any(|x| x == &p) { + out.push(p); + } +} + +/// Ordered list of paths to try for a CLI basename (e.g. `podman`, `docker`). +pub fn runtime_binary_candidates(name: &str) -> Vec { + let mut out = Vec::new(); + push_candidate(&mut out, PathBuf::from(name)); + + if let Ok(path_var) = std::env::var("PATH") { + let sep = if cfg!(windows) { ';' } else { ':' }; + for dir in path_var.split(sep) { + if dir.is_empty() { + continue; + } + push_candidate(&mut out, Path::new(dir).join(name)); + } + } + + #[cfg(target_os = "macos")] + { + push_candidate(&mut out, PathBuf::from(format!("/opt/homebrew/bin/{name}"))); + push_candidate(&mut out, PathBuf::from(format!("/usr/local/bin/{name}"))); + push_candidate(&mut out, PathBuf::from(format!("/opt/podman/bin/{name}"))); + } + + #[cfg(target_os = "linux")] + { + push_candidate(&mut out, PathBuf::from(format!("/usr/bin/{name}"))); + push_candidate(&mut out, PathBuf::from(format!("/bin/{name}"))); + } + + if let Ok(home) = std::env::var("HOME") { + push_candidate(&mut out, Path::new(&home).join(".local/bin").join(name)); + } + + out +} + +#[cfg(unix)] +fn is_executable_file(path: &Path) -> bool { + use std::fs; + use std::os::unix::fs::PermissionsExt; + match fs::metadata(path) { + Ok(m) => m.is_file() && m.permissions().mode() & 0o111 != 0, + Err(_) => false, + } +} + +#[cfg(windows)] +fn is_executable_file(path: &Path) -> bool { + path.is_file() +} + +/// If `command` is a bare executable name, return the first candidate that exists and looks +/// runnable; otherwise return `command` unchanged (absolute paths, relative paths with dirs, etc.). +pub fn resolve_command_for_spawn(command: &str) -> PathBuf { + let c = command.trim(); + if c.is_empty() { + return PathBuf::from(c); + } + + let path = Path::new(c); + if path.is_absolute() || c.contains(std::path::MAIN_SEPARATOR) { + return path.to_path_buf(); + } + + for candidate in runtime_binary_candidates(c) { + if candidate.as_os_str().is_empty() { + continue; + } + // Skip bare `docker` / `podman`: `Path::is_file()` would mean CWD, not PATH (GUI apps + // often lack PATH entries, so we only want absolute / PATH-derived candidates here). + if !candidate.is_absolute() && candidate.parent().is_none() { + continue; + } + if candidate.is_file() && is_executable_file(&candidate) { + if candidate != Path::new(c) { + log::debug!("resolved MCP stdio command `{c}` → {}", candidate.display()); + } + return candidate; + } + } + + PathBuf::from(c) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn resolve_keeps_absolute_paths() { + let p = if cfg!(windows) { + r"C:\Program Files\Docker\Docker\resources\bin\docker.exe" + } else { + "/opt/homebrew/bin/podman" + }; + assert_eq!(resolve_command_for_spawn(p), PathBuf::from(p)); + } +} diff --git a/src-tauri/src/infrastructure/mod.rs b/src-tauri/src/infrastructure/mod.rs index c058170..1d1c8d5 100644 --- a/src-tauri/src/infrastructure/mod.rs +++ b/src-tauri/src/infrastructure/mod.rs @@ -1,2 +1,3 @@ pub mod bot_lifecycle; +pub mod executable_resolve; pub mod http_server; diff --git a/src-tauri/src/modules/mcp/client.rs b/src-tauri/src/modules/mcp/client.rs index e2b20e0..90245d9 100644 --- a/src-tauri/src/modules/mcp/client.rs +++ b/src-tauri/src/modules/mcp/client.rs @@ -94,6 +94,7 @@ fn parse_tools(server_name: &str, result: &Value) -> Vec { .map(|s| s.to_string()), input_schema: t .get("inputSchema") + .or_else(|| t.get("input_schema")) .cloned() .unwrap_or_else(|| json!({"type": "object"})), direct_return: false, diff --git a/src-tauri/src/modules/mcp/registry.rs b/src-tauri/src/modules/mcp/registry.rs index e5102bc..d2febea 100644 --- a/src-tauri/src/modules/mcp/registry.rs +++ b/src-tauri/src/modules/mcp/registry.rs @@ -143,12 +143,17 @@ impl Default for ToolRegistry { } impl ToolRegistry { + /// Number of MCP tools connected (one per `servers` entry in `mcp.json`: `dice`, `te_*`, …). + pub fn mcp_tool_count(&self) -> usize { + self.providers.len() + } + pub fn new(providers: Vec) -> Self { let cached_ollama_tools = build_ollama_tools(&providers); let cached_tool_names = providers .iter() .flat_map(|p| p.tools().iter()) - .filter(|t| should_expose_to_model(t)) + .filter(|t| !is_deprecated_mcp_tool(t)) .map(|t| t.name.clone()) .collect(); Self { @@ -162,7 +167,7 @@ impl ToolRegistry { self.providers .iter() .flat_map(|p| p.tools().iter()) - .filter(|t| should_expose_to_model(t)) + .filter(|t| !is_deprecated_mcp_tool(t)) .cloned() .collect() } @@ -171,10 +176,12 @@ impl ToolRegistry { self.cached_ollama_tools.clone() } + /// Names of commands offered to the model (flattened across all MCP tools). pub fn tool_names(&self) -> &[String] { &self.cached_tool_names } + /// `true` when there is no command to expose (e.g. nothing connected yet). pub fn is_empty(&self) -> bool { self.cached_tool_names.is_empty() } @@ -235,28 +242,20 @@ impl ToolRegistry { } } -fn should_expose_to_model(tool: &ToolDef) -> bool { - let desc = tool.description.as_deref().unwrap_or(""); - if desc.to_ascii_uppercase().contains("DEPRECATED") { - return false; - } - !REDUNDANT_TOOLS.contains(&tool.name.as_str()) +/// Hide tools the server marks as deprecated (e.g. filesystem `read_file` → use `read_text_file`). +fn is_deprecated_mcp_tool(tool: &ToolDef) -> bool { + tool.description + .as_deref() + .unwrap_or("") + .to_ascii_uppercase() + .contains("DEPRECATED") } -/// Tools that add noise without value for a small local model. -const REDUNDANT_TOOLS: &[&str] = &[ - "read_media_file", - "read_multiple_files", - "list_directory_with_sizes", - "directory_tree", - "list_allowed_directories", -]; - fn build_ollama_tools(providers: &[Provider]) -> Value { let arr: Vec = providers .iter() .flat_map(|p| p.tools().iter()) - .filter(|t| should_expose_to_model(t)) + .filter(|t| !is_deprecated_mcp_tool(t)) .map(|t| { json!({ "type": "function", diff --git a/src-tauri/src/modules/mcp/service.rs b/src-tauri/src/modules/mcp/service.rs index 0bce43d..97afd8e 100644 --- a/src-tauri/src/modules/mcp/service.rs +++ b/src-tauri/src/modules/mcp/service.rs @@ -10,32 +10,54 @@ use std::sync::Arc; const FILESYSTEM_SERVER_KEY: &str = "filesystem"; -/// Prefer project `mcp.json` under `src-tauri/` (or crate-root `mcp.json`) by walking up from -/// [`std::env::current_exe`], so resolution does not depend on process CWD. Falls back to -/// `mcp.json` next to `connection.json` in app data. +fn app_data_mcp_path(store_path: &Path) -> PathBuf { + store_path + .parent() + .map(|p| p.join("mcp.json")) + .unwrap_or_else(|| PathBuf::from("mcp.json")) +} + +/// If set, absolute or relative path to `mcp.json` (overrides all other resolution). +const MCP_CONFIG_ENV: &str = "PENGINE_MCP_CONFIG"; + +/// Resolve the active `mcp.json` path. +/// +/// - Optional override: [`MCP_CONFIG_ENV`] → use that file. +/// - **Release builds** (packaged native app): always `$APP_DATA/mcp.json` next to +/// `connection.json`, so Tool Engine installs and workspace folders persist regardless of where +/// the `.app` bundle lives (e.g. `/Applications` vs still under a source tree). +/// - **Debug builds**: walk up from [`std::env::current_exe`] to find crate-root or +/// `…/src-tauri/mcp.json` for local development, then fall back to app data. pub fn resolve_mcp_config_path(store_path: &Path) -> (PathBuf, &'static str) { - if let Ok(exe) = std::env::current_exe() { - let mut dir = exe.parent().map(Path::to_path_buf); - for _ in 0..16 { - let Some(ref d) = dir else { - break; - }; - let from_repo_root = d.join("src-tauri").join("mcp.json"); - if from_repo_root.exists() { - return (from_repo_root, "project"); - } - let in_crate_root = d.join("mcp.json"); - if d.join("Cargo.toml").exists() && in_crate_root.exists() { - return (in_crate_root, "project"); + if let Ok(raw) = std::env::var(MCP_CONFIG_ENV) { + let t = raw.trim(); + if !t.is_empty() { + return (PathBuf::from(t), "env"); + } + } + + #[cfg(debug_assertions)] + { + if let Ok(exe) = std::env::current_exe() { + let mut dir = exe.parent().map(Path::to_path_buf); + for _ in 0..16 { + let Some(ref d) = dir else { + break; + }; + let from_repo_root = d.join("src-tauri").join("mcp.json"); + if from_repo_root.exists() { + return (from_repo_root, "project"); + } + let in_crate_root = d.join("mcp.json"); + if d.join("Cargo.toml").exists() && in_crate_root.exists() { + return (in_crate_root, "project"); + } + dir = d.parent().map(Path::to_path_buf); } - dir = d.parent().map(Path::to_path_buf); } } - let app_path = store_path - .parent() - .map(|p| p.join("mcp.json")) - .unwrap_or_else(|| PathBuf::from("mcp.json")); + let app_path = app_data_mcp_path(store_path); (app_path, "app_data") } @@ -135,10 +157,8 @@ pub async fn connect_one_server( ServerEntry::Native { id } => match native::native_for(server_key, id) { Ok(p) => { let n = p.tools.len(); - let msg = format!( - "{server_key} native ({n} tool{})", - if n == 1 { "" } else { "s" } - ); + let cmd_word = if n == 1 { "command" } else { "commands" }; + let msg = format!("{server_key} native ({n} {cmd_word})"); (Some(Provider::Native(Arc::new(p))), msg) } Err(e) => (None, format!("{server_key} native failed: {e}")), @@ -159,12 +179,9 @@ pub async fn connect_one_server( { Ok(client) => { let n = client.tools.len(); + let cmd_word = if n == 1 { "command" } else { "commands" }; let dr = if *direct_return { " direct_return" } else { "" }; - let msg = format!( - "{server_key} stdio ({n} tool{}{})", - if n == 1 { "" } else { "s" }, - dr - ); + let msg = format!("{server_key} stdio ({n} {cmd_word}{dr})"); (Some(Provider::Mcp(Arc::new(client))), msg) } Err(e) => (None, format!("{server_key} stdio failed: {e}")), @@ -255,7 +272,7 @@ pub async fn rebuild_registry_into_state( *state.mcp.write().await = ToolRegistry::new(providers.clone()); } - let n = state.mcp.read().await.tool_names().len(); + let n = state.mcp.read().await.mcp_tool_count(); state .emit_log( "mcp", @@ -306,4 +323,14 @@ mod tests { assert_eq!(cfg.workspace_roots, vec!["/a", "/b"]); assert!(!cfg.servers.contains_key("filesystem")); } + + /// Release binaries always use `mcp.json` next to `connection.json` (no exe walk). + #[cfg(not(debug_assertions))] + #[test] + fn resolve_mcp_config_release_uses_app_data_adjacent_to_store() { + let store = PathBuf::from("/tmp/pengine-fake-app/connection.json"); + let (path, src) = resolve_mcp_config_path(&store); + assert_eq!(src, "app_data"); + assert_eq!(path, PathBuf::from("/tmp/pengine-fake-app/mcp.json")); + } } diff --git a/src-tauri/src/modules/mcp/transport.rs b/src-tauri/src/modules/mcp/transport.rs index ccb42a2..937a115 100644 --- a/src-tauri/src/modules/mcp/transport.rs +++ b/src-tauri/src/modules/mcp/transport.rs @@ -1,4 +1,5 @@ use super::protocol::{JsonRpcRequest, JsonRpcResponse}; +use crate::infrastructure::executable_resolve; use serde_json::Value; use std::collections::HashMap; use std::process::Stdio; @@ -25,7 +26,8 @@ impl StdioTransport { args: &[String], env: &HashMap, ) -> Result { - let mut cmd = Command::new(command); + let resolved = executable_resolve::resolve_command_for_spawn(command); + let mut cmd = Command::new(&resolved); cmd.args(args) .envs(env) .stdin(Stdio::piped()) @@ -33,9 +35,13 @@ impl StdioTransport { .stderr(Stdio::piped()) .kill_on_drop(true); - let mut child = cmd - .spawn() - .map_err(|e| format!("spawn `{command}` failed: {e}"))?; + let mut child = cmd.spawn().map_err(|e| { + format!( + "spawn `{}` (resolved as `{}`) failed: {e}", + command, + resolved.display() + ) + })?; let stdin = child.stdin.take().ok_or("no stdin")?; let stdout = child.stdout.take().ok_or("no stdout")?; diff --git a/src-tauri/src/modules/mcp/types.rs b/src-tauri/src/modules/mcp/types.rs index 819172c..af7d9ba 100644 --- a/src-tauri/src/modules/mcp/types.rs +++ b/src-tauri/src/modules/mcp/types.rs @@ -1,7 +1,9 @@ use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, HashMap}; -/// Root config: `src-tauri/mcp.json` in dev or `mcp.json` next to app data (`connection.json`). +/// Root config: `$APP_DATA/mcp.json` next to `connection.json` for release/native builds; debug +/// builds may use crate `mcp.json` (see `service::resolve_mcp_config_path`). Override with +/// `PENGINE_MCP_CONFIG`. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct McpConfig { /// Host folders shared with the File Manager container (`/app/`). Replaces legacy diff --git a/src-tauri/src/modules/tool_engine/runtime.rs b/src-tauri/src/modules/tool_engine/runtime.rs index c1bc1ff..2351b61 100644 --- a/src-tauri/src/modules/tool_engine/runtime.rs +++ b/src-tauri/src/modules/tool_engine/runtime.rs @@ -1,6 +1,7 @@ use super::types::RuntimeKind; +use crate::infrastructure::executable_resolve; use serde::Serialize; -use std::path::{Path, PathBuf}; +use std::path::Path; #[derive(Debug, Clone, Serialize)] pub struct RuntimeInfo { @@ -21,52 +22,8 @@ pub async fn detect_runtime() -> Option { try_runtime("docker", RuntimeKind::Docker).await } -fn push_candidate(out: &mut Vec, p: PathBuf) { - if p.as_os_str().is_empty() { - return; - } - if !out.iter().any(|x| x == &p) { - out.push(p); - } -} - -/// Ordered list of paths to try for `podman` / `docker`. -fn runtime_binary_candidates(name: &str) -> Vec { - let mut out = Vec::new(); - push_candidate(&mut out, PathBuf::from(name)); - - if let Ok(path_var) = std::env::var("PATH") { - let sep = if cfg!(windows) { ';' } else { ':' }; - for dir in path_var.split(sep) { - if dir.is_empty() { - continue; - } - push_candidate(&mut out, Path::new(dir).join(name)); - } - } - - #[cfg(target_os = "macos")] - { - push_candidate(&mut out, PathBuf::from(format!("/opt/homebrew/bin/{name}"))); - push_candidate(&mut out, PathBuf::from(format!("/usr/local/bin/{name}"))); - push_candidate(&mut out, PathBuf::from(format!("/opt/podman/bin/{name}"))); - } - - #[cfg(target_os = "linux")] - { - push_candidate(&mut out, PathBuf::from(format!("/usr/bin/{name}"))); - push_candidate(&mut out, PathBuf::from(format!("/bin/{name}"))); - } - - if let Ok(home) = std::env::var("HOME") { - push_candidate(&mut out, Path::new(&home).join(".local/bin").join(name)); - } - - out -} - async fn try_runtime(binary_name: &str, kind: RuntimeKind) -> Option { - for path in runtime_binary_candidates(binary_name) { + for path in executable_resolve::runtime_binary_candidates(binary_name) { if let Some(info) = try_runtime_at(&path, kind).await { return Some(info); } diff --git a/src/modules/mcp/components/AddServerForm.tsx b/src/modules/mcp/components/AddServerForm.tsx index 2a61dbc..d63528a 100644 --- a/src/modules/mcp/components/AddServerForm.tsx +++ b/src/modules/mcp/components/AddServerForm.tsx @@ -59,7 +59,7 @@ export function AddServerForm({ busy, onAdd }: Props) { if ("type" in obj && (obj.type === "stdio" || obj.type === "native")) { // Direct entry — need a name from the input if (!pasteName.trim()) { - setError("Enter a server name (the JSON has no key wrapper)"); + setError("Enter a tool name (the JSON has no key wrapper)"); return; } name = pasteName.trim(); @@ -68,13 +68,13 @@ export function AddServerForm({ busy, onAdd }: Props) { // Wrapped: { "server-name": { ... } } const keys = Object.keys(obj); if (keys.length !== 1) { - setError('Expected either a server entry or { "name": { ...entry } }'); + setError('Expected either a tool entry or { "name": { ...entry } }'); return; } name = keys[0]; const inner = obj[name]; if (typeof inner !== "object" || inner === null || Array.isArray(inner)) { - setError(`Value for "${name}" is not a valid server entry`); + setError(`Value for "${name}" is not a valid tool entry`); return; } entry = normalizeEntry(inner as Record); @@ -85,7 +85,7 @@ export function AddServerForm({ busy, onAdd }: Props) { reset(); setOpen(false); } catch (e) { - setError(e instanceof Error ? e.message : "Could not add server"); + setError(e instanceof Error ? e.message : "Could not add tool"); } }; @@ -93,7 +93,7 @@ export function AddServerForm({ busy, onAdd }: Props) { setError(null); const name = formName.trim(); if (!name) { - setError("Server name is required"); + setError("Tool name is required"); return; } if (!command.trim()) { @@ -122,7 +122,7 @@ export function AddServerForm({ busy, onAdd }: Props) { reset(); setOpen(false); } catch (e) { - setError(e instanceof Error ? e.message : "Could not add server"); + setError(e instanceof Error ? e.message : "Could not add tool"); } }; @@ -136,7 +136,7 @@ export function AddServerForm({ busy, onAdd }: Props) { onClick={() => setOpen(true)} className="mt-3 w-full rounded-xl border border-dashed border-white/15 px-3 py-3 text-center font-mono text-xs text-(--mid) transition hover:border-white/30 hover:text-white" > - + Add server + + Add tool ); } @@ -144,7 +144,7 @@ export function AddServerForm({ busy, onAdd }: Props) { return (
-

Add server

+

Add tool

)} @@ -217,13 +217,13 @@ export function AddServerForm({ busy, onAdd }: Props) {
setFormName(e.target.value)} - placeholder="my-server" + placeholder="my-tool" className={inputClass} />
@@ -278,7 +278,7 @@ export function AddServerForm({ busy, onAdd }: Props) { onClick={handleFormSubmit} className="rounded-lg border border-white/20 bg-white/10 px-3 py-2 text-xs font-medium text-white hover:bg-white/15 disabled:opacity-40 md:justify-self-start md:px-4" > - Add server + Add tool
)} diff --git a/src/modules/mcp/components/McpServerCard.tsx b/src/modules/mcp/components/McpServerCard.tsx index 35fd862..2887e61 100644 --- a/src/modules/mcp/components/McpServerCard.tsx +++ b/src/modules/mcp/components/McpServerCard.tsx @@ -61,7 +61,7 @@ export function McpServerCard({ await onDelete(name); setConfirmDelete(false); } catch (e) { - setDeleteError(e instanceof Error ? e.message : "Could not remove server"); + setDeleteError(e instanceof Error ? e.message : "Could not remove tool"); } }; @@ -105,7 +105,7 @@ export function McpServerCard({ {commandPreview}

- {toolCount} tool{toolCount === 1 ? "" : "s"} + {toolCount} command{toolCount === 1 ? "" : "s"}

@@ -141,8 +141,8 @@ export function McpServerCard({ setDeleteError(null); setConfirmDelete(true); }} - aria-label={`Delete server ${name}`} - title={`Delete server ${name}`} + aria-label={`Delete tool ${name}`} + title={`Delete tool ${name}`} className="rounded-lg border border-rose-300/20 bg-transparent px-2 py-1 font-mono text-[10px] uppercase tracking-wider text-rose-300/70 hover:bg-rose-300/10 hover:text-rose-200 disabled:opacity-40" > del diff --git a/src/modules/mcp/components/McpToolsPanel.tsx b/src/modules/mcp/components/McpToolsPanel.tsx index 20cafc1..2cf4519 100644 --- a/src/modules/mcp/components/McpToolsPanel.tsx +++ b/src/modules/mcp/components/McpToolsPanel.tsx @@ -13,7 +13,7 @@ import { AddServerForm } from "./AddServerForm"; import { McpServerCard } from "./McpServerCard"; /** - * Dashboard panel: filesystem shortcut, server list with CRUD, and tool groups. + * Dashboard panel: MCP tools (config entries), CRUD, and commands grouped per tool. */ export function McpToolsPanel() { const [tools, setTools] = useState(null); @@ -48,7 +48,7 @@ export function McpToolsPanel() { setServers(s); setServersError(null); } else { - setServersError("Could not load MCP servers"); + setServersError("Could not load MCP tools"); } } @@ -58,7 +58,7 @@ export function McpToolsPanel() { setTools(t); setToolsError(null); } else { - setToolsError("Could not load MCP tools"); + setToolsError("Could not load MCP commands"); } const next = t !== null && t.length > 0 ? 10_000 : 30_000; scheduleToolsPollRef.current(next); @@ -87,7 +87,7 @@ export function McpToolsPanel() { setTools(data); setToolsError(null); } else { - setToolsError("Could not load MCP tools"); + setToolsError("Could not load MCP commands"); } const next = data !== null && data.length > 0 ? 10_000 : 30_000; schedulePoll(next); @@ -104,7 +104,7 @@ export function McpToolsPanel() { setServers(s); setServersError(null); } else { - setServersError("Could not load MCP servers"); + setServersError("Could not load MCP tools"); } }; @@ -141,7 +141,7 @@ export function McpToolsPanel() { setEditingName(null); await reload(); setBusy(false); - setNotice(`Server "${name}" saved — tools reloaded`); + setNotice(`Tool "${name}" saved — commands reloaded`); return true; }; @@ -162,7 +162,7 @@ export function McpToolsPanel() { } await reload(); setBusy(false); - setNotice(`Server "${name}" removed`); + setNotice(`Tool "${name}" removed`); }; // ── Derived data ─────────────────────────────────────────────────── @@ -199,9 +199,9 @@ export function McpToolsPanel() { )}
- {/* ── Servers ─────────────────────────────────────────────── */} + {/* ── MCP tools (mcp.json server entries) ─────────────────── */}
-

Servers

+

Tools

{serversError && servers !== null && (

@@ -245,9 +245,9 @@ export function McpToolsPanel() {

- {/* ── Available tools ─────────────────────────────────────── */} + {/* ── Commands exposed by each tool ───────────────────────── */}
-

Available tools

+

Commands

{toolsError && tools !== null && (

@@ -268,7 +268,7 @@ export function McpToolsPanel() { )} {groups !== null && groups.length === 0 && ( -

No MCP tools connected.

+

No MCP commands available.

)} {groups !== null && groups.length > 0 && ( diff --git a/src/pages/DashboardPage.tsx b/src/pages/DashboardPage.tsx index 74b8a99..c368e2d 100644 --- a/src/pages/DashboardPage.tsx +++ b/src/pages/DashboardPage.tsx @@ -208,7 +208,7 @@ export function DashboardPage() { - {/* ── Servers & tools ─────────────────────────────────────── */} + {/* ── MCP tools & commands ────────────────────────────────── */}