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
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { WebSocket } from "ws";
|
|
2
|
+
export class WebSocketService {
|
|
3
|
+
wss;
|
|
4
|
+
sessions = new Map();
|
|
5
|
+
sessionTimeoutMs;
|
|
6
|
+
port;
|
|
7
|
+
constructor(wss, sessionTimeoutMs = 600000, port = 3456) {
|
|
8
|
+
this.wss = wss;
|
|
9
|
+
this.sessionTimeoutMs = sessionTimeoutMs;
|
|
10
|
+
this.port = port;
|
|
11
|
+
this.setupWebSocket();
|
|
12
|
+
}
|
|
13
|
+
setupWebSocket() {
|
|
14
|
+
this.wss.on("connection", (ws, req) => {
|
|
15
|
+
this.handleConnection(ws, req);
|
|
16
|
+
});
|
|
17
|
+
this.wss.on("error", (error) => {
|
|
18
|
+
console.error("[WebSocket] Server error:", error);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
handleConnection(ws, req) {
|
|
22
|
+
const url = new URL(req.url || "", `http://localhost:${this.port}`);
|
|
23
|
+
const sessionId = url.searchParams.get("sessionId");
|
|
24
|
+
if (!sessionId) {
|
|
25
|
+
console.error("[WebSocket] Connection missing sessionId, closing");
|
|
26
|
+
ws.close();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const session = this.sessions.get(sessionId);
|
|
30
|
+
if (!session) {
|
|
31
|
+
console.error("[WebSocket] Session not found:", sessionId);
|
|
32
|
+
ws.close();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
console.error("[WebSocket] Client connected:", sessionId);
|
|
36
|
+
session.ws = ws;
|
|
37
|
+
// Clear connection timeout
|
|
38
|
+
if (session.timeout) {
|
|
39
|
+
clearTimeout(session.timeout);
|
|
40
|
+
session.timeout = null;
|
|
41
|
+
}
|
|
42
|
+
// Send questions to client
|
|
43
|
+
this.sendMessage(ws, {
|
|
44
|
+
type: "NEW_QUESTION",
|
|
45
|
+
payload: session.questions,
|
|
46
|
+
});
|
|
47
|
+
console.error("[WebSocket] Questions sent to client:", sessionId);
|
|
48
|
+
// Set response timeout
|
|
49
|
+
session.timeout = setTimeout(() => {
|
|
50
|
+
console.error("[WebSocket] Session timeout:", sessionId);
|
|
51
|
+
this.handleSessionError(sessionId, new Error(`Timeout: No response within ${this.sessionTimeoutMs / 1000}s`));
|
|
52
|
+
}, this.sessionTimeoutMs);
|
|
53
|
+
// Handle messages
|
|
54
|
+
ws.on("message", (rawMessage) => {
|
|
55
|
+
this.handleMessage(sessionId, rawMessage);
|
|
56
|
+
});
|
|
57
|
+
// Handle disconnect
|
|
58
|
+
ws.on("close", () => {
|
|
59
|
+
console.error("[WebSocket] Client disconnected:", sessionId);
|
|
60
|
+
});
|
|
61
|
+
// Handle errors
|
|
62
|
+
ws.on("error", (error) => {
|
|
63
|
+
console.error("[WebSocket] Connection error:", sessionId, error);
|
|
64
|
+
this.handleSessionError(sessionId, new Error("WebSocket connection error"));
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
handleMessage(sessionId, rawMessage) {
|
|
68
|
+
const session = this.sessions.get(sessionId);
|
|
69
|
+
if (!session) {
|
|
70
|
+
console.error("[WebSocket] Message received but session not found:", sessionId);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const message = JSON.parse(rawMessage.toString());
|
|
75
|
+
console.error("[WebSocket] Message received:", message.type, "from", sessionId);
|
|
76
|
+
if (message.type === "SUBMIT_ANSWERS") {
|
|
77
|
+
// Clear timeout
|
|
78
|
+
if (session.timeout) {
|
|
79
|
+
clearTimeout(session.timeout);
|
|
80
|
+
session.timeout = null;
|
|
81
|
+
}
|
|
82
|
+
// Resolve promise
|
|
83
|
+
session.resolve(message.payload);
|
|
84
|
+
console.error("[WebSocket] Answers submitted:", sessionId);
|
|
85
|
+
// Send close message
|
|
86
|
+
if (session.ws && session.ws.readyState === WebSocket.OPEN) {
|
|
87
|
+
this.sendMessage(session.ws, { type: "CLOSE" });
|
|
88
|
+
}
|
|
89
|
+
// Cleanup session
|
|
90
|
+
this.sessions.delete(sessionId);
|
|
91
|
+
}
|
|
92
|
+
else if (message.type === "ERROR") {
|
|
93
|
+
this.handleSessionError(sessionId, new Error(`Client error: ${message.error}`));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
console.error("[WebSocket] Message parse error:", sessionId, error);
|
|
98
|
+
this.handleSessionError(sessionId, new Error("Invalid message format"));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
sendMessage(ws, message) {
|
|
102
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
103
|
+
ws.send(JSON.stringify(message));
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
throw new Error("WebSocket not ready");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
handleSessionError(sessionId, error) {
|
|
110
|
+
const session = this.sessions.get(sessionId);
|
|
111
|
+
if (!session)
|
|
112
|
+
return;
|
|
113
|
+
console.error("[WebSocket] Session error:", sessionId, error.message);
|
|
114
|
+
// Clear timeout
|
|
115
|
+
if (session.timeout) {
|
|
116
|
+
clearTimeout(session.timeout);
|
|
117
|
+
session.timeout = null;
|
|
118
|
+
}
|
|
119
|
+
// Send error message to client before closing
|
|
120
|
+
if (session.ws && session.ws.readyState === WebSocket.OPEN) {
|
|
121
|
+
try {
|
|
122
|
+
this.sendMessage(session.ws, {
|
|
123
|
+
type: "ERROR",
|
|
124
|
+
error: error.message,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
console.error("[WebSocket] Failed to send error to client:", e);
|
|
129
|
+
}
|
|
130
|
+
session.ws.close();
|
|
131
|
+
}
|
|
132
|
+
// Reject promise - this will notify the MCP client (AI)
|
|
133
|
+
session.reject(error);
|
|
134
|
+
// Cleanup session
|
|
135
|
+
this.sessions.delete(sessionId);
|
|
136
|
+
}
|
|
137
|
+
// Create session and open browser
|
|
138
|
+
async createSession(sessionId, questions, resolve, reject) {
|
|
139
|
+
console.error("[WebSocket] Creating session:", sessionId);
|
|
140
|
+
const session = {
|
|
141
|
+
id: sessionId,
|
|
142
|
+
questions,
|
|
143
|
+
resolve,
|
|
144
|
+
reject,
|
|
145
|
+
ws: null,
|
|
146
|
+
timeout: null,
|
|
147
|
+
createdAt: Date.now(),
|
|
148
|
+
};
|
|
149
|
+
// Set initial timeout (waiting for client connection)
|
|
150
|
+
session.timeout = setTimeout(() => {
|
|
151
|
+
console.error("[WebSocket] Session timeout (client not connected):", sessionId);
|
|
152
|
+
this.handleSessionError(sessionId, new Error(`Timeout: Client not connected within ${this.sessionTimeoutMs / 1000}s`));
|
|
153
|
+
}, this.sessionTimeoutMs);
|
|
154
|
+
this.sessions.set(sessionId, session);
|
|
155
|
+
// Open browser
|
|
156
|
+
const url = `http://localhost:${this.port}/?sessionId=${sessionId}`;
|
|
157
|
+
console.error("[WebSocket] Opening browser:", url);
|
|
158
|
+
try {
|
|
159
|
+
const open = (await import("open")).default;
|
|
160
|
+
await open(url);
|
|
161
|
+
console.error("[WebSocket] Browser opened:", sessionId);
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
console.error("[WebSocket] Failed to open browser:", error);
|
|
165
|
+
this.handleSessionError(sessionId, new Error("Cannot open browser"));
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
async cleanup() {
|
|
170
|
+
console.error("[WebSocket] Cleaning up...");
|
|
171
|
+
// Clear all sessions
|
|
172
|
+
for (const [sessionId] of this.sessions.entries()) {
|
|
173
|
+
this.handleSessionError(sessionId, new Error("Server shutting down"));
|
|
174
|
+
}
|
|
175
|
+
// Close WebSocket server
|
|
176
|
+
return new Promise((resolve) => {
|
|
177
|
+
this.wss.close(() => {
|
|
178
|
+
console.error("[WebSocket] Server closed");
|
|
179
|
+
resolve();
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
getSessionCount() {
|
|
184
|
+
return this.sessions.size;
|
|
185
|
+
}
|
|
186
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ask-user-question-plus",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A TUI-style MCP server for asking user questions via a web interface.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"ask-user-question-plus": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"dev": "tsx src/index.ts",
|
|
14
|
+
"prepublishOnly": "npm run build"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"mcp",
|
|
18
|
+
"model-context-protocol",
|
|
19
|
+
"claude",
|
|
20
|
+
"codex",
|
|
21
|
+
"gemini-cli",
|
|
22
|
+
"AskUserQuestion"
|
|
23
|
+
],
|
|
24
|
+
"author": "JoJoJotarou",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/JoJoJotarou/AskUserQuestionPlus.git"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/JoJoJotarou/AskUserQuestionPlus#readme",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/JoJoJotarou/AskUserQuestionPlus/issues"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@modelcontextprotocol/sdk": "^1.0.1",
|
|
36
|
+
"dotenv": "^17.2.3",
|
|
37
|
+
"express": "^4.21.1",
|
|
38
|
+
"open": "^10.1.0",
|
|
39
|
+
"uuid": "^11.0.3",
|
|
40
|
+
"ws": "^8.18.0",
|
|
41
|
+
"zod": "^3.23.8"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/express": "^5.0.0",
|
|
45
|
+
"@types/node": "^22.9.0",
|
|
46
|
+
"@types/uuid": "^10.0.0",
|
|
47
|
+
"@types/ws": "^8.5.12",
|
|
48
|
+
"tsx": "^4.19.2",
|
|
49
|
+
"typescript": "^5.6.3"
|
|
50
|
+
}
|
|
51
|
+
}
|