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

- 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:
iven
2026-04-04 01:30:13 +08:00
parent 0be31bbf7e
commit 912f117ea3
2 changed files with 168 additions and 30 deletions

View File

@@ -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>
);

View File

@@ -23,7 +23,15 @@ export interface QuickConfig {
gatewayUrl?: string;
gatewayToken?: 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';
autoStart?: boolean;
showToolCalls?: boolean;