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.
- package/dist/index.js +271 -40
- 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
|
-
|
|
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
|
|
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
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
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
|
|
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
|
|
242
|
-
if (
|
|
243
|
-
return
|
|
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
|
-
*
|
|
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
|
-
//
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
// ============================================================================
|