mcp-feedback-enhanced 0.1.45 → 0.1.46
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/dist/index.js +172 -30
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ import * as z from 'zod';
|
|
|
15
15
|
import * as os from 'os';
|
|
16
16
|
import * as fs from 'fs';
|
|
17
17
|
import * as path from 'path';
|
|
18
|
+
import * as net from 'net';
|
|
18
19
|
import { createRequire } from 'module';
|
|
19
20
|
// Read version from package.json (ES module compatible)
|
|
20
21
|
const require = createRequire(import.meta.url);
|
|
@@ -26,6 +27,9 @@ const CONFIG_DIR = path.join(os.homedir(), '.config', 'mcp-feedback-enhanced');
|
|
|
26
27
|
const SERVERS_DIR = path.join(CONFIG_DIR, 'servers');
|
|
27
28
|
const CONNECTION_TIMEOUT_MS = 5000; // 5 seconds to connect
|
|
28
29
|
const HEARTBEAT_INTERVAL_MS = 30000; // 30 seconds heartbeat
|
|
30
|
+
const PORT_CHECK_TIMEOUT_MS = 1000; // 1 second to check port
|
|
31
|
+
const MAX_RECONNECT_ATTEMPTS = 3;
|
|
32
|
+
const RECONNECT_DELAY_MS = 2000;
|
|
29
33
|
// Debug logging
|
|
30
34
|
function debug(message) {
|
|
31
35
|
if (DEBUG) {
|
|
@@ -37,7 +41,88 @@ let ws = null;
|
|
|
37
41
|
let isConnected = false;
|
|
38
42
|
let pendingFeedbackResolvers = new Map();
|
|
39
43
|
/**
|
|
40
|
-
*
|
|
44
|
+
* Check if a port is actually accepting connections
|
|
45
|
+
*/
|
|
46
|
+
function checkPortConnectivity(port) {
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
const socket = new net.Socket();
|
|
49
|
+
let resolved = false;
|
|
50
|
+
const cleanup = () => {
|
|
51
|
+
if (!resolved) {
|
|
52
|
+
resolved = true;
|
|
53
|
+
socket.destroy();
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
socket.setTimeout(PORT_CHECK_TIMEOUT_MS);
|
|
57
|
+
socket.on('connect', () => {
|
|
58
|
+
cleanup();
|
|
59
|
+
resolve(true);
|
|
60
|
+
});
|
|
61
|
+
socket.on('error', () => {
|
|
62
|
+
cleanup();
|
|
63
|
+
resolve(false);
|
|
64
|
+
});
|
|
65
|
+
socket.on('timeout', () => {
|
|
66
|
+
cleanup();
|
|
67
|
+
resolve(false);
|
|
68
|
+
});
|
|
69
|
+
socket.connect(port, '127.0.0.1');
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Get all live Extension servers (with port connectivity check)
|
|
74
|
+
*/
|
|
75
|
+
async function getLiveServersAsync() {
|
|
76
|
+
try {
|
|
77
|
+
if (!fs.existsSync(SERVERS_DIR)) {
|
|
78
|
+
debug('Servers directory not found');
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
const files = fs.readdirSync(SERVERS_DIR);
|
|
82
|
+
const servers = [];
|
|
83
|
+
for (const file of files) {
|
|
84
|
+
if (!file.endsWith('.json'))
|
|
85
|
+
continue;
|
|
86
|
+
try {
|
|
87
|
+
const data = JSON.parse(fs.readFileSync(path.join(SERVERS_DIR, file), 'utf-8'));
|
|
88
|
+
// Verify process is still alive
|
|
89
|
+
try {
|
|
90
|
+
process.kill(data.pid, 0);
|
|
91
|
+
// Also check if port is actually responding (fixes stale port bug)
|
|
92
|
+
const portOk = await checkPortConnectivity(data.port);
|
|
93
|
+
if (portOk) {
|
|
94
|
+
servers.push({
|
|
95
|
+
...data,
|
|
96
|
+
workspaces: data.workspaces || []
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
debug(`Skipping server with unresponsive port: pid=${data.pid}, port=${data.port}`);
|
|
101
|
+
// Remove stale server file
|
|
102
|
+
try {
|
|
103
|
+
fs.unlinkSync(path.join(SERVERS_DIR, file));
|
|
104
|
+
debug(`Removed stale server file: ${file}`);
|
|
105
|
+
}
|
|
106
|
+
catch { /* ignore */ }
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
debug(`Skipping dead server: pid=${data.pid}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// Skip invalid files
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return servers;
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
debug(`Error reading servers: ${e}`);
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Get all live Extension servers (sync version for backward compat)
|
|
41
126
|
*/
|
|
42
127
|
function getLiveServers() {
|
|
43
128
|
try {
|
|
@@ -79,11 +164,12 @@ function getLiveServers() {
|
|
|
79
164
|
/**
|
|
80
165
|
* Find the best Extension server for a given project path
|
|
81
166
|
* Uses workspace matching as primary strategy
|
|
167
|
+
* Now async to support port connectivity checks
|
|
82
168
|
*/
|
|
83
|
-
function
|
|
84
|
-
const servers =
|
|
169
|
+
async function findExtensionForProjectAsync(projectPath) {
|
|
170
|
+
const servers = await getLiveServersAsync();
|
|
85
171
|
debug(`Looking for Extension for project: ${projectPath}`);
|
|
86
|
-
debug(`Found ${servers.length} live Extension server(s)`);
|
|
172
|
+
debug(`Found ${servers.length} live Extension server(s) with valid ports`);
|
|
87
173
|
if (servers.length === 0) {
|
|
88
174
|
return null;
|
|
89
175
|
}
|
|
@@ -124,25 +210,77 @@ function findExtensionForProject(projectPath) {
|
|
|
124
210
|
});
|
|
125
211
|
return sorted[0];
|
|
126
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* Find the best Extension server (sync version for debug/startup)
|
|
215
|
+
*/
|
|
216
|
+
function findExtensionForProject(projectPath) {
|
|
217
|
+
const servers = getLiveServers();
|
|
218
|
+
if (servers.length === 0)
|
|
219
|
+
return null;
|
|
220
|
+
// Same matching logic but sync
|
|
221
|
+
for (const server of servers) {
|
|
222
|
+
if (server.workspaces?.includes(projectPath))
|
|
223
|
+
return server;
|
|
224
|
+
}
|
|
225
|
+
for (const server of servers) {
|
|
226
|
+
for (const ws of server.workspaces || []) {
|
|
227
|
+
if (projectPath.startsWith(ws + path.sep) || ws.startsWith(projectPath + path.sep)) {
|
|
228
|
+
return server;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const parentMatch = servers.find(s => s.parentPid === process.ppid);
|
|
233
|
+
if (parentMatch)
|
|
234
|
+
return parentMatch;
|
|
235
|
+
if (servers.length === 1)
|
|
236
|
+
return servers[0];
|
|
237
|
+
return servers.sort((a, b) => b.timestamp - a.timestamp)[0];
|
|
238
|
+
}
|
|
127
239
|
// Track which server we're connected to
|
|
128
240
|
let connectedPort = null;
|
|
241
|
+
let lastProjectPath = '';
|
|
129
242
|
/**
|
|
130
243
|
* Connect to Extension's WebSocket Server for a specific project
|
|
244
|
+
* With retry logic for handling stale connections
|
|
131
245
|
*/
|
|
132
|
-
function
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
return;
|
|
246
|
+
async function connectToExtensionWithRetry(projectPath, maxRetries = MAX_RECONNECT_ATTEMPTS) {
|
|
247
|
+
let lastError = null;
|
|
248
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
249
|
+
try {
|
|
250
|
+
await connectToExtension(projectPath);
|
|
251
|
+
return; // Success
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
lastError = err;
|
|
255
|
+
debug(`Connection attempt ${attempt}/${maxRetries} failed: ${err.message}`);
|
|
256
|
+
if (attempt < maxRetries) {
|
|
257
|
+
// Wait before retry with exponential backoff
|
|
258
|
+
const delay = RECONNECT_DELAY_MS * attempt;
|
|
259
|
+
debug(`Retrying in ${delay}ms...`);
|
|
260
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
261
|
+
}
|
|
138
262
|
}
|
|
139
|
-
|
|
140
|
-
|
|
263
|
+
}
|
|
264
|
+
throw lastError || new Error('Connection failed after retries');
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Connect to Extension's WebSocket Server for a specific project
|
|
268
|
+
*/
|
|
269
|
+
async function connectToExtension(projectPath) {
|
|
270
|
+
// Use async version to check port connectivity
|
|
271
|
+
const server = await findExtensionForProjectAsync(projectPath);
|
|
272
|
+
if (!server) {
|
|
273
|
+
throw new Error(`No MCP Feedback Extension found for project: ${projectPath}. Please ensure the extension is installed and a Cursor window is open with this project.`);
|
|
274
|
+
}
|
|
275
|
+
const url = `ws://127.0.0.1:${server.port}/ws`;
|
|
276
|
+
debug(`Connecting to Extension at ${url} for project ${projectPath}`);
|
|
277
|
+
return new Promise((resolve, reject) => {
|
|
141
278
|
ws = new WebSocket(url);
|
|
142
279
|
ws.on('open', () => {
|
|
143
280
|
debug('Connected to Extension WebSocket Server');
|
|
144
281
|
isConnected = true;
|
|
145
282
|
connectedPort = server.port;
|
|
283
|
+
lastProjectPath = projectPath;
|
|
146
284
|
// Register as MCP Server
|
|
147
285
|
ws?.send(JSON.stringify({
|
|
148
286
|
type: 'register',
|
|
@@ -163,15 +301,15 @@ function connectToExtension(projectPath) {
|
|
|
163
301
|
});
|
|
164
302
|
ws.on('close', () => {
|
|
165
303
|
debug('Disconnected from Extension');
|
|
304
|
+
const wasConnected = isConnected;
|
|
166
305
|
isConnected = false;
|
|
167
306
|
connectedPort = null;
|
|
168
307
|
ws = null;
|
|
169
|
-
//
|
|
170
|
-
pendingFeedbackResolvers.
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
pendingFeedbackResolvers.clear();
|
|
308
|
+
// Only reject pending if we were previously connected (unexpected disconnect)
|
|
309
|
+
if (wasConnected && pendingFeedbackResolvers.size > 0) {
|
|
310
|
+
debug(`Connection lost with ${pendingFeedbackResolvers.size} pending requests`);
|
|
311
|
+
// Don't reject immediately - the next feedback request will trigger reconnect
|
|
312
|
+
}
|
|
175
313
|
});
|
|
176
314
|
ws.on('error', (err) => {
|
|
177
315
|
debug(`WebSocket error: ${err.message}`);
|
|
@@ -190,26 +328,28 @@ function connectToExtension(projectPath) {
|
|
|
190
328
|
}
|
|
191
329
|
/**
|
|
192
330
|
* Ensure connected to the right Extension for this project
|
|
331
|
+
* With automatic reconnection and retry
|
|
193
332
|
*/
|
|
194
333
|
async function ensureConnectedForProject(projectPath) {
|
|
195
|
-
//
|
|
196
|
-
const server =
|
|
334
|
+
// Use async version to check port connectivity
|
|
335
|
+
const server = await findExtensionForProjectAsync(projectPath);
|
|
197
336
|
if (!server) {
|
|
198
337
|
throw new Error(`No MCP Feedback Extension found for project: ${projectPath}. Please open the project in Cursor with the MCP Feedback extension installed.`);
|
|
199
338
|
}
|
|
200
|
-
// If already connected to the right server, reuse
|
|
339
|
+
// If already connected to the right server and connection is open, reuse
|
|
201
340
|
if (isConnected && ws?.readyState === WebSocket.OPEN && connectedPort === server.port) {
|
|
202
341
|
return;
|
|
203
342
|
}
|
|
204
343
|
// Close existing connection if any
|
|
205
344
|
if (ws) {
|
|
345
|
+
debug('Closing existing connection for reconnect');
|
|
206
346
|
ws.close();
|
|
207
347
|
ws = null;
|
|
208
348
|
isConnected = false;
|
|
209
349
|
connectedPort = null;
|
|
210
350
|
}
|
|
211
|
-
// Connect
|
|
212
|
-
await
|
|
351
|
+
// Connect with retry logic
|
|
352
|
+
await connectToExtensionWithRetry(projectPath);
|
|
213
353
|
}
|
|
214
354
|
/**
|
|
215
355
|
* Handle messages from Extension
|
|
@@ -249,7 +389,7 @@ function handleMessage(message) {
|
|
|
249
389
|
/**
|
|
250
390
|
* Request feedback from user via Extension
|
|
251
391
|
*/
|
|
252
|
-
async function requestFeedback(projectDirectory, summary, timeout) {
|
|
392
|
+
async function requestFeedback(projectDirectory, summary, timeout, agentName) {
|
|
253
393
|
await ensureConnectedForProject(projectDirectory);
|
|
254
394
|
const sessionId = `session_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
|
255
395
|
return new Promise((resolve, reject) => {
|
|
@@ -270,9 +410,10 @@ async function requestFeedback(projectDirectory, summary, timeout) {
|
|
|
270
410
|
session_id: sessionId,
|
|
271
411
|
project_directory: projectDirectory,
|
|
272
412
|
summary,
|
|
273
|
-
timeout
|
|
413
|
+
timeout,
|
|
414
|
+
agent_name: agentName // For multi-agent display
|
|
274
415
|
}));
|
|
275
|
-
debug(`Feedback request sent: session=${sessionId}`);
|
|
416
|
+
debug(`Feedback request sent: session=${sessionId}, agent=${agentName || 'default'}`);
|
|
276
417
|
});
|
|
277
418
|
}
|
|
278
419
|
// ============================================================================
|
|
@@ -286,11 +427,12 @@ const server = new McpServer({
|
|
|
286
427
|
server.tool('interactive_feedback', 'Collect feedback from user through VSCode extension panel. The feedback panel will display the AI summary and wait for user input.', {
|
|
287
428
|
project_directory: z.string().describe('The project directory path for context'),
|
|
288
429
|
summary: z.string().describe('Summary of AI work completed for user review'),
|
|
289
|
-
timeout: z.number().optional().default(600).describe('Timeout in seconds (default: 600)')
|
|
290
|
-
|
|
291
|
-
|
|
430
|
+
timeout: z.number().optional().default(600).describe('Timeout in seconds (default: 600)'),
|
|
431
|
+
agent_name: z.string().optional().describe('Display name for this agent/chat (e.g. "Chat 1", "Refactor Task")')
|
|
432
|
+
}, async ({ project_directory, summary, timeout, agent_name }) => {
|
|
433
|
+
debug(`interactive_feedback called: project=${project_directory}, agent=${agent_name || 'default'}`);
|
|
292
434
|
try {
|
|
293
|
-
const result = await requestFeedback(project_directory, summary, timeout || 600);
|
|
435
|
+
const result = await requestFeedback(project_directory, summary, timeout || 600, agent_name);
|
|
294
436
|
// Format response
|
|
295
437
|
let response = `User Feedback:\n${result.feedback}`;
|
|
296
438
|
if (result.images && result.images.length > 0) {
|