ask-user-question-plus 1.0.0 → 1.0.2
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 +319 -159
- package/dist/mcp-server.js +11 -11
- package/dist/ws-service.js +5 -0
- package/package.json +2 -1
- package/public/index.html +80 -20
package/README.md
CHANGED
|
@@ -3,212 +3,200 @@
|
|
|
3
3
|
[](https://opensource.org/licenses/MIT)
|
|
4
4
|
[](https://www.npmjs.com/package/ask-user-question-plus)
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
A MCP server (Streamable HTTP) for asking user questions via a web interface, inspired by the Claude Code AskUserQuestion Tool.
|
|
7
7
|
|
|
8
|
-
!
|
|
8
|
+
> 🌟 Supports Claude Code, Codex, and Gemini CLI!
|
|
9
|
+
|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
<div align="center">
|
|
13
|
+
|
|
14
|
+
[**English**](#english) | [**中文**](#chinese)
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
<a id="chinese"></a>
|
|
9
21
|
|
|
10
22
|
## ✨ 特性
|
|
11
23
|
|
|
12
|
-
- **🎨
|
|
13
|
-
- **⌨️ 全键盘操作**:支持方向键、Tab、Space、Enter
|
|
14
|
-
- **📊
|
|
15
|
-
- **🔄 实时通信**:基于 WebSocket
|
|
16
|
-
- **⏱️
|
|
17
|
-
- **📈
|
|
24
|
+
- **🎨 精美界面**:Claude Code AskUserQuestions 风格样式,支持暗/浅色主题适配
|
|
25
|
+
- **⌨️ 全键盘操作**:支持方向键、Tab、Space、Enter 快捷键,操作流畅
|
|
26
|
+
- **📊 丰富交互**:支持单选、多选、推荐选项标记以及自定义输入(Other)
|
|
27
|
+
- **🔄 实时通信**:基于 WebSocket 的实时消息传递,响应迅速
|
|
28
|
+
- **⏱️ 超时保护**:可配置会话超时时间(默认 10 分钟)
|
|
29
|
+
- **📈 高度可扩展**:支持 1-20 个问题,每个问题选项数量不限
|
|
18
30
|
|
|
19
|
-
|
|
31
|
+
## 🚀 部署
|
|
20
32
|
|
|
21
|
-
|
|
22
|
-
| ------------ | ---------------------- | ---------------------------- |
|
|
23
|
-
| 问题数量限制 | 1-4 个 | 1-20 个 |
|
|
24
|
-
| 选项数量限制 | 2-4 个 | 无限制 |
|
|
33
|
+
### 启动参数
|
|
25
34
|
|
|
26
|
-
|
|
35
|
+
| 参数 | 说明 | 默认值 | 示例 |
|
|
36
|
+
| :----------------- | :----------- | :--------------- | :----------------- |
|
|
37
|
+
| `--timeout=<毫秒>` | 会话超时时间 | 600000 (10 分钟) | `--timeout=300000` |
|
|
38
|
+
| `--port=<端口>` | 监听端口 | 3456 | `--port=8080` |
|
|
27
39
|
|
|
28
|
-
###
|
|
40
|
+
### 前台运行
|
|
29
41
|
|
|
30
42
|
```bash
|
|
31
|
-
|
|
43
|
+
npx ask-user-question-plus
|
|
44
|
+
|
|
45
|
+
# 自定义端口和超时
|
|
46
|
+
npx ask-user-question-plus -- --port=3456 --timeout=600000
|
|
32
47
|
```
|
|
33
48
|
|
|
34
|
-
###
|
|
49
|
+
### 后台运行(推荐)
|
|
50
|
+
|
|
51
|
+
#### 使用 pm2
|
|
52
|
+
|
|
53
|
+
> 需先安装 pm2: `npm install -g pm2`
|
|
35
54
|
|
|
36
55
|
```bash
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
56
|
+
pm2 start "npx ask-user-question-plus" --name ask-user-question-plus-service
|
|
57
|
+
|
|
58
|
+
# 带参数启动
|
|
59
|
+
pm2 start "npx ask-user-question-plus -- --port=3456 --timeout=600000" --name ask-user-question-plus-service
|
|
60
|
+
|
|
61
|
+
# 停止服务
|
|
62
|
+
pm2 stop ask-user-question-plus-service
|
|
63
|
+
|
|
64
|
+
# Windows
|
|
65
|
+
pm2 start cmd --name ask-user-question-plus-service -- /c npx ask-user-question-plus
|
|
41
66
|
```
|
|
42
67
|
|
|
43
|
-
|
|
68
|
+
#### 使用 Docker
|
|
44
69
|
|
|
45
|
-
|
|
70
|
+
```bash
|
|
71
|
+
docker compose up -d
|
|
72
|
+
```
|
|
46
73
|
|
|
47
|
-
|
|
74
|
+
## ⚙️ 配置
|
|
48
75
|
|
|
49
|
-
|
|
76
|
+
### Claude Code
|
|
50
77
|
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
"mcpServers": {
|
|
54
|
-
"ask-user-question-plus": {
|
|
55
|
-
"command": "npx",
|
|
56
|
-
"args": ["ask-user-question-plus"]
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
78
|
+
```bash
|
|
79
|
+
claude mcp add -s user -t http AskUserQuestionPlus http://localhost:3456/mcp
|
|
60
80
|
```
|
|
61
81
|
|
|
62
|
-
|
|
82
|
+
### Codex
|
|
63
83
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
}
|
|
84
|
+
在 `~/.codex/config.toml` 中添加配置:
|
|
85
|
+
|
|
86
|
+
```toml
|
|
87
|
+
experimental_use_rmcp_client = true
|
|
88
|
+
|
|
89
|
+
[mcp_servers.AskUserQuestionPlus]
|
|
90
|
+
url = "http://localhost:3456/mcp/"
|
|
73
91
|
```
|
|
74
92
|
|
|
75
|
-
|
|
93
|
+
或者使用命令行:
|
|
76
94
|
|
|
77
|
-
|
|
78
|
-
| ------------------ | -------------- | ---------------- | ------------------ |
|
|
79
|
-
| `--timeout=<毫秒>` | 会话超时时间 | 600000 (10 分钟) | `--timeout=300000` |
|
|
80
|
-
| `--port=<端口>` | WebSocket 端口 | 3456 | `--port=8080` |
|
|
95
|
+
在 `~/.codex/config.toml` 中添加配置:
|
|
81
96
|
|
|
82
|
-
|
|
97
|
+
```toml
|
|
98
|
+
experimental_use_rmcp_client = true
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
codex mcp add AskUserQuestionsPlus --url http://localhost:3456/mcp/
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Gemini CLI
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
gemini mcp add AskUserQuestionPlus http://localhost:3456/mcp -s user --transport http
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
> **⚠️ 注意**:如果遇到 `[Error: Unrecognized key(s) in object: 'type']` 错误 (参见 [issue #15449](https://github.com/google-gemini/gemini-cli/issues/15449)):
|
|
112
|
+
>
|
|
113
|
+
> - 请在 `~/.gemini/settings.json` 中删除 `type` 字段
|
|
114
|
+
> - 或等待 gemini cli `v0.23.0` 发布
|
|
115
|
+
|
|
116
|
+
## 🕹️ 使用方法
|
|
83
117
|
|
|
84
118
|
### 基本流程
|
|
85
119
|
|
|
86
|
-
1.
|
|
87
|
-
2.
|
|
88
|
-
3.
|
|
89
|
-
4.
|
|
90
|
-
5.
|
|
120
|
+
1. 启动与 Claude Code/Codex/Gemini 的对话。
|
|
121
|
+
2. 模型调用工具:例如 `使用 ask_user_question_plus 工具问我 3 个问题`。
|
|
122
|
+
3. 浏览器自动打开问卷界面。
|
|
123
|
+
4. 使用键盘(方向键、Space、Enter)或鼠标回答问题。
|
|
124
|
+
5. 提交后浏览器标签页自动关闭,结果返回给模型。
|
|
91
125
|
|
|
92
126
|
### 键盘快捷键
|
|
93
127
|
|
|
94
|
-
| 快捷键 | 功能
|
|
95
|
-
|
|
|
96
|
-
| `↑` `↓` | 在选项之间移动 /
|
|
97
|
-
| `←` `→` / `Tab` |
|
|
98
|
-
| `Space`
|
|
99
|
-
| `Cmd/Ctrl + Enter` |
|
|
128
|
+
| 快捷键 | 功能 |
|
|
129
|
+
| :----------------- | :------------------------ |
|
|
130
|
+
| `↑` `↓` | 在选项之间移动 / 滚动页面 |
|
|
131
|
+
| `←` `→` / `Tab` | 在不同问题标签页之间切换 |
|
|
132
|
+
| `Space` / `Enter` | 选中/取消选中当前选项 |
|
|
133
|
+
| `Cmd/Ctrl + Enter` | 进入提交 Tab / 提交答案 |
|
|
100
134
|
|
|
101
|
-
## 🔧
|
|
135
|
+
## 🔧 工具定义 (Tools)
|
|
102
136
|
|
|
103
|
-
###
|
|
137
|
+
### `ask_user_question_plus`
|
|
104
138
|
|
|
105
|
-
**输入 Schema
|
|
139
|
+
**输入 Schema (Input):**
|
|
106
140
|
|
|
107
141
|
```typescript
|
|
108
142
|
{
|
|
109
143
|
questions: [
|
|
110
144
|
{
|
|
111
145
|
id: string; // 问题唯一标识
|
|
112
|
-
header: string; //
|
|
113
|
-
text: string; //
|
|
114
|
-
type: "single" | "multiple"; //
|
|
146
|
+
header: string; // 标签标题(推荐 ≤12 字符)
|
|
147
|
+
text: string; // 问题正文
|
|
148
|
+
type: "single" | "multiple"; // 问题类型:单选或多选
|
|
115
149
|
options: [
|
|
116
150
|
{
|
|
117
151
|
value: string; // 选项值
|
|
118
|
-
label: string; //
|
|
152
|
+
label: string; // 选项显示文本
|
|
119
153
|
description?: string; // 选项描述(可选)
|
|
120
|
-
recommended?: boolean;//
|
|
154
|
+
recommended?: boolean;// 是否标记为推荐(可选)
|
|
121
155
|
}
|
|
122
156
|
]
|
|
123
157
|
}
|
|
124
|
-
] //
|
|
158
|
+
] // 限制:1-20 个问题
|
|
125
159
|
}
|
|
126
160
|
```
|
|
127
161
|
|
|
128
|
-
**输出 Schema
|
|
162
|
+
**输出 Schema (Output):**
|
|
129
163
|
|
|
130
164
|
```typescript
|
|
131
165
|
{
|
|
132
|
-
|
|
166
|
+
"answers": {
|
|
167
|
+
"questionId1": {"value" : "", "other":""},
|
|
168
|
+
"questionId2": {"value" : "", "other":""},
|
|
169
|
+
}
|
|
133
170
|
}
|
|
134
171
|
```
|
|
135
172
|
|
|
136
|
-
> **💡
|
|
173
|
+
> **💡 提示**:前端界面会自动为每个问题追加 "Other (自定义输入)" 选项。
|
|
137
174
|
|
|
138
|
-
|
|
175
|
+
## 🛠️ 开发指南
|
|
139
176
|
|
|
140
|
-
|
|
177
|
+
### 克隆项目
|
|
141
178
|
|
|
142
|
-
```
|
|
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
|
-
}
|
|
179
|
+
```bash
|
|
180
|
+
git clone https://github.com/JoJoJotarou/AskUserQuestionPlus.git && cd AskUserQuestionPlus
|
|
166
181
|
```
|
|
167
182
|
|
|
168
|
-
|
|
183
|
+
### 安装依赖
|
|
169
184
|
|
|
170
|
-
```
|
|
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
|
-
}
|
|
185
|
+
```bash
|
|
186
|
+
npm install
|
|
198
187
|
```
|
|
199
188
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
### 运行开发模式
|
|
189
|
+
### 开发模式
|
|
203
190
|
|
|
204
191
|
```bash
|
|
205
192
|
npm run dev
|
|
206
193
|
```
|
|
207
194
|
|
|
208
|
-
###
|
|
195
|
+
### 构建
|
|
209
196
|
|
|
210
197
|
```bash
|
|
211
198
|
npm run build
|
|
199
|
+
# npm start
|
|
212
200
|
```
|
|
213
201
|
|
|
214
202
|
### 目录结构
|
|
@@ -217,48 +205,220 @@ npm run build
|
|
|
217
205
|
ask-user-question-plus/
|
|
218
206
|
├── src/
|
|
219
207
|
│ ├── index.ts # 主入口文件
|
|
220
|
-
│ ├── ws-service.ts # WebSocket
|
|
221
|
-
│ └── mcp-server.ts # MCP
|
|
208
|
+
│ ├── ws-service.ts # WebSocket 服务逻辑
|
|
209
|
+
│ └── mcp-server.ts # MCP 服务器定义
|
|
222
210
|
├── public/
|
|
223
|
-
│ └── index.html #
|
|
224
|
-
├── dist/ #
|
|
225
|
-
└── package.json
|
|
211
|
+
│ └── index.html # 前端 Web 界面
|
|
212
|
+
├── dist/ # 编译输出
|
|
213
|
+
└── package.json
|
|
226
214
|
```
|
|
227
215
|
|
|
228
|
-
|
|
216
|
+
## 🤝 贡献
|
|
217
|
+
|
|
218
|
+
欢迎提交 [Issues](https://github.com/JoJoJotarou/AskUserQuestionPlus/issues) 或 Pull Requests 来改进这个项目。
|
|
229
219
|
|
|
230
|
-
|
|
231
|
-
|
|
220
|
+
## 📄 许可证
|
|
221
|
+
|
|
222
|
+
[MIT License](LICENSE)
|
|
232
223
|
|
|
233
|
-
##
|
|
224
|
+
## 🔗 相关链接
|
|
225
|
+
|
|
226
|
+
- [Model Context Protocol](https://modelcontextprotocol.io/)
|
|
227
|
+
- [Codex Support for streamable HTTP MCP](https://github.com/openai/codex/pull/4317)
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
<a id="english"></a>
|
|
232
|
+
|
|
233
|
+
## ✨ Features
|
|
234
|
+
|
|
235
|
+
- **🎨 Beautiful Interface**: Replicates the Claude Code AskUserQuestions style with support for Dark/Light themes.
|
|
236
|
+
- **⌨️ Full Keyboard Control**: Seamless navigation using Arrow keys, Tab, Space, and Enter.
|
|
237
|
+
- **📊 Rich Interactions**: Supports single choice, multiple choice, recommended options, and custom input ("Other").
|
|
238
|
+
- **🔄 Real-time Communication**: WebSocket-based messaging for instant feedback.
|
|
239
|
+
- **⏱️ Timeout Protection**: Configurable session timeout (Default: 10 minutes).
|
|
240
|
+
- **📈 Scalable**: Supports 1 to 20 questions with unlimited options per question.
|
|
241
|
+
|
|
242
|
+
## 🚀 Deployment
|
|
243
|
+
|
|
244
|
+
### Startup Parameters
|
|
245
|
+
|
|
246
|
+
| Argument | Description | Default | Example |
|
|
247
|
+
| :--------------- | :------------------------------ | :--------------- | :----------------- |
|
|
248
|
+
| `--timeout=<ms>` | Session timeout in milliseconds | 600000 (10 mins) | `--timeout=300000` |
|
|
249
|
+
| `--port=<port>` | Server port | 3456 | `--port=8080` |
|
|
250
|
+
|
|
251
|
+
### Foreground
|
|
234
252
|
|
|
235
253
|
```bash
|
|
236
|
-
|
|
237
|
-
docker-compose up -d
|
|
254
|
+
npx ask-user-question-plus
|
|
238
255
|
|
|
239
|
-
#
|
|
240
|
-
|
|
256
|
+
# Custom port and timeout
|
|
257
|
+
npx ask-user-question-plus -- --port=3456 --timeout=600000
|
|
258
|
+
```
|
|
241
259
|
|
|
242
|
-
|
|
243
|
-
|
|
260
|
+
### Background (Recommended)
|
|
261
|
+
|
|
262
|
+
#### Using pm2
|
|
263
|
+
|
|
264
|
+
> Install pm2 first: `npm install -g pm2`
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
pm2 start "npx ask-user-question-plus" --name ask-user-question-plus-service
|
|
268
|
+
|
|
269
|
+
# With arguments
|
|
270
|
+
pm2 start "npx ask-user-question-plus -- --port=3456 --timeout=600000" --name ask-user-question-plus-service
|
|
271
|
+
|
|
272
|
+
# Stop service
|
|
273
|
+
pm2 stop ask-user-question-plus-service
|
|
274
|
+
|
|
275
|
+
# Windows
|
|
276
|
+
pm2 start cmd --name ask-user-question-plus-service -- /c npx ask-user-question-plu
|
|
244
277
|
```
|
|
245
278
|
|
|
246
|
-
|
|
279
|
+
#### Using Docker
|
|
247
280
|
|
|
248
|
-
|
|
281
|
+
```bash
|
|
282
|
+
docker compose up -d
|
|
283
|
+
```
|
|
249
284
|
|
|
250
|
-
##
|
|
285
|
+
## ⚙️ Configuration
|
|
251
286
|
|
|
252
|
-
|
|
287
|
+
### Claude Code
|
|
253
288
|
|
|
254
|
-
|
|
289
|
+
```bash
|
|
290
|
+
claude mcp add -s user -t http AskUserQuestionPlus http://localhost:3456/mcp
|
|
291
|
+
```
|
|
255
292
|
|
|
256
|
-
|
|
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)
|
|
293
|
+
### Codex
|
|
261
294
|
|
|
262
|
-
|
|
295
|
+
Add to `~/.codex/config.toml`:
|
|
296
|
+
|
|
297
|
+
```toml
|
|
298
|
+
experimental_use_rmcp_client = true
|
|
299
|
+
|
|
300
|
+
[mcp_servers.AskUserQuestionPlus]
|
|
301
|
+
url = "http://localhost:3456/mcp/"
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Or via CLI:
|
|
305
|
+
|
|
306
|
+
Add to `~/.codex/config.toml` first:
|
|
307
|
+
|
|
308
|
+
```toml
|
|
309
|
+
experimental_use_rmcp_client = true
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
codex mcp add AskUserQuestionsPlus --url http://localhost:3456/mcp/
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Gemini CLI
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
gemini mcp add AskUserQuestionPlus http://localhost:3456/mcp -s user --transport http
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
> **⚠️ Note**: If you encounter `[Error: Unrecognized key(s) in object: 'type']` ([Issue #15449](https://github.com/google-gemini/gemini-cli/issues/15449)):
|
|
323
|
+
>
|
|
324
|
+
> - Remove the `type` field in `~/.gemini/settings.json`.
|
|
325
|
+
> - Or wait for gemini cli `v0.23.0`.
|
|
326
|
+
|
|
327
|
+
## 🕹️ Usage
|
|
328
|
+
|
|
329
|
+
### Basic Workflow
|
|
330
|
+
|
|
331
|
+
1. Start a conversation with Claude, Codex, or Gemini.
|
|
332
|
+
2. Ask the model to use the tool: e.g., "Use ask_user_question_plus to ask me 3 questions."
|
|
333
|
+
3. A browser tab will automatically open with the questionnaire.
|
|
334
|
+
4. Answer using your keyboard (Arrows, Space, Enter) or mouse.
|
|
335
|
+
5. Upon submission, the tab closes, and answers are sent back to the model.
|
|
336
|
+
|
|
337
|
+
### Keyboard Shortcuts
|
|
338
|
+
|
|
339
|
+
| Shortcut | Function |
|
|
340
|
+
| :----------------- | :---------------------------- |
|
|
341
|
+
| `↑` `↓` | Move between options / Scroll |
|
|
342
|
+
| `←` `→` / `Tab` | Switch between question tabs |
|
|
343
|
+
| `Space` / `Enter` | Select/Deselect option |
|
|
344
|
+
| `Cmd/Ctrl + Enter` | Submit Tab / Submit answers |
|
|
345
|
+
|
|
346
|
+
## 🔧 Tools Definition
|
|
347
|
+
|
|
348
|
+
### `ask_user_question_plus`
|
|
349
|
+
|
|
350
|
+
**Input Schema:**
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
{
|
|
354
|
+
questions: [
|
|
355
|
+
{
|
|
356
|
+
id: string; // Unique identifier
|
|
357
|
+
header: string; // Tab header (Rec. ≤12 chars)
|
|
358
|
+
text: string; // Question text
|
|
359
|
+
type: "single" | "multiple"; // Question type
|
|
360
|
+
options: [
|
|
361
|
+
{
|
|
362
|
+
value: string; // Option value
|
|
363
|
+
label: string; // Option label
|
|
364
|
+
description?: string; // Option description (optional)
|
|
365
|
+
recommended?: boolean;// Recommended flag (optional)
|
|
366
|
+
}
|
|
367
|
+
]
|
|
368
|
+
}
|
|
369
|
+
] // Limit: 1-20 questions
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
**Output Schema:**
|
|
374
|
+
|
|
375
|
+
```json
|
|
376
|
+
{
|
|
377
|
+
"answers": {
|
|
378
|
+
"questionId1": { "value": "", "other": "" },
|
|
379
|
+
"questionId2": { "value": "", "other": "" }
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
> **💡 Tip**: The "Other" (custom input) option is automatically added by the frontend.
|
|
385
|
+
|
|
386
|
+
## 🛠️ Development
|
|
387
|
+
|
|
388
|
+
### Clone
|
|
389
|
+
|
|
390
|
+
```bash
|
|
391
|
+
git clone https://github.com/JoJoJotarou/AskUserQuestionPlus.git && cd AskUserQuestionPlus
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Install Dependencies
|
|
263
395
|
|
|
264
|
-
|
|
396
|
+
```bash
|
|
397
|
+
npm install
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### Run Dev Mode
|
|
401
|
+
|
|
402
|
+
```bash
|
|
403
|
+
npm run dev
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### Build
|
|
407
|
+
|
|
408
|
+
```bash
|
|
409
|
+
npm run build
|
|
410
|
+
# npm start
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
## 🤝 Contribution
|
|
414
|
+
|
|
415
|
+
Contributions are welcome! Please submit [Issues](https://github.com/JoJoJotarou/AskUserQuestionPlus/issues) or Pull Requests.
|
|
416
|
+
|
|
417
|
+
## 📄 License
|
|
418
|
+
|
|
419
|
+
[MIT License](LICENSE)
|
|
420
|
+
|
|
421
|
+
## 🔗 Related Links
|
|
422
|
+
|
|
423
|
+
- [Model Context Protocol](https://modelcontextprotocol.io/)
|
|
424
|
+
- [Codex Support for streamable HTTP MCP](https://github.com/openai/codex/pull/4317)
|
package/dist/mcp-server.js
CHANGED
|
@@ -22,37 +22,37 @@ export class MCPServerService {
|
|
|
22
22
|
}
|
|
23
23
|
registerTools(server) {
|
|
24
24
|
server.registerTool("ask_user_question_plus", {
|
|
25
|
-
description: "
|
|
25
|
+
description: "An MCP-based user interaction tool for asking structured questions and collecting feedback. It supports single-choice and multiple-choice questions, marking recommended options, and flexible numbers of questions and options. An 'Other' (custom input) option is automatically added to each question, making it suitable for complex configuration and decision-making workflows.",
|
|
26
26
|
inputSchema: {
|
|
27
27
|
questions: z
|
|
28
28
|
.array(z.object({
|
|
29
|
-
id: z.string().describe("
|
|
29
|
+
id: z.string().describe("Unique identifier for the question"),
|
|
30
30
|
header: z
|
|
31
31
|
.string()
|
|
32
32
|
.max(12)
|
|
33
|
-
.describe("
|
|
34
|
-
text: z.string().describe("
|
|
33
|
+
.describe("Short header for the question tab (max 12 chars)"),
|
|
34
|
+
text: z.string().describe("Main text of the question"),
|
|
35
35
|
type: z
|
|
36
36
|
.enum(["single", "multiple"])
|
|
37
|
-
.describe("
|
|
37
|
+
.describe("Question type: 'single' or 'multiple'"),
|
|
38
38
|
options: z
|
|
39
39
|
.array(z.object({
|
|
40
|
-
value: z.string().describe("
|
|
41
|
-
label: z.string().describe("
|
|
40
|
+
value: z.string().describe("Technical value of the option"),
|
|
41
|
+
label: z.string().describe("Display label for the option"),
|
|
42
42
|
description: z
|
|
43
43
|
.string()
|
|
44
44
|
.optional()
|
|
45
|
-
.describe("
|
|
45
|
+
.describe("Additional description for the option"),
|
|
46
46
|
recommended: z
|
|
47
47
|
.boolean()
|
|
48
48
|
.optional()
|
|
49
|
-
.describe("
|
|
49
|
+
.describe("Whether this is a recommended option"),
|
|
50
50
|
}))
|
|
51
|
-
.describe("
|
|
51
|
+
.describe("List of options. Note: An 'Other' option is automatically added for custom user input."),
|
|
52
52
|
}))
|
|
53
53
|
.min(1)
|
|
54
54
|
.max(20)
|
|
55
|
-
.describe("
|
|
55
|
+
.describe("List of questions (up to 20 items)"),
|
|
56
56
|
},
|
|
57
57
|
}, async ({ questions }) => {
|
|
58
58
|
console.error("[MCP] Tool invoked: ask_user_question_plus");
|
package/dist/ws-service.js
CHANGED
|
@@ -39,6 +39,11 @@ export class WebSocketService {
|
|
|
39
39
|
clearTimeout(session.timeout);
|
|
40
40
|
session.timeout = null;
|
|
41
41
|
}
|
|
42
|
+
// Send session meta first so the client can start countdown when it receives NEW_QUESTION.
|
|
43
|
+
this.sendMessage(ws, {
|
|
44
|
+
type: "SESSION_META",
|
|
45
|
+
payload: { timeoutMs: this.sessionTimeoutMs },
|
|
46
|
+
});
|
|
42
47
|
// Send questions to client
|
|
43
48
|
this.sendMessage(ws, {
|
|
44
49
|
type: "NEW_QUESTION",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ask-user-question-plus",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "A TUI-style MCP server for asking user questions via a web interface.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"build": "tsc",
|
|
12
12
|
"start": "node dist/index.js",
|
|
13
13
|
"dev": "tsx src/index.ts",
|
|
14
|
+
"test": "echo 'No tests yet' && exit 0",
|
|
14
15
|
"prepublishOnly": "npm run build"
|
|
15
16
|
},
|
|
16
17
|
"keywords": [
|
package/public/index.html
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html lang="
|
|
2
|
+
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
@@ -545,7 +545,7 @@
|
|
|
545
545
|
|
|
546
546
|
<!-- GitHub Star -->
|
|
547
547
|
<a
|
|
548
|
-
href="https://github.com/
|
|
548
|
+
href="https://github.com/JoJoJotarou/AskUserQuestionPlus"
|
|
549
549
|
target="_blank"
|
|
550
550
|
class="icon-btn"
|
|
551
551
|
title="Star on GitHub"
|
|
@@ -575,10 +575,10 @@
|
|
|
575
575
|
<div class="footer">
|
|
576
576
|
<div class="key-hints">
|
|
577
577
|
<div class="key-hint-item">
|
|
578
|
-
<span class="key-tag">↑↓</span>
|
|
578
|
+
<span class="key-tag">↑↓</span> Select/Scroll
|
|
579
579
|
</div>
|
|
580
580
|
<div class="key-hint-item">
|
|
581
|
-
<span class="key-tag">← →/Tab</span>
|
|
581
|
+
<span class="key-tag">← →/Tab</span> Switch
|
|
582
582
|
</div>
|
|
583
583
|
<div class="key-hint-item">
|
|
584
584
|
<span class="key-tag">Enter/Space</span>
|
|
@@ -586,7 +586,7 @@
|
|
|
586
586
|
</div>
|
|
587
587
|
</div>
|
|
588
588
|
<button class="submit-btn" id="submit-all" disabled>
|
|
589
|
-
|
|
589
|
+
Review & Submit
|
|
590
590
|
<span class="submit-shortcut" id="submit-shortcut-text">⌘+Enter</span>
|
|
591
591
|
</button>
|
|
592
592
|
</div>
|
|
@@ -601,11 +601,57 @@
|
|
|
601
601
|
let activeReviewIndex = 0;
|
|
602
602
|
let shouldFocusOther = false;
|
|
603
603
|
let ws = null;
|
|
604
|
+
let sessionTimeoutMs = null;
|
|
605
|
+
let countdownInterval = null;
|
|
606
|
+
let countdownEndsAt = null;
|
|
607
|
+
let hasReceivedQuestions = false;
|
|
604
608
|
let sessionId =
|
|
605
609
|
new URLSearchParams(window.location.search).get("sessionId") ||
|
|
606
610
|
"default";
|
|
607
611
|
const isMac = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
|
|
608
612
|
|
|
613
|
+
function formatMmSs(ms) {
|
|
614
|
+
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
|
|
615
|
+
const minutes = String(Math.floor(totalSeconds / 60)).padStart(2, "0");
|
|
616
|
+
const seconds = String(totalSeconds % 60).padStart(2, "0");
|
|
617
|
+
return `${minutes}:${seconds}`;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function setSessionInfo(statusText) {
|
|
621
|
+
document.getElementById("session-info").textContent = statusText;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function setSessionInfoConnected() {
|
|
625
|
+
const base = `Session: ${sessionId} (Connected)`;
|
|
626
|
+
if (countdownEndsAt) {
|
|
627
|
+
const remaining = countdownEndsAt - Date.now();
|
|
628
|
+
setSessionInfo(`${base} | Timeout in ${formatMmSs(remaining)}`);
|
|
629
|
+
} else {
|
|
630
|
+
setSessionInfo(base);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function stopCountdown() {
|
|
635
|
+
if (countdownInterval) {
|
|
636
|
+
clearInterval(countdownInterval);
|
|
637
|
+
countdownInterval = null;
|
|
638
|
+
}
|
|
639
|
+
countdownEndsAt = null;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function startCountdown(timeoutMs) {
|
|
643
|
+
stopCountdown();
|
|
644
|
+
countdownEndsAt = Date.now() + timeoutMs;
|
|
645
|
+
|
|
646
|
+
// Tick immediately to avoid showing stale text for up to 1s.
|
|
647
|
+
setSessionInfoConnected();
|
|
648
|
+
countdownInterval = setInterval(() => {
|
|
649
|
+
const remaining = countdownEndsAt - Date.now();
|
|
650
|
+
setSessionInfoConnected();
|
|
651
|
+
if (remaining <= 0) stopCountdown();
|
|
652
|
+
}, 1000);
|
|
653
|
+
}
|
|
654
|
+
|
|
609
655
|
// --- WebSocket Logic ---
|
|
610
656
|
function connectWS() {
|
|
611
657
|
// Robust WS URL construction
|
|
@@ -628,9 +674,7 @@
|
|
|
628
674
|
|
|
629
675
|
ws.onopen = () => {
|
|
630
676
|
console.error("WS Open");
|
|
631
|
-
|
|
632
|
-
"session-info"
|
|
633
|
-
).textContent = `Session: ${sessionId} (Connected)`;
|
|
677
|
+
setSessionInfoConnected();
|
|
634
678
|
document.getElementById("session-info").style.color =
|
|
635
679
|
"var(--green-color)";
|
|
636
680
|
ws.send(JSON.stringify({ type: "GET_STATE" }));
|
|
@@ -649,6 +693,19 @@
|
|
|
649
693
|
try {
|
|
650
694
|
const message = JSON.parse(event.data);
|
|
651
695
|
|
|
696
|
+
if (message.type === "SESSION_META" && message.payload) {
|
|
697
|
+
sessionTimeoutMs =
|
|
698
|
+
typeof message.payload.timeoutMs === "number"
|
|
699
|
+
? message.payload.timeoutMs
|
|
700
|
+
: null;
|
|
701
|
+
|
|
702
|
+
// For B: start countdown when we have received the questions.
|
|
703
|
+
if (hasReceivedQuestions && sessionTimeoutMs) {
|
|
704
|
+
startCountdown(sessionTimeoutMs);
|
|
705
|
+
}
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
|
|
652
709
|
if (
|
|
653
710
|
message.type === "SYNC_STATE" ||
|
|
654
711
|
message.type === "NEW_QUESTION"
|
|
@@ -660,6 +717,8 @@
|
|
|
660
717
|
|
|
661
718
|
if (questions && questions.length > 0) {
|
|
662
719
|
QUESTIONS_DATA = questions;
|
|
720
|
+
hasReceivedQuestions = true;
|
|
721
|
+
if (sessionTimeoutMs) startCountdown(sessionTimeoutMs);
|
|
663
722
|
initAnswers();
|
|
664
723
|
activeQuestionIndex = 0;
|
|
665
724
|
activeOptionIndex = 0;
|
|
@@ -675,6 +734,7 @@
|
|
|
675
734
|
};
|
|
676
735
|
|
|
677
736
|
ws.onclose = () => {
|
|
737
|
+
stopCountdown();
|
|
678
738
|
document.getElementById(
|
|
679
739
|
"session-info"
|
|
680
740
|
).textContent = `Disconnected. Reconnecting...`;
|
|
@@ -799,7 +859,7 @@
|
|
|
799
859
|
${opt.label}
|
|
800
860
|
${
|
|
801
861
|
opt.recommended
|
|
802
|
-
? '<span class="recommended-tag"
|
|
862
|
+
? '<span class="recommended-tag">(Recommended)</span>'
|
|
803
863
|
: ""
|
|
804
864
|
}
|
|
805
865
|
</div>
|
|
@@ -833,7 +893,7 @@
|
|
|
833
893
|
<div class="question-header">
|
|
834
894
|
<span class="q-text">${q.text}${
|
|
835
895
|
q.type === "multiple"
|
|
836
|
-
? ' <span style="color:var(--dim-color); font-size:0.8em"
|
|
896
|
+
? ' <span style="color:var(--dim-color); font-size:0.8em">(Multi-select)</span>'
|
|
837
897
|
: ""
|
|
838
898
|
}</span>
|
|
839
899
|
</div>
|
|
@@ -875,7 +935,7 @@
|
|
|
875
935
|
|
|
876
936
|
if (q.type === "single") {
|
|
877
937
|
if (!ans.value) {
|
|
878
|
-
error = "
|
|
938
|
+
error = "No answer selected";
|
|
879
939
|
} else {
|
|
880
940
|
const opt = [
|
|
881
941
|
...q.options,
|
|
@@ -885,7 +945,7 @@
|
|
|
885
945
|
displayValue = opt.label;
|
|
886
946
|
if (opt.isOther) {
|
|
887
947
|
if (!ans.otherText.trim()) {
|
|
888
|
-
error = "
|
|
948
|
+
error = "Please provide custom input";
|
|
889
949
|
displayValue += ": [Empty]";
|
|
890
950
|
} else {
|
|
891
951
|
displayValue += `: ${ans.otherText}`;
|
|
@@ -895,7 +955,7 @@
|
|
|
895
955
|
}
|
|
896
956
|
} else {
|
|
897
957
|
if (!ans.values || ans.values.length === 0) {
|
|
898
|
-
error = "
|
|
958
|
+
error = "Please select at least one option";
|
|
899
959
|
} else {
|
|
900
960
|
displayValue = ans.values
|
|
901
961
|
.map((v) => {
|
|
@@ -907,7 +967,7 @@
|
|
|
907
967
|
let label = opt.label;
|
|
908
968
|
if (opt.isOther) {
|
|
909
969
|
if (!ans.otherText.trim()) {
|
|
910
|
-
error = "
|
|
970
|
+
error = "Please provide custom input";
|
|
911
971
|
label += ": [Empty]";
|
|
912
972
|
} else {
|
|
913
973
|
label += `: ${ans.otherText}`;
|
|
@@ -939,7 +999,7 @@
|
|
|
939
999
|
container.innerHTML = `
|
|
940
1000
|
<div class="question-block">
|
|
941
1001
|
<div class="question-header">
|
|
942
|
-
<span class="q-text"
|
|
1002
|
+
<span class="q-text">Please review your answers before submitting!</span>
|
|
943
1003
|
</div>
|
|
944
1004
|
${summaryHtml}
|
|
945
1005
|
</div>
|
|
@@ -961,13 +1021,13 @@
|
|
|
961
1021
|
const shortcutText = isMac ? "⌘+Enter" : "Ctrl+Enter";
|
|
962
1022
|
|
|
963
1023
|
if (isReview) {
|
|
964
|
-
submitBtn.innerHTML = `
|
|
1024
|
+
submitBtn.innerHTML = `Submit Answers <span class="submit-shortcut" id="submit-shortcut-text">${shortcutText}</span>`;
|
|
965
1025
|
submitBtn.disabled = !isValid;
|
|
966
|
-
if (spaceHint) spaceHint.textContent = "
|
|
1026
|
+
if (spaceHint) spaceHint.textContent = "Jump";
|
|
967
1027
|
} else {
|
|
968
|
-
submitBtn.innerHTML = `
|
|
1028
|
+
submitBtn.innerHTML = `Review & Submit <span class="submit-shortcut" id="submit-shortcut-text">${shortcutText}</span>`;
|
|
969
1029
|
submitBtn.disabled = QUESTIONS_DATA.length === 0;
|
|
970
|
-
if (spaceHint) spaceHint.textContent = "
|
|
1030
|
+
if (spaceHint) spaceHint.textContent = "Select";
|
|
971
1031
|
}
|
|
972
1032
|
}
|
|
973
1033
|
|
|
@@ -1091,7 +1151,7 @@
|
|
|
1091
1151
|
submitBtn.disabled = true;
|
|
1092
1152
|
|
|
1093
1153
|
setTimeout(() => {
|
|
1094
|
-
alert("
|
|
1154
|
+
alert("Submitted successfully! Closing...");
|
|
1095
1155
|
submitBtn.textContent = "DONE";
|
|
1096
1156
|
window.close();
|
|
1097
1157
|
document.body.innerHTML =
|