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.
@@ -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
+ }