mcp-feedback-enhanced 0.1.65 → 0.1.67

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 +271 -40
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -169,7 +169,7 @@ function getLiveServers() {
169
169
  * Now async to support port connectivity checks
170
170
  */
171
171
  async function findExtensionForProjectAsync(projectPath) {
172
- const servers = await getLiveServersAsync();
172
+ let servers = await getLiveServersAsync();
173
173
  debug(`Looking for Extension for project: ${projectPath}`);
174
174
  debug(`Found ${servers.length} live Extension server(s) with valid ports`);
175
175
  if (servers.length === 0) {
@@ -178,15 +178,40 @@ async function findExtensionForProjectAsync(projectPath) {
178
178
  // Get MCP server's CURSOR_TRACE_ID (if running in Cursor)
179
179
  const myTraceId = process.env['CURSOR_TRACE_ID'] || '';
180
180
  debug(`My CURSOR_TRACE_ID: ${myTraceId || '(not set)'}`);
181
- // Strategy 0: CURSOR_TRACE_ID match (HIGHEST PRIORITY - same Cursor window)
181
+ // Strategy 0: CURSOR_TRACE_ID match - but only if UNIQUE match
182
+ // (Multiple windows can share the same trace ID, so we need workspace matching as tiebreaker)
182
183
  if (myTraceId) {
183
- const traceMatch = servers.find(s => s.cursorTraceId === myTraceId);
184
- if (traceMatch) {
185
- debug(`✓ CURSOR_TRACE_ID match: port=${traceMatch.port}, traceId=${myTraceId}`);
186
- return traceMatch;
184
+ const traceMatches = servers.filter(s => s.cursorTraceId === myTraceId);
185
+ if (traceMatches.length === 1) {
186
+ // Unique trace ID match - perfect!
187
+ debug(`✓ CURSOR_TRACE_ID unique match: port=${traceMatches[0].port}`);
188
+ return traceMatches[0];
189
+ }
190
+ else if (traceMatches.length > 1) {
191
+ // Multiple servers with same trace ID - use workspace matching among them
192
+ debug(`Multiple servers (${traceMatches.length}) with same CURSOR_TRACE_ID, using workspace match`);
193
+ // Try exact workspace match among trace-matched servers
194
+ const exactMatch = traceMatches.find(s => s.workspaces?.includes(projectPath));
195
+ if (exactMatch) {
196
+ debug(`✓ CURSOR_TRACE_ID + workspace match: port=${exactMatch.port}`);
197
+ return exactMatch;
198
+ }
199
+ // Try prefix match among trace-matched servers
200
+ for (const server of traceMatches) {
201
+ for (const ws of server.workspaces || []) {
202
+ if (projectPath.startsWith(ws + path.sep) || ws.startsWith(projectPath + path.sep)) {
203
+ debug(`✓ CURSOR_TRACE_ID + prefix match: port=${server.port}`);
204
+ return server;
205
+ }
206
+ }
207
+ }
208
+ // Fall back to most recent among trace-matched servers
209
+ const sorted = traceMatches.sort((a, b) => b.timestamp - a.timestamp);
210
+ debug(`✓ CURSOR_TRACE_ID + most recent: port=${sorted[0].port}`);
211
+ return sorted[0];
187
212
  }
188
213
  }
189
- // Strategy 1: Exact workspace match + traceId filter if available
214
+ // Strategy 1: Exact workspace match (when no trace ID or no trace match)
190
215
  const workspaceMatches = servers.filter(s => s.workspaces?.includes(projectPath));
191
216
  if (workspaceMatches.length === 1) {
192
217
  debug(`✓ Exact workspace match (single): port=${workspaceMatches[0].port}`);
@@ -235,14 +260,31 @@ function findExtensionForProject(projectPath) {
235
260
  const servers = getLiveServers();
236
261
  if (servers.length === 0)
237
262
  return null;
238
- // Strategy 0: CURSOR_TRACE_ID match (highest priority)
263
+ // Strategy 0: CURSOR_TRACE_ID match - but only if UNIQUE
239
264
  const myTraceId = process.env['CURSOR_TRACE_ID'] || '';
240
265
  if (myTraceId) {
241
- const traceMatch = servers.find(s => s.cursorTraceId === myTraceId);
242
- if (traceMatch)
243
- return traceMatch;
266
+ const traceMatches = servers.filter(s => s.cursorTraceId === myTraceId);
267
+ if (traceMatches.length === 1) {
268
+ return traceMatches[0];
269
+ }
270
+ else if (traceMatches.length > 1) {
271
+ // Multiple servers with same trace ID - try workspace match among them
272
+ const exactMatch = traceMatches.find(s => s.workspaces?.includes(projectPath));
273
+ if (exactMatch)
274
+ return exactMatch;
275
+ // Prefix match among trace-matched
276
+ for (const server of traceMatches) {
277
+ for (const ws of server.workspaces || []) {
278
+ if (projectPath.startsWith(ws + path.sep) || ws.startsWith(projectPath + path.sep)) {
279
+ return server;
280
+ }
281
+ }
282
+ }
283
+ // Most recent among trace-matched
284
+ return traceMatches.sort((a, b) => b.timestamp - a.timestamp)[0];
285
+ }
244
286
  }
245
- // Strategy 1: Exact workspace match
287
+ // Strategy 1: Exact workspace match (when no trace ID or no trace match)
246
288
  const workspaceMatches = servers.filter(s => s.workspaces?.includes(projectPath));
247
289
  if (workspaceMatches.length >= 1) {
248
290
  return workspaceMatches.sort((a, b) => b.timestamp - a.timestamp)[0];
@@ -414,43 +456,232 @@ function handleMessage(message) {
414
456
  break;
415
457
  }
416
458
  }
459
+ // Track tried servers to avoid retry loops
460
+ let triedServerPorts = new Set();
417
461
  /**
418
462
  * Request feedback from user via Extension
419
- * Falls back to browser if extension is not available
463
+ * Uses single-connection with fast retry strategy:
464
+ * - Sorts servers by relevance (workspace match, most recent)
465
+ * - Tries one server at a time
466
+ * - If rejected (PROJECT_NOT_IN_WINDOW or no webview), immediately tries next
467
+ * - Falls back to browser if all fail
420
468
  */
421
469
  async function requestFeedback(projectDirectory, summary, timeout, agentName) {
422
- // Try to connect to extension first
423
- try {
424
- await ensureConnectedForProject(projectDirectory);
425
- }
426
- catch (e) {
427
- // Extension not available, fall back to browser
428
- debug(`Extension not available, falling back to browser: ${e.message}`);
470
+ // Reset tried servers
471
+ triedServerPorts.clear();
472
+ // Get all servers sorted by relevance
473
+ const servers = await getSortedServers(projectDirectory);
474
+ if (servers.length === 0) {
475
+ debug('No Extension servers found, falling back to browser');
429
476
  return requestFeedbackViaBrowser(projectDirectory, summary, timeout);
430
477
  }
478
+ debug(`Found ${servers.length} server(s), trying in order of relevance...`);
479
+ // Try servers one by one until success
480
+ return tryServersSequentially(servers, projectDirectory, summary, timeout, agentName);
481
+ }
482
+ /**
483
+ * Get servers sorted by relevance for the project
484
+ */
485
+ async function getSortedServers(projectPath) {
486
+ const servers = await getLiveServersAsync();
487
+ if (servers.length === 0) {
488
+ return [];
489
+ }
490
+ // Calculate workspace frequency for uniqueness bonus
491
+ const workspaceFrequency = new Map();
492
+ servers.forEach(s => {
493
+ s.workspaces?.forEach(ws => {
494
+ workspaceFrequency.set(ws, (workspaceFrequency.get(ws) || 0) + 1);
495
+ });
496
+ });
497
+ // Score each server
498
+ const scored = servers.map(server => {
499
+ let score = 0;
500
+ let reasons = [];
501
+ // 1. FOCUSED WINDOW (+500) - Highest priority!
502
+ if (server.focused) {
503
+ score += 500;
504
+ reasons.push('focused');
505
+ }
506
+ // 2. Workspace UNIQUENESS bonus (+1000)
507
+ // If this project is ONLY in this server's workspaces, huge bonus
508
+ if (server.workspaces?.includes(projectPath)) {
509
+ const freq = workspaceFrequency.get(projectPath) || 1;
510
+ if (freq === 1) {
511
+ score += 1000;
512
+ reasons.push('unique-ws');
513
+ }
514
+ else {
515
+ score += 100;
516
+ reasons.push('shared-ws');
517
+ }
518
+ }
519
+ // 3. Prefix match (+50)
520
+ for (const ws of server.workspaces || []) {
521
+ if (projectPath.startsWith(ws + path.sep)) {
522
+ score += 50;
523
+ reasons.push('prefix');
524
+ break;
525
+ }
526
+ }
527
+ // 4. CURSOR_TRACE_ID match (+10) - Low priority since often shared
528
+ const myTraceId = process.env['CURSOR_TRACE_ID'] || '';
529
+ if (myTraceId && server.cursorTraceId === myTraceId) {
530
+ score += 10;
531
+ reasons.push('trace');
532
+ }
533
+ // 5. More recent timestamp (tiny bonus)
534
+ score += server.timestamp / (10 ** 14);
535
+ return { server, score, reasons };
536
+ });
537
+ // Sort by score descending
538
+ scored.sort((a, b) => b.score - a.score);
539
+ debug(`Server scores:`);
540
+ scored.forEach(s => {
541
+ debug(` port=${s.server.port} score=${s.score.toFixed(0)} [${s.reasons.join(', ')}]`);
542
+ });
543
+ return scored.map(s => s.server);
544
+ }
545
+ /**
546
+ * Try servers one by one until success or all fail
547
+ */
548
+ async function tryServersSequentially(servers, projectDirectory, summary, timeout, agentName) {
549
+ for (const server of servers) {
550
+ // Skip already tried servers
551
+ if (triedServerPorts.has(server.port)) {
552
+ continue;
553
+ }
554
+ triedServerPorts.add(server.port);
555
+ debug(`Trying server port ${server.port}...`);
556
+ try {
557
+ const result = await tryServerOnce(server, projectDirectory, summary, timeout, agentName);
558
+ return result;
559
+ }
560
+ catch (err) {
561
+ const errorMessage = err.message || '';
562
+ // These errors mean we should try next server
563
+ if (errorMessage === 'PROJECT_NOT_IN_WINDOW' ||
564
+ errorMessage.includes('No feedback panel') ||
565
+ errorMessage.includes('Connection failed')) {
566
+ debug(`Server ${server.port} rejected: ${errorMessage}, trying next...`);
567
+ continue;
568
+ }
569
+ // Other errors (timeout, user cancelled) should propagate
570
+ throw err;
571
+ }
572
+ }
573
+ // All servers failed, fall back to browser
574
+ debug('All servers failed, falling back to browser');
575
+ return requestFeedbackViaBrowser(projectDirectory, summary, timeout);
576
+ }
577
+ /**
578
+ * Try a single server, returns result or throws error
579
+ */
580
+ async function tryServerOnce(server, projectDirectory, summary, timeout, agentName) {
581
+ const url = `ws://127.0.0.1:${server.port}/ws`;
431
582
  const sessionId = `session_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
432
583
  return new Promise((resolve, reject) => {
433
- // Set timeout
434
- const timeoutHandle = setTimeout(() => {
435
- pendingFeedbackResolvers.delete(sessionId);
436
- reject(new Error(`Feedback timeout after ${timeout} seconds`));
584
+ let conn = null;
585
+ let resolved = false;
586
+ const cleanup = () => {
587
+ if (conn && conn.readyState === WebSocket.OPEN) {
588
+ conn.close();
589
+ }
590
+ };
591
+ // Timeout for this attempt (use a shorter timeout for connection phase)
592
+ const connectionTimeout = setTimeout(() => {
593
+ if (!resolved) {
594
+ resolved = true;
595
+ cleanup();
596
+ reject(new Error('Connection failed'));
597
+ }
598
+ }, CONNECTION_TIMEOUT_MS);
599
+ // Overall timeout
600
+ const overallTimeout = setTimeout(() => {
601
+ if (!resolved) {
602
+ resolved = true;
603
+ cleanup();
604
+ reject(new Error(`Feedback timeout after ${timeout} seconds`));
605
+ }
437
606
  }, timeout * 1000);
438
- // Store resolver
439
- pendingFeedbackResolvers.set(sessionId, {
440
- resolve,
441
- reject,
442
- timeout: timeoutHandle
443
- });
444
- // Send request to Extension
445
- ws?.send(JSON.stringify({
446
- type: 'feedback_request',
447
- session_id: sessionId,
448
- project_directory: projectDirectory,
449
- summary,
450
- timeout,
451
- agent_name: agentName // For multi-agent display
452
- }));
453
- debug(`Feedback request sent: session=${sessionId}, agent=${agentName || 'default'}`);
607
+ try {
608
+ conn = new WebSocket(url);
609
+ conn.on('open', () => {
610
+ clearTimeout(connectionTimeout);
611
+ debug(`Connected to server port ${server.port}`);
612
+ // Register
613
+ conn.send(JSON.stringify({
614
+ type: 'register',
615
+ clientType: 'mcp-server',
616
+ pid: process.pid,
617
+ parentPid: process.ppid
618
+ }));
619
+ // Send feedback request
620
+ conn.send(JSON.stringify({
621
+ type: 'feedback_request',
622
+ session_id: sessionId,
623
+ project_directory: projectDirectory,
624
+ summary,
625
+ timeout,
626
+ agent_name: agentName
627
+ }));
628
+ debug(`Feedback request sent: session=${sessionId}`);
629
+ });
630
+ conn.on('message', (data) => {
631
+ try {
632
+ const message = JSON.parse(data.toString());
633
+ debug(`Received from port ${server.port}: ${message.type}`);
634
+ switch (message.type) {
635
+ case 'feedback_result':
636
+ if (message.session_id === sessionId && !resolved) {
637
+ resolved = true;
638
+ clearTimeout(overallTimeout);
639
+ cleanup();
640
+ resolve({
641
+ feedback: message.feedback,
642
+ images: message.images || []
643
+ });
644
+ }
645
+ break;
646
+ case 'feedback_error':
647
+ if (message.session_id === sessionId && !resolved) {
648
+ resolved = true;
649
+ clearTimeout(overallTimeout);
650
+ cleanup();
651
+ reject(new Error(message.error));
652
+ }
653
+ break;
654
+ }
655
+ }
656
+ catch (e) {
657
+ debug(`Parse error: ${e}`);
658
+ }
659
+ });
660
+ conn.on('error', (err) => {
661
+ if (!resolved) {
662
+ resolved = true;
663
+ clearTimeout(connectionTimeout);
664
+ clearTimeout(overallTimeout);
665
+ reject(new Error(`Connection failed: ${err.message}`));
666
+ }
667
+ });
668
+ conn.on('close', () => {
669
+ if (!resolved) {
670
+ resolved = true;
671
+ clearTimeout(connectionTimeout);
672
+ clearTimeout(overallTimeout);
673
+ reject(new Error('Connection closed'));
674
+ }
675
+ });
676
+ }
677
+ catch (e) {
678
+ if (!resolved) {
679
+ resolved = true;
680
+ clearTimeout(connectionTimeout);
681
+ clearTimeout(overallTimeout);
682
+ reject(new Error(`Connection failed: ${e}`));
683
+ }
684
+ }
454
685
  });
455
686
  }
456
687
  // ============================================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-feedback-enhanced",
3
- "version": "0.1.65",
3
+ "version": "0.1.67",
4
4
  "description": "MCP Feedback Enhanced Server - Interactive feedback collection for AI assistants",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",