mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-14 18:17:29 +08:00
feat: add MCP tool and web UI management
This commit is contained in:
@@ -9,6 +9,7 @@ import Config from './pages/Config';
|
||||
import Cron from './pages/Cron';
|
||||
import Logs from './pages/Logs';
|
||||
import Skills from './pages/Skills';
|
||||
import MCP from './pages/MCP';
|
||||
import Memory from './pages/Memory';
|
||||
import TaskAudit from './pages/TaskAudit';
|
||||
import EKG from './pages/EKG';
|
||||
@@ -28,6 +29,7 @@ export default function App() {
|
||||
<Route path="logs" element={<Logs />} />
|
||||
<Route path="log-codes" element={<LogCodes />} />
|
||||
<Route path="skills" element={<Skills />} />
|
||||
<Route path="mcp" element={<MCP />} />
|
||||
<Route path="config" element={<Config />} />
|
||||
<Route path="cron" element={<Cron />} />
|
||||
<Route path="memory" element={<Memory />} />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { LayoutDashboard, MessageSquare, Settings, Clock, Terminal, Zap, FolderOpen, ClipboardList, BrainCircuit, Hash, Bot, Boxes, PanelLeftClose, PanelLeftOpen } from 'lucide-react';
|
||||
import { LayoutDashboard, MessageSquare, Settings, Clock, Terminal, Zap, FolderOpen, ClipboardList, BrainCircuit, Hash, Bot, Boxes, PanelLeftClose, PanelLeftOpen, Plug } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAppContext } from '../context/AppContext';
|
||||
import NavItem from './NavItem';
|
||||
@@ -29,6 +29,7 @@ const Sidebar: React.FC = () => {
|
||||
title: t('sidebarConfig'),
|
||||
items: [
|
||||
{ icon: <Settings className="w-5 h-5" />, label: t('config'), to: '/config' },
|
||||
{ icon: <Plug className="w-5 h-5" />, label: t('mcpServices'), to: '/mcp' },
|
||||
{ icon: <Bot className="w-5 h-5" />, label: t('subagentProfiles'), to: '/subagent-profiles' },
|
||||
{ icon: <Clock className="w-5 h-5" />, label: t('cronJobs'), to: '/cron' },
|
||||
],
|
||||
|
||||
@@ -8,6 +8,8 @@ const resources = {
|
||||
dashboard: 'Dashboard',
|
||||
chat: 'Chat',
|
||||
config: 'Config',
|
||||
mcpServices: 'MCP',
|
||||
mcpServicesHint: 'Manage MCP servers, install packages, and inspect discovered remote tools.',
|
||||
cronJobs: 'Cron Jobs',
|
||||
nodes: 'Nodes',
|
||||
agentTree: 'Agent Tree',
|
||||
@@ -298,6 +300,23 @@ const resources = {
|
||||
configProxies: 'Proxies',
|
||||
configNewProviderName: 'new provider name',
|
||||
configNoCustomProviders: 'No custom providers yet.',
|
||||
configMCPServers: 'MCP Servers',
|
||||
configNewMCPServerName: 'new MCP server name',
|
||||
configNoMCPServers: 'No MCP servers configured yet.',
|
||||
configMCPInstallTitle: 'Install MCP Server Package',
|
||||
configMCPInstallMessage: 'Install an npm package for MCP server "{{name}}"?',
|
||||
configMCPInstallPlaceholder: '@scope/package',
|
||||
configMCPInstalling: 'Installing MCP package...',
|
||||
configMCPInstallFailedTitle: 'MCP install failed',
|
||||
configMCPInstallFailedMessage: 'Failed to install MCP package',
|
||||
configMCPInstallDoneTitle: 'MCP package installed',
|
||||
configMCPInstallDoneMessage: 'Installed {{package}} and resolved binary {{bin}}.',
|
||||
configMCPInstallDoneFallback: 'MCP package installed.',
|
||||
configMCPDiscoveredTools: 'Discovered MCP Tools',
|
||||
configMCPDiscoveredToolsCount: '{{count}} discovered',
|
||||
configNoMCPDiscoveredTools: 'No MCP tools discovered yet.',
|
||||
configDeleteMCPServerConfirmTitle: 'Delete MCP Server',
|
||||
configDeleteMCPServerConfirmMessage: 'Delete MCP server "{{name}}" from current config?',
|
||||
configNoGroups: 'No config groups found.',
|
||||
configDiffPreviewCount: 'Diff Preview ({{count}} items)',
|
||||
saveConfigFailed: 'Failed to save config',
|
||||
@@ -388,6 +407,7 @@ const resources = {
|
||||
api_base: 'API Base',
|
||||
protocol: 'Protocol',
|
||||
models: 'Models',
|
||||
command: 'Command',
|
||||
responses: 'Responses',
|
||||
streaming: 'Streaming',
|
||||
web_search_enabled: 'Web Search Enabled',
|
||||
@@ -403,6 +423,7 @@ const resources = {
|
||||
version: 'Version',
|
||||
name: 'Name',
|
||||
description: 'Description',
|
||||
package: 'Package',
|
||||
system_prompt: 'System Prompt',
|
||||
tools: 'Tools',
|
||||
auth: 'Authentication',
|
||||
@@ -479,8 +500,14 @@ const resources = {
|
||||
sandbox: 'Sandbox',
|
||||
image: 'Image',
|
||||
web: 'Web',
|
||||
mcp: 'MCP',
|
||||
search: 'Search',
|
||||
max_results: 'Max Results',
|
||||
request_timeout_sec: 'Request Timeout (Seconds)',
|
||||
servers: 'Servers',
|
||||
transport: 'Transport',
|
||||
args: 'Arguments',
|
||||
env: 'Environment',
|
||||
proxies: 'Proxies',
|
||||
cross_session_call_id: 'Cross-session Call ID',
|
||||
supports_responses_compact: 'Supports Responses Compact',
|
||||
@@ -502,6 +529,8 @@ const resources = {
|
||||
dashboard: '仪表盘',
|
||||
chat: '对话',
|
||||
config: '配置',
|
||||
mcpServices: 'MCP',
|
||||
mcpServicesHint: '管理 MCP 服务、安装服务包,并查看已发现的远端工具。',
|
||||
cronJobs: '定时任务',
|
||||
nodes: '节点',
|
||||
agentTree: '代理树',
|
||||
@@ -792,6 +821,23 @@ const resources = {
|
||||
configProxies: '代理配置',
|
||||
configNewProviderName: '新 provider 名称',
|
||||
configNoCustomProviders: '暂无自定义 provider。',
|
||||
configMCPServers: 'MCP 服务',
|
||||
configNewMCPServerName: '新的 MCP 服务名',
|
||||
configNoMCPServers: '暂无 MCP 服务配置。',
|
||||
configMCPInstallTitle: '安装 MCP 服务包',
|
||||
configMCPInstallMessage: '是否为 MCP 服务 “{{name}}” 安装 npm 包?',
|
||||
configMCPInstallPlaceholder: '@scope/package',
|
||||
configMCPInstalling: '正在安装 MCP 包...',
|
||||
configMCPInstallFailedTitle: 'MCP 安装失败',
|
||||
configMCPInstallFailedMessage: '安装 MCP 包失败',
|
||||
configMCPInstallDoneTitle: 'MCP 包安装完成',
|
||||
configMCPInstallDoneMessage: '已安装 {{package}},并解析到可执行文件 {{bin}}。',
|
||||
configMCPInstallDoneFallback: 'MCP 包已安装。',
|
||||
configMCPDiscoveredTools: '已发现的 MCP 工具',
|
||||
configMCPDiscoveredToolsCount: '已发现 {{count}} 个',
|
||||
configNoMCPDiscoveredTools: '暂未发现 MCP 工具。',
|
||||
configDeleteMCPServerConfirmTitle: '删除 MCP 服务',
|
||||
configDeleteMCPServerConfirmMessage: '确认从当前配置中删除 MCP 服务 “{{name}}”吗?',
|
||||
configNoGroups: '未找到配置分组。',
|
||||
configDiffPreviewCount: '配置差异预览({{count}}项)',
|
||||
saveConfigFailed: '保存配置失败',
|
||||
@@ -882,6 +928,7 @@ const resources = {
|
||||
api_base: 'API 基础地址',
|
||||
protocol: '协议',
|
||||
models: '模型列表',
|
||||
command: '命令',
|
||||
responses: 'Responses 配置',
|
||||
streaming: '流式输出',
|
||||
web_search_enabled: '启用网页搜索',
|
||||
@@ -897,6 +944,7 @@ const resources = {
|
||||
version: '版本',
|
||||
name: '名称',
|
||||
description: '描述',
|
||||
package: '包名',
|
||||
system_prompt: '系统提示词',
|
||||
tools: '工具',
|
||||
auth: '身份验证',
|
||||
@@ -973,8 +1021,14 @@ const resources = {
|
||||
sandbox: '沙箱',
|
||||
image: '镜像',
|
||||
web: 'Web',
|
||||
mcp: 'MCP',
|
||||
search: '搜索',
|
||||
max_results: '最大结果数',
|
||||
request_timeout_sec: '请求超时(秒)',
|
||||
servers: '服务列表',
|
||||
transport: '传输方式',
|
||||
args: '参数',
|
||||
env: '环境变量',
|
||||
proxies: '代理集合',
|
||||
cross_session_call_id: '跨会话调用 ID',
|
||||
supports_responses_compact: '支持紧凑 responses',
|
||||
|
||||
308
webui/src/pages/MCP.tsx
Normal file
308
webui/src/pages/MCP.tsx
Normal file
@@ -0,0 +1,308 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { RefreshCw, Save } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAppContext } from '../context/AppContext';
|
||||
import { useUI } from '../context/UIContext';
|
||||
|
||||
function setPath(obj: any, path: string, value: any) {
|
||||
const keys = path.split('.');
|
||||
const next = JSON.parse(JSON.stringify(obj || {}));
|
||||
let cur = next;
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
const k = keys[i];
|
||||
if (typeof cur[k] !== 'object' || cur[k] === null) cur[k] = {};
|
||||
cur = cur[k];
|
||||
}
|
||||
cur[keys[keys.length - 1]] = value;
|
||||
return next;
|
||||
}
|
||||
|
||||
const MCP: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { cfg, setCfg, q, loadConfig, setConfigEditing } = useAppContext();
|
||||
const ui = useUI();
|
||||
const [newMCPServerName, setNewMCPServerName] = useState('');
|
||||
const [mcpTools, setMcpTools] = useState<Array<{ name: string; description?: string; mcp?: { server?: string; remote_tool?: string } }>>([]);
|
||||
const [baseline, setBaseline] = useState<any>(null);
|
||||
|
||||
const currentPayload = useMemo(() => cfg || {}, [cfg]);
|
||||
const isDirty = useMemo(() => {
|
||||
if (baseline == null) return false;
|
||||
return JSON.stringify(baseline) !== JSON.stringify(currentPayload);
|
||||
}, [baseline, currentPayload]);
|
||||
|
||||
useEffect(() => {
|
||||
if (baseline == null && cfg && Object.keys(cfg).length > 0) {
|
||||
setBaseline(JSON.parse(JSON.stringify(cfg)));
|
||||
}
|
||||
}, [cfg, baseline]);
|
||||
|
||||
useEffect(() => {
|
||||
setConfigEditing(isDirty);
|
||||
return () => setConfigEditing(false);
|
||||
}, [isDirty, setConfigEditing]);
|
||||
|
||||
async function refreshMCPTools(cancelled = false) {
|
||||
try {
|
||||
const r = await fetch(`/webui/api/tools${q}`);
|
||||
if (!r.ok) throw new Error('Failed to load tools');
|
||||
const data = await r.json();
|
||||
if (!cancelled) {
|
||||
setMcpTools(Array.isArray(data?.mcp_tools) ? data.mcp_tools : []);
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled) setMcpTools([]);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void refreshMCPTools(cancelled);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [q]);
|
||||
|
||||
function updateMCPServerField(name: string, field: string, value: any) {
|
||||
setCfg((v) => setPath(v, `tools.mcp.servers.${name}.${field}`, value));
|
||||
}
|
||||
|
||||
function addMCPServer() {
|
||||
const name = newMCPServerName.trim();
|
||||
if (!name) return;
|
||||
setCfg((v) => {
|
||||
const next = JSON.parse(JSON.stringify(v || {}));
|
||||
if (!next.tools || typeof next.tools !== 'object') next.tools = {};
|
||||
if (!next.tools.mcp || typeof next.tools.mcp !== 'object') {
|
||||
next.tools.mcp = { enabled: true, request_timeout_sec: 20, servers: {} };
|
||||
}
|
||||
if (!next.tools.mcp.servers || typeof next.tools.mcp.servers !== 'object' || Array.isArray(next.tools.mcp.servers)) {
|
||||
next.tools.mcp.servers = {};
|
||||
}
|
||||
if (!next.tools.mcp.servers[name]) {
|
||||
next.tools.mcp.servers[name] = {
|
||||
enabled: true,
|
||||
transport: 'stdio',
|
||||
command: '',
|
||||
args: [],
|
||||
env: {},
|
||||
working_dir: '',
|
||||
description: '',
|
||||
package: '',
|
||||
};
|
||||
}
|
||||
return next;
|
||||
});
|
||||
setNewMCPServerName('');
|
||||
}
|
||||
|
||||
async function removeMCPServer(name: string) {
|
||||
const ok = await ui.confirmDialog({
|
||||
title: t('configDeleteMCPServerConfirmTitle'),
|
||||
message: t('configDeleteMCPServerConfirmMessage', { name }),
|
||||
danger: true,
|
||||
confirmText: t('delete'),
|
||||
});
|
||||
if (!ok) return;
|
||||
setCfg((v) => {
|
||||
const next = JSON.parse(JSON.stringify(v || {}));
|
||||
if (next?.tools?.mcp?.servers && typeof next.tools.mcp.servers === 'object') {
|
||||
delete next.tools.mcp.servers[name];
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
function inferMCPPackage(server: any): string {
|
||||
if (typeof server?.package === 'string' && server.package.trim()) return server.package.trim();
|
||||
const command = String(server?.command || '').trim();
|
||||
const args = Array.isArray(server?.args) ? server.args.map((x: any) => String(x).trim()).filter(Boolean) : [];
|
||||
if (command === 'npx' || command.endsWith('/npx')) {
|
||||
const pkg = args.find((arg: string) => !arg.startsWith('-'));
|
||||
return pkg || '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
async function installMCPServerPackage(name: string, server: any) {
|
||||
const defaultPkg = inferMCPPackage(server);
|
||||
const pkg = await ui.promptDialog({
|
||||
title: t('configMCPInstallTitle'),
|
||||
message: t('configMCPInstallMessage', { name }),
|
||||
inputPlaceholder: defaultPkg || t('configMCPInstallPlaceholder'),
|
||||
initialValue: defaultPkg,
|
||||
confirmText: t('install'),
|
||||
});
|
||||
const packageName = String(pkg || '').trim();
|
||||
if (!packageName) return;
|
||||
|
||||
ui.showLoading(t('configMCPInstalling'));
|
||||
try {
|
||||
const r = await fetch(`/webui/api/mcp/install${q}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ package: packageName }),
|
||||
});
|
||||
const text = await r.text();
|
||||
if (!r.ok) {
|
||||
await ui.notify({ title: t('configMCPInstallFailedTitle'), message: text || t('configMCPInstallFailedMessage') });
|
||||
return;
|
||||
}
|
||||
let data: any = null;
|
||||
try {
|
||||
data = JSON.parse(text);
|
||||
} catch {
|
||||
data = null;
|
||||
}
|
||||
if (data?.bin_path) {
|
||||
updateMCPServerField(name, 'command', data.bin_path);
|
||||
updateMCPServerField(name, 'args', []);
|
||||
updateMCPServerField(name, 'package', packageName);
|
||||
} else {
|
||||
updateMCPServerField(name, 'package', packageName);
|
||||
}
|
||||
await ui.notify({
|
||||
title: t('configMCPInstallDoneTitle'),
|
||||
message: data?.bin_path
|
||||
? t('configMCPInstallDoneMessage', { package: packageName, bin: data.bin_path })
|
||||
: (text || t('configMCPInstallDoneFallback')),
|
||||
});
|
||||
} finally {
|
||||
ui.hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
async function saveConfig() {
|
||||
try {
|
||||
const payload = cfg;
|
||||
const submit = async (confirmRisky: boolean) => {
|
||||
const body = confirmRisky ? { ...payload, confirm_risky: true } : payload;
|
||||
return ui.withLoading(async () => {
|
||||
const r = await fetch(`/webui/api/config${q}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const text = await r.text();
|
||||
let data: any = null;
|
||||
try {
|
||||
data = text ? JSON.parse(text) : null;
|
||||
} catch {
|
||||
data = null;
|
||||
}
|
||||
return { ok: r.ok, text, data };
|
||||
}, t('saving'));
|
||||
};
|
||||
|
||||
let result = await submit(false);
|
||||
if (!result.ok && result.data?.requires_confirm) {
|
||||
const changedFields = Array.isArray(result.data?.changed_fields) ? result.data.changed_fields.join(', ') : '';
|
||||
const ok = await ui.confirmDialog({
|
||||
title: t('configRiskyChangeConfirmTitle'),
|
||||
message: t('configRiskyChangeConfirmMessage', { fields: changedFields || '-' }),
|
||||
danger: true,
|
||||
confirmText: t('saveChanges'),
|
||||
});
|
||||
if (!ok) return;
|
||||
result = await submit(true);
|
||||
}
|
||||
|
||||
if (!result.ok) {
|
||||
throw new Error(result.data?.error || result.text || 'save failed');
|
||||
}
|
||||
|
||||
await ui.notify({ title: t('saved'), message: t('configSaved') });
|
||||
setBaseline(JSON.parse(JSON.stringify(payload)));
|
||||
setConfigEditing(false);
|
||||
await loadConfig(true);
|
||||
await refreshMCPTools();
|
||||
} catch (e) {
|
||||
await ui.notify({ title: t('requestFailed'), message: `${t('saveConfigFailed')}: ${e}` });
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 md:p-8 w-full space-y-6">
|
||||
<div className="flex items-start justify-between gap-3 flex-wrap">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">{t('mcpServices')}</h1>
|
||||
<p className="text-sm text-zinc-500 mt-1">{t('mcpServicesHint')}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={async () => { setBaseline(null); await loadConfig(true); }}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-zinc-800 hover:bg-zinc-700 rounded-xl text-sm font-medium transition-colors"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" /> {t('reload')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-3 flex-wrap">
|
||||
<button
|
||||
onClick={saveConfig}
|
||||
disabled={!isDirty}
|
||||
className="brand-button flex items-center gap-2 px-4 py-2 text-white rounded-xl text-sm font-medium transition-colors shadow-sm disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<Save className="w-4 h-4" /> {t('saveChanges')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="brand-card-subtle rounded-2xl border border-zinc-800 p-3 space-y-3">
|
||||
<div className="flex items-center justify-between gap-2 flex-wrap">
|
||||
<div className="text-sm font-semibold text-zinc-200">{t('configMCPServers')}</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input value={newMCPServerName} onChange={(e)=>setNewMCPServerName(e.target.value)} placeholder={t('configNewMCPServerName')} className="px-2 py-1 rounded-lg bg-zinc-900/70 border border-zinc-700 text-xs" />
|
||||
<button onClick={addMCPServer} className="brand-button px-2 py-1 rounded-lg text-xs text-white">{t('add')}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{Object.entries((((cfg as any)?.tools?.mcp?.servers) || {}) as Record<string, any>).map(([name, server]) => (
|
||||
<div key={name} className="grid grid-cols-1 md:grid-cols-12 gap-2 rounded-xl border border-zinc-800 bg-zinc-900/30 p-2 text-xs">
|
||||
<div className="md:col-span-2 font-mono text-zinc-300 flex items-center">{name}</div>
|
||||
<label className="md:col-span-1 flex items-center gap-2 text-zinc-300">
|
||||
<input type="checkbox" checked={!!server?.enabled} onChange={(e)=>updateMCPServerField(name, 'enabled', e.target.checked)} />
|
||||
{t('enable')}
|
||||
</label>
|
||||
<input value={String(server?.command || '')} onChange={(e)=>updateMCPServerField(name, 'command', e.target.value)} placeholder={t('configLabels.command')} className="md:col-span-2 px-2 py-1 rounded-lg bg-zinc-950/70 border border-zinc-800" />
|
||||
<input value={String(server?.working_dir || '')} onChange={(e)=>updateMCPServerField(name, 'working_dir', e.target.value)} placeholder={t('configLabels.working_dir')} className="md:col-span-2 px-2 py-1 rounded-lg bg-zinc-950/70 border border-zinc-800" />
|
||||
<input value={Array.isArray(server?.args) ? server.args.join(',') : ''} onChange={(e)=>updateMCPServerField(name, 'args', e.target.value.split(',').map(s=>s.trim()).filter(Boolean))} placeholder={`${t('configLabels.args')}${t('configCommaSeparatedHint')}`} className="md:col-span-2 px-2 py-1 rounded-lg bg-zinc-950/70 border border-zinc-800" />
|
||||
<input value={String(server?.package || '')} onChange={(e)=>updateMCPServerField(name, 'package', e.target.value)} placeholder={t('configLabels.package')} className="md:col-span-1 px-2 py-1 rounded-lg bg-zinc-950/70 border border-zinc-800" />
|
||||
<input value={String(server?.description || '')} onChange={(e)=>updateMCPServerField(name, 'description', e.target.value)} placeholder={t('configLabels.description')} className="md:col-span-1 px-2 py-1 rounded-lg bg-zinc-950/70 border border-zinc-800" />
|
||||
<button onClick={()=>installMCPServerPackage(name, server)} className="md:col-span-1 px-2 py-1 rounded bg-emerald-900/60 hover:bg-emerald-800 text-emerald-100">{t('install')}</button>
|
||||
<button onClick={()=>removeMCPServer(name)} className="md:col-span-1 px-2 py-1 rounded bg-red-900/60 hover:bg-red-800 text-red-100">{t('delete')}</button>
|
||||
</div>
|
||||
))}
|
||||
{Object.keys((((cfg as any)?.tools?.mcp?.servers) || {}) as Record<string, any>).length === 0 && (
|
||||
<div className="text-xs text-zinc-500">{t('configNoMCPServers')}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="brand-card-subtle rounded-2xl border border-zinc-800 p-3 space-y-3">
|
||||
<div className="flex items-center justify-between gap-2 flex-wrap">
|
||||
<div className="text-sm font-semibold text-zinc-200">{t('configMCPDiscoveredTools')}</div>
|
||||
<div className="text-xs text-zinc-500">{t('configMCPDiscoveredToolsCount', { count: mcpTools.length })}</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{mcpTools.map((tool) => (
|
||||
<div key={tool.name} className="rounded-xl border border-zinc-800 bg-zinc-900/30 p-3">
|
||||
<div className="flex items-center justify-between gap-3 flex-wrap">
|
||||
<div className="font-mono text-xs text-zinc-200">{tool.name}</div>
|
||||
<div className="text-[11px] text-zinc-500">
|
||||
{(tool.mcp?.server || '-')}{' · '}{(tool.mcp?.remote_tool || '-')}
|
||||
</div>
|
||||
</div>
|
||||
{tool.description && (
|
||||
<div className="mt-2 text-xs text-zinc-400">{tool.description}</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{mcpTools.length === 0 && (
|
||||
<div className="text-xs text-zinc-500">{t('configNoMCPDiscoveredTools')}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MCP;
|
||||
Reference in New Issue
Block a user