diff --git a/docs/spec.md b/docs/spec.md index 3bdbe69..be0bc3b 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -263,7 +263,19 @@ _代码修改确认_ ![命令执行确认](/screenshots/spec-bash-confirm.png) _命令执行确认_ -### 4.4 计划执行确认 (Plan Confirmation) {#plan-confirmation} +### 4.4 MCP 工具确认 (MCP Tool Confirmation) {#mcp-tool-confirmation} + +当 AI 调用通过 MCP(Model Context Protocol)连接的外部服务工具时,确认框会展示该工具的完整参数,让用户了解即将传递给 MCP 服务器的数据。 + +**主要特性:** +- **参数展示**:以格式化的 JSON 显示 MCP 工具的全部输入参数,方便用户审核。 +- **持久化规则**:支持为特定 MCP 工具设置"不再询问"规则(按工具名匹配,如 `mcp__wave_requirements__create_requirement`)。 +- **反馈机制**:支持用户拒绝并提供反馈,指导 AI 调整参数后重新调用。 + +![MCP 工具确认](/screenshots/spec-mcp-tool-confirm.png) +_MCP 工具确认_ + +### 4.5 计划执行确认 (Plan Confirmation) {#plan-confirmation} 当 AI 制定了详细的执行计划并准备退出计划模式时,会向用户展示计划内容并请求确认。用户可以选择批准执行、自动接受后续修改,或提供反馈意见。 @@ -276,7 +288,7 @@ _命令执行确认_ ![计划执行确认](/screenshots/spec-plan-confirm.png) _计划执行确认_ -### 4.5 进入计划模式确认 (Enter Plan Mode Confirmation) {#enter-plan-mode} +### 4.6 进入计划模式确认 (Enter Plan Mode Confirmation) {#enter-plan-mode} 当 AI 判断当前任务较为复杂时,会主动请求用户确认是否进入计划模式。与计划执行确认不同,进入计划模式确认仅提供两个选项:批准进入计划模式,或拒绝并直接开始实现。 @@ -292,7 +304,7 @@ _计划执行确认_ ![进入计划模式确认](/screenshots/spec-enter-plan-mode.png) _进入计划模式确认_ -### 4.6 错误消息展示 {#error-message-display} +### 4.7 错误消息展示 {#error-message-display} 当工具执行出错或 AI 返回错误信息时,Wave 会以醒目的方式展示错误内容。为了防止过长的错误消息占据过多屏幕空间,错误消息区域设置了最大高度,并支持内部滚动。 @@ -376,13 +388,25 @@ _Skill 系统_ ### 6.3 MCP 协议集成 {#mcp-integration} -支持 Model Context Protocol (MCP),允许 AI 连接到外部的 MCP 服务器,从而获取更多的上下文信息或调用外部工具。 +支持 Model Context Protocol (MCP),允许 AI 连接到外部的 MCP 服务器,从而获取更多的上下文信息或调用外部工具。用户可通过配置弹窗中的 **MCP 服务器** 标签页查看和管理所有已配置的 MCP 服务器。 -**提示:**用户可以通过内置的 `/settings` skill 来管理 MCP 配置,例如输入 `/settings 增加mcp:xxx` 即可快速添加新的 MCP 服务。 +**主要特性:** + +- **服务器列表**:展示所有通过 `.mcp.json` 配置的 MCP 服务器,包括名称、连接状态(○ 未连接 / ● 已连接 / ⟳ 连接中 / ✗ 错误)、工具数量和连接命令/URL。 +- **连接/断开控制**:支持一键连接或断开 MCP 服务器,连接状态实时刷新。 +- **错误信息展示**:连接失败的服务器会显示具体错误原因。 +- **空状态引导**:未配置任何服务器时,提示用户创建 `.mcp.json` 文件。 +- **配置方式**:在项目根目录创建 `.mcp.json` 文件,支持 stdio(本地进程)和 SSE/HTTP(远程服务)两种连接方式。 +- **工具确认**:AI 调用 MCP 工具时,确认框会展示格式化的 JSON 参数,用户可审核后再批准(详见 [第 4.4 节 MCP 工具确认](#mcp-tool-confirmation))。 + +**提示:**用户也可以通过内置的 `/settings` skill 来管理 MCP 配置,例如输入 `/settings 增加mcp:xxx` 即可快速添加新的 MCP 服务。 ![MCP 集成](/screenshots/spec-mcp.png) _MCP 集成_ +![MCP 服务器管理](/screenshots/spec-mcp-server-tab.png) +_MCP 服务器管理(配置弹窗中的 MCP 标签页)_ + ### 6.4 内置 Skills {#builtin-skills} #### settings — 配置管理 {#skill-settings} @@ -590,15 +614,18 @@ Wave 在后台自动维护项目记忆,帮助 AI 持续了解项目演变: ### 10.1 配置设置 {#configuration-settings} -用户可以自定义 AI 模型、API Key、Base URL 等关键参数,以适配不同的 AI 服务提供商。配置界面中的表单字段仅显示用户手动输入的值,不会被环境变量填充;但如果设置了相应的环境变量(如 `WAVE_BASE_URL`),其值会作为 placeholder 提示显示在输入框中。服务端链接字段默认 placeholder 为"请联系管理员获取"。 +用户可以自定义 AI 模型、API Key、Base URL 等关键参数,以适配不同的 AI 服务提供商。配置界面包含三个标签页:**常规设置**、**插件** 和 **MCP 服务器**。配置界面中的表单字段仅显示用户手动输入的值,不会被环境变量填充;但如果设置了相应的环境变量(如 `WAVE_BASE_URL`),其值会作为 placeholder 提示显示在输入框中。服务端链接字段默认 placeholder 为"请联系管理员获取"。 **主要特性:** -- **服务端链接**:配置 Wave AI 服务端地址,用于 SSO 认证。支持环境变量 `WAVE_SERVER_URL` 作为 fallback,默认 placeholder 提示"请联系管理员获取"。 -- **Model / Fast Model**:SSO 模式下也可配置主模型和快速模型名称,支持环境变量 `WAVE_MODEL` / `WAVE_FAST_MODEL` 作为 placeholder 提示。 -- **SSO 登录/登出**:当配置了服务端链接后,用户可通过 SSO 认证进行登录,无需手动配置 API Key。登录后所有 API 请求自动通过 Wave AI 代理路由。 - - **浏览器登录**:点击"SSO 登录"后自动打开浏览器,用户在 Wave AI 登录页完成认证(支持 SSO 企业身份提供商或账号密码登录),授权码通过 localhost 回调自动交换为 JWT 并保存。VS Code Remote SSH 环境会自动转发端口,远程服务器体验与本地一致。 - - **登录状态显示**:已认证时显示用户邮箱/ID 和登出按钮;登出后自动恢复为直接 LLM API 模式。 +- **常规设置**:配置 AI 模型、API Key、Base URL、Headers、语言等。 + - **服务端链接**:配置 Wave AI 服务端地址,用于 SSO 认证。支持环境变量 `WAVE_SERVER_URL` 作为 fallback,默认 placeholder 提示"请联系管理员获取"。 + - **Model / Fast Model**:SSO 模式下也可配置主模型和快速模型名称,支持环境变量 `WAVE_MODEL` / `WAVE_FAST_MODEL` 作为 placeholder 提示。 + - **SSO 登录/登出**:当配置了服务端链接后,用户可通过 SSO 认证进行登录,无需手动配置 API Key。登录后所有 API 请求自动通过 Wave AI 代理路由。 + - **浏览器登录**:点击"SSO 登录"后自动打开浏览器,用户在 Wave AI 登录页完成认证(支持 SSO 企业身份提供商或账号密码登录),授权码通过 localhost 回调自动交换为 JWT 并保存。VS Code Remote SSH 环境会自动转发端口,远程服务器体验与本地一致。 + - **登录状态显示**:已认证时显示用户邮箱/ID 和登出按钮;登出后自动恢复为直接 LLM API 模式。 +- **插件**:管理插件的安装、更新、卸载和插件市场(详见 [第 11 章 插件系统](#plugin-system))。 +- **MCP 服务器**:查看和管理 MCP 服务器连接状态(详见 [第 6.3 节 MCP 协议集成](#mcp-integration))。 ![配置设置](/screenshots/spec-configuration.png) _配置设置_ diff --git a/e2e/demo/mcp-server-tab.demo.ts b/e2e/demo/mcp-server-tab.demo.ts new file mode 100644 index 0000000..34abe81 --- /dev/null +++ b/e2e/demo/mcp-server-tab.demo.ts @@ -0,0 +1,193 @@ +import { test, expect } from '../utils/webviewTestHarness.js'; + +test.describe('MCP Server Tab Demo', () => { + test('should show MCP server tab with configured servers', async ({ webviewPage }) => { + // 1. Open the configuration dialog + await webviewPage.evaluate(() => { + (window as any).simulateExtensionMessage({ + command: 'showConfiguration', + configurationData: { + apiKey: 'test-key', + baseURL: 'https://api.example.com', + model: 'test-model', + fastModel: 'fast-model', + language: 'Chinese' + } + }); + }); + + // Verify dialog is visible + await expect(webviewPage.getByText('配置设置', { exact: true })).toBeVisible(); + + // 2. Click on "MCP 服务器" tab + await webviewPage.getByText('MCP 服务器', { exact: true }).click(); + + // 3. Simulate receiving MCP servers list + await webviewPage.evaluate(() => { + window.postMessage({ + command: 'mcpServersResponse', + servers: [ + { + name: 'sqlite', + config: { + command: 'uvx', + args: ['mcp-server-sqlite', '--db-path', '/path/to/db.db'] + }, + status: 'connected', + toolCount: 5, + capabilities: ['tools'], + lastConnected: Date.now() - 60000 + }, + { + name: 'github', + config: { + command: 'npx', + args: ['-y', '@modelcontextprotocol/server-github'], + env: { + GITHUB_PERSONAL_ACCESS_TOKEN: 'your-token-here' + } + }, + status: 'disconnected', + toolCount: 0, + capabilities: [] + }, + { + name: 'remote-server', + config: { + url: 'https://mcp-server.example.com/sse' + }, + status: 'error', + toolCount: 0, + error: 'Connection refused', + capabilities: [] + }, + { + name: 'fetch', + config: { + command: 'npx', + args: ['-y', '@mcp/server-fetch'] + }, + status: 'connecting', + toolCount: 0, + capabilities: ['tools'] + } + ] + }, '*'); + }); + + // Verify all four servers are visible + await expect(webviewPage.getByText('sqlite', { exact: true })).toBeVisible(); + await expect(webviewPage.getByText('github', { exact: true })).toBeVisible(); + await expect(webviewPage.getByText('remote-server', { exact: true })).toBeVisible(); + await expect(webviewPage.getByText('fetch', { exact: true })).toBeVisible(); + + // Verify connected server shows tool count + await expect(webviewPage.getByText('5 tools')).toBeVisible(); + + // Verify error server shows error message + await expect(webviewPage.getByText('Connection refused')).toBeVisible(); + + // Verify connect button for disconnected server (two servers are not connected) + await expect(webviewPage.getByRole('button', { name: '连接' }).first()).toBeVisible(); + + // Verify disconnect button for connected server + await expect(webviewPage.getByRole('button', { name: '断开' })).toBeVisible(); + + await webviewPage.screenshot({ path: 'docs/public/screenshots/spec-mcp-server-tab.png' }); + }); + + test('should show empty state when no MCP servers configured', async ({ webviewPage }) => { + // 1. Open the configuration dialog + await webviewPage.evaluate(() => { + (window as any).simulateExtensionMessage({ + command: 'showConfiguration', + configurationData: { + apiKey: 'test-key', + baseURL: 'https://api.example.com', + model: 'test-model', + fastModel: 'fast-model', + language: 'Chinese' + } + }); + }); + + // 2. Click on "MCP 服务器" tab + await webviewPage.getByText('MCP 服务器', { exact: true }).click(); + + // 3. Simulate empty servers list + await webviewPage.evaluate(() => { + window.postMessage({ + command: 'mcpServersResponse', + servers: [] + }, '*'); + }); + + // Verify empty state message + await expect(webviewPage.getByText('未配置 MCP 服务器')).toBeVisible(); + await expect(webviewPage.locator('code', { hasText: '.mcp.json' })).toBeVisible(); + + await webviewPage.screenshot({ path: 'docs/public/screenshots/spec-mcp-server-empty.png' }); + }); + + test('should handle connect/disconnect actions', async ({ webviewPage }) => { + // 1. Open the configuration dialog + await webviewPage.evaluate(() => { + (window as any).simulateExtensionMessage({ + command: 'showConfiguration', + configurationData: { + apiKey: 'test-key', + baseURL: 'https://api.example.com', + model: 'test-model', + fastModel: 'fast-model', + language: 'Chinese' + } + }); + }); + + // 2. Click on "MCP 服务器" tab + await webviewPage.getByText('MCP 服务器', { exact: true }).click(); + + // 3. Simulate servers list + await webviewPage.evaluate(() => { + window.postMessage({ + command: 'mcpServersResponse', + servers: [ + { + name: 'sqlite', + config: { command: 'uvx', args: ['mcp-server-sqlite'] }, + status: 'disconnected', + toolCount: 0, + capabilities: [] + } + ] + }, '*'); + }); + + // 4. Click connect button + await webviewPage.getByRole('button', { name: '连接' }).click(); + + // Verify backend receives the connect command + // (In demo mode, we verify the UI interaction rather than backend response) + + // 5. Simulate the backend response after connection + await webviewPage.evaluate(() => { + window.postMessage({ + command: 'mcpServersResponse', + servers: [ + { + name: 'sqlite', + config: { command: 'uvx', args: ['mcp-server-sqlite'] }, + status: 'connected', + toolCount: 5, + capabilities: ['tools'], + lastConnected: Date.now() + } + ] + }, '*'); + }); + + // Verify status changed to connected and disconnect button appears + await expect(webviewPage.getByRole('button', { name: '断开' })).toBeVisible(); + await expect(webviewPage.getByText('5 tools')).toBeVisible(); + }); +}); diff --git a/e2e/demo/product-spec-confirmations.demo.ts b/e2e/demo/product-spec-confirmations.demo.ts index 32d3217..e4c562f 100644 --- a/e2e/demo/product-spec-confirmations.demo.ts +++ b/e2e/demo/product-spec-confirmations.demo.ts @@ -1,8 +1,8 @@ import { test, expect } from '../utils/webviewTestHarness.js'; import { MessageInjector } from '../utils/messageInjector.js'; import { UIStateVerifier } from '../utils/uiStateVerifier.js'; -import { - EDIT_TOOL_NAME, +import { + EDIT_TOOL_NAME, BASH_TOOL_NAME, ASK_USER_QUESTION_TOOL_NAME, ENTER_PLAN_MODE_TOOL_NAME, @@ -201,7 +201,27 @@ test.describe('Product Specification Screenshots - Confirmations', () => { await webviewPage.click('.confirmation-close-btn'); await editConfirmDialog.waitFor({ state: 'detached' }); - // 17. Bash运行确认对话框 - 只显示确认对话框组件 + // 17. MCP 工具确认对话框 + await injector.simulateExtensionMessage('showConfirmation', { + confirmationId: 'mcp-confirm-001', + confirmationType: 'MCP 工具确认', + toolName: 'mcp__wave_requirements__create_requirement', + toolInput: { + title: '前端改动', + description: '在需求管理界面添加列表页', + priority: 'high', + tags: ['frontend', 'ui'] + } + }); + const mcpConfirmDialog = webviewPage.locator('.confirmation-dialog'); + await mcpConfirmDialog.waitFor({ state: 'visible' }); + await mcpConfirmDialog.screenshot({ path: 'docs/public/screenshots/spec-mcp-tool-confirm.png' }); + + // 关闭当前确认对话框 + await webviewPage.click('.confirmation-close-btn'); + await mcpConfirmDialog.waitFor({ state: 'detached' }); + + // 18. Bash运行确认对话框 - 只显示确认对话框组件 await injector.simulateExtensionMessage('showConfirmation', { confirmationId: 'bash-confirm-001', confirmationType: 'Bash 命令执行确认', diff --git a/package-lock.json b/package-lock.json index c05101b..d2ec11c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wave-vscode-chat", - "version": "0.4.7", + "version": "0.4.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wave-vscode-chat", - "version": "0.4.7", + "version": "0.4.9", "dependencies": { "@vscode/codicons": "^0.0.43", "diff": "^8.0.2", diff --git a/package.json b/package.json index f1ee885..6afc48c 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "wave-vscode-chat", "displayName": "Wave 代码智聊", "description": "Wave code for VS Code", - "version": "0.4.7", + "version": "0.4.9", "publisher": "wave-code", "icon": "LOGO.png", "repository": { diff --git a/src/chatProvider.ts b/src/chatProvider.ts index 260d001..d66474d 100644 --- a/src/chatProvider.ts +++ b/src/chatProvider.ts @@ -160,6 +160,9 @@ export class ChatProvider implements vscode.WebviewViewProvider { }, onError: (error) => { this.sendErrorToView(error, viewType, windowId); + }, + onMcpServersChange: (servers) => { + this.webviewManager.postMessage({ command: 'mcpServersUpdate', servers }, viewType, windowId); } }); } diff --git a/src/session/chatSession.ts b/src/session/chatSession.ts index e6ffe79..59539cd 100644 --- a/src/session/chatSession.ts +++ b/src/session/chatSession.ts @@ -1,5 +1,5 @@ import * as vscode from 'vscode'; -import { Agent, Message, PermissionDecision, ToolPermissionContext, AgentCallbacks, PermissionMode, Task, PromptHistoryManager, TextBlock, QueuedMessage } from 'wave-agent-sdk'; +import { Agent, Message, PermissionDecision, ToolPermissionContext, AgentCallbacks, PermissionMode, Task, PromptHistoryManager, TextBlock, QueuedMessage, McpServerStatus } from 'wave-agent-sdk'; import { ConfigurationData } from '../services/configurationService'; import { VscodeLspAdapter } from '../services/lspAdapter'; @@ -13,6 +13,7 @@ export interface ChatSessionCallbacks { onPermissionModeChange: (mode: PermissionMode) => void; onToolPermissionRequest: (context: ToolPermissionContext) => Promise; onError: (error: any) => void; + onMcpServersChange?: (servers: McpServerStatus[]) => void; } export class ChatSession { @@ -92,6 +93,9 @@ export class ChatSession { onQueuedMessagesChange: (messages: QueuedMessage[]) => { this.messageQueue = messages; this.callbacks.onQueueChange(messages); + }, + onMcpServersChange: (servers: McpServerStatus[]) => { + this.callbacks.onMcpServersChange?.(servers); } }; @@ -358,7 +362,7 @@ export class ChatSession { clearTimeout(this.updateTimer); this.updateTimer = undefined; } - + if (this.agent) { try { await this.agent.destroy(); @@ -367,7 +371,7 @@ export class ChatSession { } this.agent = undefined; } - + this.messages = []; this.tasks = []; this.inputContent = ''; @@ -378,4 +382,26 @@ export class ChatSession { this.pendingUpdate = false; this.messageQueue = []; } + + // MCP server management + public getMcpServers(): McpServerStatus[] { + if (!this.agent) { + return []; + } + return this.agent.getMcpServers(); + } + + public async connectMcpServer(serverName: string): Promise { + if (!this.agent) { + return false; + } + return this.agent.connectMcpServer(serverName); + } + + public async disconnectMcpServer(serverName: string): Promise { + if (!this.agent) { + return false; + } + return this.agent.disconnectMcpServer(serverName); + } } diff --git a/src/session/messageHandler.ts b/src/session/messageHandler.ts index f9c44d2..eb831ee 100644 --- a/src/session/messageHandler.ts +++ b/src/session/messageHandler.ts @@ -145,6 +145,15 @@ export class MessageHandler { case 'logout': await this.handleLogout(viewType, windowId); break; + case 'getMcpServers': + await this.handleGetMcpServers(viewType, windowId); + break; + case 'connectMcpServer': + await this.handleConnectMcpServer(message.serverName, viewType, windowId); + break; + case 'disconnectMcpServer': + await this.handleDisconnectMcpServer(message.serverName, viewType, windowId); + break; } } @@ -691,4 +700,41 @@ export class MessageHandler { }, viewType, windowId); } } + + private async handleGetMcpServers(viewType?: 'sidebar' | 'tab' | 'window', windowId?: string) { + const session = this.context.getChatSession(viewType || 'tab', windowId); + const servers = session.getMcpServers(); + this.context.postMessage({ + command: 'mcpServersResponse', + servers + }, viewType, windowId); + } + + private async handleConnectMcpServer(serverName: string, viewType?: 'sidebar' | 'tab' | 'window', windowId?: string) { + const session = this.context.getChatSession(viewType || 'tab', windowId); + try { + const success = await session.connectMcpServer(serverName); + // SDK's onMcpServersChange callback will push the updated state to frontend + if (success) { + vscode.window.showInformationMessage(`MCP 服务器 "${serverName}" 连接请求已发送`); + } + } catch (error) { + console.error('连接 MCP 服务器失败:', error); + vscode.window.showErrorMessage('连接 MCP 服务器失败: ' + error); + } + } + + private async handleDisconnectMcpServer(serverName: string, viewType?: 'sidebar' | 'tab' | 'window', windowId?: string) { + const session = this.context.getChatSession(viewType || 'tab', windowId); + try { + const success = await session.disconnectMcpServer(serverName); + // SDK's onMcpServersChange callback will push the updated state to frontend + if (success) { + vscode.window.showInformationMessage(`MCP 服务器 "${serverName}" 断开请求已发送`); + } + } catch (error) { + console.error('断开 MCP 服务器失败:', error); + vscode.window.showErrorMessage('断开 MCP 服务器失败: ' + error); + } + } } diff --git a/webview/src/components/ConfigurationDialog.tsx b/webview/src/components/ConfigurationDialog.tsx index 798e4a7..f6b8691 100644 --- a/webview/src/components/ConfigurationDialog.tsx +++ b/webview/src/components/ConfigurationDialog.tsx @@ -7,7 +7,7 @@ */ import React, { useState, useEffect, useRef } from 'react'; -import { ConfigurationDialogProps, ConfigurationData, PluginInfo, MarketplaceInfo, PluginScope } from '../types'; +import { ConfigurationDialogProps, ConfigurationData, PluginInfo, MarketplaceInfo, PluginScope, McpServerStatus } from '../types'; import '../styles/ConfigurationDialog.css'; const ConfigurationDialog: React.FC = ({ @@ -19,7 +19,7 @@ const ConfigurationDialog: React.FC onCancel, vscode }) => { - const [activeTab, setActiveTab] = useState<'general' | 'plugins'>('general'); + const [activeTab, setActiveTab] = useState<'general' | 'plugins' | 'mcp'>('general'); const [activePluginTab, setActivePluginTab] = useState<'explore' | 'installed' | 'marketplaces'>('explore'); const [formData, setFormData] = useState({ @@ -39,6 +39,10 @@ const ConfigurationDialog: React.FC const [selectedPlugin, setSelectedPlugin] = useState(null); const [searchQuery, setSearchQuery] = useState(''); + // MCP state + const [mcpServers, setMcpServers] = useState([]); + const [mcpConnecting, setMcpConnecting] = useState>({}); + // SSO auth state const [isAuthenticated, setIsAuthenticated] = useState(false); const [authUser, setAuthUser] = useState<{ id: string; email?: string } | null>(null); @@ -56,6 +60,9 @@ const ConfigurationDialog: React.FC if (isVisible && activeTab === 'general') { vscode?.postMessage({ command: 'getAuthStatus' }); } + if (isVisible && activeTab === 'mcp') { + vscode?.postMessage({ command: 'getMcpServers' }); + } setSearchQuery(''); setSelectedPlugin(null); setAuthMessage(''); @@ -91,6 +98,15 @@ const ConfigurationDialog: React.FC setAuthUser(null); setAuthMessage('已登出'); break; + case 'mcpServersResponse': + setMcpServers(message.servers || []); + setMcpConnecting({}); + break; + case 'mcpServersUpdate': + // Push update from backend (e.g., after connect/disconnect completes) + setMcpServers(message.servers || []); + setMcpConnecting({}); + break; } }; window.addEventListener('message', handleMessage); @@ -135,6 +151,17 @@ const ConfigurationDialog: React.FC vscode?.postMessage({ command: 'logout' }); }; + // MCP handlers + const handleConnectMcpServer = (serverName: string) => { + setMcpConnecting(prev => ({ ...prev, [serverName]: true })); + vscode?.postMessage({ command: 'connectMcpServer', serverName }); + }; + + const handleDisconnectMcpServer = (serverName: string) => { + setMcpConnecting(prev => ({ ...prev, [serverName]: true })); + vscode?.postMessage({ command: 'disconnectMcpServer', serverName }); + }; + const filteredPlugins = plugins.filter(p => { const q = searchQuery.toLowerCase(); if (!q) return true; @@ -206,18 +233,24 @@ const ConfigurationDialog: React.FC

配置设置

- - +
@@ -372,6 +405,75 @@ const ConfigurationDialog: React.FC + ) : activeTab === 'mcp' ? ( +
+ {mcpServers.length === 0 ? ( +
+

未配置 MCP 服务器

+

在项目根目录创建 .mcp.json 文件来添加服务器

+
+ ) : ( +
+ {mcpServers.map(server => ( +
+
+
+ + {server.status === 'connected' ? '●' : server.status === 'connecting' ? '⟳' : server.status === 'error' ? '✗' : '○'} + + {server.name} + {server.toolCount !== undefined && server.status === 'connected' && ( + {server.toolCount} tools + )} +
+ {server.config.command && ( +
{server.config.command}{server.config.args ? ` ${server.config.args.join(' ')}` : ''}
+ )} + {server.config.url && ( +
{server.config.url}
+ )} + {server.error && ( +
{server.error}
+ )} + {server.lastConnected && ( +
最近连接: {new Date(server.lastConnected).toLocaleTimeString()}
+ )} +
+
+ {(server.status === 'disconnected' || server.status === 'error') && ( + + )} + {server.status === 'connected' && ( + + )} +
+
+ ))} +
+ )} + +
+ +
+
) : (
diff --git a/webview/src/components/ConfirmationDialog.tsx b/webview/src/components/ConfirmationDialog.tsx index 86abd6a..27a01cc 100644 --- a/webview/src/components/ConfirmationDialog.tsx +++ b/webview/src/components/ConfirmationDialog.tsx @@ -462,6 +462,11 @@ export const ConfirmationDialog: React.FC = ({ {confirmation.toolInput.command}
)} + {confirmation.toolName.startsWith('mcp__') && confirmation.toolInput && ( +
+
{JSON.stringify(confirmation.toolInput, null, 2)}
+
+ )}
工具: {confirmation.toolName}
diff --git a/webview/src/styles/ConfigurationDialog.css b/webview/src/styles/ConfigurationDialog.css index b13b0bb..f5df3ca 100644 --- a/webview/src/styles/ConfigurationDialog.css +++ b/webview/src/styles/ConfigurationDialog.css @@ -672,4 +672,141 @@ input:checked + .slider:before { .sso-message.error { color: var(--vscode-errorForeground); +} + +/* MCP Server Management */ +.mcp-container { + display: flex; + flex: 1; + flex-direction: column; + gap: 12px; + min-height: 0; +} + +.mcp-server-list { + display: flex; + flex-direction: column; + gap: 8px; + overflow-y: auto; + flex: 1; + min-height: 0; +} + +.mcp-server-item { + display: flex; + justify-content: space-between; + align-items: flex-start; + padding: 10px; + background-color: var(--vscode-sideBar-background); + border: 1px solid var(--vscode-panel-border); + border-radius: 4px; +} + +.mcp-server-info { + display: flex; + flex-direction: column; + gap: 4px; + flex: 1; + min-width: 0; +} + +.mcp-server-header { + display: flex; + align-items: center; + gap: 6px; +} + +.mcp-status-icon { + font-size: 12px; + flex-shrink: 0; +} + +.mcp-status-connected { + color: var(--vscode-testing-iconPassed); +} + +.mcp-status-connecting { + color: var(--vscode-charts-yellow); +} + +.mcp-status-error { + color: var(--vscode-errorForeground); +} + +.mcp-status-disconnected { + color: var(--vscode-descriptionForeground); +} + +.mcp-server-name { + font-weight: 600; + font-size: 13px; +} + +.mcp-tool-count { + font-size: 11px; + color: var(--vscode-testing-iconPassed); + background-color: var(--vscode-badge-background); + color: var(--vscode-badge-foreground); + padding: 1px 6px; + border-radius: 3px; +} + +.mcp-server-command { + font-size: 11px; + color: var(--vscode-descriptionForeground); + font-family: var(--vscode-editor-font-family, monospace); + word-break: break-all; +} + +.mcp-server-error { + font-size: 11px; + color: var(--vscode-errorForeground); + word-break: break-all; +} + +.mcp-server-last-connected { + font-size: 11px; + color: var(--vscode-descriptionForeground); +} + +.mcp-server-actions { + display: flex; + gap: 4px; + flex-shrink: 0; + margin-left: 8px; +} + +.mcp-connect-btn, .mcp-disconnect-btn { + background-color: var(--vscode-button-secondaryBackground); + color: var(--vscode-button-secondaryForeground); + border: 1px solid var(--vscode-button-border, transparent); + padding: 4px 10px; + border-radius: 3px; + cursor: pointer; + font-size: 11px; + transition: background-color 0.15s ease; + white-space: nowrap; +} + +.mcp-connect-btn:hover, .mcp-disconnect-btn:hover { + background-color: var(--vscode-button-secondaryHoverBackground); +} + +.mcp-connect-btn:disabled, .mcp-disconnect-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.mcp-hint { + font-size: 12px; + color: var(--vscode-descriptionForeground); + margin-top: 4px; +} + +.mcp-hint code { + background-color: var(--vscode-textCodeBlock-background); + padding: 2px 4px; + border-radius: 3px; + font-family: var(--vscode-editor-font-family, monospace); + font-size: 12px; } \ No newline at end of file diff --git a/webview/src/styles/ConfirmationDialog.css b/webview/src/styles/ConfirmationDialog.css index a3ef6c8..41bc0d2 100644 --- a/webview/src/styles/ConfirmationDialog.css +++ b/webview/src/styles/ConfirmationDialog.css @@ -66,6 +66,25 @@ word-break: break-all; } +.confirmation-mcp-params { + background-color: var(--vscode-textCodeBlock-background); + padding: 8px 10px; + border-radius: 4px; + font-family: var(--vscode-editor-font-family); + font-size: 12px; + color: var(--vscode-editor-foreground); + margin: 4px 0; + border: 1px solid var(--vscode-panel-border); + white-space: pre-wrap; + word-break: break-all; + max-height: 200px; + overflow-y: auto; +} + +.confirmation-mcp-params pre { + margin: 0; +} + .confirmation-details { font-size: 11px; color: var(--vscode-descriptionForeground); diff --git a/webview/src/types/index.ts b/webview/src/types/index.ts index 13caae2..e552d17 100644 --- a/webview/src/types/index.ts +++ b/webview/src/types/index.ts @@ -6,10 +6,10 @@ */ // Import message structures and session types from wave-agent-sdk -import type { Message, MessageBlock, TextBlock, ErrorBlock, ToolBlock, ImageBlock, BangBlock, CompactBlock, PermissionMode, AskUserQuestion, AskUserQuestionInput, AskUserQuestionOption, Task, TaskStatus, TaskNotificationBlock } from 'wave-agent-sdk/dist/types/index.js'; +import type { Message, MessageBlock, TextBlock, ErrorBlock, ToolBlock, ImageBlock, BangBlock, CompactBlock, PermissionMode, AskUserQuestion, AskUserQuestionInput, AskUserQuestionOption, Task, TaskStatus, TaskNotificationBlock, McpServerStatus, McpServerConfig } from 'wave-agent-sdk/dist/types/index.js'; import type { SessionMetadata, SessionData } from 'wave-agent-sdk/dist/services/session.js'; -export type { Message, MessageBlock, TextBlock, ErrorBlock, ToolBlock, ImageBlock, BangBlock, CompactBlock, TaskNotificationBlock, SessionData, SessionMetadata, PermissionMode, AskUserQuestion, AskUserQuestionInput, AskUserQuestionOption, Task, TaskStatus }; +export type { Message, MessageBlock, TextBlock, ErrorBlock, ToolBlock, ImageBlock, BangBlock, CompactBlock, TaskNotificationBlock, SessionData, SessionMetadata, PermissionMode, AskUserQuestion, AskUserQuestionInput, AskUserQuestionOption, Task, TaskStatus, McpServerStatus, McpServerConfig }; // Slash command types export interface SlashCommand {