diff --git a/nanobot/webui/mcp_presets_api.py b/nanobot/webui/mcp_presets_api.py index 24221a4d7..753f4c5d1 100644 --- a/nanobot/webui/mcp_presets_api.py +++ b/nanobot/webui/mcp_presets_api.py @@ -56,7 +56,6 @@ _MCP_ATTACHMENT_KEYS = ( "status", "configured", ) -_MAX_TEST_TOOLS = 16 _DEFAULT_TEST_TIMEOUT = 20 _DEFAULT_CUSTOM_TIMEOUT = 30 _CUSTOM_ACTIONS = {"custom", "import", "import-cursor", "tools"} @@ -1097,7 +1096,7 @@ async def mcp_presets_test_action( *, config_path: Path | None = None, ) -> dict[str, Any]: - """Connect to an enabled MCP preset and report its tool surface.""" + """Connect to an enabled MCP preset and report its complete tool surface.""" from nanobot.agent.tools.mcp import connect_mcp_servers name = (_query_first(query, "name") or "").strip() @@ -1157,9 +1156,10 @@ async def mcp_presets_test_action( registry = ToolRegistry() stacks: dict[str, Any] = {} + inspection_cfg = cfg.model_copy(update={"enabled_tools": ["*"]}) try: stacks = await asyncio.wait_for( - connect_mcp_servers({name: cfg}, registry), + connect_mcp_servers({name: inspection_cfg}, registry), timeout=_test_timeout(cfg), ) tool_prefix = f"mcp_{name}_" @@ -1178,7 +1178,7 @@ async def mcp_presets_test_action( else f"{display_name} connected, but reported no tools." ), "tool_count": len(tool_names), - "tool_names": tool_names[:_MAX_TEST_TOOLS], + "tool_names": tool_names, "checked_at": _checked_at(), } else: diff --git a/tests/webui/test_mcp_presets_api.py b/tests/webui/test_mcp_presets_api.py index 91b89bf19..f75d68cbe 100644 --- a/tests/webui/test_mcp_presets_api.py +++ b/tests/webui/test_mcp_presets_api.py @@ -10,7 +10,7 @@ from mcp.shared.auth import OAuthToken from nanobot.agent.plugins import AGENT_PLUGIN_MCP_SCHEMA, AGENT_PLUGIN_SCHEMA from nanobot.agent.tools.mcp_oauth import MCPOAuthStorage, mcp_oauth_has_credentials -from nanobot.config.loader import load_config +from nanobot.config.loader import load_config, save_config from nanobot.webui.mcp_presets_api import ( McpPresetError, custom_mcp_action, @@ -454,6 +454,46 @@ def test_test_mcp_preset_connects_and_reports_tools( assert payload["last_action"]["tool_names"] == ["mcp_playwright_browser_navigate"] +def test_test_mcp_preset_inspects_tools_outside_the_enabled_allowlist( + tmp_path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + _use_config(tmp_path, monkeypatch) + mcp_presets_action("enable", {"name": ["playwright"]}) + config = load_config() + config.tools.mcp_servers["playwright"].enabled_tools = [ + "mcp_playwright_browser_navigate", + ] + save_config(config) + + class FakeStack: + async def aclose(self) -> None: + return None + + async def fake_connect(servers, registry): + assert servers["playwright"].enabled_tools == ["*"] + + class FakeTool: + def __init__(self, name: str) -> None: + self.name = name + + def to_schema(self): + return {"name": self.name, "description": "", "parameters": {}} + + for index in range(20): + registry.register(FakeTool(f"mcp_playwright_tool_{index:02d}")) + return {"playwright": FakeStack()} + + monkeypatch.setattr("nanobot.agent.tools.mcp.connect_mcp_servers", fake_connect) + + payload = asyncio.run(mcp_presets_test_action({"name": ["playwright"]})) + + assert payload["last_action"]["tool_count"] == 20 + assert len(payload["last_action"]["tool_names"]) == 20 + row = next(item for item in payload["presets"] if item["name"] == "playwright") + assert row["enabled_tools"] == ["mcp_playwright_browser_navigate"] + + def test_test_mcp_preset_scrubs_connection_errors( tmp_path, monkeypatch: pytest.MonkeyPatch, diff --git a/webui/src/components/settings/system/AppsSettings.tsx b/webui/src/components/settings/system/AppsSettings.tsx index 18d34ad1e..e5000d6de 100644 --- a/webui/src/components/settings/system/AppsSettings.tsx +++ b/webui/src/components/settings/system/AppsSettings.tsx @@ -1,6 +1,5 @@ import { forwardRef, - useEffect, useId, useMemo, useState, @@ -17,7 +16,6 @@ import { Database, ExternalLink, Loader2, - MoreHorizontal, PauseCircle, PlayCircle, Plus, @@ -36,6 +34,10 @@ import { SETTINGS_SEARCH_INPUT_CLASS, SettingsSectionTitle, } from "@/components/settings/shared/SettingsControls"; +import { + McpManagementDialog, + type McpManagementTab, +} from "@/components/settings/system/McpManagementDialog"; import { Button } from "@/components/ui/button"; import { DropdownMenu, @@ -521,8 +523,8 @@ function McpAppsCatalogRow({ }) { const { t } = useTranslation(); const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback }); - const [setupOpen, setSetupOpen] = useState(false); - const [toolsOpen, setToolsOpen] = useState(false); + const [managementOpen, setManagementOpen] = useState(false); + const [managementTab, setManagementTab] = useState("overview"); const enableBusy = actionKey === `enable:${preset.name}`; const disableBusy = actionKey === `disable:${preset.name}`; const removeBusy = actionKey === `remove:${preset.name}`; @@ -546,18 +548,14 @@ function McpAppsCatalogRow({ const statusLabel = toggleable ? tx("settings.nanobotFeatures.enabled", "Enabled") : runtimeConnected - ? tx("settings.mcp.connected", "Connected.") + ? tx("connection.open", "Connected") : mcpPresetStatusLabel(preset.status, tx); const failureLabel = tx("settings.mcp.connectionFailed", "Connection failed."); - const reconnectLabel = tx("settings.mcp.reconnect", "Reconnect"); - const canEnable = - preset.install_supported && - (missingFields.length === 0 || missingFields.every((field) => Boolean(values[field.name]?.trim()))); - const toolNames = preset.tool_names ?? []; - const enabledTools = preset.enabled_tools ?? ["*"]; - const allowAllTools = enabledTools.includes("*"); - const enabledSet = new Set(allowAllTools ? toolNames : enabledTools); - const description = preset.description || preset.note || preset.name; + const failureStatusLabel = failureLabel.replace(/[.!。!]+$/u, ""); + const description = tx( + `settings.mcp.presetDescriptions.${preset.name}`, + preset.description || preset.note || preset.name, + ); const detail = agentPlugin && preset.requires ? `${description} · ${preset.requires}` : description || preset.requires; @@ -567,32 +565,21 @@ function McpAppsCatalogRow({ const callbackHelpId = `${callbackInputId}-help`; const callbackErrorId = `${callbackInputId}-error`; - useEffect(() => { - if (preset.configured || !preset.install_supported) setSetupOpen(false); - }, [preset.configured, preset.install_supported]); - const enableOrOpenSetup = () => { if (isOAuth) { onOAuthConnect(preset.name); return; } if (needsSetupInput || (preset.installed && !preset.configured && hasFields)) { - setSetupOpen(true); + setManagementTab("connection"); + setManagementOpen(true); return; } onAction("enable", preset.name, values); }; - const submitSetup = () => { - if (!canEnable) return; - onAction("enable", preset.name, values); - }; - const setTools = (next: string[]) => onToolsChange(preset.name, next); - const toggleTool = (toolName: string) => { - const next = new Set(allowAllTools ? toolNames : enabledTools); - if (next.has(toolName)) next.delete(toolName); - else next.add(toolName); - const nextValues = Array.from(next); - setTools(nextValues.length === toolNames.length ? ["*"] : nextValues); + const openManagement = (tab: McpManagementTab = "overview") => { + setManagementTab(tab); + setManagementOpen(true); }; return ( @@ -660,112 +647,56 @@ function McpAppsCatalogRow({ ) : runtimeFailed && configuredInstalled ? ( - <> - { - if (isOAuth) onOAuthConnect(preset.name, true); - else onAction("reconnect", preset.name); - }} - > - - - - - - - - - - onAction("test", preset.name)}> - - {tx("settings.mcp.test", "Test")} - - {toolNames.length ? ( - setToolsOpen((open) => !open)}> - - {tx("settings.mcp.toolScope", "Tools")} - - ) : null} - onAction("remove", preset.name)} - > - - {tx("settings.mcp.remove", "Remove")} - - - - + openManagement("connection")} + > + + ) : readyInstalled ? ( - <> + toggleable ? ( - {toggleable || runtimeConnected ? ( - - ) : ( - - )} + - {!toggleable ? ( - onAction("test", preset.name)}> - - {tx("settings.mcp.test", "Test")} - - ) : null} - {!toggleable && toolNames.length ? ( - setToolsOpen((open) => !open)}> - - {tx("settings.mcp.toolScope", "Tools")} - - ) : null} onAction(toggleable ? "disable" : "remove", preset.name)} + onClick={() => onAction("disable", preset.name)} > - {toggleable ? : } - {toggleable - ? tx("settings.nanobotFeatures.disable", "Disable") - : tx("settings.mcp.remove", "Remove")} + + {tx("settings.nanobotFeatures.disable", "Disable")} - {!toggleable ? ( - onAction("remove", preset.name)} - > - - - ) : null} - + ) : ( + openManagement("overview")} + > + + + ) ) : preset.enabled === false ? ( { - if (hasFields) setSetupOpen(true); + if (hasFields) openManagement("connection"); else onAction("enable", preset.name, values); }} /> @@ -920,128 +851,22 @@ function McpAppsCatalogRow({ ) : null} - {setupOpen && preset.install_supported && hasFields ? ( -
-
-
-
- {t("settings.mcp.connectTitle", { - name: preset.display_name, - defaultValue: "Connect {{name}}", - })} -
-

- {tx("settings.mcp.connectHint", "Add the key from your account settings.")} -

-
- -
-
- {preset.required_fields.map((field) => ( - - ))} -
-
- -
-
- ) : null} - - {toolsOpen && configuredInstalled && toolNames.length ? ( -
-
-
- {tx("settings.mcp.toolScope", "Tools")} -
-
- - -
-
-
- {toolNames.map((toolName) => { - const selected = enabledSet.has(toolName); - return ( - - ); - })} -
-
+ {managementOpen ? ( + } + onTabChange={setManagementTab} + onOpenChange={setManagementOpen} + onFieldChange={onFieldChange} + onAction={onAction} + onOAuthConnect={onOAuthConnect} + onToolsChange={onToolsChange} + /> ) : null} ); @@ -1502,7 +1327,15 @@ function mcpPresetStatusLabel( } } -function McpPresetLogo({ preset, showBrandLogos }: { preset: McpPresetInfo; showBrandLogos: boolean }) { +function McpPresetLogo({ + preset, + showBrandLogos, + compact = false, +}: { + preset: McpPresetInfo; + showBrandLogos: boolean; + compact?: boolean; +}) { const bg = preset.brand_color || "hsl(var(--muted))"; const logoUrls = useMemo(() => logoFallbackUrls(preset.logo_url), [preset.logo_url]); const { logoUrl, onLogoError, onLogoLoad } = useLogoFallback(logoUrls); @@ -1517,14 +1350,17 @@ function McpPresetLogo({ preset, showBrandLogos }: { preset: McpPresetInfo; show if ((showBrandLogos || packagedLogo) && logoUrl) { return ( @@ -1533,7 +1369,12 @@ function McpPresetLogo({ preset, showBrandLogos }: { preset: McpPresetInfo; show } return ( {initials} diff --git a/webui/src/components/settings/system/McpManagementDialog.tsx b/webui/src/components/settings/system/McpManagementDialog.tsx new file mode 100644 index 000000000..ee8b12bdd --- /dev/null +++ b/webui/src/components/settings/system/McpManagementDialog.tsx @@ -0,0 +1,701 @@ +import { useEffect, useMemo, useRef, useState, type ReactNode } from "react"; +import { + Check, + ExternalLink, + Loader2, + RotateCcw, + Search, + Server, + SlidersHorizontal, + Trash2, + X, +} from "lucide-react"; +import { useTranslation } from "react-i18next"; + +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { SegmentedControl } from "@/components/ui/segmented-control"; +import type { McpPresetInfo } from "@/lib/types"; +import { cn } from "@/lib/utils"; + +export type McpManagementTab = "overview" | "tools" | "connection"; + +type McpAction = "enable" | "disable" | "remove" | "test" | "reconnect"; + +interface McpManagementDialogProps { + preset: McpPresetInfo; + values: Record; + actionKey: string | null; + statusLabel: string; + statusTone: "success" | "warning" | "neutral"; + tab: McpManagementTab; + icon: ReactNode; + onTabChange: (tab: McpManagementTab) => void; + onOpenChange: (open: boolean) => void; + onFieldChange: (presetName: string, fieldName: string, value: string) => void; + onAction: (action: McpAction, name: string, values?: Record) => void; + onOAuthConnect: (name: string, reset?: boolean) => void; + onToolsChange: (name: string, enabledTools: string[]) => void; +} + +export function McpManagementDialog({ + preset, + values, + actionKey, + statusLabel, + statusTone, + tab, + icon, + onTabChange, + onOpenChange, + onFieldChange, + onAction, + onOAuthConnect, + onToolsChange, +}: McpManagementDialogProps) { + const { t } = useTranslation(); + const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback }); + const inventoryRef = useRef(preset.tool_names ?? []); + if (preset.tool_names?.length) inventoryRef.current = preset.tool_names; + const toolNames = inventoryRef.current; + const [toolQuery, setToolQuery] = useState(""); + const [draftEnabledTools, setDraftEnabledTools] = useState( + preset.enabled_tools ?? ["*"], + ); + const testBusy = actionKey === `test:${preset.name}`; + const reconnectBusy = actionKey === `reconnect:${preset.name}`; + const removeBusy = actionKey === `remove:${preset.name}`; + const enableBusy = actionKey === `enable:${preset.name}`; + const toolsBusy = actionKey === `tools:${preset.name}`; + const oauthBusy = actionKey === `oauth:${preset.name}`; + const busy = testBusy || reconnectBusy || removeBusy || enableBusy || toolsBusy || oauthBusy; + const configuredInstalled = preset.installed && preset.configured; + const isOAuth = preset.auth === "oauth"; + const inspectionRequestedRef = useRef(false); + const allowAllTools = draftEnabledTools.includes("*"); + const selectedTools = new Set(allowAllTools ? toolNames : draftEnabledTools); + const normalizedQuery = toolQuery.trim().toLowerCase(); + const filteredTools = useMemo( + () => toolNames.filter((name) => name.toLowerCase().includes(normalizedQuery)), + [normalizedQuery, toolNames], + ); + const knownToolCount = preset.tool_count ?? toolNames.length; + const selectedToolCount = allowAllTools ? knownToolCount : selectedTools.size; + const requiredFieldsComplete = preset.required_fields + .filter((field) => field.required && !field.configured) + .every((field) => Boolean(values[field.name]?.trim())); + const initialTools = normalizeTools(preset.enabled_tools ?? ["*"]); + const draftTools = normalizeTools(draftEnabledTools); + const description = tx( + `settings.mcp.presetDescriptions.${preset.name}`, + preset.description || preset.note || preset.name, + ); + const toolsDirty = initialTools.join("\n") !== draftTools.join("\n"); + const tabs: Array<{ value: McpManagementTab; label: ReactNode }> = [ + { value: "overview", label: tx("settings.mcp.overviewTab", "Overview") }, + { + value: "tools", + label: tx("settings.mcp.toolScope", "Tools"), + }, + { value: "connection", label: tx("settings.mcp.connectionTab", "Connection") }, + ]; + const activePanelLabel = tab === "overview" + ? tx("settings.mcp.overviewTab", "Overview") + : tab === "tools" + ? tx("settings.mcp.toolScope", "Tools") + : tx("settings.mcp.connectionTab", "Connection"); + + const toggleTool = (toolName: string) => { + const next = new Set(allowAllTools ? toolNames : draftEnabledTools); + if (next.has(toolName)) next.delete(toolName); + else next.add(toolName); + setDraftEnabledTools(next.size === toolNames.length ? ["*"] : Array.from(next)); + }; + + const connect = () => { + onOpenChange(false); + if (isOAuth) { + onOAuthConnect(preset.name, configuredInstalled); + return; + } + onAction(configuredInstalled ? "reconnect" : "enable", preset.name, values); + }; + + const inspectTools = () => { + inspectionRequestedRef.current = true; + onAction("test", preset.name); + }; + + useEffect(() => { + if ( + tab !== "tools" || + !configuredInstalled || + toolNames.length || + busy || + preset.error || + inspectionRequestedRef.current + ) { + return; + } + inspectionRequestedRef.current = true; + onAction("test", preset.name); + }, [busy, configuredInstalled, onAction, preset.error, preset.name, tab, toolNames.length]); + + return ( + + +
+
{icon}
+
+ + {preset.display_name} + + + {description} + +
+
+ {statusLabel} + +
+
+ +
+ +
+ +
+ {tab === "overview" ? ( + + ) : tab === "tools" ? ( + setDraftEnabledTools(["*"])} + onClear={() => setDraftEnabledTools([])} + onTest={inspectTools} + onOpenConnection={() => onTabChange("connection")} + /> + ) : ( + { + onOpenChange(false); + onAction("remove", preset.name); + }} + /> + )} +
+ + {tab === "tools" && toolsDirty ? ( +
+ +
+ ) : null} +
+
+ ); +} + +function OverviewPanel({ + preset, + description, + statusLabel, + knownToolCount, + selectedToolCount, +}: { + preset: McpPresetInfo; + description: string; + statusLabel: string; + knownToolCount: number; + selectedToolCount: number; +}) { + const { t } = useTranslation(); + const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback }); + const previewTools = (preset.tool_names ?? []).slice(0, 4); + return ( +
+
+

+ {tx("settings.mcp.about", "About this MCP")} +

+

+ {description} +

+ {preset.docs_url ? ( + + {tx("settings.mcp.openDocs", "Open docs")} + + + ) : null} +
+ +
+ + + {knownToolCount ? ( + + ) : null} +
+ + {previewTools.length ? ( +
+

+ {tx("settings.mcp.toolPreview", "Tools")} +

+
+ {previewTools.map((tool) => ( + + {displayToolName(tool, preset.name)} + + ))} + {knownToolCount > previewTools.length ? ( + + +{knownToolCount - previewTools.length} + + ) : null} +
+
+ ) : null} +
+ ); +} + +function ToolsPanel({ + preset, + toolNames, + filteredTools, + query, + selectedTools, + selectedToolCount, + toolsBusy, + testBusy, + configuredInstalled, + onQueryChange, + onToggleTool, + onSelectAll, + onClear, + onTest, + onOpenConnection, +}: { + preset: McpPresetInfo; + toolNames: string[]; + filteredTools: string[]; + query: string; + selectedTools: Set; + selectedToolCount: number; + toolsBusy: boolean; + testBusy: boolean; + configuredInstalled: boolean; + onQueryChange: (value: string) => void; + onToggleTool: (name: string) => void; + onSelectAll: () => void; + onClear: () => void; + onTest: () => void; + onOpenConnection: () => void; +}) { + const { t } = useTranslation(); + const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback }); + if (!toolNames.length) { + return ( +
+
+
+ {testBusy ? ( + + ) : ( + + )} +
+

+ {testBusy + ? tx("common.loading", "Loading…") + : preset.error || tx("settings.mcp.noToolsAvailable", "No tools available")} +

+
+ {!testBusy ? ( + + ) : null} +
+ ); + } + + return ( +
+
+ +
+ + {tx("settings.mcp.selectedCount", "{{count}} selected").replace("{{count}}", String(selectedToolCount))} + + + +
+
+ +
+ {filteredTools.length ? filteredTools.map((toolName) => { + const selected = selectedTools.has(toolName); + return ( + + ); + }) : ( +
+ {tx("settings.mcp.noMatchingTools", "No tools match this search.")} +
+ )} +
+
+ ); +} + +function ConnectionPanel({ + preset, + values, + busy, + connectBusy, + removeBusy, + requiredFieldsComplete, + onFieldChange, + onConnect, + onRemove, +}: { + preset: McpPresetInfo; + values: Record; + busy: boolean; + connectBusy: boolean; + removeBusy: boolean; + requiredFieldsComplete: boolean; + onFieldChange: (presetName: string, fieldName: string, value: string) => void; + onConnect: () => void; + onRemove: () => void; +}) { + const { t } = useTranslation(); + const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback }); + const configuredInstalled = preset.installed && preset.configured; + const hasFields = preset.required_fields.length > 0; + const authentication = preset.auth === "oauth" + ? "OAuth" + : hasFields + ? tx("settings.mcp.credentials", "Credentials") + : tx("settings.mcp.none", "None"); + const connectionLabel = preset.transport === "stdio" + ? tx("settings.mcp.command", "Command") + : tx("settings.mcp.endpointLabel", "Endpoint"); + const connectLabel = configuredInstalled + ? tx("settings.mcp.reconnect", "Reconnect") + : tx("settings.mcp.setup", "Connect"); + return ( +
+
+

+ {tx("settings.mcp.connectionDetails", "Connection details")} +

+
+ {preset.connection_summary ? ( +
+

{connectionLabel}

+ + {preset.connection_summary} + +
+ ) : null} +
+ {formatTransport(preset.transport)} + · + {authentication} +
+ {preset.docs_url ? ( + + {tx("settings.mcp.openDocs", "Open docs")} + + + ) : null} + {configuredInstalled && preset.install_supported ? ( + + ) : null} +
+
+
+
+ + {preset.error ? ( +
+ {preset.error} +
+ ) : null} + + {hasFields ? ( +
+

+ {tx("settings.mcp.credentials", "Credentials")} +

+
+ {preset.required_fields.map((field) => { + const inputId = `mcp-manage-${preset.name}-${field.name}`; + return ( + + ); + })} +
+
+ ) : null} + + {!configuredInstalled && preset.install_supported ? ( +
+ +
+ ) : null} + + {preset.installed && preset.enabled === undefined ? ( +
+ +
+ ) : null} +
+ ); +} + +function MetricCard({ label, value }: { label: string; value: string }) { + return ( +
+
{label}
+
{value}
+
+ ); +} + +function StatusPill({ children, tone }: { children: ReactNode; tone: "success" | "warning" | "neutral" }) { + return ( + + + {children} + + ); +} + +function normalizeTools(tools: string[]): string[] { + return tools.includes("*") ? ["*"] : [...tools].sort((left, right) => left.localeCompare(right)); +} + +function displayToolName(toolName: string, serverName: string): string { + const wrappedPrefix = `mcp_${serverName}_`; + return toolName.startsWith(wrappedPrefix) ? toolName.slice(wrappedPrefix.length) : toolName; +} + +function formatTransport(transport: string): string { + if (transport === "streamableHttp") return "Streamable HTTP"; + if (transport === "stdio") return "STDIO"; + if (transport === "sse") return "SSE"; + return transport || "MCP"; +} diff --git a/webui/src/i18n/locales/en/common.json b/webui/src/i18n/locales/en/common.json index bd874b282..60a0dce5e 100644 --- a/webui/src/i18n/locales/en/common.json +++ b/webui/src/i18n/locales/en/common.json @@ -384,10 +384,49 @@ "statusComingSoon": "Coming soon", "comingSoon": "Coming soon", "statusNotInstalled": "Not enabled", + "manage": "Manage", + "manageTitle": "Manage {{name}}", + "fixConnection": "Fix connection", + "overviewTab": "Overview", + "connectionTab": "Connection", + "manageTabs": "MCP management sections", + "about": "About", + "statusLabel": "Status", + "transportLabel": "Transport", + "endpointLabel": "Endpoint", + "toolPreview": "Tools", + "noToolsAvailable": "No tools available", + "searchTools": "Search tools", + "searchToolsPlaceholder": "Search tools", + "selectedCount": "{{count}} enabled", + "noMatchingTools": "No tools match this search.", + "applyChanges": "Save changes", + "connectionDetails": "Connection details", + "credentials": "Credentials", + "none": "None", + "dangerZone": "Remove connection", "toolScope": "Tools", - "allTools": "All", - "noTools": "None", - "testForTools": "Run Test to inspect and choose individual tools." + "allTools": "Enable all", + "noTools": "Disable all", + "reloadTools": "Reload tools", + "presetDescriptions": { + "browserbase": "Automate cloud browsers with Browserbase.", + "playwright": "Inspect and automate local browsers with Playwright.", + "context7": "Fetch current library docs and code examples.", + "firecrawl": "Scrape, crawl, search, and extract web content.", + "parallel-search": "Search the web and read relevant pages.", + "exa": "Search the web and extract clean page content.", + "microsoft-learn": "Search and read Microsoft Learn documentation.", + "aws-docs": "Search AWS documentation and service guidance.", + "brave-search": "Search the web, news, images, videos, and local results with Brave Search.", + "postman": "Inspect and manage Postman APIs, collections, and workspaces.", + "figma": "Read Figma design context through the local Dev Mode MCP.", + "xmind": "Create, read, and edit Xmind cloud mind maps.", + "notion": "Read and update your Notion workspace.", + "linear": "Find and manage Linear issues, projects, and comments.", + "github": "Manage GitHub repositories, issues, and pull requests.", + "supabase": "Inspect and manage Supabase projects." + } }, "values": { "light": "Light", diff --git a/webui/src/i18n/locales/es/common.json b/webui/src/i18n/locales/es/common.json index 449472581..c673d1573 100644 --- a/webui/src/i18n/locales/es/common.json +++ b/webui/src/i18n/locales/es/common.json @@ -569,10 +569,49 @@ "statusComingSoon": "Próximamente", "comingSoon": "Próximamente", "statusNotInstalled": "No habilitado", + "manage": "Administrar", + "manageTitle": "Administrar {{name}}", + "fixConnection": "Corregir conexión", + "overviewTab": "Resumen", + "connectionTab": "Conexión", + "manageTabs": "Administración de MCP", + "about": "Información", + "statusLabel": "Estado", + "transportLabel": "Transporte", + "endpointLabel": "URL", + "toolPreview": "Herramientas", + "noToolsAvailable": "No hay herramientas disponibles", + "searchTools": "Buscar herramientas", + "searchToolsPlaceholder": "Buscar herramientas", + "selectedCount": "{{count}} activadas", + "noMatchingTools": "No hay herramientas coincidentes", + "applyChanges": "Guardar cambios", + "connectionDetails": "Detalles de conexión", + "credentials": "Credenciales", + "none": "Ninguna", + "dangerZone": "Eliminar conexión", "toolScope": "Herramientas", - "allTools": "Todas", - "noTools": "Ninguna", - "testForTools": "Ejecuta Probar para inspeccionar y elegir herramientas individuales." + "allTools": "Activar todas", + "noTools": "Desactivar todas", + "reloadTools": "Recargar herramientas", + "presetDescriptions": { + "browserbase": "Automatiza navegadores en la nube con Browserbase.", + "playwright": "Inspecciona y automatiza navegadores locales con Playwright.", + "context7": "Obtén documentación actual y ejemplos de código.", + "firecrawl": "Rastrea, busca y extrae contenido web.", + "parallel-search": "Busca en la web y lee páginas relevantes.", + "exa": "Busca en la web y extrae contenido limpio.", + "microsoft-learn": "Busca y lee documentación de Microsoft Learn.", + "aws-docs": "Busca documentación y guías de servicios de AWS.", + "brave-search": "Busca web, noticias, imágenes, vídeos y resultados locales con Brave Search.", + "postman": "Consulta y administra API, colecciones y espacios de trabajo de Postman.", + "figma": "Lee el contexto de diseño de Figma mediante el MCP local de Dev Mode.", + "xmind": "Crea, lee y edita mapas mentales de Xmind en la nube.", + "notion": "Lee y actualiza tu espacio de trabajo de Notion.", + "linear": "Busca y administra incidencias, proyectos y comentarios de Linear.", + "github": "Administra repositorios, incidencias y pull requests de GitHub.", + "supabase": "Consulta y administra proyectos de Supabase." + } }, "api": { "title": "Servidor API", "openaiCompatible": "API compatible con OpenAI", diff --git a/webui/src/i18n/locales/fr/common.json b/webui/src/i18n/locales/fr/common.json index 844431460..05c18899c 100644 --- a/webui/src/i18n/locales/fr/common.json +++ b/webui/src/i18n/locales/fr/common.json @@ -569,10 +569,49 @@ "statusComingSoon": "Bientôt disponible", "comingSoon": "Bientôt disponible", "statusNotInstalled": "Non activé", + "manage": "Gérer", + "manageTitle": "Gérer {{name}}", + "fixConnection": "Réparer la connexion", + "overviewTab": "Aperçu", + "connectionTab": "Connexion", + "manageTabs": "Gestion MCP", + "about": "À propos", + "statusLabel": "État", + "transportLabel": "Transport", + "endpointLabel": "URL", + "toolPreview": "Outils", + "noToolsAvailable": "Aucun outil disponible", + "searchTools": "Rechercher des outils", + "searchToolsPlaceholder": "Rechercher des outils", + "selectedCount": "{{count}} activés", + "noMatchingTools": "Aucun outil correspondant", + "applyChanges": "Enregistrer", + "connectionDetails": "Détails de connexion", + "credentials": "Identifiants", + "none": "Aucun", + "dangerZone": "Supprimer la connexion", "toolScope": "Outils", - "allTools": "Tous", - "noTools": "Aucun", - "testForTools": "Exécutez Tester pour inspecter et choisir des outils individuels." + "allTools": "Tout activer", + "noTools": "Tout désactiver", + "reloadTools": "Recharger les outils", + "presetDescriptions": { + "browserbase": "Automatisez des navigateurs cloud avec Browserbase.", + "playwright": "Inspectez et automatisez les navigateurs locaux avec Playwright.", + "context7": "Obtenez la documentation à jour et des exemples de code.", + "firecrawl": "Explorez, recherchez et extrayez du contenu web.", + "parallel-search": "Recherchez sur le web et lisez les pages pertinentes.", + "exa": "Recherchez sur le web et extrayez du contenu propre.", + "microsoft-learn": "Recherchez et consultez la documentation Microsoft Learn.", + "aws-docs": "Recherchez la documentation et les guides de services AWS.", + "brave-search": "Recherchez le web, les actualités, les images, les vidéos et les résultats locaux avec Brave Search.", + "postman": "Consultez et gérez les API, collections et espaces de travail Postman.", + "figma": "Lisez le contexte de conception Figma via le MCP Dev Mode local.", + "xmind": "Créez, consultez et modifiez des cartes mentales Xmind dans le cloud.", + "notion": "Consultez et mettez à jour votre espace de travail Notion.", + "linear": "Recherchez et gérez les tickets, projets et commentaires Linear.", + "github": "Gérez les dépôts, tickets et pull requests GitHub.", + "supabase": "Consultez et gérez les projets Supabase." + } }, "api": { "title": "Serveur API", "openaiCompatible": "API compatible OpenAI", "description": "Connectez des SDK et agents via un endpoint /v1 local.", diff --git a/webui/src/i18n/locales/id/common.json b/webui/src/i18n/locales/id/common.json index bca59a019..84a019a6b 100644 --- a/webui/src/i18n/locales/id/common.json +++ b/webui/src/i18n/locales/id/common.json @@ -569,10 +569,49 @@ "statusComingSoon": "Segera hadir", "comingSoon": "Segera hadir", "statusNotInstalled": "Tidak aktif", + "manage": "Kelola", + "manageTitle": "Kelola {{name}}", + "fixConnection": "Perbaiki koneksi", + "overviewTab": "Ringkasan", + "connectionTab": "Koneksi", + "manageTabs": "Pengelolaan MCP", + "about": "Tentang", + "statusLabel": "Status", + "transportLabel": "Transport", + "endpointLabel": "Endpoint", + "toolPreview": "Alat", + "noToolsAvailable": "Tidak ada alat yang tersedia", + "searchTools": "Cari alat", + "searchToolsPlaceholder": "Cari alat", + "selectedCount": "{{count}} aktif", + "noMatchingTools": "Tidak ada alat yang cocok", + "applyChanges": "Simpan perubahan", + "connectionDetails": "Detail koneksi", + "credentials": "Kredensial", + "none": "Tidak ada", + "dangerZone": "Hapus koneksi", "toolScope": "Alat", - "allTools": "Semua", - "noTools": "Tidak ada", - "testForTools": "Jalankan Uji untuk memeriksa dan memilih alat individual." + "allTools": "Aktifkan semua", + "noTools": "Nonaktifkan semua", + "reloadTools": "Muat ulang alat", + "presetDescriptions": { + "browserbase": "Otomatiskan browser cloud dengan Browserbase.", + "playwright": "Periksa dan otomatiskan browser lokal dengan Playwright.", + "context7": "Ambil dokumentasi pustaka terbaru dan contoh kode.", + "firecrawl": "Jelajahi, cari, dan ekstrak konten web.", + "parallel-search": "Cari di web dan baca halaman yang relevan.", + "exa": "Cari di web dan ekstrak konten halaman yang bersih.", + "microsoft-learn": "Cari dan baca dokumentasi Microsoft Learn.", + "aws-docs": "Cari dokumentasi dan panduan layanan AWS.", + "brave-search": "Cari web, berita, gambar, video, dan hasil lokal dengan Brave Search.", + "postman": "Periksa dan kelola API, koleksi, dan ruang kerja Postman.", + "figma": "Baca konteks desain Figma melalui MCP Dev Mode lokal.", + "xmind": "Buat, baca, dan edit peta pikiran cloud Xmind.", + "notion": "Baca dan perbarui ruang kerja Notion Anda.", + "linear": "Cari dan kelola isu, proyek, dan komentar Linear.", + "github": "Kelola repositori, isu, dan pull request GitHub.", + "supabase": "Periksa dan kelola proyek Supabase." + } }, "api": { "title": "Server API", "openaiCompatible": "API kompatibel OpenAI", "description": "Hubungkan SDK dan agen melalui endpoint /v1 lokal.", diff --git a/webui/src/i18n/locales/ja/common.json b/webui/src/i18n/locales/ja/common.json index b5331e574..4693ebe99 100644 --- a/webui/src/i18n/locales/ja/common.json +++ b/webui/src/i18n/locales/ja/common.json @@ -569,10 +569,49 @@ "statusComingSoon": "近日公開", "comingSoon": "近日公開", "statusNotInstalled": "未有効", + "manage": "管理", + "manageTitle": "{{name}} を管理", + "fixConnection": "接続を修復", + "overviewTab": "概要", + "connectionTab": "接続", + "manageTabs": "MCP 管理", + "about": "概要", + "statusLabel": "状態", + "transportLabel": "転送方式", + "endpointLabel": "エンドポイント", + "toolPreview": "ツール", + "noToolsAvailable": "利用可能なツールはありません", + "searchTools": "ツールを検索", + "searchToolsPlaceholder": "ツールを検索", + "selectedCount": "{{count}} 件有効", + "noMatchingTools": "一致するツールはありません", + "applyChanges": "変更を保存", + "connectionDetails": "接続情報", + "credentials": "認証情報", + "none": "なし", + "dangerZone": "接続を削除", "toolScope": "ツール", - "allTools": "すべて", - "noTools": "なし", - "testForTools": "テストを実行して個別のツールを確認・選択します。" + "allTools": "すべて有効化", + "noTools": "すべて無効化", + "reloadTools": "ツールを再読込", + "presetDescriptions": { + "browserbase": "Browserbase でクラウドブラウザーを自動操作します。", + "playwright": "Playwright でローカルブラウザーを検査・自動操作します。", + "context7": "最新のライブラリ資料とコード例を取得します。", + "firecrawl": "Web コンテンツをクロール、検索、抽出します。", + "parallel-search": "Web を検索して関連ページを読み込みます。", + "exa": "Web を検索して整形済みのページ内容を抽出します。", + "microsoft-learn": "Microsoft Learn の資料を検索・閲覧します。", + "aws-docs": "AWS の資料とサービスガイドを検索します。", + "brave-search": "Brave Search で Web、ニュース、画像、動画、ローカル情報を検索します。", + "postman": "Postman の API、コレクション、ワークスペースを確認・管理します。", + "figma": "ローカルの Dev Mode MCP から Figma のデザイン情報を読み込みます。", + "xmind": "Xmind のクラウドマインドマップを作成、閲覧、編集します。", + "notion": "Notion ワークスペースを閲覧・更新します。", + "linear": "Linear の課題、プロジェクト、コメントを検索・管理します。", + "github": "GitHub のリポジトリ、Issue、Pull Request を管理します。", + "supabase": "Supabase プロジェクトを確認・管理します。" + } }, "api": { "title": "API サーバー", "openaiCompatible": "OpenAI 互換 API", "description": "ローカルの /v1 エンドポイントから SDK やエージェントを接続します。", diff --git a/webui/src/i18n/locales/ko/common.json b/webui/src/i18n/locales/ko/common.json index 6ea9523ae..3b93b924f 100644 --- a/webui/src/i18n/locales/ko/common.json +++ b/webui/src/i18n/locales/ko/common.json @@ -569,10 +569,49 @@ "statusComingSoon": "곧 제공", "comingSoon": "곧 제공", "statusNotInstalled": "비활성", + "manage": "관리", + "manageTitle": "{{name}} 관리", + "fixConnection": "연결 복구", + "overviewTab": "개요", + "connectionTab": "연결", + "manageTabs": "MCP 관리", + "about": "정보", + "statusLabel": "상태", + "transportLabel": "전송 방식", + "endpointLabel": "엔드포인트", + "toolPreview": "도구", + "noToolsAvailable": "사용 가능한 도구가 없습니다", + "searchTools": "도구 검색", + "searchToolsPlaceholder": "도구 검색", + "selectedCount": "{{count}}개 활성화", + "noMatchingTools": "일치하는 도구가 없습니다", + "applyChanges": "변경 사항 저장", + "connectionDetails": "연결 정보", + "credentials": "인증 정보", + "none": "없음", + "dangerZone": "연결 삭제", "toolScope": "도구", - "allTools": "전체", - "noTools": "없음", - "testForTools": "테스트를 실행해 개별 도구를 확인하고 선택하세요." + "allTools": "모두 활성화", + "noTools": "모두 비활성화", + "reloadTools": "도구 다시 불러오기", + "presetDescriptions": { + "browserbase": "Browserbase로 클라우드 브라우저를 자동화합니다.", + "playwright": "Playwright로 로컬 브라우저를 검사하고 자동화합니다.", + "context7": "최신 라이브러리 문서와 코드 예제를 가져옵니다.", + "firecrawl": "웹 콘텐츠를 크롤링, 검색, 추출합니다.", + "parallel-search": "웹을 검색하고 관련 페이지를 읽습니다.", + "exa": "웹을 검색하고 정리된 페이지 콘텐츠를 추출합니다.", + "microsoft-learn": "Microsoft Learn 문서를 검색하고 읽습니다.", + "aws-docs": "AWS 문서와 서비스 가이드를 검색합니다.", + "brave-search": "Brave Search로 웹, 뉴스, 이미지, 동영상, 지역 정보를 검색합니다.", + "postman": "Postman API, 컬렉션, 작업 공간을 확인하고 관리합니다.", + "figma": "로컬 Dev Mode MCP를 통해 Figma 디자인 컨텍스트를 읽습니다.", + "xmind": "Xmind 클라우드 마인드맵을 만들고 읽고 편집합니다.", + "notion": "Notion 작업 공간을 읽고 업데이트합니다.", + "linear": "Linear 이슈, 프로젝트, 댓글을 찾고 관리합니다.", + "github": "GitHub 저장소, 이슈, 풀 리퀘스트를 관리합니다.", + "supabase": "Supabase 프로젝트를 확인하고 관리합니다." + } }, "api": { "title": "API 서버", "openaiCompatible": "OpenAI 호환 API", "description": "로컬 /v1 엔드포인트로 SDK와 에이전트를 연결합니다.", diff --git a/webui/src/i18n/locales/pt-BR/common.json b/webui/src/i18n/locales/pt-BR/common.json index 19b1ef338..d25c0f49a 100644 --- a/webui/src/i18n/locales/pt-BR/common.json +++ b/webui/src/i18n/locales/pt-BR/common.json @@ -384,10 +384,49 @@ "statusComingSoon": "Em breve", "comingSoon": "Em breve", "statusNotInstalled": "Não habilitado", + "manage": "Gerenciar", + "manageTitle": "Gerenciar {{name}}", + "fixConnection": "Corrigir conexão", + "overviewTab": "Visão geral", + "connectionTab": "Conexão", + "manageTabs": "Gerenciamento de MCP", + "about": "Sobre", + "statusLabel": "Status", + "transportLabel": "Transporte", + "endpointLabel": "Endpoint", + "toolPreview": "Ferramentas", + "noToolsAvailable": "Nenhuma ferramenta disponível", + "searchTools": "Buscar ferramentas", + "searchToolsPlaceholder": "Buscar ferramentas", + "selectedCount": "{{count}} ativadas", + "noMatchingTools": "Nenhuma ferramenta encontrada", + "applyChanges": "Salvar alterações", + "connectionDetails": "Detalhes da conexão", + "credentials": "Credenciais", + "none": "Nenhuma", + "dangerZone": "Remover conexão", "toolScope": "Ferramentas", - "allTools": "Todas", - "noTools": "Nenhuma", - "testForTools": "Execute Testar para inspecionar e escolher ferramentas individuais." + "allTools": "Ativar todas", + "noTools": "Desativar todas", + "reloadTools": "Recarregar ferramentas", + "presetDescriptions": { + "browserbase": "Automatize navegadores na nuvem com o Browserbase.", + "playwright": "Inspecione e automatize navegadores locais com o Playwright.", + "context7": "Obtenha documentação atualizada e exemplos de código.", + "firecrawl": "Rastreie, pesquise e extraia conteúdo da web.", + "parallel-search": "Pesquise na web e leia páginas relevantes.", + "exa": "Pesquise na web e extraia conteúdo limpo das páginas.", + "microsoft-learn": "Pesquise e leia a documentação do Microsoft Learn.", + "aws-docs": "Pesquise a documentação e os guias de serviços da AWS.", + "brave-search": "Pesquise web, notícias, imagens, vídeos e resultados locais com o Brave Search.", + "postman": "Inspecione e gerencie APIs, coleções e espaços de trabalho do Postman.", + "figma": "Leia o contexto de design do Figma pelo MCP local do Dev Mode.", + "xmind": "Crie, leia e edite mapas mentais do Xmind na nuvem.", + "notion": "Leia e atualize seu espaço de trabalho do Notion.", + "linear": "Encontre e gerencie issues, projetos e comentários do Linear.", + "github": "Gerencie repositórios, issues e pull requests do GitHub.", + "supabase": "Inspecione e gerencie projetos do Supabase." + } }, "values": { "light": "Claro", diff --git a/webui/src/i18n/locales/vi/common.json b/webui/src/i18n/locales/vi/common.json index f7daf6292..92444c1df 100644 --- a/webui/src/i18n/locales/vi/common.json +++ b/webui/src/i18n/locales/vi/common.json @@ -569,10 +569,49 @@ "statusComingSoon": "Sắp ra mắt", "comingSoon": "Sắp ra mắt", "statusNotInstalled": "Chưa bật", + "manage": "Quản lý", + "manageTitle": "Quản lý {{name}}", + "fixConnection": "Khắc phục kết nối", + "overviewTab": "Tổng quan", + "connectionTab": "Kết nối", + "manageTabs": "Quản lý MCP", + "about": "Giới thiệu", + "statusLabel": "Trạng thái", + "transportLabel": "Giao thức", + "endpointLabel": "Điểm cuối", + "toolPreview": "Công cụ", + "noToolsAvailable": "Không có công cụ khả dụng", + "searchTools": "Tìm công cụ", + "searchToolsPlaceholder": "Tìm công cụ", + "selectedCount": "Đã bật {{count}}", + "noMatchingTools": "Không có công cụ phù hợp", + "applyChanges": "Lưu thay đổi", + "connectionDetails": "Chi tiết kết nối", + "credentials": "Thông tin xác thực", + "none": "Không có", + "dangerZone": "Xóa kết nối", "toolScope": "Công cụ", - "allTools": "Tất cả", - "noTools": "Không có", - "testForTools": "Chạy Kiểm tra để xem và chọn từng công cụ." + "allTools": "Bật tất cả", + "noTools": "Tắt tất cả", + "reloadTools": "Tải lại công cụ", + "presetDescriptions": { + "browserbase": "Tự động hóa trình duyệt đám mây bằng Browserbase.", + "playwright": "Kiểm tra và tự động hóa trình duyệt cục bộ bằng Playwright.", + "context7": "Tải tài liệu thư viện mới nhất và ví dụ mã.", + "firecrawl": "Thu thập, tìm kiếm và trích xuất nội dung web.", + "parallel-search": "Tìm kiếm trên web và đọc các trang liên quan.", + "exa": "Tìm kiếm trên web và trích xuất nội dung trang đã làm sạch.", + "microsoft-learn": "Tìm kiếm và đọc tài liệu Microsoft Learn.", + "aws-docs": "Tìm kiếm tài liệu và hướng dẫn dịch vụ AWS.", + "brave-search": "Tìm kiếm web, tin tức, hình ảnh, video và kết quả địa phương bằng Brave Search.", + "postman": "Kiểm tra và quản lý API, bộ sưu tập và không gian làm việc Postman.", + "figma": "Đọc ngữ cảnh thiết kế Figma qua MCP Dev Mode cục bộ.", + "xmind": "Tạo, đọc và chỉnh sửa sơ đồ tư duy Xmind trên đám mây.", + "notion": "Đọc và cập nhật không gian làm việc Notion.", + "linear": "Tìm và quản lý vấn đề, dự án và bình luận Linear.", + "github": "Quản lý kho mã, issue và pull request GitHub.", + "supabase": "Kiểm tra và quản lý dự án Supabase." + } }, "api": { "title": "Máy chủ API", "openaiCompatible": "API tương thích OpenAI", "description": "Kết nối SDK và tác nhân qua điểm cuối /v1 cục bộ.", diff --git a/webui/src/i18n/locales/zh-CN/common.json b/webui/src/i18n/locales/zh-CN/common.json index 45b48c889..7fa85cbe4 100644 --- a/webui/src/i18n/locales/zh-CN/common.json +++ b/webui/src/i18n/locales/zh-CN/common.json @@ -384,10 +384,49 @@ "statusComingSoon": "暂不支持", "comingSoon": "即将推出", "statusNotInstalled": "未启用", + "manage": "管理", + "manageTitle": "管理 {{name}}", + "fixConnection": "修复连接", + "overviewTab": "概览", + "connectionTab": "连接", + "manageTabs": "MCP 管理", + "about": "简介", + "statusLabel": "状态", + "transportLabel": "传输方式", + "endpointLabel": "端点", + "toolPreview": "工具", + "noToolsAvailable": "没有可用工具", + "searchTools": "搜索工具", + "searchToolsPlaceholder": "搜索工具", + "selectedCount": "已启用 {{count}} 个", + "noMatchingTools": "没有匹配的工具", + "applyChanges": "保存更改", + "connectionDetails": "连接详情", + "credentials": "凭据", + "none": "无", + "dangerZone": "移除连接", "toolScope": "工具", - "allTools": "全部", - "noTools": "不暴露", - "testForTools": "运行测试后,可以查看并选择单个工具。" + "allTools": "全选", + "noTools": "全部停用", + "reloadTools": "重新读取", + "presetDescriptions": { + "browserbase": "使用 Browserbase 自动操作云端浏览器。", + "playwright": "使用 Playwright 检查和自动操作本地浏览器。", + "context7": "获取最新的库文档和代码示例。", + "firecrawl": "抓取、遍历、搜索并提取网页内容。", + "parallel-search": "搜索网页并读取相关页面。", + "exa": "搜索网页并提取整洁的页面内容。", + "microsoft-learn": "搜索并读取 Microsoft Learn 文档。", + "aws-docs": "搜索 AWS 文档和服务指南。", + "brave-search": "使用 Brave Search 搜索网页、新闻、图片、视频和本地信息。", + "postman": "查看并管理 Postman API、集合和工作区。", + "figma": "通过本地 Dev Mode MCP 读取 Figma 设计上下文。", + "xmind": "创建、读取和编辑 Xmind 云端思维导图。", + "notion": "读取和更新 Notion 工作区。", + "linear": "查找和管理 Linear 议题、项目和评论。", + "github": "管理 GitHub 仓库、议题和拉取请求。", + "supabase": "查看和管理 Supabase 项目。" + } }, "values": { "light": "浅色", diff --git a/webui/src/i18n/locales/zh-TW/common.json b/webui/src/i18n/locales/zh-TW/common.json index d6375bd11..925faa1ad 100644 --- a/webui/src/i18n/locales/zh-TW/common.json +++ b/webui/src/i18n/locales/zh-TW/common.json @@ -569,10 +569,49 @@ "statusComingSoon": "即將推出", "comingSoon": "即將推出", "statusNotInstalled": "未啟用", + "manage": "管理", + "manageTitle": "管理 {{name}}", + "fixConnection": "修復連線", + "overviewTab": "概覽", + "connectionTab": "連線", + "manageTabs": "MCP 管理", + "about": "簡介", + "statusLabel": "狀態", + "transportLabel": "傳輸方式", + "endpointLabel": "端點", + "toolPreview": "工具", + "noToolsAvailable": "沒有可用工具", + "searchTools": "搜尋工具", + "searchToolsPlaceholder": "搜尋工具", + "selectedCount": "已啟用 {{count}} 個", + "noMatchingTools": "找不到符合的工具", + "applyChanges": "儲存變更", + "connectionDetails": "連線詳細資料", + "credentials": "憑證", + "none": "無", + "dangerZone": "移除連線", "toolScope": "工具", - "allTools": "全部", - "noTools": "無", - "testForTools": "執行 [測試] 以檢視並選擇個別工具。" + "allTools": "全選", + "noTools": "全部停用", + "reloadTools": "重新讀取", + "presetDescriptions": { + "browserbase": "使用 Browserbase 自動操作雲端瀏覽器。", + "playwright": "使用 Playwright 檢查和自動操作本機瀏覽器。", + "context7": "取得最新的程式庫文件和程式碼範例。", + "firecrawl": "擷取、檢索、搜尋並提取網頁內容。", + "parallel-search": "搜尋網頁並讀取相關頁面。", + "exa": "搜尋網頁並提取乾淨的頁面內容。", + "microsoft-learn": "搜尋並讀取 Microsoft Learn 文件。", + "aws-docs": "搜尋 AWS 文件和服務指南。", + "brave-search": "使用 Brave Search 搜尋網頁、新聞、圖片、影片和本地資訊。", + "postman": "查看並管理 Postman API、集合和工作區。", + "figma": "透過本機 Dev Mode MCP 讀取 Figma 設計內容。", + "xmind": "建立、讀取和編輯 Xmind 雲端心智圖。", + "notion": "讀取和更新 Notion 工作區。", + "linear": "尋找和管理 Linear 議題、專案和留言。", + "github": "管理 GitHub 儲存庫、議題和 Pull Request。", + "supabase": "查看並管理 Supabase 專案。" + } }, "api": { "title": "API 伺服器", "openaiCompatible": "OpenAI 相容 API", "description": "讓 SDK 與其他智能體透過本機 /v1 端點連線 nanobot。", diff --git a/webui/src/tests/mcp-management-dialog.test.tsx b/webui/src/tests/mcp-management-dialog.test.tsx new file mode 100644 index 000000000..a45ebae25 --- /dev/null +++ b/webui/src/tests/mcp-management-dialog.test.tsx @@ -0,0 +1,135 @@ +import { fireEvent, render, screen, waitFor, within } from "@testing-library/react"; +import { useState } from "react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { + McpManagementDialog, + type McpManagementTab, +} from "@/components/settings/system/McpManagementDialog"; +import i18n from "@/i18n"; +import type { McpPresetInfo } from "@/lib/types"; + +const connectedPreset: McpPresetInfo = { + name: "docs", + display_name: "Docs MCP", + category: "productivity", + description: "Search and maintain the team knowledge base.", + docs_url: "https://example.com/docs-mcp", + transport: "streamableHttp", + auth: null, + requires: "", + note: "", + install_supported: true, + installed: true, + configured: true, + available: true, + status: "configured", + runtime_status: "connected", + required_fields: [], + connection_summary: "https://mcp.example.com/mcp", + tool_count: 3, + tool_names: ["search_docs", "write_note", "list_sources"], + enabled_tools: ["*"], + checked_at: "2026-08-12T08:00:00Z", + source: "custom", +}; + +describe("McpManagementDialog", () => { + beforeEach(async () => { + await i18n.changeLanguage("en"); + }); + + it("keeps tool scope changes as a draft until Save changes", () => { + const onToolsChange = vi.fn(); + renderDialog({ initialTab: "tools", onToolsChange }); + + const dialog = screen.getByRole("dialog", { name: "Docs MCP" }); + expect(dialog).toHaveClass("h-[min(34rem,calc(100dvh-2rem))]"); + expect(within(dialog).getByRole("tab", { name: /Tools/ })).toHaveAttribute("aria-selected", "true"); + expect(within(dialog).getByText("3 enabled")).toBeInTheDocument(); + expect(within(dialog).queryByText("Close")).not.toBeInTheDocument(); + + const searchDocs = within(dialog).getByRole("checkbox", { name: /search_docs/ }); + expect(searchDocs).toBeChecked(); + fireEvent.click(searchDocs); + + expect(searchDocs).not.toBeChecked(); + expect(within(dialog).getByText("2 enabled")).toBeInTheDocument(); + expect(onToolsChange).not.toHaveBeenCalled(); + + fireEvent.click(within(dialog).getByRole("button", { name: "Save changes" })); + expect(onToolsChange).toHaveBeenCalledWith("docs", ["write_note", "list_sources"]); + }); + + it("searches the inventory and exposes connection management in the same modal", () => { + const onAction = vi.fn(); + renderDialog({ initialTab: "tools", onAction }); + + const dialog = screen.getByRole("dialog", { name: "Docs MCP" }); + fireEvent.change(within(dialog).getByRole("textbox", { name: "Search tools" }), { + target: { value: "write" }, + }); + expect(within(dialog).getByRole("checkbox", { name: /write_note/ })).toBeInTheDocument(); + expect(within(dialog).queryByRole("checkbox", { name: /search_docs/ })).not.toBeInTheDocument(); + + fireEvent.click(within(dialog).getByRole("tab", { name: "Connection" })); + expect(within(dialog).getByText("https://mcp.example.com/mcp")).toBeInTheDocument(); + expect(within(dialog).getByText("Streamable HTTP")).toBeInTheDocument(); + expect(within(dialog).getByRole("link", { name: "Open docs" })).toBeInTheDocument(); + expect(within(dialog).getByRole("button", { name: "Remove connection" })).toBeInTheDocument(); + expect(within(dialog).queryByText("Last checked")).not.toBeInTheDocument(); + expect(within(dialog).queryByText("Connection actions apply immediately.")).not.toBeInTheDocument(); + fireEvent.click(within(dialog).getByRole("button", { name: "Reconnect" })); + expect(onAction).toHaveBeenCalledWith("reconnect", "docs", {}); + }); + + it("loads tools on entry and replaces passive inspection copy with recovery", async () => { + const onAction = vi.fn(); + renderDialog({ + initialTab: "tools", + onAction, + preset: { ...connectedPreset, tool_count: 0, tool_names: [], enabled_tools: ["*"] }, + }); + + const dialog = screen.getByRole("dialog", { name: "Docs MCP" }); + await waitFor(() => expect(onAction).toHaveBeenCalledWith("test", "docs")); + expect(within(dialog).queryByText("Not inspected")).not.toBeInTheDocument(); + expect(within(dialog).getByText("No tools available")).toBeInTheDocument(); + fireEvent.click(within(dialog).getByRole("button", { name: "Reload tools" })); + expect(onAction).toHaveBeenCalledTimes(2); + }); +}); + +function renderDialog({ + initialTab, + onAction = vi.fn(), + onToolsChange = vi.fn(), + preset = connectedPreset, +}: { + initialTab: McpManagementTab; + onAction?: ReturnType; + onToolsChange?: ReturnType; + preset?: McpPresetInfo; +}) { + function Harness() { + const [tab, setTab] = useState(initialTab); + return ( + DM} + onTabChange={setTab} + onOpenChange={vi.fn()} + onFieldChange={vi.fn()} + onAction={onAction} + onOAuthConnect={vi.fn()} + onToolsChange={onToolsChange} + /> + ); + } + return render(); +} diff --git a/webui/src/tests/settings-apps-oauth.test.tsx b/webui/src/tests/settings-apps-oauth.test.tsx index 54e2baa72..70e506dbf 100644 --- a/webui/src/tests/settings-apps-oauth.test.tsx +++ b/webui/src/tests/settings-apps-oauth.test.tsx @@ -128,8 +128,8 @@ describe("SettingsView Apps catalog", () => { ); expect(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument(); - expect(await screen.findByRole("button", { name: "Xmind: Configured" }, { timeout: 2500 })) - .toHaveTextContent("Configured"); + expect(await screen.findByRole("button", { name: "Manage Xmind" }, { timeout: 2500 })) + .toHaveTextContent("Manage"); expect(screen.queryByRole("button", { name: "Cancel" })).not.toBeInTheDocument(); expect(screen.queryByText("Xmind connected.")).not.toBeInTheDocument(); expect(screen.queryByText(/some servers did not connect: notion/i)).not.toBeInTheDocument(); @@ -182,22 +182,21 @@ describe("SettingsView Apps catalog", () => { expect(failed.closest("button")).toBeNull(); expect(failed.closest("p")?.querySelector(".lucide-triangle-alert")).not.toBeNull(); expect(row?.querySelector(".lucide-check")).toBeNull(); - expect(within(row as HTMLElement).getByRole("button", { name: "Reconnect Xmind" })).toHaveTextContent( - "Reconnect", + expect(within(row as HTMLElement).getByRole("button", { name: "Manage Xmind" })).toHaveTextContent( + "Fix connection", ); - const actions = within(row as HTMLElement).getByRole("button", { name: "Actions for Xmind" }); - expect(actions).toHaveAttribute("aria-haspopup", "menu"); - fireEvent.pointerDown(actions, { button: 0, ctrlKey: false }); - expect(await screen.findByRole("menuitem", { name: "Test" })).toBeInTheDocument(); - expect(screen.getByRole("menuitem", { name: "Remove" })).toBeInTheDocument(); - fireEvent.keyDown(document, { key: "Escape" }); await act(() => i18n.changeLanguage("zh-CN")); expect(within(row as HTMLElement).getByText("连接失败。")).toBeInTheDocument(); - expect(within(row as HTMLElement).getByRole("button", { name: "Xmind 操作" })) - .toBeInTheDocument(); - const reconnect = screen.getByRole("button", { name: "重新连接 Xmind" }); - expect(reconnect).toHaveTextContent("重新连接"); + const manage = within(row as HTMLElement).getByRole("button", { name: "管理 Xmind" }); + expect(manage).toHaveTextContent("修复连接"); + fireEvent.click(manage); + const dialog = screen.getByRole("dialog", { name: "Xmind" }); + expect(within(dialog).getByRole("tab", { name: "连接" })).toHaveAttribute("aria-selected", "true"); + expect(within(dialog).getByText("创建、读取和编辑 Xmind 云端思维导图。")).toHaveClass("sr-only"); + expect(within(dialog).getByText("连接失败", { exact: true })).toBeInTheDocument(); + expect(within(dialog).getByRole("button", { name: "移除连接" })).toBeInTheDocument(); + const reconnect = within(dialog).getByRole("button", { name: "重新连接" }); fireEvent.click(reconnect); await waitFor(() => expect(requestMutationMock).toHaveBeenCalledWith( @@ -246,9 +245,9 @@ describe("SettingsView Apps catalog", () => { .toHaveTextContent("Connecting…"); expect(await screen.findByRole( "button", - { name: "Xmind: Connected." }, + { name: "Manage Xmind" }, { timeout: 2_500 }, - )).toHaveTextContent("Connected."); + )).toHaveTextContent("Manage"); expect(mcpPresetRequests).toBe(2); fireEvent.click(screen.getByRole("button", { name: "Ready" })); @@ -299,15 +298,17 @@ describe("SettingsView Apps catalog", () => { fireEvent.click(await screen.findByRole("button", { name: "MCP" })); expect(await screen.findByText("Connection failed.")).toBeInTheDocument(); - fireEvent.click(screen.getByRole("button", { name: "Reconnect team-docs" })); + fireEvent.click(screen.getByRole("button", { name: "Manage team-docs" })); + fireEvent.click(within(screen.getByRole("dialog", { name: "team-docs" })) + .getByRole("button", { name: "Reconnect" })); await waitFor(() => expect(requestMutationMock).toHaveBeenCalledWith( "settings.mcp.reconnect", { name: "team-docs" }, 20_000, )); - const connected = await screen.findByRole("button", { name: "team-docs: Connected." }); - expect(connected).toHaveTextContent("Connected."); + const connected = await screen.findByRole("button", { name: "Manage team-docs" }); + expect(connected).toHaveTextContent("Manage"); expect(connected.querySelector(".lucide-check")).not.toBeNull(); fireEvent.click(screen.getByRole("button", { name: "Ready" })); const readyHeading = await screen.findByRole("heading", { name: "team-docs" }); @@ -500,8 +501,8 @@ describe("SettingsView Apps catalog", () => { { flow_id: "flow-manual", callback_url: callbackUrl }, 20_000, )); - expect(await screen.findByRole("button", { name: "Xmind: Configured" }, { timeout: 2500 })) - .toHaveTextContent("Configured"); + expect(await screen.findByRole("button", { name: "Manage Xmind" }, { timeout: 2500 })) + .toHaveTextContent("Manage"); expect(screen.queryByRole("textbox", { name: "Full callback URL" })).not.toBeInTheDocument(); expect(popup.close).toHaveBeenCalledTimes(1); }); @@ -610,7 +611,10 @@ describe("SettingsView Apps catalog", () => { renderSettingsView({ initialSection: "apps" }); fireEvent.click(await screen.findByRole("button", { name: "MCP" })); - fireEvent.click(await screen.findByRole("button", { name: "Remove" })); + fireEvent.click(await screen.findByRole("button", { name: "Manage Xmind" })); + const dialog = screen.getByRole("dialog", { name: "Xmind" }); + fireEvent.click(within(dialog).getByRole("tab", { name: "Connection" })); + fireEvent.click(within(dialog).getByRole("button", { name: "Remove connection" })); await waitFor(() => expect(requestMutationMock).toHaveBeenCalledWith( "settings.mcp.remove",