@cqsjjb/meter-sphere-mcp-server 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +189 -0
- package/mcp-config.example.json +20 -0
- package/mcp-server.mjs +846 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# @cqsjjb/meter-sphere-mcp-server
|
|
2
|
+
|
|
3
|
+
MCP (Model Context Protocol) 服务器,用于 MeterSphere 测试用例平台。让 Cursor 等 AI 编辑器可以直接调用测试用例平台,获取测试用例列表、生成 AI 测试提示语,并跟踪测试进度。
|
|
4
|
+
|
|
5
|
+
## 功能特性
|
|
6
|
+
|
|
7
|
+
- 🤖 **MCP 工具集成**:提供 MCP 服务器,让 Cursor 可以直接调用测试用例平台
|
|
8
|
+
- 📋 **测试用例清单**:自动获取所有优先级的测试用例,按优先级排序(P0 > P1 > P2 > P3)
|
|
9
|
+
- 🔍 **AI 测试提示语**:自动生成 AI 测试提示语,方便开发者进行测试自检
|
|
10
|
+
- 📊 **进度跟踪**:支持测试进度跟踪,可以暂停和恢复测试
|
|
11
|
+
- 🎯 **智能分析**:集成 AI 模型进行测试用例分析(支持 Deepseek、OpenAI 等)
|
|
12
|
+
|
|
13
|
+
## 安装
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @cqsjjb/meter-sphere-mcp-server
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 配置
|
|
20
|
+
|
|
21
|
+
在 Cursor 的 MCP 配置文件中添加以下配置(通常在 `~/.cursor/mcp.json` 或 Cursor 设置中):
|
|
22
|
+
|
|
23
|
+
**Windows 路径示例:**
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"mcpServers": {
|
|
27
|
+
"meter-sphere": {
|
|
28
|
+
"command": "node",
|
|
29
|
+
"args": ["./node_modules/@cqsjjb/meter-sphere-mcp-server/mcp-server.mjs"],
|
|
30
|
+
"env": {
|
|
31
|
+
"PLATFORM_URL": "http://your-platform-url?organization=xxx&project=xxx&testPlanId=xxx",
|
|
32
|
+
"X_AUTH_TOKEN": "your-x-auth-token-here",
|
|
33
|
+
"CSRF_TOKEN": "your-csrf-token-here",
|
|
34
|
+
"API_BASE_URL": "http://192.168.3.26:8081",
|
|
35
|
+
"MODEL_BASE_URL": "https://api.deepseek.com",
|
|
36
|
+
"MODEL_API_KEY": "your-model-api-key-here",
|
|
37
|
+
"MODEL_ID": "deepseek-chat"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**macOS/Linux 路径示例:**
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"mcpServers": {
|
|
48
|
+
"meter-sphere": {
|
|
49
|
+
"command": "node",
|
|
50
|
+
"args": ["./node_modules/@cqsjjb/meter-sphere-mcp-server/mcp-server.mjs"],
|
|
51
|
+
"env": {
|
|
52
|
+
"PLATFORM_URL": "http://your-platform-url?organization=xxx&project=xxx&testPlanId=xxx",
|
|
53
|
+
"X_AUTH_TOKEN": "your-x-auth-token-here",
|
|
54
|
+
"CSRF_TOKEN": "your-csrf-token-here",
|
|
55
|
+
"API_BASE_URL": "http://192.168.3.26:8081",
|
|
56
|
+
"MODEL_BASE_URL": "https://api.deepseek.com",
|
|
57
|
+
"MODEL_API_KEY": "your-model-api-key-here",
|
|
58
|
+
"MODEL_ID": "deepseek-chat"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**使用全局安装:**
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"mcpServers": {
|
|
69
|
+
"meter-sphere": {
|
|
70
|
+
"command": "npx",
|
|
71
|
+
"args": ["-y", "@cqsjjb/meter-sphere-mcp-server"],
|
|
72
|
+
"env": {
|
|
73
|
+
"PLATFORM_URL": "http://your-platform-url?organization=xxx&project=xxx&testPlanId=xxx",
|
|
74
|
+
"X_AUTH_TOKEN": "your-x-auth-token-here",
|
|
75
|
+
"CSRF_TOKEN": "your-csrf-token-here"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## 环境变量说明
|
|
83
|
+
|
|
84
|
+
- `PLATFORM_URL` (必需): 测试用例平台的完整URL(包含查询参数)
|
|
85
|
+
- `X_AUTH_TOKEN` (必需): x-auth-token认证令牌
|
|
86
|
+
- `CSRF_TOKEN` (必需): Csrf-token认证令牌
|
|
87
|
+
- `API_BASE_URL` (可选): API服务器的基础URL,默认值为 `http://192.168.3.26:8081`
|
|
88
|
+
- `MODEL_BASE_URL` (可选): AI模型API的基础URL,默认值为 `https://api.deepseek.com`
|
|
89
|
+
- `MODEL_API_KEY` (可选): AI模型API密钥,如果未配置则跳过AI分析
|
|
90
|
+
- `MODEL_ID` (可选): AI模型ID,默认值为 `deepseek-chat`
|
|
91
|
+
- `PROGRESS_FILE_DIR` (可选): 进度文件存储目录,默认值为当前工作目录(`process.cwd()`)
|
|
92
|
+
|
|
93
|
+
## 可用工具
|
|
94
|
+
|
|
95
|
+
### 1. `get_test_list` - 获取测试用例列表
|
|
96
|
+
|
|
97
|
+
获取所有优先级的测试用例列表,按优先级排序(P0 > P1 > P2 > P3),生成 TODO 清单。支持过滤已完成用例和显示完成状态。
|
|
98
|
+
|
|
99
|
+
**参数:**
|
|
100
|
+
- `excludeCompleted` (boolean, 可选): 是否排除已完成的测试用例,默认为 false
|
|
101
|
+
|
|
102
|
+
**使用示例:**
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# 获取所有测试用例(包括已完成)
|
|
106
|
+
帮我获取所有测试用例的TODO清单
|
|
107
|
+
|
|
108
|
+
# 只获取未完成的测试用例
|
|
109
|
+
帮我获取未完成的测试用例列表
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### 2. `get_test_detail` - 获取测试用例详情
|
|
113
|
+
|
|
114
|
+
根据测试用例 ID 获取详细信息,包括测试步骤和 AI 测试提示语。会自动调用 AI 模型进行测试用例分析。
|
|
115
|
+
|
|
116
|
+
**参数:**
|
|
117
|
+
- `testCaseId` (string, 必需): 测试用例 ID
|
|
118
|
+
- `testPlanCollectionName` (string, 必需): 测试点名称(从 get_test_list 返回)
|
|
119
|
+
|
|
120
|
+
**使用示例:**
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
获取测试用例ID为12345的详细信息,测试点是登录功能
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### 3. `mark_test_completed` - 标记测试用例完成
|
|
127
|
+
|
|
128
|
+
标记指定测试用例为已完成状态,用于进度跟踪。
|
|
129
|
+
|
|
130
|
+
**参数:**
|
|
131
|
+
- `testCaseId` (string, 必需): 测试用例 ID
|
|
132
|
+
- `priority` (string, 必需): 测试用例优先级(P0/P1/P2/P3)
|
|
133
|
+
|
|
134
|
+
**使用示例:**
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
标记测试用例ID为12345已完成,优先级是P0
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### 4. `get_test_progress` - 获取测试进度
|
|
141
|
+
|
|
142
|
+
获取当前测试进度,包括已完成和未完成的测试用例统计信息。
|
|
143
|
+
|
|
144
|
+
**参数:**
|
|
145
|
+
- 无需参数
|
|
146
|
+
|
|
147
|
+
**使用示例:**
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
查看当前测试进度
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## 工作流程
|
|
154
|
+
|
|
155
|
+
1. **获取测试清单**:使用 `get_test_list` 获取所有测试用例的 TODO 清单
|
|
156
|
+
2. **逐个测试自检**:根据 TODO 清单,使用 `get_test_detail` 逐个获取测试用例详情
|
|
157
|
+
3. **标记完成**:完成测试后,使用 `mark_test_completed` 标记测试用例为已完成
|
|
158
|
+
4. **查看进度**:使用 `get_test_progress` 查看当前测试进度
|
|
159
|
+
5. **继续执行**:使用 `get_test_list(excludeCompleted: true)` 获取未完成的测试用例,继续测试
|
|
160
|
+
6. **AI 辅助测试**:将 AI 测试提示语直接用于 Cursor,进行代码位置定位和测试检查
|
|
161
|
+
|
|
162
|
+
## 进度跟踪
|
|
163
|
+
|
|
164
|
+
MCP 工具会自动将测试进度保存到 `test-progress.json` 文件中。进度文件存储位置优先级:
|
|
165
|
+
1. **环境变量 `PROGRESS_FILE_DIR`** 指定的目录
|
|
166
|
+
2. **当前工作目录**(`process.cwd()`,通常是项目根目录)- **默认值**
|
|
167
|
+
3. **用户配置目录**(`~/.config/meter-sphere-mcp/`)- 回退方案
|
|
168
|
+
|
|
169
|
+
进度文件包含:
|
|
170
|
+
- 测试计划ID
|
|
171
|
+
- 已完成用例列表(包含用例ID、优先级、完成时间)
|
|
172
|
+
- 总用例数和已完成数量
|
|
173
|
+
|
|
174
|
+
**注意**:
|
|
175
|
+
- 如果切换了不同的测试计划(testPlanId),进度会自动重置
|
|
176
|
+
- 进度文件默认保存在用户项目根目录中,不同项目有独立的进度文件
|
|
177
|
+
- 建议将 `test-progress.json` 添加到 `.gitignore`,避免提交到代码仓库
|
|
178
|
+
|
|
179
|
+
## 注意事项
|
|
180
|
+
|
|
181
|
+
1. **URL 格式**:PLATFORM_URL 需要包含完整的查询参数,支持标准 query 参数和 hash 中的参数(SPA 路由格式)
|
|
182
|
+
2. **参数映射**:URL 中的 `id` 会自动映射为 `testPlanId`,`pId` 会自动映射为 `project`,`orgId` 会自动映射为 `organization`
|
|
183
|
+
3. **环境变量配置**:必须在 Cursor 的 MCP 配置中通过 `env` 字段设置 `PLATFORM_URL`、`X_AUTH_TOKEN`、`CSRF_TOKEN`
|
|
184
|
+
4. **API服务器地址**:可通过 `API_BASE_URL` 环境变量配置API服务器地址,默认值为 `http://192.168.3.26:8081`
|
|
185
|
+
5. **AI模型配置**:支持任意兼容 OpenAI API 格式的 AI 模型(Deepseek、OpenAI、Claude 等)
|
|
186
|
+
|
|
187
|
+
## License
|
|
188
|
+
|
|
189
|
+
ISC
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mcpServers": {
|
|
3
|
+
"meter-sphere": {
|
|
4
|
+
"command": "node",
|
|
5
|
+
"args": [
|
|
6
|
+
"./node_modules/@cqsjjb/meter-sphere-mcp-server/mcp-server.mjs"
|
|
7
|
+
],
|
|
8
|
+
"env": {
|
|
9
|
+
"PLATFORM_URL": "http://your-platform-url?organization=xxx&project=xxx&testPlanId=xxx",
|
|
10
|
+
"X_AUTH_TOKEN": "your-x-auth-token-here",
|
|
11
|
+
"CSRF_TOKEN": "your-csrf-token-here",
|
|
12
|
+
"API_BASE_URL": "http://192.168.3.26:8081",
|
|
13
|
+
"MODEL_BASE_URL": "https://api.deepseek.com",
|
|
14
|
+
"MODEL_API_KEY": "your-model-api-key-here",
|
|
15
|
+
"MODEL_ID": "deepseek-chat"
|
|
16
|
+
},
|
|
17
|
+
"description": "MeterSphere 测试用例平台 MCP 工具"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
package/mcp-server.mjs
ADDED
|
@@ -0,0 +1,846 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
4
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
|
+
import {
|
|
6
|
+
CallToolRequestSchema,
|
|
7
|
+
ListToolsRequestSchema,
|
|
8
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
9
|
+
import http from 'http';
|
|
10
|
+
import https from 'https';
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import os from 'os';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 获取API基础URL,从环境变量读取,默认值为 http://192.168.3.26:8081
|
|
17
|
+
*/
|
|
18
|
+
function getApiBaseUrl() {
|
|
19
|
+
return process.env.API_BASE_URL || 'http://192.168.3.26:8081';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 获取AI模型配置,从环境变量读取
|
|
24
|
+
*/
|
|
25
|
+
function getModelConfig() {
|
|
26
|
+
return {
|
|
27
|
+
baseURL: process.env.MODEL_BASE_URL || 'https://api.deepseek.com',
|
|
28
|
+
apiKey: process.env.MODEL_API_KEY || '',
|
|
29
|
+
model: process.env.MODEL_ID || 'deepseek-chat'
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 获取进度文件路径
|
|
35
|
+
* 优先级:环境变量 > 当前工作目录 > 用户主目录下的配置目录
|
|
36
|
+
*/
|
|
37
|
+
function getProgressFilePath() {
|
|
38
|
+
// 优先使用环境变量指定的目录
|
|
39
|
+
if (process.env.PROGRESS_FILE_DIR) {
|
|
40
|
+
return path.join(process.env.PROGRESS_FILE_DIR, 'test-progress.json');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 其次使用当前工作目录(用户运行命令的目录,通常是项目根目录)
|
|
44
|
+
// 这对于npm包来说是最合适的,因为进度文件应该跟随项目
|
|
45
|
+
const cwd = process.cwd();
|
|
46
|
+
if (cwd && cwd !== '/') {
|
|
47
|
+
return path.join(cwd, 'test-progress.json');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 最后回退到用户主目录下的配置目录
|
|
51
|
+
const homeDir = os.homedir();
|
|
52
|
+
const configDir = path.join(homeDir, '.config', 'meter-sphere-mcp');
|
|
53
|
+
|
|
54
|
+
// 确保配置目录存在
|
|
55
|
+
try {
|
|
56
|
+
if (!fs.existsSync(configDir)) {
|
|
57
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
58
|
+
}
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('创建配置目录失败:', error);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return path.join(configDir, 'test-progress.json');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 加载测试进度
|
|
68
|
+
*/
|
|
69
|
+
function loadProgress() {
|
|
70
|
+
const progressPath = getProgressFilePath();
|
|
71
|
+
try {
|
|
72
|
+
if (fs.existsSync(progressPath)) {
|
|
73
|
+
const content = fs.readFileSync(progressPath, 'utf8');
|
|
74
|
+
return JSON.parse(content);
|
|
75
|
+
}
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error('加载进度文件失败:', error);
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
testPlanId: null,
|
|
81
|
+
lastUpdate: null,
|
|
82
|
+
completed: [],
|
|
83
|
+
total: 0,
|
|
84
|
+
completedCount: 0
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 保存测试进度
|
|
90
|
+
*/
|
|
91
|
+
function saveProgress(progress) {
|
|
92
|
+
const progressPath = getProgressFilePath();
|
|
93
|
+
try {
|
|
94
|
+
progress.lastUpdate = new Date().toISOString();
|
|
95
|
+
fs.writeFileSync(progressPath, JSON.stringify(progress, null, 2), 'utf8');
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error('保存进度文件失败:', error);
|
|
98
|
+
throw new Error(`保存进度失败: ${error.message}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 检查测试用例是否已完成
|
|
104
|
+
*/
|
|
105
|
+
function isCompleted(testCaseId, progress) {
|
|
106
|
+
return progress.completed.some(item => item.testCaseId === testCaseId);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 标记测试用例为已完成
|
|
111
|
+
*/
|
|
112
|
+
function markCompleted(testCaseId, priority, progress) {
|
|
113
|
+
if (!isCompleted(testCaseId, progress)) {
|
|
114
|
+
progress.completed.push({
|
|
115
|
+
testCaseId: testCaseId,
|
|
116
|
+
priority: priority,
|
|
117
|
+
completedAt: new Date().toISOString()
|
|
118
|
+
});
|
|
119
|
+
progress.completedCount = progress.completed.length;
|
|
120
|
+
saveProgress(progress);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 发送 HTTP POST 请求
|
|
126
|
+
*/
|
|
127
|
+
function httpPost(url, headers = {}, data = {}) {
|
|
128
|
+
return new Promise((resolve, reject) => {
|
|
129
|
+
const urlObj = new URL(url);
|
|
130
|
+
const postData = JSON.stringify(data);
|
|
131
|
+
|
|
132
|
+
const requestHeaders = {
|
|
133
|
+
'Content-Type': 'application/json;charset=UTF-8',
|
|
134
|
+
'Content-Length': Buffer.byteLength(postData),
|
|
135
|
+
...headers
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const options = {
|
|
139
|
+
hostname: urlObj.hostname,
|
|
140
|
+
port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
|
|
141
|
+
path: urlObj.pathname + (urlObj.search || ''),
|
|
142
|
+
method: 'POST',
|
|
143
|
+
headers: requestHeaders
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const req = http.request(options, res => {
|
|
147
|
+
const chunks = [];
|
|
148
|
+
res.on('data', chunk => chunks.push(chunk));
|
|
149
|
+
res.on('end', () => {
|
|
150
|
+
const buffer = Buffer.concat(chunks);
|
|
151
|
+
const responseData = buffer.toString('utf8');
|
|
152
|
+
resolve(responseData);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
req.on('error', error => reject(error));
|
|
157
|
+
req.write(postData);
|
|
158
|
+
req.end();
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 发送 HTTP GET 请求
|
|
164
|
+
*/
|
|
165
|
+
function httpGet(url, headers = {}) {
|
|
166
|
+
return new Promise((resolve, reject) => {
|
|
167
|
+
const urlObj = new URL(url);
|
|
168
|
+
const options = {
|
|
169
|
+
hostname: urlObj.hostname,
|
|
170
|
+
port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
|
|
171
|
+
path: urlObj.pathname + (urlObj.search || ''),
|
|
172
|
+
method: 'GET',
|
|
173
|
+
headers: headers
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const req = http.request(options, res => {
|
|
177
|
+
const chunks = [];
|
|
178
|
+
res.on('data', chunk => chunks.push(chunk));
|
|
179
|
+
res.on('end', () => {
|
|
180
|
+
const buffer = Buffer.concat(chunks);
|
|
181
|
+
const responseData = buffer.toString('utf8');
|
|
182
|
+
resolve(responseData);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
req.on('error', error => reject(error));
|
|
187
|
+
req.end();
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* 解析URL获取查询参数(与前端index.html中的逻辑保持一致)
|
|
193
|
+
*/
|
|
194
|
+
function parseQueryParams(url) {
|
|
195
|
+
try {
|
|
196
|
+
// 创建URL对象来解析URL
|
|
197
|
+
const urlObj = new URL(url);
|
|
198
|
+
|
|
199
|
+
// 获取标准位置的query参数(在?之后,在#之前)
|
|
200
|
+
const params = {};
|
|
201
|
+
urlObj.searchParams.forEach((value, key) => {
|
|
202
|
+
params[key] = value;
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// 检查hash中是否包含query参数(SPA路由格式)
|
|
206
|
+
const hash = urlObj.hash;
|
|
207
|
+
if (hash && hash.includes('?')) {
|
|
208
|
+
// 提取hash中的query部分
|
|
209
|
+
const hashQueryString = hash.substring(hash.indexOf('?') + 1);
|
|
210
|
+
// 解析hash中的query参数
|
|
211
|
+
const hashParams = new URLSearchParams(hashQueryString);
|
|
212
|
+
hashParams.forEach((value, key) => {
|
|
213
|
+
// 如果标准位置已有同名参数,hash中的参数会覆盖它
|
|
214
|
+
params[key] = value;
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 参数映射(与前端保持一致)
|
|
219
|
+
if (params.id) {
|
|
220
|
+
params.testPlanId = params.id;
|
|
221
|
+
delete params.id;
|
|
222
|
+
}
|
|
223
|
+
if (params.pId) {
|
|
224
|
+
params.project = params.pId;
|
|
225
|
+
delete params.pId;
|
|
226
|
+
}
|
|
227
|
+
if (params.orgId) {
|
|
228
|
+
params.organization = params.orgId;
|
|
229
|
+
delete params.orgId;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return params;
|
|
233
|
+
} catch (error) {
|
|
234
|
+
throw new Error(`URL解析失败: ${error.message}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* 生成AI测试提示语
|
|
240
|
+
*/
|
|
241
|
+
function generateAIPrompt(detailData, testPlanCollectionName) {
|
|
242
|
+
try {
|
|
243
|
+
const name = detailData.name || '';
|
|
244
|
+
const steps = detailData.steps || [];
|
|
245
|
+
const stepsArray = typeof steps === 'string' ? JSON.parse(steps) : steps;
|
|
246
|
+
|
|
247
|
+
if (!Array.isArray(stepsArray) || stepsArray.length === 0) {
|
|
248
|
+
return `测试点:${testPlanCollectionName || ''}\n用例名称:${name}\n测试步骤:暂无测试步骤`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const sortedSteps = stepsArray.sort((a, b) => (a.num || 0) - (b.num || 0));
|
|
252
|
+
let prompt = `测试点:${testPlanCollectionName || ''}\n用例名称:${name}\n测试步骤:\n`;
|
|
253
|
+
|
|
254
|
+
sortedSteps.forEach((step, index) => {
|
|
255
|
+
const stepNum = index + 1;
|
|
256
|
+
const desc = step.desc || '';
|
|
257
|
+
const result = step.result || '';
|
|
258
|
+
const stepText = `${desc}${result ? ' 期望结果: ' + result : ''}`;
|
|
259
|
+
prompt += `${stepNum}. ${stepText}\n`;
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
return prompt.trim();
|
|
263
|
+
} catch (error) {
|
|
264
|
+
console.error('生成AI提示语错误:', error);
|
|
265
|
+
return '生成提示语失败';
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* 获取优先级等级(用于排序)
|
|
271
|
+
*/
|
|
272
|
+
function getPriorityLevel(priority) {
|
|
273
|
+
const priorityMap = {
|
|
274
|
+
'P0': 0,
|
|
275
|
+
'P1': 1,
|
|
276
|
+
'P2': 2,
|
|
277
|
+
'P3': 3
|
|
278
|
+
};
|
|
279
|
+
return priorityMap[priority] !== undefined ? priorityMap[priority] : 999;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* 调用 AI 模型 API 进行流式分析
|
|
284
|
+
*/
|
|
285
|
+
async function analyzeWithModel(promptText) {
|
|
286
|
+
const config = getModelConfig();
|
|
287
|
+
|
|
288
|
+
if (!config.apiKey) {
|
|
289
|
+
throw new Error('缺少 MODEL_API_KEY 环境变量配置');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const analysisPrompt = `你是一个专业的测试用例分析专家。请仔细分析以下测试用例,将其中的测试步骤按照以下三个类别进行分类:
|
|
293
|
+
|
|
294
|
+
**分类说明:**
|
|
295
|
+
1. **配置优化** - 涉及系统配置、参数设置、选项调整、权限配置、环境配置等
|
|
296
|
+
2. **前端交互功能** - 涉及用户界面操作、页面交互、表单填写、按钮点击、数据展示、样式验证等前端功能
|
|
297
|
+
3. **后端数据** - 涉及接口调用、数据处理、数据库操作、数据同步、API请求、数据查询等后端功能
|
|
298
|
+
|
|
299
|
+
**测试用例内容:**
|
|
300
|
+
${promptText}
|
|
301
|
+
|
|
302
|
+
**要求:**
|
|
303
|
+
1. 仔细分析每个测试步骤,判断它主要属于哪个类别
|
|
304
|
+
2. 对于每个测试步骤,明确标注其类别(配置优化/前端交互功能/后端数据)
|
|
305
|
+
3. 如果某个步骤涉及多个方面,选择最主要的一个类别
|
|
306
|
+
4. 最后给出整体分类结论:这个用例主要属于哪个类别
|
|
307
|
+
5. 如果是前端自测,请明确指出哪些步骤是前端需要测试的,哪些不需要
|
|
308
|
+
|
|
309
|
+
**输出格式:**
|
|
310
|
+
请按照以下格式输出:
|
|
311
|
+
|
|
312
|
+
1. 测试点:[测试点名称]
|
|
313
|
+
2. 用例名称:[用例名称]
|
|
314
|
+
3. 测试类别:[前端交互功能 / 配置优化 / 后端数据]
|
|
315
|
+
4. 测试步骤
|
|
316
|
+
- 需要测试的步骤:[列出前端相关的步骤]
|
|
317
|
+
- 不需要测试的步骤:[列出非前端相关的步骤]`;
|
|
318
|
+
|
|
319
|
+
return new Promise((resolve, reject) => {
|
|
320
|
+
const urlObj = new URL(`${config.baseURL}/v1/chat/completions`);
|
|
321
|
+
const postData = JSON.stringify({
|
|
322
|
+
model: config.model,
|
|
323
|
+
messages: [
|
|
324
|
+
{
|
|
325
|
+
role: 'user',
|
|
326
|
+
content: analysisPrompt
|
|
327
|
+
}
|
|
328
|
+
],
|
|
329
|
+
stream: true,
|
|
330
|
+
temperature: 0.3
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const options = {
|
|
334
|
+
hostname: urlObj.hostname,
|
|
335
|
+
port: urlObj.port || 443,
|
|
336
|
+
path: urlObj.pathname,
|
|
337
|
+
method: 'POST',
|
|
338
|
+
headers: {
|
|
339
|
+
'Content-Type': 'application/json',
|
|
340
|
+
'Authorization': `Bearer ${config.apiKey}`,
|
|
341
|
+
'Content-Length': Buffer.byteLength(postData)
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const req = https.request(options, (res) => {
|
|
346
|
+
if (res.statusCode !== 200) {
|
|
347
|
+
let errorData = '';
|
|
348
|
+
res.on('data', chunk => errorData += chunk.toString());
|
|
349
|
+
res.on('end', () => {
|
|
350
|
+
reject(new Error(`AI模型API请求失败: ${res.statusCode} - ${errorData}`));
|
|
351
|
+
});
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
let buffer = '';
|
|
356
|
+
let fullContent = '';
|
|
357
|
+
|
|
358
|
+
res.on('data', (chunk) => {
|
|
359
|
+
buffer += chunk.toString();
|
|
360
|
+
const lines = buffer.split('\n');
|
|
361
|
+
buffer = lines.pop() || '';
|
|
362
|
+
|
|
363
|
+
for (const line of lines) {
|
|
364
|
+
if (line.trim() === '') continue;
|
|
365
|
+
if (line.startsWith('data: ')) {
|
|
366
|
+
const data = line.slice(6);
|
|
367
|
+
if (data === '[DONE]') {
|
|
368
|
+
resolve(fullContent);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
try {
|
|
372
|
+
const json = JSON.parse(data);
|
|
373
|
+
const content = json.choices?.[0]?.delta?.content || '';
|
|
374
|
+
if (content) {
|
|
375
|
+
fullContent += content;
|
|
376
|
+
}
|
|
377
|
+
} catch (e) {
|
|
378
|
+
// 忽略JSON解析错误
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
res.on('end', () => {
|
|
385
|
+
if (fullContent) {
|
|
386
|
+
resolve(fullContent);
|
|
387
|
+
} else {
|
|
388
|
+
reject(new Error('AI模型API响应为空'));
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
req.on('error', (error) => {
|
|
394
|
+
reject(new Error(`AI模型API请求错误: ${error.message}`));
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
req.write(postData);
|
|
398
|
+
req.end();
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
class MeterSphereMCPServer {
|
|
403
|
+
constructor() {
|
|
404
|
+
this.server = new Server(
|
|
405
|
+
{
|
|
406
|
+
name: 'meter-sphere-test-cases',
|
|
407
|
+
version: '1.0.0',
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
capabilities: {
|
|
411
|
+
tools: {},
|
|
412
|
+
},
|
|
413
|
+
}
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
this.setupHandlers();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
setupHandlers() {
|
|
420
|
+
// 列出可用工具
|
|
421
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
422
|
+
return {
|
|
423
|
+
tools: [
|
|
424
|
+
{
|
|
425
|
+
name: 'get_test_list',
|
|
426
|
+
description: '获取所有优先级的测试用例列表,按优先级排序(P0 > P1 > P2 > P3),生成TODO清单。支持过滤已完成用例和显示完成状态。配置信息从环境变量读取(PLATFORM_URL、X_AUTH_TOKEN、CSRF_TOKEN)。重要提示:当用户要求"继续测试"、"继续自检"、"跳过已完成的"或类似需求时,必须设置 excludeCompleted=true 来排除已完成的测试用例,只返回未完成的用例列表。',
|
|
427
|
+
inputSchema: {
|
|
428
|
+
type: 'object',
|
|
429
|
+
properties: {
|
|
430
|
+
excludeCompleted: {
|
|
431
|
+
type: 'boolean',
|
|
432
|
+
description: '是否排除已完成的测试用例。默认为false(显示所有用例)。当用户要求继续测试、跳过已完成用例时,必须设置为true。设置为true后,返回的列表将只包含未完成的测试用例,方便继续测试工作。',
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
required: [],
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
name: 'get_test_detail',
|
|
440
|
+
description: '根据测试用例ID获取详细信息,包括测试步骤和AI测试提示语。会自动调用AI模型进行测试用例分析。配置信息从环境变量读取(PLATFORM_URL、X_AUTH_TOKEN、CSRF_TOKEN、MODEL_BASE_URL、MODEL_API_KEY、MODEL_ID)。',
|
|
441
|
+
inputSchema: {
|
|
442
|
+
type: 'object',
|
|
443
|
+
properties: {
|
|
444
|
+
testCaseId: {
|
|
445
|
+
type: 'string',
|
|
446
|
+
description: '测试用例ID',
|
|
447
|
+
},
|
|
448
|
+
testPlanCollectionName: {
|
|
449
|
+
type: 'string',
|
|
450
|
+
description: '测试点名称(从get_test_list返回)',
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
required: ['testCaseId', 'testPlanCollectionName'],
|
|
454
|
+
},
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
name: 'mark_test_completed',
|
|
458
|
+
description: '标记指定测试用例为已完成状态,用于进度跟踪。',
|
|
459
|
+
inputSchema: {
|
|
460
|
+
type: 'object',
|
|
461
|
+
properties: {
|
|
462
|
+
testCaseId: {
|
|
463
|
+
type: 'string',
|
|
464
|
+
description: '测试用例ID',
|
|
465
|
+
},
|
|
466
|
+
priority: {
|
|
467
|
+
type: 'string',
|
|
468
|
+
description: '测试用例优先级(P0/P1/P2/P3)',
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
required: ['testCaseId', 'priority'],
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
name: 'get_test_progress',
|
|
476
|
+
description: '获取当前测试进度,包括已完成和未完成的测试用例统计信息。',
|
|
477
|
+
inputSchema: {
|
|
478
|
+
type: 'object',
|
|
479
|
+
properties: {},
|
|
480
|
+
required: [],
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
],
|
|
484
|
+
};
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// 处理工具调用
|
|
488
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
489
|
+
const { name, arguments: args } = request.params;
|
|
490
|
+
|
|
491
|
+
try {
|
|
492
|
+
if (name === 'get_test_list') {
|
|
493
|
+
return await this.handleGetTestList(args);
|
|
494
|
+
} else if (name === 'get_test_detail') {
|
|
495
|
+
return await this.handleGetTestDetail(args);
|
|
496
|
+
} else if (name === 'mark_test_completed') {
|
|
497
|
+
return await this.handleMarkTestCompleted(args);
|
|
498
|
+
} else if (name === 'get_test_progress') {
|
|
499
|
+
return await this.handleGetTestProgress(args);
|
|
500
|
+
} else {
|
|
501
|
+
throw new Error(`未知工具: ${name}`);
|
|
502
|
+
}
|
|
503
|
+
} catch (error) {
|
|
504
|
+
return {
|
|
505
|
+
content: [
|
|
506
|
+
{
|
|
507
|
+
type: 'text',
|
|
508
|
+
text: `错误: ${error.message}\n${error.stack || ''}`,
|
|
509
|
+
},
|
|
510
|
+
],
|
|
511
|
+
isError: true,
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
async handleGetTestList(args) {
|
|
518
|
+
const { excludeCompleted = false } = args || {};
|
|
519
|
+
|
|
520
|
+
// 从环境变量读取配置
|
|
521
|
+
const platformUrl = process.env.PLATFORM_URL;
|
|
522
|
+
const xAuthToken = process.env.X_AUTH_TOKEN;
|
|
523
|
+
const csrfToken = process.env.CSRF_TOKEN;
|
|
524
|
+
|
|
525
|
+
if (!platformUrl || !xAuthToken || !csrfToken) {
|
|
526
|
+
throw new Error('缺少必要配置。请在 Cursor 的 MCP 配置中通过环境变量设置 PLATFORM_URL、X_AUTH_TOKEN、CSRF_TOKEN');
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// 解析URL获取参数
|
|
530
|
+
const parsedParams = parseQueryParams(platformUrl);
|
|
531
|
+
const { organization, project, testPlanId } = parsedParams;
|
|
532
|
+
|
|
533
|
+
if (!organization || !project || !testPlanId) {
|
|
534
|
+
throw new Error('URL中缺少必要参数(organization、project、testPlanId)');
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// 加载进度
|
|
538
|
+
const progress = loadProgress();
|
|
539
|
+
if (progress.testPlanId && progress.testPlanId !== testPlanId) {
|
|
540
|
+
// 如果testPlanId变化,重置进度
|
|
541
|
+
progress.testPlanId = testPlanId;
|
|
542
|
+
progress.completed = [];
|
|
543
|
+
progress.completedCount = 0;
|
|
544
|
+
saveProgress(progress);
|
|
545
|
+
} else if (!progress.testPlanId) {
|
|
546
|
+
progress.testPlanId = testPlanId;
|
|
547
|
+
saveProgress(progress);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// 构建目标API URL
|
|
551
|
+
const apiBaseUrl = getApiBaseUrl();
|
|
552
|
+
const targetUrl = `${apiBaseUrl}/test-plan/functional/case/page`;
|
|
553
|
+
|
|
554
|
+
const headers = {
|
|
555
|
+
'Csrf-token': csrfToken,
|
|
556
|
+
'organization': organization,
|
|
557
|
+
'project': project,
|
|
558
|
+
'x-auth-token': xAuthToken,
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
const requestData = {
|
|
562
|
+
current: 1,
|
|
563
|
+
pageSize: 500,
|
|
564
|
+
sort: {},
|
|
565
|
+
keyword: '',
|
|
566
|
+
viewId: '',
|
|
567
|
+
combineSearch: {
|
|
568
|
+
searchMode: 'AND',
|
|
569
|
+
conditions: [],
|
|
570
|
+
},
|
|
571
|
+
filter: {},
|
|
572
|
+
treeType: 'COLLECTION',
|
|
573
|
+
testPlanId: testPlanId,
|
|
574
|
+
collectionId: '',
|
|
575
|
+
projectId: '',
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
const response = await httpPost(targetUrl, headers, requestData);
|
|
579
|
+
const data = JSON.parse(response);
|
|
580
|
+
|
|
581
|
+
if (!data || !data.data || !data.data.list) {
|
|
582
|
+
throw new Error('获取测试用例列表失败:响应数据格式不正确');
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const testCases = data.data.list;
|
|
586
|
+
|
|
587
|
+
// 处理所有测试用例,提取优先级并按优先级排序
|
|
588
|
+
let processedTestCases = testCases
|
|
589
|
+
.map(item => {
|
|
590
|
+
const priority = (item.customFields && item.customFields[0] && item.customFields[0].defaultValue) || '-';
|
|
591
|
+
return {
|
|
592
|
+
id: item.id,
|
|
593
|
+
name: item.name,
|
|
594
|
+
priority: priority.toUpperCase(),
|
|
595
|
+
testPlanCollectionName: item.testPlanCollectionName || '',
|
|
596
|
+
createUserName: item.createUserName || '',
|
|
597
|
+
moduleName: item.moduleName || '',
|
|
598
|
+
};
|
|
599
|
+
})
|
|
600
|
+
.sort((a, b) => {
|
|
601
|
+
return getPriorityLevel(a.priority) - getPriorityLevel(b.priority);
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
// 更新进度中的总数
|
|
605
|
+
progress.total = processedTestCases.length;
|
|
606
|
+
saveProgress(progress);
|
|
607
|
+
|
|
608
|
+
// 如果excludeCompleted为true,过滤已完成的用例
|
|
609
|
+
if (excludeCompleted) {
|
|
610
|
+
processedTestCases = processedTestCases.filter(testCase => {
|
|
611
|
+
return !isCompleted(testCase.id, progress);
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// 统计各优先级的数量
|
|
616
|
+
const priorityCounts = {
|
|
617
|
+
P0: 0,
|
|
618
|
+
P1: 0,
|
|
619
|
+
P2: 0,
|
|
620
|
+
P3: 0,
|
|
621
|
+
other: 0
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
processedTestCases.forEach(testCase => {
|
|
625
|
+
const priority = testCase.priority;
|
|
626
|
+
if (priorityCounts.hasOwnProperty(priority)) {
|
|
627
|
+
priorityCounts[priority]++;
|
|
628
|
+
} else {
|
|
629
|
+
priorityCounts.other++;
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
const totalCount = processedTestCases.length;
|
|
634
|
+
|
|
635
|
+
// 生成统计信息
|
|
636
|
+
const stats = [
|
|
637
|
+
`共找到 (${totalCount}) 个测试用例;`,
|
|
638
|
+
`P0: ${priorityCounts.P0}个`,
|
|
639
|
+
`P1: ${priorityCounts.P1}个`,
|
|
640
|
+
`P2: ${priorityCounts.P2}个`,
|
|
641
|
+
`P3: ${priorityCounts.P3}个`
|
|
642
|
+
].join('\n');
|
|
643
|
+
|
|
644
|
+
// 生成TODO清单格式:✅/⬜ 用例ID:1245456454654545(P0)
|
|
645
|
+
const todoList = processedTestCases.map(testCase => {
|
|
646
|
+
const status = isCompleted(testCase.id, progress) ? '✅' : '⬜';
|
|
647
|
+
return `${status} 用例ID:${testCase.id}(${testCase.priority})`;
|
|
648
|
+
}).join('\n');
|
|
649
|
+
|
|
650
|
+
const progressInfo = excludeCompleted
|
|
651
|
+
? `\n(已过滤已完成用例,剩余 ${processedTestCases.length} 个未完成用例)`
|
|
652
|
+
: `\n(已完成: ${progress.completedCount}/${progress.total})\n提示:如需继续测试未完成的用例,请使用 excludeCompleted=true 参数调用此工具`;
|
|
653
|
+
|
|
654
|
+
const result = `${stats}${progressInfo}\n\nTODO:\n${todoList}`;
|
|
655
|
+
|
|
656
|
+
return {
|
|
657
|
+
content: [
|
|
658
|
+
{
|
|
659
|
+
type: 'text',
|
|
660
|
+
text: result,
|
|
661
|
+
},
|
|
662
|
+
],
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
async handleGetTestDetail(args) {
|
|
667
|
+
const { testCaseId, testPlanCollectionName } = args;
|
|
668
|
+
|
|
669
|
+
// 检查用例是否已完成
|
|
670
|
+
const progress = loadProgress();
|
|
671
|
+
const isCaseCompleted = isCompleted(testCaseId, progress);
|
|
672
|
+
let completedInfo = '';
|
|
673
|
+
if (isCaseCompleted) {
|
|
674
|
+
const completedItem = progress.completed.find(item => item.testCaseId === testCaseId);
|
|
675
|
+
const completedTime = completedItem ? new Date(completedItem.completedAt).toLocaleString('zh-CN') : '';
|
|
676
|
+
completedInfo = `\n\n⚠️ **注意:此测试用例已完成** ✅\n完成时间:${completedTime}\n如需继续测试其他用例,请使用 get_test_list 工具并设置 excludeCompleted=true 获取未完成的用例列表。\n`;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// 从环境变量读取配置
|
|
680
|
+
const platformUrl = process.env.PLATFORM_URL;
|
|
681
|
+
const xAuthToken = process.env.X_AUTH_TOKEN;
|
|
682
|
+
const csrfToken = process.env.CSRF_TOKEN;
|
|
683
|
+
|
|
684
|
+
if (!platformUrl || !xAuthToken || !csrfToken) {
|
|
685
|
+
throw new Error('缺少必要配置。请在 Cursor 的 MCP 配置中通过环境变量设置 PLATFORM_URL、X_AUTH_TOKEN、CSRF_TOKEN');
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// 解析URL获取参数
|
|
689
|
+
const parsedParams = parseQueryParams(platformUrl);
|
|
690
|
+
const { organization, project } = parsedParams;
|
|
691
|
+
|
|
692
|
+
if (!organization || !project) {
|
|
693
|
+
throw new Error('URL中缺少必要参数(organization、project)');
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// 构建目标API URL
|
|
697
|
+
const apiBaseUrl = getApiBaseUrl();
|
|
698
|
+
const targetUrl = `${apiBaseUrl}/test-plan/functional/case/detail/${testCaseId}`;
|
|
699
|
+
|
|
700
|
+
const headers = {
|
|
701
|
+
'Csrf-token': csrfToken,
|
|
702
|
+
'organization': organization,
|
|
703
|
+
'project': project,
|
|
704
|
+
'x-auth-token': xAuthToken,
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
const response = await httpGet(targetUrl, headers);
|
|
708
|
+
const data = JSON.parse(response);
|
|
709
|
+
|
|
710
|
+
if (!data || !data.data) {
|
|
711
|
+
throw new Error('获取测试用例详情失败:响应数据格式不正确');
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const detailData = data.data;
|
|
715
|
+
const aiPrompt = generateAIPrompt(detailData, testPlanCollectionName);
|
|
716
|
+
|
|
717
|
+
// 尝试调用AI模型进行分析
|
|
718
|
+
let modelAnalysis = '';
|
|
719
|
+
try {
|
|
720
|
+
modelAnalysis = await analyzeWithModel(aiPrompt);
|
|
721
|
+
} catch (error) {
|
|
722
|
+
console.error('AI模型分析失败:', error);
|
|
723
|
+
modelAnalysis = `\n\n⚠️ AI模型分析失败: ${error.message}\n(如果未配置MODEL_API_KEY,可以忽略此错误)`;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// 添加AI测试提示语前缀
|
|
727
|
+
const fullPrompt = '请根据下列提示语中的测试点、用例名称、测试类别、测试步骤、分析说明找到当前项目中对应的代码位置,进行测试检查\n\n' + aiPrompt;
|
|
728
|
+
|
|
729
|
+
let resultText = `## 测试用例详情\n\n` +
|
|
730
|
+
`**用例ID**: ${testCaseId}${completedInfo}` +
|
|
731
|
+
`## 测试内容\n\n\`\`\`\n${fullPrompt}\n\`\`\`\n`;
|
|
732
|
+
|
|
733
|
+
if (modelAnalysis && !modelAnalysis.includes('AI模型分析失败')) {
|
|
734
|
+
resultText += `\n## AI分析结果\n\n\`\`\`\n${modelAnalysis}\n\`\`\`\n`;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
return {
|
|
738
|
+
content: [
|
|
739
|
+
{
|
|
740
|
+
type: 'text',
|
|
741
|
+
text: resultText
|
|
742
|
+
},
|
|
743
|
+
],
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
async handleMarkTestCompleted(args) {
|
|
748
|
+
const { testCaseId, priority } = args;
|
|
749
|
+
|
|
750
|
+
if (!testCaseId || !priority) {
|
|
751
|
+
throw new Error('缺少必要参数:testCaseId 和 priority');
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const progress = loadProgress();
|
|
755
|
+
markCompleted(testCaseId, priority, progress);
|
|
756
|
+
|
|
757
|
+
return {
|
|
758
|
+
content: [
|
|
759
|
+
{
|
|
760
|
+
type: 'text',
|
|
761
|
+
text: `✅ 已标记测试用例 ${testCaseId}(${priority}) 为已完成\n\n当前进度: ${progress.completedCount}/${progress.total}`
|
|
762
|
+
},
|
|
763
|
+
],
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
async handleGetTestProgress(args) {
|
|
768
|
+
const progress = loadProgress();
|
|
769
|
+
|
|
770
|
+
if (!progress.testPlanId || progress.total === 0) {
|
|
771
|
+
return {
|
|
772
|
+
content: [
|
|
773
|
+
{
|
|
774
|
+
type: 'text',
|
|
775
|
+
text: '暂无测试进度记录。请先调用 get_test_list 获取测试用例列表。'
|
|
776
|
+
},
|
|
777
|
+
],
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const completedCount = progress.completedCount;
|
|
782
|
+
const total = progress.total;
|
|
783
|
+
const remaining = total - completedCount;
|
|
784
|
+
const percentage = total > 0 ? Math.round((completedCount / total) * 100) : 0;
|
|
785
|
+
|
|
786
|
+
// 按优先级统计已完成用例
|
|
787
|
+
const completedByPriority = {
|
|
788
|
+
P0: 0,
|
|
789
|
+
P1: 0,
|
|
790
|
+
P2: 0,
|
|
791
|
+
P3: 0,
|
|
792
|
+
other: 0
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
progress.completed.forEach(item => {
|
|
796
|
+
const priority = item.priority || 'other';
|
|
797
|
+
if (completedByPriority.hasOwnProperty(priority)) {
|
|
798
|
+
completedByPriority[priority]++;
|
|
799
|
+
} else {
|
|
800
|
+
completedByPriority.other++;
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
const progressText = [
|
|
805
|
+
`## 测试进度统计`,
|
|
806
|
+
``,
|
|
807
|
+
`总用例数: ${total}`,
|
|
808
|
+
`已完成: ${completedCount}`,
|
|
809
|
+
`剩余: ${remaining}`,
|
|
810
|
+
`完成率: ${percentage}%`,
|
|
811
|
+
``,
|
|
812
|
+
`### 已完成用例按优先级分布:`,
|
|
813
|
+
`P0: ${completedByPriority.P0}个`,
|
|
814
|
+
`P1: ${completedByPriority.P1}个`,
|
|
815
|
+
`P2: ${completedByPriority.P2}个`,
|
|
816
|
+
`P3: ${completedByPriority.P3}个`,
|
|
817
|
+
``,
|
|
818
|
+
`### 已完成用例列表:`,
|
|
819
|
+
...(progress.completed.length > 0
|
|
820
|
+
? progress.completed.map(item =>
|
|
821
|
+
`✅ 用例ID:${item.testCaseId}(${item.priority}) - ${new Date(item.completedAt).toLocaleString('zh-CN')}`
|
|
822
|
+
)
|
|
823
|
+
: ['暂无']
|
|
824
|
+
)
|
|
825
|
+
].join('\n');
|
|
826
|
+
|
|
827
|
+
return {
|
|
828
|
+
content: [
|
|
829
|
+
{
|
|
830
|
+
type: 'text',
|
|
831
|
+
text: progressText
|
|
832
|
+
},
|
|
833
|
+
],
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
async run() {
|
|
838
|
+
const transport = new StdioServerTransport();
|
|
839
|
+
await this.server.connect(transport);
|
|
840
|
+
console.error('MeterSphere MCP Server 已启动');
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// 启动服务器
|
|
845
|
+
const server = new MeterSphereMCPServer();
|
|
846
|
+
server.run().catch(console.error);
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cqsjjb/meter-sphere-mcp-server",
|
|
3
|
+
"version": "1.0.0-beta.1",
|
|
4
|
+
"description": "MCP server for MeterSphere test cases platform - Get test cases, generate AI test prompts, and track testing progress",
|
|
5
|
+
"main": "mcp-server.mjs",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"meter-sphere-mcp": "./mcp-server.mjs"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node mcp-server.mjs"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"mcp",
|
|
15
|
+
"model-context-protocol",
|
|
16
|
+
"meter-sphere",
|
|
17
|
+
"test-cases",
|
|
18
|
+
"testing",
|
|
19
|
+
"ai",
|
|
20
|
+
"cursor"
|
|
21
|
+
],
|
|
22
|
+
"author": "",
|
|
23
|
+
"license": "ISC",
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@modelcontextprotocol/sdk": "^0.5.0"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18.0.0"
|
|
29
|
+
},
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "http://192.168.1.242:10985/jjb/ai-agent/mcp/meter-sphere-mcp-server.git"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"mcp-server.mjs",
|
|
36
|
+
"README.md",
|
|
37
|
+
"mcp-config.example.json"
|
|
38
|
+
]
|
|
39
|
+
}
|