Skip to content

Commit 3d42bed

Browse files
committed
feat: add MCP server tab to configuration dialog
- Add MCP tab in ConfigurationDialog to view and manage MCP servers - Display server status (connected/disconnected/connecting/error), tool count, and connection details - Support connect/disconnect buttons with loading state for each server - Show empty state with .mcp.json configuration hint when no servers configured - Backend: add getMcpServers/connectMcpServer/disconnectMcpServer handlers in messageHandler and chatSession - Update spec.md with MCP tab features and three-tab structure description - Add e2e demo tests for MCP tab: server list, empty state, and connect/disconnect actions
1 parent 443bbb3 commit 3d42bed

7 files changed

Lines changed: 534 additions & 17 deletions

File tree

docs/spec.md

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -376,13 +376,24 @@ _Skill 系统_
376376

377377
### 6.3 MCP 协议集成 {#mcp-integration}
378378

379-
支持 Model Context Protocol (MCP),允许 AI 连接到外部的 MCP 服务器,从而获取更多的上下文信息或调用外部工具。
379+
支持 Model Context Protocol (MCP),允许 AI 连接到外部的 MCP 服务器,从而获取更多的上下文信息或调用外部工具。用户可通过配置弹窗中的 **MCP 服务器** 标签页查看和管理所有已配置的 MCP 服务器。
380380

381-
**提示:**用户可以通过内置的 `/settings` skill 来管理 MCP 配置,例如输入 `/settings 增加mcp:xxx` 即可快速添加新的 MCP 服务。
381+
**主要特性:**
382+
383+
- **服务器列表**:展示所有通过 `.mcp.json` 配置的 MCP 服务器,包括名称、连接状态(○ 未连接 / ● 已连接 / ⟳ 连接中 / ✗ 错误)、工具数量和连接命令/URL。
384+
- **连接/断开控制**:支持一键连接或断开 MCP 服务器,连接状态实时刷新。
385+
- **错误信息展示**:连接失败的服务器会显示具体错误原因。
386+
- **空状态引导**:未配置任何服务器时,提示用户创建 `.mcp.json` 文件。
387+
- **配置方式**:在项目根目录创建 `.mcp.json` 文件,支持 stdio(本地进程)和 SSE/HTTP(远程服务)两种连接方式。
388+
389+
**提示:**用户也可以通过内置的 `/settings` skill 来管理 MCP 配置,例如输入 `/settings 增加mcp:xxx` 即可快速添加新的 MCP 服务。
382390

383391
![MCP 集成](/screenshots/spec-mcp.png)
384392
_MCP 集成_
385393

394+
![MCP 服务器管理](/screenshots/spec-mcp-server-tab.png)
395+
_MCP 服务器管理(配置弹窗中的 MCP 标签页)_
396+
386397
### 6.4 内置 Skills {#builtin-skills}
387398

388399
#### settings — 配置管理 {#skill-settings}
@@ -590,15 +601,18 @@ Wave 在后台自动维护项目记忆,帮助 AI 持续了解项目演变:
590601

591602
### 10.1 配置设置 {#configuration-settings}
592603

593-
用户可以自定义 AI 模型、API Key、Base URL 等关键参数,以适配不同的 AI 服务提供商。配置界面中的表单字段仅显示用户手动输入的值,不会被环境变量填充;但如果设置了相应的环境变量(如 `WAVE_BASE_URL`),其值会作为 placeholder 提示显示在输入框中。服务端链接字段默认 placeholder 为"请联系管理员获取"。
604+
用户可以自定义 AI 模型、API Key、Base URL 等关键参数,以适配不同的 AI 服务提供商。配置界面包含三个标签页:**常规设置****插件****MCP 服务器**配置界面中的表单字段仅显示用户手动输入的值,不会被环境变量填充;但如果设置了相应的环境变量(如 `WAVE_BASE_URL`),其值会作为 placeholder 提示显示在输入框中。服务端链接字段默认 placeholder 为"请联系管理员获取"。
594605

595606
**主要特性:**
596607

597-
- **服务端链接**:配置 Wave AI 服务端地址,用于 SSO 认证。支持环境变量 `WAVE_SERVER_URL` 作为 fallback,默认 placeholder 提示"请联系管理员获取"。
598-
- **Model / Fast Model**:SSO 模式下也可配置主模型和快速模型名称,支持环境变量 `WAVE_MODEL` / `WAVE_FAST_MODEL` 作为 placeholder 提示。
599-
- **SSO 登录/登出**:当配置了服务端链接后,用户可通过 SSO 认证进行登录,无需手动配置 API Key。登录后所有 API 请求自动通过 Wave AI 代理路由。
600-
- **浏览器登录**:点击"SSO 登录"后自动打开浏览器,用户在 Wave AI 登录页完成认证(支持 SSO 企业身份提供商或账号密码登录),授权码通过 localhost 回调自动交换为 JWT 并保存。VS Code Remote SSH 环境会自动转发端口,远程服务器体验与本地一致。
601-
- **登录状态显示**:已认证时显示用户邮箱/ID 和登出按钮;登出后自动恢复为直接 LLM API 模式。
608+
- **常规设置**:配置 AI 模型、API Key、Base URL、Headers、语言等。
609+
- **服务端链接**:配置 Wave AI 服务端地址,用于 SSO 认证。支持环境变量 `WAVE_SERVER_URL` 作为 fallback,默认 placeholder 提示"请联系管理员获取"。
610+
- **Model / Fast Model**:SSO 模式下也可配置主模型和快速模型名称,支持环境变量 `WAVE_MODEL` / `WAVE_FAST_MODEL` 作为 placeholder 提示。
611+
- **SSO 登录/登出**:当配置了服务端链接后,用户可通过 SSO 认证进行登录,无需手动配置 API Key。登录后所有 API 请求自动通过 Wave AI 代理路由。
612+
- **浏览器登录**:点击"SSO 登录"后自动打开浏览器,用户在 Wave AI 登录页完成认证(支持 SSO 企业身份提供商或账号密码登录),授权码通过 localhost 回调自动交换为 JWT 并保存。VS Code Remote SSH 环境会自动转发端口,远程服务器体验与本地一致。
613+
- **登录状态显示**:已认证时显示用户邮箱/ID 和登出按钮;登出后自动恢复为直接 LLM API 模式。
614+
- **插件**:管理插件的安装、更新、卸载和插件市场(详见 [第 11 章 插件系统](#plugin-system))。
615+
- **MCP 服务器**:查看和管理 MCP 服务器连接状态(详见 [第 6.3 节 MCP 协议集成](#mcp-integration))。
602616

603617
![配置设置](/screenshots/spec-configuration.png)
604618
_配置设置_

e2e/demo/mcp-server-tab.demo.ts

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import { test, expect } from '../utils/webviewTestHarness.js';
2+
3+
test.describe('MCP Server Tab Demo', () => {
4+
test('should show MCP server tab with configured servers', async ({ webviewPage }) => {
5+
// 1. Open the configuration dialog
6+
await webviewPage.evaluate(() => {
7+
(window as any).simulateExtensionMessage({
8+
command: 'showConfiguration',
9+
configurationData: {
10+
apiKey: 'test-key',
11+
baseURL: 'https://api.example.com',
12+
model: 'test-model',
13+
fastModel: 'fast-model',
14+
language: 'Chinese'
15+
}
16+
});
17+
});
18+
19+
// Verify dialog is visible
20+
await expect(webviewPage.getByText('配置设置', { exact: true })).toBeVisible();
21+
22+
// 2. Click on "MCP 服务器" tab
23+
await webviewPage.getByText('MCP 服务器', { exact: true }).click();
24+
25+
// 3. Simulate receiving MCP servers list
26+
await webviewPage.evaluate(() => {
27+
window.postMessage({
28+
command: 'mcpServersResponse',
29+
servers: [
30+
{
31+
name: 'sqlite',
32+
config: {
33+
command: 'uvx',
34+
args: ['mcp-server-sqlite', '--db-path', '/path/to/db.db']
35+
},
36+
status: 'connected',
37+
toolCount: 5,
38+
capabilities: ['tools'],
39+
lastConnected: Date.now() - 60000
40+
},
41+
{
42+
name: 'github',
43+
config: {
44+
command: 'npx',
45+
args: ['-y', '@modelcontextprotocol/server-github'],
46+
env: {
47+
GITHUB_PERSONAL_ACCESS_TOKEN: 'your-token-here'
48+
}
49+
},
50+
status: 'disconnected',
51+
toolCount: 0,
52+
capabilities: []
53+
},
54+
{
55+
name: 'remote-server',
56+
config: {
57+
url: 'https://mcp-server.example.com/sse'
58+
},
59+
status: 'error',
60+
toolCount: 0,
61+
error: 'Connection refused',
62+
capabilities: []
63+
},
64+
{
65+
name: 'fetch',
66+
config: {
67+
command: 'npx',
68+
args: ['-y', '@mcp/server-fetch']
69+
},
70+
status: 'connecting',
71+
toolCount: 0,
72+
capabilities: ['tools']
73+
}
74+
]
75+
}, '*');
76+
});
77+
78+
// Verify all four servers are visible
79+
await expect(webviewPage.getByText('sqlite', { exact: true })).toBeVisible();
80+
await expect(webviewPage.getByText('github', { exact: true })).toBeVisible();
81+
await expect(webviewPage.getByText('remote-server', { exact: true })).toBeVisible();
82+
await expect(webviewPage.getByText('fetch', { exact: true })).toBeVisible();
83+
84+
// Verify connected server shows tool count
85+
await expect(webviewPage.getByText('5 tools')).toBeVisible();
86+
87+
// Verify error server shows error message
88+
await expect(webviewPage.getByText('Connection refused')).toBeVisible();
89+
90+
// Verify connect button for disconnected server (two servers are not connected)
91+
await expect(webviewPage.getByRole('button', { name: '连接' }).first()).toBeVisible();
92+
93+
// Verify disconnect button for connected server
94+
await expect(webviewPage.getByRole('button', { name: '断开' })).toBeVisible();
95+
96+
await webviewPage.screenshot({ path: 'docs/public/screenshots/spec-mcp-server-tab.png' });
97+
});
98+
99+
test('should show empty state when no MCP servers configured', async ({ webviewPage }) => {
100+
// 1. Open the configuration dialog
101+
await webviewPage.evaluate(() => {
102+
(window as any).simulateExtensionMessage({
103+
command: 'showConfiguration',
104+
configurationData: {
105+
apiKey: 'test-key',
106+
baseURL: 'https://api.example.com',
107+
model: 'test-model',
108+
fastModel: 'fast-model',
109+
language: 'Chinese'
110+
}
111+
});
112+
});
113+
114+
// 2. Click on "MCP 服务器" tab
115+
await webviewPage.getByText('MCP 服务器', { exact: true }).click();
116+
117+
// 3. Simulate empty servers list
118+
await webviewPage.evaluate(() => {
119+
window.postMessage({
120+
command: 'mcpServersResponse',
121+
servers: []
122+
}, '*');
123+
});
124+
125+
// Verify empty state message
126+
await expect(webviewPage.getByText('未配置 MCP 服务器')).toBeVisible();
127+
await expect(webviewPage.locator('code', { hasText: '.mcp.json' })).toBeVisible();
128+
129+
await webviewPage.screenshot({ path: 'docs/public/screenshots/spec-mcp-server-empty.png' });
130+
});
131+
132+
test('should handle connect/disconnect actions', async ({ webviewPage }) => {
133+
// 1. Open the configuration dialog
134+
await webviewPage.evaluate(() => {
135+
(window as any).simulateExtensionMessage({
136+
command: 'showConfiguration',
137+
configurationData: {
138+
apiKey: 'test-key',
139+
baseURL: 'https://api.example.com',
140+
model: 'test-model',
141+
fastModel: 'fast-model',
142+
language: 'Chinese'
143+
}
144+
});
145+
});
146+
147+
// 2. Click on "MCP 服务器" tab
148+
await webviewPage.getByText('MCP 服务器', { exact: true }).click();
149+
150+
// 3. Simulate servers list
151+
await webviewPage.evaluate(() => {
152+
window.postMessage({
153+
command: 'mcpServersResponse',
154+
servers: [
155+
{
156+
name: 'sqlite',
157+
config: { command: 'uvx', args: ['mcp-server-sqlite'] },
158+
status: 'disconnected',
159+
toolCount: 0,
160+
capabilities: []
161+
}
162+
]
163+
}, '*');
164+
});
165+
166+
// 4. Click connect button
167+
await webviewPage.getByRole('button', { name: '连接' }).click();
168+
169+
// Verify backend receives the connect command
170+
// (In demo mode, we verify the UI interaction rather than backend response)
171+
172+
// 5. Simulate the backend response after connection
173+
await webviewPage.evaluate(() => {
174+
window.postMessage({
175+
command: 'mcpServersResponse',
176+
servers: [
177+
{
178+
name: 'sqlite',
179+
config: { command: 'uvx', args: ['mcp-server-sqlite'] },
180+
status: 'connected',
181+
toolCount: 5,
182+
capabilities: ['tools'],
183+
lastConnected: Date.now()
184+
}
185+
]
186+
}, '*');
187+
});
188+
189+
// Verify status changed to connected and disconnect button appears
190+
await expect(webviewPage.getByRole('button', { name: '断开' })).toBeVisible();
191+
await expect(webviewPage.getByText('5 tools')).toBeVisible();
192+
});
193+
});

src/session/chatSession.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as vscode from 'vscode';
2-
import { Agent, Message, PermissionDecision, ToolPermissionContext, AgentCallbacks, PermissionMode, Task, PromptHistoryManager, TextBlock, QueuedMessage } from 'wave-agent-sdk';
2+
import { Agent, Message, PermissionDecision, ToolPermissionContext, AgentCallbacks, PermissionMode, Task, PromptHistoryManager, TextBlock, QueuedMessage, McpServerStatus } from 'wave-agent-sdk';
33
import { ConfigurationData } from '../services/configurationService';
44
import { VscodeLspAdapter } from '../services/lspAdapter';
55

@@ -358,7 +358,7 @@ export class ChatSession {
358358
clearTimeout(this.updateTimer);
359359
this.updateTimer = undefined;
360360
}
361-
361+
362362
if (this.agent) {
363363
try {
364364
await this.agent.destroy();
@@ -367,7 +367,7 @@ export class ChatSession {
367367
}
368368
this.agent = undefined;
369369
}
370-
370+
371371
this.messages = [];
372372
this.tasks = [];
373373
this.inputContent = '';
@@ -378,4 +378,26 @@ export class ChatSession {
378378
this.pendingUpdate = false;
379379
this.messageQueue = [];
380380
}
381+
382+
// MCP server management
383+
public getMcpServers(): McpServerStatus[] {
384+
if (!this.agent) {
385+
return [];
386+
}
387+
return this.agent.getMcpServers();
388+
}
389+
390+
public async connectMcpServer(serverName: string): Promise<boolean> {
391+
if (!this.agent) {
392+
return false;
393+
}
394+
return this.agent.connectMcpServer(serverName);
395+
}
396+
397+
public async disconnectMcpServer(serverName: string): Promise<boolean> {
398+
if (!this.agent) {
399+
return false;
400+
}
401+
return this.agent.disconnectMcpServer(serverName);
402+
}
381403
}

src/session/messageHandler.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,15 @@ export class MessageHandler {
145145
case 'logout':
146146
await this.handleLogout(viewType, windowId);
147147
break;
148+
case 'getMcpServers':
149+
await this.handleGetMcpServers(viewType, windowId);
150+
break;
151+
case 'connectMcpServer':
152+
await this.handleConnectMcpServer(message.serverName, viewType, windowId);
153+
break;
154+
case 'disconnectMcpServer':
155+
await this.handleDisconnectMcpServer(message.serverName, viewType, windowId);
156+
break;
148157
}
149158
}
150159

@@ -691,4 +700,49 @@ export class MessageHandler {
691700
}, viewType, windowId);
692701
}
693702
}
703+
704+
private async handleGetMcpServers(viewType?: 'sidebar' | 'tab' | 'window', windowId?: string) {
705+
const session = this.context.getChatSession(viewType || 'tab', windowId);
706+
const servers = session.getMcpServers();
707+
this.context.postMessage({
708+
command: 'mcpServersResponse',
709+
servers
710+
}, viewType, windowId);
711+
}
712+
713+
private async handleConnectMcpServer(serverName: string, viewType?: 'sidebar' | 'tab' | 'window', windowId?: string) {
714+
const session = this.context.getChatSession(viewType || 'tab', windowId);
715+
try {
716+
const success = await session.connectMcpServer(serverName);
717+
const servers = session.getMcpServers();
718+
this.context.postMessage({
719+
command: 'mcpServersResponse',
720+
servers
721+
}, viewType, windowId);
722+
if (success) {
723+
vscode.window.showInformationMessage(`MCP 服务器 "${serverName}" 已连接`);
724+
}
725+
} catch (error) {
726+
console.error('连接 MCP 服务器失败:', error);
727+
vscode.window.showErrorMessage('连接 MCP 服务器失败: ' + error);
728+
}
729+
}
730+
731+
private async handleDisconnectMcpServer(serverName: string, viewType?: 'sidebar' | 'tab' | 'window', windowId?: string) {
732+
const session = this.context.getChatSession(viewType || 'tab', windowId);
733+
try {
734+
const success = await session.disconnectMcpServer(serverName);
735+
const servers = session.getMcpServers();
736+
this.context.postMessage({
737+
command: 'mcpServersResponse',
738+
servers
739+
}, viewType, windowId);
740+
if (success) {
741+
vscode.window.showInformationMessage(`MCP 服务器 "${serverName}" 已断开`);
742+
}
743+
} catch (error) {
744+
console.error('断开 MCP 服务器失败:', error);
745+
vscode.window.showErrorMessage('断开 MCP 服务器失败: ' + error);
746+
}
747+
}
694748
}

0 commit comments

Comments
 (0)