ask-user-question-plus 1.0.0
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/LICENSE +21 -0
- package/README.md +264 -0
- package/dist/index.js +147 -0
- package/dist/mcp-server.js +137 -0
- package/dist/ws-service.js +186 -0
- package/package.json +51 -0
- package/public/index.html +1184 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 JoJoJotarou
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# AskUserQuestionPlus
|
|
2
|
+
|
|
3
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
|
+
[](https://www.npmjs.com/package/ask-user-question-plus)
|
|
5
|
+
|
|
6
|
+
一个功能强大的 MCP (Model Context Protocol) 服务器,通过精美的 TUI 风格 Web 界面让 LLM(如 Claude)向用户提问。
|
|
7
|
+
|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
## ✨ 特性
|
|
11
|
+
|
|
12
|
+
- **🎨 TUI 风格界面**:浏览器中呈现像素级完美的类终端外观(暗/浅色主题)
|
|
13
|
+
- **⌨️ 全键盘操作**:支持方向键、Tab、Space、Enter 快捷键
|
|
14
|
+
- **📊 丰富交互**:单选、多选、推荐选项、自定义输入(Other)
|
|
15
|
+
- **🔄 实时通信**:基于 WebSocket 的实时消息传递
|
|
16
|
+
- **⏱️ 超时保护**:可配置超时(默认 10 分钟)
|
|
17
|
+
- **📈 可扩展**:支持 1-20 个问题,每个问题选项数量不限
|
|
18
|
+
|
|
19
|
+
### Claude Code AskUserQuestion Tool vs AskUserQuestionPlus
|
|
20
|
+
|
|
21
|
+
| 对比项 | AskUserQuestion (内置) | ask-user-question-plus (MCP) |
|
|
22
|
+
| ------------ | ---------------------- | ---------------------------- |
|
|
23
|
+
| 问题数量限制 | 1-4 个 | 1-20 个 |
|
|
24
|
+
| 选项数量限制 | 2-4 个 | 无限制 |
|
|
25
|
+
|
|
26
|
+
## 📦 安装
|
|
27
|
+
|
|
28
|
+
### 方式 1:直接安装(推荐)
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install -g ask-user-question-plus
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### 方式 2:从源码构建
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
git clone https://github.com/JoJoJotarou/AskUserQuestionPlus.git
|
|
38
|
+
cd AskUserQuestionPlus
|
|
39
|
+
npm install
|
|
40
|
+
npm run build
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 🚀 配置
|
|
44
|
+
|
|
45
|
+
将此服务器添加到你的 MCP 客户端配置文件(Claude Desktop 的 `claude_desktop_config.json` 或 Claude Code 配置)。
|
|
46
|
+
|
|
47
|
+
### Claude Desktop 配置
|
|
48
|
+
|
|
49
|
+
**基础配置:**
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"mcpServers": {
|
|
54
|
+
"ask-user-question-plus": {
|
|
55
|
+
"command": "npx",
|
|
56
|
+
"args": ["ask-user-question-plus"]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**自定义超时(毫秒):**
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"mcpServers": {
|
|
67
|
+
"ask-user-question-plus": {
|
|
68
|
+
"command": "npx",
|
|
69
|
+
"args": ["ask-user-question-plus", "--timeout=300000"]
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 启动参数
|
|
76
|
+
|
|
77
|
+
| 参数 | 说明 | 默认值 | 示例 |
|
|
78
|
+
| ------------------ | -------------- | ---------------- | ------------------ |
|
|
79
|
+
| `--timeout=<毫秒>` | 会话超时时间 | 600000 (10 分钟) | `--timeout=300000` |
|
|
80
|
+
| `--port=<端口>` | WebSocket 端口 | 3456 | `--port=8080` |
|
|
81
|
+
|
|
82
|
+
## 📖 使用方法
|
|
83
|
+
|
|
84
|
+
### 基本流程
|
|
85
|
+
|
|
86
|
+
1. 启动与 Claude 的对话
|
|
87
|
+
2. 让 Claude 使用工具:`使用 ask_user_question_plus 工具问我 3 个问题`
|
|
88
|
+
3. 浏览器自动打开问卷界面
|
|
89
|
+
4. 使用键盘(方向键、Space、Enter)回答问题
|
|
90
|
+
5. 提交后浏览器标签页自动关闭
|
|
91
|
+
|
|
92
|
+
### 键盘快捷键
|
|
93
|
+
|
|
94
|
+
| 快捷键 | 功能 |
|
|
95
|
+
| ------------------ | ----------------------------- |
|
|
96
|
+
| `↑` `↓` | 在选项之间移动 / 滚动问题内容 |
|
|
97
|
+
| `←` `→` / `Tab` | 在问题标签页之间切换 |
|
|
98
|
+
| `Space` | 选中/取消选中当前选项 |
|
|
99
|
+
| `Cmd/Ctrl + Enter` | 进入审查模式 / 提交答案 |
|
|
100
|
+
|
|
101
|
+
## 🔧 工具 API
|
|
102
|
+
|
|
103
|
+
### MCP 工具:`ask_user_question_plus`
|
|
104
|
+
|
|
105
|
+
**输入 Schema:**
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
{
|
|
109
|
+
questions: [
|
|
110
|
+
{
|
|
111
|
+
id: string; // 问题唯一标识
|
|
112
|
+
header: string; // 标签标题(≤12 字符推荐)
|
|
113
|
+
text: string; // 问题文本
|
|
114
|
+
type: "single" | "multiple"; // 单选或多选
|
|
115
|
+
options: [
|
|
116
|
+
{
|
|
117
|
+
value: string; // 选项值
|
|
118
|
+
label: string; // 选项标签
|
|
119
|
+
description?: string; // 选项描述(可选)
|
|
120
|
+
recommended?: boolean;// 是否推荐(可选)
|
|
121
|
+
}
|
|
122
|
+
]
|
|
123
|
+
}
|
|
124
|
+
] // 最少 1 个,最多 20 个问题
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**输出 Schema:**
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
{
|
|
132
|
+
[questionId: string]: string | string[] // 单选为字符串,多选为数组
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
> **💡 提示**:前端会自动为每个问题添加 "Other (自定义输入)" 选项,无需手动指定。
|
|
137
|
+
|
|
138
|
+
### 使用示例
|
|
139
|
+
|
|
140
|
+
**单选问题:**
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"questions": [
|
|
145
|
+
{
|
|
146
|
+
"id": "framework",
|
|
147
|
+
"header": "Framework",
|
|
148
|
+
"text": "选择你的前端框架:",
|
|
149
|
+
"type": "single",
|
|
150
|
+
"options": [
|
|
151
|
+
{
|
|
152
|
+
"value": "react",
|
|
153
|
+
"label": "React",
|
|
154
|
+
"description": "用于构建用户界面的 JavaScript 库",
|
|
155
|
+
"recommended": true
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"value": "vue",
|
|
159
|
+
"label": "Vue",
|
|
160
|
+
"description": "渐进式 JavaScript 框架"
|
|
161
|
+
}
|
|
162
|
+
]
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**多选问题:**
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
{
|
|
172
|
+
"questions": [
|
|
173
|
+
{
|
|
174
|
+
"id": "features",
|
|
175
|
+
"header": "Features",
|
|
176
|
+
"text": "你需要哪些功能?(可多选)",
|
|
177
|
+
"type": "multiple",
|
|
178
|
+
"options": [
|
|
179
|
+
{
|
|
180
|
+
"value": "auth",
|
|
181
|
+
"label": "用户认证",
|
|
182
|
+
"description": "登录、注册、密码重置"
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"value": "api",
|
|
186
|
+
"label": "REST API",
|
|
187
|
+
"description": "RESTful API 端点"
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"value": "db",
|
|
191
|
+
"label": "数据库",
|
|
192
|
+
"description": "PostgreSQL/MySQL 支持"
|
|
193
|
+
}
|
|
194
|
+
]
|
|
195
|
+
}
|
|
196
|
+
]
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## 🛠️ 开发
|
|
201
|
+
|
|
202
|
+
### 运行开发模式
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
npm run dev
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### 构建项目
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
npm run build
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### 目录结构
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
ask-user-question-plus/
|
|
218
|
+
├── src/
|
|
219
|
+
│ ├── index.ts # 主入口文件
|
|
220
|
+
│ ├── ws-service.ts # WebSocket 服务
|
|
221
|
+
│ └── mcp-server.ts # MCP 服务器逻辑
|
|
222
|
+
├── public/
|
|
223
|
+
│ └── index.html # TUI 前端界面
|
|
224
|
+
├── dist/ # 编译输出目录
|
|
225
|
+
└── package.json # 项目配置
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### 技术栈
|
|
229
|
+
|
|
230
|
+
- **后端**:Node.js 20+, TypeScript, Express, ws, @modelcontextprotocol/sdk, Zod
|
|
231
|
+
- **前端**:原生 HTML/CSS/JavaScript(单文件 1184 行)
|
|
232
|
+
|
|
233
|
+
## 🐳 Docker 部署
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
# 使用 Docker Compose
|
|
237
|
+
docker-compose up -d
|
|
238
|
+
|
|
239
|
+
# 查看日志
|
|
240
|
+
docker-compose logs -f
|
|
241
|
+
|
|
242
|
+
# 停止服务
|
|
243
|
+
docker-compose down
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## 🤝 贡献
|
|
247
|
+
|
|
248
|
+
欢迎贡献!请提交 [Issues](https://github.com/JoJoJotarou/AskUserQuestionPlus/issues) 或 Pull Requests。
|
|
249
|
+
|
|
250
|
+
## 📄 许可证
|
|
251
|
+
|
|
252
|
+
[MIT License](LICENSE)
|
|
253
|
+
|
|
254
|
+
## 🔗 相关链接
|
|
255
|
+
|
|
256
|
+
- [GitHub 仓库](https://github.com/JoJoJotarou/AskUserQuestionPlus)
|
|
257
|
+
- [npm 包](https://www.npmjs.com/package/ask-user-question-plus)
|
|
258
|
+
- [问题反馈](https://github.com/JoJoJotarou/AskUserQuestionPlus/issues)
|
|
259
|
+
- [Model Context Protocol](https://modelcontextprotocol.io/)
|
|
260
|
+
- [Claude Code](https://github.com/anthropics/claude-code)
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
**注意**:此工具需要浏览器支持,确保系统可以自动打开默认浏览器。
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "dotenv/config"; // Load .env file
|
|
3
|
+
import express from "express";
|
|
4
|
+
import { WebSocketServer } from "ws";
|
|
5
|
+
import { createServer } from "http";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
import { WebSocketService } from "./ws-service.js";
|
|
9
|
+
import { MCPServerService } from "./mcp-server.js";
|
|
10
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const DEFAULT_CONFIG = {
|
|
12
|
+
PORT: 3456,
|
|
13
|
+
TIMEOUT: 600000, // 10 minutes
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Parse configuration from environment variables and command line arguments
|
|
17
|
+
* Priority: CLI args > Environment variables > Defaults
|
|
18
|
+
*/
|
|
19
|
+
function parseConfig() {
|
|
20
|
+
const config = {
|
|
21
|
+
// 1. Defaults
|
|
22
|
+
...DEFAULT_CONFIG,
|
|
23
|
+
};
|
|
24
|
+
// 2. Environment variables
|
|
25
|
+
if (process.env.PORT) {
|
|
26
|
+
const port = parseInt(process.env.PORT, 10);
|
|
27
|
+
if (!isNaN(port) && port > 0 && port <= 65535) {
|
|
28
|
+
config.PORT = port;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
console.error(`[Config] Invalid PORT environment variable: ${process.env.PORT}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (process.env.TIMEOUT) {
|
|
35
|
+
const timeout = parseInt(process.env.TIMEOUT, 10);
|
|
36
|
+
if (!isNaN(timeout) && timeout > 0) {
|
|
37
|
+
config.TIMEOUT = timeout;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
console.error(`[Config] Invalid TIMEOUT environment variable: ${process.env.TIMEOUT}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// 3. Command line arguments (override environment variables)
|
|
44
|
+
const args = process.argv.slice(2);
|
|
45
|
+
for (const arg of args) {
|
|
46
|
+
if (arg.startsWith("--port=")) {
|
|
47
|
+
const port = parseInt(arg.split("=")[1], 10);
|
|
48
|
+
if (!isNaN(port) && port > 0 && port <= 65535) {
|
|
49
|
+
config.PORT = port;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
console.error(`[Config] Invalid --port argument: ${arg}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else if (arg.startsWith("--timeout=")) {
|
|
56
|
+
const timeout = parseInt(arg.split("=")[1], 10);
|
|
57
|
+
if (!isNaN(timeout) && timeout > 0) {
|
|
58
|
+
config.TIMEOUT = timeout;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
console.error(`[Config] Invalid --timeout argument: ${arg}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return config;
|
|
66
|
+
}
|
|
67
|
+
// --- Main ---
|
|
68
|
+
async function main() {
|
|
69
|
+
console.error("=== Server Starting ===");
|
|
70
|
+
try {
|
|
71
|
+
const config = parseConfig();
|
|
72
|
+
console.error(`[Main] Configuration:`);
|
|
73
|
+
console.error(`[Main] - Port: ${config.PORT}`);
|
|
74
|
+
console.error(`[Main] - Timeout: ${config.TIMEOUT}ms (${config.TIMEOUT / 1000}s)`);
|
|
75
|
+
// Create Express app
|
|
76
|
+
const app = express();
|
|
77
|
+
// Middleware
|
|
78
|
+
app.use(express.json());
|
|
79
|
+
app.use(express.static(path.join(__dirname, "../public")));
|
|
80
|
+
// Health check endpoint
|
|
81
|
+
app.get("/health", (_req, res) => {
|
|
82
|
+
res.json({
|
|
83
|
+
status: "ok",
|
|
84
|
+
timestamp: new Date().toISOString(),
|
|
85
|
+
activeSessions: wsService.getSessionCount(),
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
// Create HTTP server
|
|
89
|
+
const server = createServer(app);
|
|
90
|
+
// Create WebSocket server with /ws path
|
|
91
|
+
const wss = new WebSocketServer({
|
|
92
|
+
server,
|
|
93
|
+
path: "/ws",
|
|
94
|
+
});
|
|
95
|
+
// Initialize services
|
|
96
|
+
const wsService = new WebSocketService(wss, config.TIMEOUT, config.PORT);
|
|
97
|
+
const mcpService = new MCPServerService(wsService);
|
|
98
|
+
// MCP routes
|
|
99
|
+
app.post("/mcp", async (req, res) => {
|
|
100
|
+
await mcpService.handleRequest(req, res);
|
|
101
|
+
});
|
|
102
|
+
app.get("/mcp", (_req, res) => {
|
|
103
|
+
res.status(405).json({
|
|
104
|
+
jsonrpc: "2.0",
|
|
105
|
+
error: {
|
|
106
|
+
code: -32000,
|
|
107
|
+
message: "Method not allowed. Use POST.",
|
|
108
|
+
},
|
|
109
|
+
id: null,
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
app.delete("/mcp", (_req, res) => {
|
|
113
|
+
res.status(405).json({
|
|
114
|
+
jsonrpc: "2.0",
|
|
115
|
+
error: {
|
|
116
|
+
code: -32000,
|
|
117
|
+
message: "Method not allowed. Use POST.",
|
|
118
|
+
},
|
|
119
|
+
id: null,
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
// Start server
|
|
123
|
+
server.listen(config.PORT, () => {
|
|
124
|
+
console.error(`[Main] Server listening on port ${config.PORT}`);
|
|
125
|
+
console.error(`[Main] - HTTP: http://localhost:${config.PORT}`);
|
|
126
|
+
console.error(`[Main] - WebSocket: ws://localhost:${config.PORT}/ws`);
|
|
127
|
+
console.error(`[Main] - MCP: http://localhost:${config.PORT}/mcp`);
|
|
128
|
+
console.error(`[Main] - Health: http://localhost:${config.PORT}/health`);
|
|
129
|
+
});
|
|
130
|
+
// Graceful shutdown
|
|
131
|
+
const shutdown = async () => {
|
|
132
|
+
console.error("[Main] Shutting down...");
|
|
133
|
+
await wsService.cleanup();
|
|
134
|
+
server.close(() => {
|
|
135
|
+
console.error("[Main] Server closed");
|
|
136
|
+
process.exit(0);
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
process.on("SIGINT", shutdown);
|
|
140
|
+
process.on("SIGTERM", shutdown);
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
console.error("[Main] Startup failed:", error);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
main();
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
3
|
+
import { v4 as uuidv4 } from "uuid";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
export class MCPServerService {
|
|
6
|
+
wsService;
|
|
7
|
+
constructor(wsService) {
|
|
8
|
+
this.wsService = wsService;
|
|
9
|
+
}
|
|
10
|
+
// Create a new server instance for each request (stateless)
|
|
11
|
+
createServer() {
|
|
12
|
+
const server = new McpServer({
|
|
13
|
+
name: "ask-user-question-plus",
|
|
14
|
+
version: "1.0.0",
|
|
15
|
+
}, {
|
|
16
|
+
capabilities: {
|
|
17
|
+
tools: {},
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
this.registerTools(server);
|
|
21
|
+
return server;
|
|
22
|
+
}
|
|
23
|
+
registerTools(server) {
|
|
24
|
+
server.registerTool("ask_user_question_plus", {
|
|
25
|
+
description: "一个基于 MCP 的用户交互工具,用于向用户提出结构化问题并收集反馈。该工具支持单选、多选类型的问题,允许标记推荐选项,并支持任意数量的问题与答案选项。前端会自动为每个问题添加 'Other'(自定义输入)选项,适用于复杂配置与决策流程。",
|
|
26
|
+
inputSchema: {
|
|
27
|
+
questions: z
|
|
28
|
+
.array(z.object({
|
|
29
|
+
id: z.string().describe("问题唯一标识"),
|
|
30
|
+
header: z
|
|
31
|
+
.string()
|
|
32
|
+
.max(12)
|
|
33
|
+
.describe("问题标题(最多 12 个字符)"),
|
|
34
|
+
text: z.string().describe("问题文本"),
|
|
35
|
+
type: z
|
|
36
|
+
.enum(["single", "multiple"])
|
|
37
|
+
.describe("问题类型:single(单选)或 multiple(多选)"),
|
|
38
|
+
options: z
|
|
39
|
+
.array(z.object({
|
|
40
|
+
value: z.string().describe("选项值"),
|
|
41
|
+
label: z.string().describe("选项标签"),
|
|
42
|
+
description: z
|
|
43
|
+
.string()
|
|
44
|
+
.optional()
|
|
45
|
+
.describe("选项描述(可选)"),
|
|
46
|
+
recommended: z
|
|
47
|
+
.boolean()
|
|
48
|
+
.optional()
|
|
49
|
+
.describe("是否为推荐选项(可选)"),
|
|
50
|
+
}))
|
|
51
|
+
.describe("选项列表(不限数量)。注意:前端会自动添加 'Other'(自定义输入)选项,用户可自定义输入。"),
|
|
52
|
+
}))
|
|
53
|
+
.min(1)
|
|
54
|
+
.max(20)
|
|
55
|
+
.describe("问题列表(最多可支持 20 个问题)"),
|
|
56
|
+
},
|
|
57
|
+
}, async ({ questions }) => {
|
|
58
|
+
console.error("[MCP] Tool invoked: ask_user_question_plus");
|
|
59
|
+
if (!questions || !Array.isArray(questions) || questions.length === 0) {
|
|
60
|
+
const error = "Questions list cannot be empty";
|
|
61
|
+
console.error("[MCP] Validation error:", error);
|
|
62
|
+
return {
|
|
63
|
+
content: [{ type: "text", text: `Error: ${error}` }],
|
|
64
|
+
isError: true,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (questions.length > 20) {
|
|
68
|
+
const error = "Questions list cannot exceed 20 items";
|
|
69
|
+
console.error("[MCP] Validation error:", error);
|
|
70
|
+
return {
|
|
71
|
+
content: [{ type: "text", text: `Error: ${error}` }],
|
|
72
|
+
isError: true,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const sessionId = uuidv4();
|
|
76
|
+
console.error("[MCP] Session created:", sessionId);
|
|
77
|
+
try {
|
|
78
|
+
const answer = await new Promise((resolve, reject) => {
|
|
79
|
+
this.wsService
|
|
80
|
+
.createSession(sessionId, questions, resolve, reject)
|
|
81
|
+
.catch(reject);
|
|
82
|
+
});
|
|
83
|
+
console.error("[MCP] User answer received:", sessionId);
|
|
84
|
+
return {
|
|
85
|
+
content: [
|
|
86
|
+
{
|
|
87
|
+
type: "text",
|
|
88
|
+
text: JSON.stringify(answer, null, 2),
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
console.error("[MCP] Session failed:", sessionId, error.message);
|
|
95
|
+
const errorMessage = error.message || "Unknown error occurred";
|
|
96
|
+
return {
|
|
97
|
+
content: [
|
|
98
|
+
{
|
|
99
|
+
type: "text",
|
|
100
|
+
text: errorMessage,
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
isError: true,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
// Handle StreamableHTTP request
|
|
109
|
+
async handleRequest(req, res) {
|
|
110
|
+
const server = this.createServer();
|
|
111
|
+
try {
|
|
112
|
+
const transport = new StreamableHTTPServerTransport({
|
|
113
|
+
sessionIdGenerator: undefined,
|
|
114
|
+
});
|
|
115
|
+
await server.connect(transport);
|
|
116
|
+
await transport.handleRequest(req, res, req.body);
|
|
117
|
+
res.on("close", () => {
|
|
118
|
+
console.error("[MCP] Request closed");
|
|
119
|
+
transport.close();
|
|
120
|
+
server.close();
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
console.error("[MCP] Request handling error:", error);
|
|
125
|
+
if (!res.headersSent) {
|
|
126
|
+
res.status(500).json({
|
|
127
|
+
jsonrpc: "2.0",
|
|
128
|
+
error: {
|
|
129
|
+
code: -32603,
|
|
130
|
+
message: "Internal server error",
|
|
131
|
+
},
|
|
132
|
+
id: null,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|