mcp-feedback-enhanced 0.1.45 → 0.1.55

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.
Files changed (2) hide show
  1. package/dist/index.js +188 -36
  2. 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
- * Get all live Extension servers
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 findExtensionForProject(projectPath) {
84
- const servers = getLiveServers();
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 connectToExtension(projectPath) {
133
- return new Promise((resolve, reject) => {
134
- const server = findExtensionForProject(projectPath);
135
- if (!server) {
136
- reject(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.`));
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
- const url = `ws://127.0.0.1:${server.port}/ws`;
140
- debug(`Connecting to Extension at ${url} for project ${projectPath}`);
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
- // Reject all pending feedback requests
170
- pendingFeedbackResolvers.forEach((resolver, sessionId) => {
171
- clearTimeout(resolver.timeout);
172
- resolver.reject(new Error('Connection to Extension lost'));
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
- // Check if we need to reconnect (different project might need different Extension)
196
- const server = findExtensionForProject(projectPath);
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 connection
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 to the right server
212
- await connectToExtension(projectPath);
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,19 +427,30 @@ 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
- }, async ({ project_directory, summary, timeout }) => {
291
- debug(`interactive_feedback called: project=${project_directory}`);
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);
294
- // Format response
295
- let response = `User Feedback:\n${result.feedback}`;
435
+ const result = await requestFeedback(project_directory, summary, timeout || 600, agent_name);
436
+ // Build content array with text and images
437
+ const content = [{ type: 'text', text: `User Feedback:\n${result.feedback}` }];
438
+ // Add images as MCP image content items
296
439
  if (result.images && result.images.length > 0) {
297
- response += `\n\n[${result.images.length} image(s) attached]`;
440
+ debug(`Processing ${result.images.length} image(s)`);
441
+ for (const img of result.images) {
442
+ // img is { name: string, data: string (base64) }
443
+ if (img && img.data) {
444
+ content.push({
445
+ type: 'image',
446
+ data: img.data,
447
+ mimeType: 'image/png' // Default to PNG; could infer from name
448
+ });
449
+ debug(`Added image: ${img.name || 'unnamed'}`);
450
+ }
451
+ }
298
452
  }
299
- return {
300
- content: [{ type: 'text', text: response }]
301
- };
453
+ return { content };
302
454
  }
303
455
  catch (error) {
304
456
  debug(`Feedback error: ${error.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-feedback-enhanced",
3
- "version": "0.1.45",
3
+ "version": "0.1.55",
4
4
  "description": "MCP Feedback Enhanced Server - Interactive feedback collection for AI assistants",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",