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 { useConfigStore } from '../../store/configStore';
|
||||||
import { silentErrorHandler } from '../../lib/error-utils';
|
import { silentErrorHandler } from '../../lib/error-utils';
|
||||||
|
import {
|
||||||
|
listMcpServices,
|
||||||
|
startMcpService,
|
||||||
|
stopMcpService,
|
||||||
|
type McpServiceConfig,
|
||||||
|
type McpServiceStatus,
|
||||||
|
type McpToolInfo,
|
||||||
|
} from '../../lib/mcp-client';
|
||||||
|
|
||||||
export function MCPServices() {
|
export function MCPServices() {
|
||||||
const quickConfig = useConfigStore((s) => s.quickConfig);
|
const quickConfig = useConfigStore((s) => s.quickConfig);
|
||||||
const saveQuickConfig = useConfigStore((s) => s.saveQuickConfig);
|
const saveQuickConfig = useConfigStore((s) => s.saveQuickConfig);
|
||||||
|
|
||||||
const services = quickConfig.mcpServices || [];
|
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 toggleService = async (id: string) => {
|
||||||
const nextServices = services.map((service) =>
|
const svc = services.find((s) => s.id === id);
|
||||||
service.id === id ? { ...service, enabled: !service.enabled } : service
|
if (!svc) return;
|
||||||
);
|
|
||||||
await saveQuickConfig({ mcpServices: nextServices });
|
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 (
|
return (
|
||||||
<div className="max-w-3xl">
|
<div className="max-w-3xl">
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<h1 className="text-xl font-bold text-gray-900">MCP 服务</h1>
|
<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>
|
||||||
<div className="text-xs text-gray-500 mb-6">
|
<div className="text-xs text-gray-500 mb-6">
|
||||||
MCP(模型上下文协议)服务为 Agent 扩展外部工具 — 文件系统、数据库、网页搜索等。
|
MCP(模型上下文协议)服务为 Agent 扩展外部工具 — 文件系统、数据库、网页搜索等。
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white rounded-xl border border-gray-200 divide-y divide-gray-100 shadow-sm mb-6">
|
<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) => (
|
{services.length > 0 ? services.map((svc) => {
|
||||||
<div key={svc.id} className="flex justify-between items-center p-4">
|
const isRunning = runningMap.has(svc.name || svc.id);
|
||||||
<div className="flex items-center gap-3">
|
const status = runningMap.get(svc.name || svc.id);
|
||||||
{svc.id === 'filesystem'
|
const isLoading = loading === svc.id;
|
||||||
? <FileText className="w-4 h-4 text-gray-500" />
|
const showTools = expandedTools.has(svc.id);
|
||||||
: <Globe className="w-4 h-4 text-gray-500" />}
|
|
||||||
<div>
|
return (
|
||||||
<div className="text-sm text-gray-900">{svc.name}</div>
|
<div key={svc.id}>
|
||||||
<div className="text-xs text-gray-400 mt-1">{svc.id}</div>
|
<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>
|
</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>
|
||||||
<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">
|
<div className="p-8 text-center text-sm text-gray-400">
|
||||||
当前快速配置中尚未声明 MCP 服务
|
当前快速配置中尚未声明 MCP 服务
|
||||||
</div>
|
</div>
|
||||||
@@ -57,7 +187,7 @@ export function MCPServices() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-xs text-amber-700 bg-amber-50 rounded-lg p-3">
|
<div className="text-xs text-amber-700 bg-amber-50 rounded-lg p-3">
|
||||||
当前页面只支持查看和启停已保存在快速配置中的 MCP 服务;新增服务、删除服务和详细参数配置尚未在桌面端接入。
|
新增服务、删除服务和详细参数配置尚未在桌面端接入。可通过配置文件手动添加。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -23,7 +23,15 @@ export interface QuickConfig {
|
|||||||
gatewayUrl?: string;
|
gatewayUrl?: string;
|
||||||
gatewayToken?: string;
|
gatewayToken?: string;
|
||||||
skillsExtraDirs?: string[];
|
skillsExtraDirs?: string[];
|
||||||
mcpServices?: Array<{ id: string; name: string; enabled: boolean }>;
|
mcpServices?: Array<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
enabled: boolean;
|
||||||
|
command?: string;
|
||||||
|
args?: string[];
|
||||||
|
env?: Record<string, string>;
|
||||||
|
cwd?: string;
|
||||||
|
}>;
|
||||||
theme?: 'light' | 'dark';
|
theme?: 'light' | 'dark';
|
||||||
autoStart?: boolean;
|
autoStart?: boolean;
|
||||||
showToolCalls?: boolean;
|
showToolCalls?: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user