diff --git a/packages/desktop/src/App.tsx b/packages/desktop/src/App.tsx
index 2762dab..04b425f 100644
--- a/packages/desktop/src/App.tsx
+++ b/packages/desktop/src/App.tsx
@@ -8,6 +8,7 @@ import {
FileBrowser,
ConfigPanel,
CommandPanel,
+ MCPPanel,
Toaster,
listSessions,
createSession,
@@ -21,6 +22,7 @@ export function App() {
const [showFileBrowser, setShowFileBrowser] = useState(false);
const [showConfig, setShowConfig] = useState(false);
const [showCommands, setShowCommands] = useState(false);
+ const [showMCP, setShowMCP] = useState(false);
const [sessionTitleUpdate, setSessionTitleUpdate] = useState<{ sessionId: string; name: string } | null>(null);
// 初始化:加载或创建会话
@@ -92,6 +94,7 @@ export function App() {
onToggleFileBrowser={() => setShowFileBrowser(!showFileBrowser)}
onOpenConfig={() => setShowConfig(true)}
onOpenCommands={() => setShowCommands(true)}
+ onOpenMCP={() => setShowMCP(true)}
/>
) : (
@@ -118,6 +121,9 @@ export function App() {
{/* 命令面板 */}
{showCommands && setShowCommands(false)} />}
+ {/* MCP 面板 */}
+ {showMCP && setShowMCP(false)} />}
+
{/* Toast 通知 */}
diff --git a/packages/desktop/src/pages/Chat.tsx b/packages/desktop/src/pages/Chat.tsx
index 638efc2..c68c8a5 100644
--- a/packages/desktop/src/pages/Chat.tsx
+++ b/packages/desktop/src/pages/Chat.tsx
@@ -3,7 +3,7 @@
*/
import { useEffect, useRef } from 'react';
-import { WifiOff, MessageSquare, Settings, FolderOpen, Terminal } from 'lucide-react';
+import { WifiOff, MessageSquare, Settings, FolderOpen, Terminal, Plug } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import {
useChat,
@@ -21,6 +21,7 @@ interface ChatPageProps {
onToggleFileBrowser?: () => void;
onOpenConfig?: () => void;
onOpenCommands?: () => void;
+ onOpenMCP?: () => void;
}
export function ChatPage({
@@ -30,6 +31,7 @@ export function ChatPage({
onToggleFileBrowser,
onOpenConfig,
onOpenCommands,
+ onOpenMCP,
}: ChatPageProps) {
const {
messages,
@@ -123,8 +125,21 @@ export function ChatPage({
{/* 工具栏按钮 */}
- {(onOpenConfig || onToggleFileBrowser || onOpenCommands) && (
+ {(onOpenConfig || onToggleFileBrowser || onOpenCommands || onOpenMCP) && (
+ {/* MCP 按钮 */}
+ {onOpenMCP && (
+
+
+
+ )}
+
{/* 命令按钮 */}
{onOpenCommands && (
MCPManager;
+ loadMCPConfig: (workdir: string) => Promise;
+}
+
+interface MCPManager {
+ initialize(config: MCPConfig): Promise;
+ shutdown(): Promise;
+ reconnect(serverName: string): Promise;
+ setServerEnabled(serverName: string, enabled: boolean): Promise;
+ getServerStatuses(): MCPServerStatus[];
+ getServerStatus(name: string): MCPServerStatus | undefined;
+ getTools(): MCPTool[];
+ getTool(name: string): MCPTool | undefined;
+ isInitialized(): boolean;
+}
+
+interface MCPConfig {
+ mcp?: Record;
+ tools?: Record;
+}
+
+interface MCPServerConfig {
+ type: 'local' | 'remote';
+ command?: string[];
+ url?: string;
+ env?: Record;
+ cwd?: string;
+ enabled?: boolean;
+ timeout?: number;
+}
+
+interface MCPServerStatus {
+ name: string;
+ type: 'local' | 'remote';
+ status: 'connected' | 'connecting' | 'disconnected' | 'disabled' | 'error';
+ toolCount: number;
+ error?: string;
+ lastConnected?: Date;
+}
+
+interface MCPTool {
+ server: string;
+ name: string;
+ originalName: string;
+ description: string;
+ inputSchema: Record;
+}
+
+export const mcpRouter = new Hono();
+
+// Core 模块缓存
+let mcpModule: MCPModule | null = null;
+let currentConfig: MCPConfig | null = null;
+
+/**
+ * 初始化 MCP 模块
+ */
+async function initMCPModule(): Promise {
+ if (mcpModule) return mcpModule;
+
+ try {
+ const corePath = '@ai-assistant/core';
+ const core = (await import(corePath)) as Record;
+
+ if (
+ typeof core.getMCPManager !== 'function' ||
+ typeof core.loadMCPConfig !== 'function'
+ ) {
+ console.warn('[MCP] Core module missing MCP exports');
+ return null;
+ }
+
+ mcpModule = {
+ getMCPManager: core.getMCPManager as () => MCPManager,
+ loadMCPConfig: core.loadMCPConfig as (workdir: string) => Promise,
+ };
+
+ // 初始化 MCP Manager
+ const config = getConfig();
+ currentConfig = await mcpModule.loadMCPConfig(config.workdir);
+ const manager = mcpModule.getMCPManager();
+
+ if (!manager.isInitialized() && currentConfig.mcp) {
+ await manager.initialize(currentConfig);
+ }
+
+ console.log('[MCP] MCP module initialized');
+ return mcpModule;
+ } catch (error) {
+ console.warn('[MCP] Failed to load MCP module:', error);
+ return null;
+ }
+}
+
+/**
+ * GET /mcp/servers - 获取所有服务器状态
+ */
+mcpRouter.get('/servers', async (c) => {
+ const module = await initMCPModule();
+
+ if (!module) {
+ return c.json(
+ {
+ success: false,
+ error: 'MCP module not available',
+ },
+ 503
+ );
+ }
+
+ const manager = module.getMCPManager();
+ const statuses = manager.getServerStatuses();
+
+ // 添加配置信息
+ const serversWithConfig = statuses.map((status) => {
+ const serverConfig = currentConfig?.mcp?.[status.name];
+ return {
+ ...status,
+ config: serverConfig
+ ? {
+ type: serverConfig.type,
+ command: serverConfig.type === 'local' ? serverConfig.command : undefined,
+ url: serverConfig.type === 'remote' ? serverConfig.url : undefined,
+ timeout: serverConfig.timeout,
+ }
+ : undefined,
+ };
+ });
+
+ return c.json({
+ success: true,
+ data: serversWithConfig,
+ });
+});
+
+/**
+ * GET /mcp/servers/:name - 获取单个服务器详情
+ */
+mcpRouter.get('/servers/:name', async (c) => {
+ const name = c.req.param('name');
+ const module = await initMCPModule();
+
+ if (!module) {
+ return c.json(
+ {
+ success: false,
+ error: 'MCP module not available',
+ },
+ 503
+ );
+ }
+
+ const manager = module.getMCPManager();
+ const status = manager.getServerStatus(name);
+
+ if (!status) {
+ return c.json(
+ {
+ success: false,
+ error: `Server not found: ${name}`,
+ },
+ 404
+ );
+ }
+
+ const serverConfig = currentConfig?.mcp?.[name];
+ const tools = manager
+ .getTools()
+ .filter((tool) => tool.server === name);
+
+ return c.json({
+ success: true,
+ data: {
+ ...status,
+ config: serverConfig
+ ? {
+ type: serverConfig.type,
+ command: serverConfig.type === 'local' ? serverConfig.command : undefined,
+ url: serverConfig.type === 'remote' ? serverConfig.url : undefined,
+ timeout: serverConfig.timeout,
+ cwd: serverConfig.type === 'local' ? serverConfig.cwd : undefined,
+ }
+ : undefined,
+ tools: tools.map((t) => ({
+ name: t.name,
+ originalName: t.originalName,
+ description: t.description,
+ })),
+ },
+ });
+});
+
+/**
+ * POST /mcp/servers/:name/connect - 连接服务器
+ */
+mcpRouter.post('/servers/:name/connect', async (c) => {
+ const name = c.req.param('name');
+ const module = await initMCPModule();
+
+ if (!module) {
+ return c.json(
+ {
+ success: false,
+ error: 'MCP module not available',
+ },
+ 503
+ );
+ }
+
+ const manager = module.getMCPManager();
+
+ try {
+ await manager.reconnect(name);
+
+ return c.json({
+ success: true,
+ data: {
+ message: `Server ${name} connected`,
+ status: manager.getServerStatus(name),
+ },
+ });
+ } catch (error) {
+ return c.json(
+ {
+ success: false,
+ error: error instanceof Error ? error.message : 'Connection failed',
+ },
+ 500
+ );
+ }
+});
+
+/**
+ * POST /mcp/servers/:name/disconnect - 断开服务器
+ */
+mcpRouter.post('/servers/:name/disconnect', async (c) => {
+ const name = c.req.param('name');
+ const module = await initMCPModule();
+
+ if (!module) {
+ return c.json(
+ {
+ success: false,
+ error: 'MCP module not available',
+ },
+ 503
+ );
+ }
+
+ const manager = module.getMCPManager();
+
+ try {
+ // 通过禁用来断开连接
+ await manager.setServerEnabled(name, false);
+
+ return c.json({
+ success: true,
+ data: {
+ message: `Server ${name} disconnected`,
+ status: manager.getServerStatus(name),
+ },
+ });
+ } catch (error) {
+ return c.json(
+ {
+ success: false,
+ error: error instanceof Error ? error.message : 'Disconnect failed',
+ },
+ 500
+ );
+ }
+});
+
+/**
+ * POST /mcp/servers/:name/enable - 启用服务器
+ */
+mcpRouter.post('/servers/:name/enable', async (c) => {
+ const name = c.req.param('name');
+ const module = await initMCPModule();
+
+ if (!module) {
+ return c.json(
+ {
+ success: false,
+ error: 'MCP module not available',
+ },
+ 503
+ );
+ }
+
+ const manager = module.getMCPManager();
+
+ try {
+ await manager.setServerEnabled(name, true);
+
+ return c.json({
+ success: true,
+ data: {
+ message: `Server ${name} enabled`,
+ status: manager.getServerStatus(name),
+ },
+ });
+ } catch (error) {
+ return c.json(
+ {
+ success: false,
+ error: error instanceof Error ? error.message : 'Enable failed',
+ },
+ 500
+ );
+ }
+});
+
+/**
+ * POST /mcp/servers/:name/disable - 禁用服务器
+ */
+mcpRouter.post('/servers/:name/disable', async (c) => {
+ const name = c.req.param('name');
+ const module = await initMCPModule();
+
+ if (!module) {
+ return c.json(
+ {
+ success: false,
+ error: 'MCP module not available',
+ },
+ 503
+ );
+ }
+
+ const manager = module.getMCPManager();
+
+ try {
+ await manager.setServerEnabled(name, false);
+
+ return c.json({
+ success: true,
+ data: {
+ message: `Server ${name} disabled`,
+ status: manager.getServerStatus(name),
+ },
+ });
+ } catch (error) {
+ return c.json(
+ {
+ success: false,
+ error: error instanceof Error ? error.message : 'Disable failed',
+ },
+ 500
+ );
+ }
+});
+
+/**
+ * GET /mcp/tools - 获取所有 MCP 工具
+ */
+mcpRouter.get('/tools', async (c) => {
+ const module = await initMCPModule();
+
+ if (!module) {
+ return c.json(
+ {
+ success: false,
+ error: 'MCP module not available',
+ },
+ 503
+ );
+ }
+
+ const manager = module.getMCPManager();
+ const tools = manager.getTools();
+
+ return c.json({
+ success: true,
+ data: tools.map((tool) => ({
+ name: tool.name,
+ server: tool.server,
+ originalName: tool.originalName,
+ description: tool.description,
+ inputSchema: tool.inputSchema,
+ })),
+ });
+});
+
+/**
+ * GET /mcp/tools/:name - 获取单个工具详情
+ */
+mcpRouter.get('/tools/:name', async (c) => {
+ const name = c.req.param('name');
+ const module = await initMCPModule();
+
+ if (!module) {
+ return c.json(
+ {
+ success: false,
+ error: 'MCP module not available',
+ },
+ 503
+ );
+ }
+
+ const manager = module.getMCPManager();
+ const tool = manager.getTool(name);
+
+ if (!tool) {
+ return c.json(
+ {
+ success: false,
+ error: `Tool not found: ${name}`,
+ },
+ 404
+ );
+ }
+
+ return c.json({
+ success: true,
+ data: {
+ name: tool.name,
+ server: tool.server,
+ originalName: tool.originalName,
+ description: tool.description,
+ inputSchema: tool.inputSchema,
+ },
+ });
+});
+
+/**
+ * GET /mcp/config - 获取 MCP 配置
+ */
+mcpRouter.get('/config', async (c) => {
+ const module = await initMCPModule();
+
+ if (!module) {
+ return c.json(
+ {
+ success: false,
+ error: 'MCP module not available',
+ },
+ 503
+ );
+ }
+
+ // 返回配置(隐藏敏感信息)
+ const safeConfig: MCPConfig = {
+ mcp: {},
+ tools: currentConfig?.tools,
+ };
+
+ if (currentConfig?.mcp) {
+ for (const [name, config] of Object.entries(currentConfig.mcp)) {
+ safeConfig.mcp![name] = {
+ type: config.type,
+ command: config.type === 'local' ? config.command : undefined,
+ url: config.type === 'remote' ? config.url : undefined,
+ enabled: config.enabled,
+ timeout: config.timeout,
+ };
+ }
+ }
+
+ return c.json({
+ success: true,
+ data: safeConfig,
+ });
+});
diff --git a/packages/ui/src/api/client.ts b/packages/ui/src/api/client.ts
index 8a978fb..5f7f262 100644
--- a/packages/ui/src/api/client.ts
+++ b/packages/ui/src/api/client.ts
@@ -17,6 +17,9 @@ import type {
CreateCommandInput,
UpdateCommandInput,
CommandContent,
+ MCPServerStatus,
+ MCPToolInfo,
+ MCPConfig,
} from './types.js';
// Re-export types
@@ -37,6 +40,11 @@ export type {
CreateCommandInput,
UpdateCommandInput,
CommandContent,
+ MCPServerStatus,
+ MCPServerStatusType,
+ MCPToolInfo,
+ MCPConfig,
+ MCPServerConfigInfo,
} from './types.js';
// API Configuration
@@ -235,3 +243,104 @@ export async function deleteCommand(
): Promise<{ success: boolean; data?: { name: string; path: string }; error?: string }> {
return request('DELETE', `/commands/${encodeURIComponent(name)}`);
}
+
+// ============ MCP API ============
+
+/**
+ * 获取所有 MCP 服务器状态
+ */
+export async function listMCPServers(): Promise<{
+ success: boolean;
+ data: MCPServerStatus[];
+ error?: string;
+}> {
+ return request('GET', '/mcp/servers');
+}
+
+/**
+ * 获取单个 MCP 服务器详情
+ */
+export async function getMCPServer(name: string): Promise<{
+ success: boolean;
+ data?: MCPServerStatus;
+ error?: string;
+}> {
+ return request('GET', `/mcp/servers/${encodeURIComponent(name)}`);
+}
+
+/**
+ * 连接 MCP 服务器
+ */
+export async function connectMCPServer(name: string): Promise<{
+ success: boolean;
+ data?: { message: string; status: MCPServerStatus };
+ error?: string;
+}> {
+ return request('POST', `/mcp/servers/${encodeURIComponent(name)}/connect`);
+}
+
+/**
+ * 断开 MCP 服务器
+ */
+export async function disconnectMCPServer(name: string): Promise<{
+ success: boolean;
+ data?: { message: string; status: MCPServerStatus };
+ error?: string;
+}> {
+ return request('POST', `/mcp/servers/${encodeURIComponent(name)}/disconnect`);
+}
+
+/**
+ * 启用 MCP 服务器
+ */
+export async function enableMCPServer(name: string): Promise<{
+ success: boolean;
+ data?: { message: string; status: MCPServerStatus };
+ error?: string;
+}> {
+ return request('POST', `/mcp/servers/${encodeURIComponent(name)}/enable`);
+}
+
+/**
+ * 禁用 MCP 服务器
+ */
+export async function disableMCPServer(name: string): Promise<{
+ success: boolean;
+ data?: { message: string; status: MCPServerStatus };
+ error?: string;
+}> {
+ return request('POST', `/mcp/servers/${encodeURIComponent(name)}/disable`);
+}
+
+/**
+ * 获取所有 MCP 工具
+ */
+export async function listMCPTools(): Promise<{
+ success: boolean;
+ data: MCPToolInfo[];
+ error?: string;
+}> {
+ return request('GET', '/mcp/tools');
+}
+
+/**
+ * 获取单个 MCP 工具详情
+ */
+export async function getMCPTool(name: string): Promise<{
+ success: boolean;
+ data?: MCPToolInfo;
+ error?: string;
+}> {
+ return request('GET', `/mcp/tools/${encodeURIComponent(name)}`);
+}
+
+/**
+ * 获取 MCP 配置
+ */
+export async function getMCPConfig(): Promise<{
+ success: boolean;
+ data?: MCPConfig;
+ error?: string;
+}> {
+ return request('GET', '/mcp/config');
+}
diff --git a/packages/ui/src/api/types.ts b/packages/ui/src/api/types.ts
index 1f8d643..8b34f0a 100644
--- a/packages/ui/src/api/types.ts
+++ b/packages/ui/src/api/types.ts
@@ -171,3 +171,68 @@ export interface CommandContent {
source: string;
sourcePath?: string;
}
+
+// ============ MCP 相关 ============
+
+/** MCP 服务器状态类型 */
+export type MCPServerStatusType =
+ | 'connected'
+ | 'connecting'
+ | 'disconnected'
+ | 'disabled'
+ | 'error';
+
+/** MCP 服务器状态 */
+export interface MCPServerStatus {
+ /** 服务器名称 */
+ name: string;
+ /** 服务器类型 */
+ type: 'local' | 'remote';
+ /** 当前状态 */
+ status: MCPServerStatusType;
+ /** 工具数量 */
+ toolCount: number;
+ /** 错误信息 */
+ error?: string;
+ /** 配置信息 */
+ config?: {
+ type: 'local' | 'remote';
+ command?: string[];
+ url?: string;
+ timeout?: number;
+ cwd?: string;
+ };
+ /** 工具列表(仅详情接口返回) */
+ tools?: MCPToolInfo[];
+}
+
+/** MCP 工具信息 */
+export interface MCPToolInfo {
+ /** 完整工具名: {server}-{originalName} */
+ name: string;
+ /** 来源服务器名称 */
+ server: string;
+ /** MCP 服务器中的原始名称 */
+ originalName: string;
+ /** 工具描述 */
+ description: string;
+ /** 输入参数 JSON Schema */
+ inputSchema?: Record;
+}
+
+/** MCP 配置 */
+export interface MCPConfig {
+ /** MCP 服务器配置 */
+ mcp?: Record;
+ /** 工具启用/禁用配置 */
+ tools?: Record;
+}
+
+/** MCP 服务器配置信息 */
+export interface MCPServerConfigInfo {
+ type: 'local' | 'remote';
+ command?: string[];
+ url?: string;
+ enabled?: boolean;
+ timeout?: number;
+}
diff --git a/packages/ui/src/components/MCPPanel.tsx b/packages/ui/src/components/MCPPanel.tsx
new file mode 100644
index 0000000..f4f7a8b
--- /dev/null
+++ b/packages/ui/src/components/MCPPanel.tsx
@@ -0,0 +1,509 @@
+/**
+ * MCPPanel Component
+ *
+ * MCP 服务器管理面板:列出服务器状态、连接/断开、启用/禁用
+ */
+
+import { useState, useEffect, useCallback } from 'react';
+import {
+ X,
+ RefreshCw,
+ Plug,
+ PlugZap,
+ Power,
+ PowerOff,
+ ChevronDown,
+ ChevronRight,
+ Server,
+ Globe,
+ Wrench,
+ AlertCircle,
+} from 'lucide-react';
+import { motion, AnimatePresence } from 'framer-motion';
+import { toast } from 'sonner';
+import { cn } from '../utils/cn';
+import { modalOverlay, modalContent, smoothTransition } from '../utils/animations';
+import { Button } from '../primitives/Button';
+import { Skeleton } from './Skeleton';
+import {
+ listMCPServers,
+ connectMCPServer,
+ disconnectMCPServer,
+ enableMCPServer,
+ disableMCPServer,
+ type MCPServerStatus,
+} from '../api/client.js';
+
+interface MCPPanelProps {
+ onClose: () => void;
+ /** 是否启用响应式布局 */
+ responsive?: boolean;
+}
+
+// 状态颜色映射
+function getStatusColor(status: MCPServerStatus['status']) {
+ switch (status) {
+ case 'connected':
+ return 'bg-green-500';
+ case 'connecting':
+ return 'bg-yellow-500 animate-pulse';
+ case 'disconnected':
+ return 'bg-gray-500';
+ case 'disabled':
+ return 'bg-gray-600';
+ case 'error':
+ return 'bg-red-500';
+ default:
+ return 'bg-gray-500';
+ }
+}
+
+// 状态文字
+function getStatusText(status: MCPServerStatus['status']) {
+ switch (status) {
+ case 'connected':
+ return 'Connected';
+ case 'connecting':
+ return 'Connecting...';
+ case 'disconnected':
+ return 'Disconnected';
+ case 'disabled':
+ return 'Disabled';
+ case 'error':
+ return 'Error';
+ default:
+ return status;
+ }
+}
+
+// 类型图标
+function getTypeIcon(type: 'local' | 'remote') {
+ return type === 'local' ? (
+
+ ) : (
+
+ );
+}
+
+export function MCPPanel({ onClose, responsive = false }: MCPPanelProps) {
+ // 数据状态
+ const [servers, setServers] = useState([]);
+ const [expandedServers, setExpandedServers] = useState>(new Set());
+
+ // UI 状态
+ const [loading, setLoading] = useState(true);
+ const [refreshing, setRefreshing] = useState(false);
+ const [actionLoading, setActionLoading] = useState(null);
+
+ // 加载服务器列表
+ const loadServers = useCallback(async (showToast = false) => {
+ try {
+ const result = await listMCPServers();
+ if (result.success) {
+ setServers(result.data);
+ if (showToast) {
+ toast.success('Servers refreshed');
+ }
+ } else {
+ toast.error(result.error || 'Failed to load servers');
+ }
+ } catch (err) {
+ toast.error(err instanceof Error ? err.message : 'Failed to load servers');
+ }
+ }, []);
+
+ // 初始加载
+ useEffect(() => {
+ setLoading(true);
+ loadServers().finally(() => setLoading(false));
+ }, [loadServers]);
+
+ // 刷新
+ const handleRefresh = async () => {
+ setRefreshing(true);
+ await loadServers(true);
+ setRefreshing(false);
+ };
+
+ // 连接服务器
+ const handleConnect = async (name: string) => {
+ setActionLoading(name);
+ try {
+ const result = await connectMCPServer(name);
+ if (result.success) {
+ toast.success(`Server "${name}" connected`);
+ await loadServers();
+ } else {
+ toast.error(result.error || 'Connection failed');
+ }
+ } catch (err) {
+ toast.error(err instanceof Error ? err.message : 'Connection failed');
+ } finally {
+ setActionLoading(null);
+ }
+ };
+
+ // 断开服务器
+ const handleDisconnect = async (name: string) => {
+ setActionLoading(name);
+ try {
+ const result = await disconnectMCPServer(name);
+ if (result.success) {
+ toast.success(`Server "${name}" disconnected`);
+ await loadServers();
+ } else {
+ toast.error(result.error || 'Disconnect failed');
+ }
+ } catch (err) {
+ toast.error(err instanceof Error ? err.message : 'Disconnect failed');
+ } finally {
+ setActionLoading(null);
+ }
+ };
+
+ // 启用服务器
+ const handleEnable = async (name: string) => {
+ setActionLoading(name);
+ try {
+ const result = await enableMCPServer(name);
+ if (result.success) {
+ toast.success(`Server "${name}" enabled`);
+ await loadServers();
+ } else {
+ toast.error(result.error || 'Enable failed');
+ }
+ } catch (err) {
+ toast.error(err instanceof Error ? err.message : 'Enable failed');
+ } finally {
+ setActionLoading(null);
+ }
+ };
+
+ // 禁用服务器
+ const handleDisable = async (name: string) => {
+ setActionLoading(name);
+ try {
+ const result = await disableMCPServer(name);
+ if (result.success) {
+ toast.success(`Server "${name}" disabled`);
+ await loadServers();
+ } else {
+ toast.error(result.error || 'Disable failed');
+ }
+ } catch (err) {
+ toast.error(err instanceof Error ? err.message : 'Disable failed');
+ } finally {
+ setActionLoading(null);
+ }
+ };
+
+ // 切换展开
+ const toggleExpanded = (name: string) => {
+ const newExpanded = new Set(expandedServers);
+ if (newExpanded.has(name)) {
+ newExpanded.delete(name);
+ } else {
+ newExpanded.add(name);
+ }
+ setExpandedServers(newExpanded);
+ };
+
+ // 统计
+ const connectedCount = servers.filter((s) => s.status === 'connected').length;
+ const totalToolCount = servers.reduce((sum, s) => sum + s.toolCount, 0);
+
+ // Loading 骨架屏
+ const LoadingSkeleton = () => (
+
+ {[1, 2, 3].map((i) => (
+
+ ))}
+
+ );
+
+ return (
+
+
+ e.stopPropagation()}
+ className={cn(
+ 'bg-gray-800 max-h-[90vh] overflow-hidden flex flex-col',
+ responsive
+ ? 'w-full md:w-full md:max-w-2xl md:mx-4 rounded-t-2xl md:rounded-lg'
+ : 'rounded-lg w-full max-w-2xl mx-4'
+ )}
+ >
+ {/* Header */}
+
+ {responsive && (
+
+ )}
+
+
+
+ MCP Servers
+
+
+ {servers.length} servers ({connectedCount} connected, {totalToolCount} tools)
+
+
+
+
+
+
+
+
+ {/* Server List */}
+
+ {loading ? (
+
+ ) : servers.length === 0 ? (
+
+
+
No MCP servers configured
+
+ Configure MCP servers in your .ai-assist/config.json file
+
+
+ ) : (
+
+ {servers.map((server) => {
+ const isExpanded = expandedServers.has(server.name);
+ const isLoading = actionLoading === server.name;
+
+ return (
+
+ {/* Server Header */}
+ toggleExpanded(server.name)}
+ >
+ {/* Expand Icon */}
+
+
+ {/* Status Indicator */}
+
+
+ {/* Type Icon */}
+ {getTypeIcon(server.type)}
+
+ {/* Info */}
+
+
+ {server.name}
+ {server.type}
+
+
+ {getStatusText(server.status)}
+ {server.toolCount > 0 && (
+ <>
+ ·
+
+
+ {server.toolCount} tools
+
+ >
+ )}
+
+
+
+ {/* Actions */}
+
e.stopPropagation()}
+ >
+ {isLoading ? (
+
+ ) : server.status === 'disabled' ? (
+
+ ) : server.status === 'connected' ? (
+ <>
+
+
+ >
+ ) : server.status === 'disconnected' || server.status === 'error' ? (
+
+ ) : null}
+
+
+
+ {/* Expanded Content */}
+
+ {isExpanded && (
+
+
+ {/* Error Message */}
+ {server.error && (
+
+ )}
+
+ {/* Config Info */}
+ {server.config && (
+
+ {server.config.command && (
+
+ Command:{' '}
+
+ {server.config.command.join(' ')}
+
+
+ )}
+ {server.config.url && (
+
+ URL:{' '}
+
+ {server.config.url}
+
+
+ )}
+ {server.config.timeout && (
+
+ Timeout:{' '}
+ {server.config.timeout}ms
+
+ )}
+
+ )}
+
+ {/* Tools List */}
+ {server.tools && server.tools.length > 0 && (
+
+
Tools:
+
+ {server.tools.map((tool) => (
+
+ {tool.originalName}
+
+ ))}
+
+
+ )}
+
+
+ )}
+
+
+ );
+ })}
+
+ )}
+
+
+ {/* Footer Info */}
+
+ Configure servers in .ai-assist/config.json
+
+
+
+
+ );
+}
diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts
index 743c9f0..15ee077 100644
--- a/packages/ui/src/index.ts
+++ b/packages/ui/src/index.ts
@@ -32,6 +32,16 @@ export {
getCommandContent,
updateCommand,
deleteCommand,
+ // MCP API
+ listMCPServers,
+ getMCPServer,
+ connectMCPServer,
+ disconnectMCPServer,
+ enableMCPServer,
+ disableMCPServer,
+ listMCPTools,
+ getMCPTool,
+ getMCPConfig,
} from './api/client.js';
// Types
@@ -54,6 +64,12 @@ export type {
CreateCommandInput,
UpdateCommandInput,
CommandContent,
+ // MCP types
+ MCPServerStatus,
+ MCPServerStatusType,
+ MCPToolInfo,
+ MCPConfig,
+ MCPServerConfigInfo,
} from './api/client.js';
// Primitives (shadcn/ui style)
@@ -69,6 +85,7 @@ export { ChatInput } from './components/ChatInput.js';
export { CommandMenu, type CommandMenuItem } from './components/CommandMenu.js';
export { CommandPanel } from './components/CommandPanel.js';
export { CommandEditor } from './components/CommandEditor.js';
+export { MCPPanel } from './components/MCPPanel.js';
export { Sidebar } from './components/Sidebar.js';
export { FileBrowser } from './components/FileBrowser.js';
export { ConfigPanel } from './components/ConfigPanel.js';
diff --git a/packages/web/src/App.tsx b/packages/web/src/App.tsx
index 999abf4..710ac6d 100644
--- a/packages/web/src/App.tsx
+++ b/packages/web/src/App.tsx
@@ -10,6 +10,7 @@ import {
FileBrowser,
ConfigPanel,
CommandPanel,
+ MCPPanel,
Toaster,
listSessions,
createSession,
@@ -23,6 +24,7 @@ export function App() {
const [showFileBrowser, setShowFileBrowser] = useState(false);
const [showConfig, setShowConfig] = useState(false);
const [showCommands, setShowCommands] = useState(false);
+ const [showMCP, setShowMCP] = useState(false);
const [sessionTitleUpdate, setSessionTitleUpdate] = useState<{ sessionId: string; name: string } | null>(null);
// 初始化:加载或创建会话
@@ -108,6 +110,7 @@ export function App() {
onToggleFileBrowser={() => setShowFileBrowser(!showFileBrowser)}
onOpenConfig={() => setShowConfig(true)}
onOpenCommands={() => setShowCommands(true)}
+ onOpenMCP={() => setShowMCP(true)}
/>
) : (
@@ -159,6 +162,9 @@ export function App() {
{/* 命令面板 */}
{showCommands &&
setShowCommands(false)} responsive />}
+ {/* MCP 面板 */}
+ {showMCP && setShowMCP(false)} responsive />}
+
{/* 移动端底部文件按钮 */}