feat(desktop): wire MCP client to settings UI
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
- MCPServices.tsx now calls real Tauri commands (start/stop/list) instead of only toggling config flags - Show running service count, discovered tools per service - Expand/collapse tool list for each running MCP service - Extended QuickConfig mcpServices type with command/args/env/cwd - Config change persists enabled state, MCP start/stop happens live
This commit is contained in:
@@ -1,55 +1,185 @@
|
||||
import { FileText, Globe } from 'lucide-react';
|
||||
import { FileText, Globe, RefreshCw, Wrench } from 'lucide-react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useConfigStore } from '../../store/configStore';
|
||||
import { silentErrorHandler } from '../../lib/error-utils';
|
||||
import {
|
||||
listMcpServices,
|
||||
startMcpService,
|
||||
stopMcpService,
|
||||
type McpServiceConfig,
|
||||
type McpServiceStatus,
|
||||
type McpToolInfo,
|
||||
} from '../../lib/mcp-client';
|
||||
|
||||
export function MCPServices() {
|
||||
const quickConfig = useConfigStore((s) => s.quickConfig);
|
||||
const saveQuickConfig = useConfigStore((s) => s.saveQuickConfig);
|
||||
|
||||
const services = quickConfig.mcpServices || [];
|
||||
const [runningServices, setRunningServices] = useState<McpServiceStatus[]>([]);
|
||||
const [loading, setLoading] = useState<string | null>(null);
|
||||
const [expandedTools, setExpandedTools] = useState<Set<string>>(new Set());
|
||||
|
||||
// Fetch running services on mount
|
||||
const refreshRunning = useCallback(async () => {
|
||||
try {
|
||||
const running = await listMcpServices();
|
||||
setRunningServices(running);
|
||||
} catch {
|
||||
// MCP might not be available yet
|
||||
setRunningServices([]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
refreshRunning();
|
||||
}, [refreshRunning]);
|
||||
|
||||
const toggleTools = (name: string) => {
|
||||
setExpandedTools((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(name)) next.delete(name);
|
||||
else next.add(name);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const toggleService = async (id: string) => {
|
||||
const nextServices = services.map((service) =>
|
||||
service.id === id ? { ...service, enabled: !service.enabled } : service
|
||||
);
|
||||
await saveQuickConfig({ mcpServices: nextServices });
|
||||
const svc = services.find((s) => s.id === id);
|
||||
if (!svc) return;
|
||||
|
||||
setLoading(id);
|
||||
try {
|
||||
if (svc.enabled) {
|
||||
// Currently enabled → stop it
|
||||
await stopMcpService(svc.name || svc.id).catch(() => {});
|
||||
} else {
|
||||
// Currently disabled → start it
|
||||
const config: McpServiceConfig = {
|
||||
name: svc.name || svc.id,
|
||||
command: svc.command || '',
|
||||
args: svc.args,
|
||||
env: svc.env,
|
||||
cwd: svc.cwd,
|
||||
};
|
||||
await startMcpService(config);
|
||||
}
|
||||
// Update config flag
|
||||
const nextServices = services.map((s) =>
|
||||
s.id === id ? { ...s, enabled: !s.enabled } : s
|
||||
);
|
||||
await saveQuickConfig({ mcpServices: nextServices });
|
||||
// Refresh running status
|
||||
await refreshRunning();
|
||||
} catch (err) {
|
||||
console.error('[MCP] Toggle failed:', err);
|
||||
} finally {
|
||||
setLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Build a map of service name → running status for quick lookup
|
||||
const runningMap = new Map(runningServices.map((rs) => [rs.name, rs]));
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h1 className="text-xl font-bold text-gray-900">MCP 服务</h1>
|
||||
<span className="text-xs text-gray-400">{services.length} 个已声明服务</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-gray-400">
|
||||
{runningServices.length} 个运行中 / {services.length} 个已声明
|
||||
</span>
|
||||
<button
|
||||
onClick={() => refreshRunning().catch(() => {})}
|
||||
className="p-1.5 rounded-lg hover:bg-gray-100 transition-colors"
|
||||
title="刷新状态"
|
||||
>
|
||||
<RefreshCw className="w-3.5 h-3.5 text-gray-400" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mb-6">
|
||||
MCP(模型上下文协议)服务为 Agent 扩展外部工具 — 文件系统、数据库、网页搜索等。
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl border border-gray-200 divide-y divide-gray-100 shadow-sm mb-6">
|
||||
{services.length > 0 ? services.map((svc) => (
|
||||
<div key={svc.id} className="flex justify-between items-center p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{svc.id === 'filesystem'
|
||||
? <FileText className="w-4 h-4 text-gray-500" />
|
||||
: <Globe className="w-4 h-4 text-gray-500" />}
|
||||
<div>
|
||||
<div className="text-sm text-gray-900">{svc.name}</div>
|
||||
<div className="text-xs text-gray-400 mt-1">{svc.id}</div>
|
||||
{services.length > 0 ? services.map((svc) => {
|
||||
const isRunning = runningMap.has(svc.name || svc.id);
|
||||
const status = runningMap.get(svc.name || svc.id);
|
||||
const isLoading = loading === svc.id;
|
||||
const showTools = expandedTools.has(svc.id);
|
||||
|
||||
return (
|
||||
<div key={svc.id}>
|
||||
<div className="flex justify-between items-center p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{svc.id === 'filesystem'
|
||||
? <FileText className="w-4 h-4 text-gray-500" />
|
||||
: <Globe className="w-4 h-4 text-gray-500" />}
|
||||
<div>
|
||||
<div className="text-sm text-gray-900">{svc.name}</div>
|
||||
<div className="text-xs text-gray-400 mt-0.5">
|
||||
{svc.id}
|
||||
{svc.command && (
|
||||
<span className="ml-2 text-gray-300">|</span>
|
||||
)}
|
||||
{svc.command && (
|
||||
<span className="ml-2 font-mono text-gray-400">{svc.command}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
{isRunning && status && (
|
||||
<button
|
||||
onClick={() => toggleTools(svc.id)}
|
||||
className="flex items-center gap-1 text-xs px-2 py-1 rounded-full bg-blue-50 text-blue-600 hover:bg-blue-100 transition-colors"
|
||||
>
|
||||
<Wrench className="w-3 h-3" />
|
||||
{status.tool_count} 工具
|
||||
</button>
|
||||
)}
|
||||
<span className={`text-xs px-2 py-1 rounded-full ${
|
||||
isLoading ? 'bg-yellow-50 text-yellow-600' :
|
||||
isRunning ? 'bg-green-50 text-green-600' :
|
||||
svc.enabled ? 'bg-amber-50 text-amber-600' :
|
||||
'bg-gray-100 text-gray-500'
|
||||
}`}>
|
||||
{isLoading ? '处理中...' :
|
||||
isRunning ? '运行中' :
|
||||
svc.enabled ? '已启用' : '已停用'}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => toggleService(svc.id).catch(silentErrorHandler('MCPServices'))}
|
||||
disabled={isLoading}
|
||||
className="text-xs px-3 py-1.5 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{svc.enabled ? '停用' : '启用'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/* Expanded tools list */}
|
||||
{showTools && status && status.tools.length > 0 && (
|
||||
<div className="px-4 pb-3 border-t border-gray-50">
|
||||
<div className="text-xs text-gray-500 mb-2 mt-2">已发现的工具:</div>
|
||||
<div className="space-y-1">
|
||||
{status.tools.map((tool: McpToolInfo) => (
|
||||
<div key={`${tool.service_name}-${tool.tool_name}`} className="flex items-start gap-2 text-xs py-1">
|
||||
<Wrench className="w-3 h-3 text-gray-400 mt-0.5 shrink-0" />
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">{tool.tool_name}</span>
|
||||
{tool.description && (
|
||||
<span className="text-gray-400 ml-2">{tool.description}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<span className={`text-xs px-2 py-1 rounded-full ${svc.enabled ? 'bg-green-50 text-green-600' : 'bg-gray-100 text-gray-500'}`}>
|
||||
{svc.enabled ? '已启用' : '已停用'}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => { toggleService(svc.id).catch(silentErrorHandler('MCPServices')); }}
|
||||
className="text-xs px-3 py-1.5 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
{svc.enabled ? '停用' : '启用'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)) : (
|
||||
);
|
||||
}) : (
|
||||
<div className="p-8 text-center text-sm text-gray-400">
|
||||
当前快速配置中尚未声明 MCP 服务
|
||||
</div>
|
||||
@@ -57,7 +187,7 @@ export function MCPServices() {
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-amber-700 bg-amber-50 rounded-lg p-3">
|
||||
当前页面只支持查看和启停已保存在快速配置中的 MCP 服务;新增服务、删除服务和详细参数配置尚未在桌面端接入。
|
||||
新增服务、删除服务和详细参数配置尚未在桌面端接入。可通过配置文件手动添加。
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user