mcp-feedback-enhanced 0.1.55 → 0.1.65

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 +346 -12
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -16,7 +16,9 @@ import * as os from 'os';
16
16
  import * as fs from 'fs';
17
17
  import * as path from 'path';
18
18
  import * as net from 'net';
19
+ import * as http from 'http';
19
20
  import { createRequire } from 'module';
21
+ import { exec } from 'child_process';
20
22
  // Read version from package.json (ES module compatible)
21
23
  const require = createRequire(import.meta.url);
22
24
  const packageJson = require('../package.json');
@@ -173,13 +175,29 @@ async function findExtensionForProjectAsync(projectPath) {
173
175
  if (servers.length === 0) {
174
176
  return null;
175
177
  }
176
- // Strategy 1: Exact workspace match
177
- for (const server of servers) {
178
- if (server.workspaces?.includes(projectPath)) {
179
- debug(`✓ Exact workspace match: port=${server.port}`);
180
- return server;
178
+ // Get MCP server's CURSOR_TRACE_ID (if running in Cursor)
179
+ const myTraceId = process.env['CURSOR_TRACE_ID'] || '';
180
+ debug(`My CURSOR_TRACE_ID: ${myTraceId || '(not set)'}`);
181
+ // Strategy 0: CURSOR_TRACE_ID match (HIGHEST PRIORITY - same Cursor window)
182
+ 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;
181
187
  }
182
188
  }
189
+ // Strategy 1: Exact workspace match + traceId filter if available
190
+ const workspaceMatches = servers.filter(s => s.workspaces?.includes(projectPath));
191
+ if (workspaceMatches.length === 1) {
192
+ debug(`✓ Exact workspace match (single): port=${workspaceMatches[0].port}`);
193
+ return workspaceMatches[0];
194
+ }
195
+ if (workspaceMatches.length > 1) {
196
+ // Multiple windows have this workspace - take most recent
197
+ const sorted = workspaceMatches.sort((a, b) => b.timestamp - a.timestamp);
198
+ debug(`✓ Exact workspace match (multiple, taking most recent): port=${sorted[0].port}`);
199
+ return sorted[0];
200
+ }
183
201
  // Strategy 2: Prefix match (project is inside a workspace)
184
202
  for (const server of servers) {
185
203
  for (const ws of server.workspaces || []) {
@@ -206,7 +224,7 @@ async function findExtensionForProjectAsync(projectPath) {
206
224
  debug(`✓ Using most recent server: port=${sorted[0].port}`);
207
225
  debug(` Available servers:`);
208
226
  sorted.forEach(s => {
209
- debug(` - pid=${s.pid}, port=${s.port}, workspaces=${s.workspaces?.join(', ')}`);
227
+ debug(` - pid=${s.pid}, port=${s.port}, traceId=${s.cursorTraceId}, workspaces=${s.workspaces?.join(', ')}`);
210
228
  });
211
229
  return sorted[0];
212
230
  }
@@ -217,11 +235,19 @@ function findExtensionForProject(projectPath) {
217
235
  const servers = getLiveServers();
218
236
  if (servers.length === 0)
219
237
  return null;
220
- // Same matching logic but sync
221
- for (const server of servers) {
222
- if (server.workspaces?.includes(projectPath))
223
- return server;
238
+ // Strategy 0: CURSOR_TRACE_ID match (highest priority)
239
+ const myTraceId = process.env['CURSOR_TRACE_ID'] || '';
240
+ if (myTraceId) {
241
+ const traceMatch = servers.find(s => s.cursorTraceId === myTraceId);
242
+ if (traceMatch)
243
+ return traceMatch;
244
+ }
245
+ // Strategy 1: Exact workspace match
246
+ const workspaceMatches = servers.filter(s => s.workspaces?.includes(projectPath));
247
+ if (workspaceMatches.length >= 1) {
248
+ return workspaceMatches.sort((a, b) => b.timestamp - a.timestamp)[0];
224
249
  }
250
+ // Strategy 2: Prefix match
225
251
  for (const server of servers) {
226
252
  for (const ws of server.workspaces || []) {
227
253
  if (projectPath.startsWith(ws + path.sep) || ws.startsWith(projectPath + path.sep)) {
@@ -229,9 +255,11 @@ function findExtensionForProject(projectPath) {
229
255
  }
230
256
  }
231
257
  }
258
+ // Strategy 3: parentPid match
232
259
  const parentMatch = servers.find(s => s.parentPid === process.ppid);
233
260
  if (parentMatch)
234
261
  return parentMatch;
262
+ // Fallbacks
235
263
  if (servers.length === 1)
236
264
  return servers[0];
237
265
  return servers.sort((a, b) => b.timestamp - a.timestamp)[0];
@@ -388,9 +416,18 @@ function handleMessage(message) {
388
416
  }
389
417
  /**
390
418
  * Request feedback from user via Extension
419
+ * Falls back to browser if extension is not available
391
420
  */
392
421
  async function requestFeedback(projectDirectory, summary, timeout, agentName) {
393
- await ensureConnectedForProject(projectDirectory);
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}`);
429
+ return requestFeedbackViaBrowser(projectDirectory, summary, timeout);
430
+ }
394
431
  const sessionId = `session_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
395
432
  return new Promise((resolve, reject) => {
396
433
  // Set timeout
@@ -417,6 +454,303 @@ async function requestFeedback(projectDirectory, summary, timeout, agentName) {
417
454
  });
418
455
  }
419
456
  // ============================================================================
457
+ // Browser Fallback
458
+ // ============================================================================
459
+ /**
460
+ * Open system default browser
461
+ */
462
+ function openBrowser(url) {
463
+ const platform = os.platform();
464
+ let cmd;
465
+ if (platform === 'darwin') {
466
+ cmd = `open "${url}"`;
467
+ }
468
+ else if (platform === 'win32') {
469
+ cmd = `start "" "${url}"`;
470
+ }
471
+ else {
472
+ cmd = `xdg-open "${url}"`;
473
+ }
474
+ exec(cmd, (error) => {
475
+ if (error) {
476
+ debug(`Failed to open browser: ${error.message}`);
477
+ }
478
+ });
479
+ }
480
+ /**
481
+ * Generate HTML page for browser feedback
482
+ */
483
+ function generateBrowserHtml(summary, port) {
484
+ const escapedSummary = summary
485
+ .replace(/&/g, '&')
486
+ .replace(/</g, '&lt;')
487
+ .replace(/>/g, '&gt;')
488
+ .replace(/"/g, '&quot;')
489
+ .replace(/\n/g, '<br>');
490
+ return `<!DOCTYPE html>
491
+ <html lang="en">
492
+ <head>
493
+ <meta charset="UTF-8">
494
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
495
+ <title>MCP Feedback</title>
496
+ <style>
497
+ * { box-sizing: border-box; margin: 0; padding: 0; }
498
+ body {
499
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
500
+ background: #1e1e1e;
501
+ color: #d4d4d4;
502
+ min-height: 100vh;
503
+ display: flex;
504
+ align-items: center;
505
+ justify-content: center;
506
+ padding: 20px;
507
+ }
508
+ .container {
509
+ max-width: 800px;
510
+ width: 100%;
511
+ background: #252526;
512
+ border-radius: 12px;
513
+ box-shadow: 0 8px 32px rgba(0,0,0,0.3);
514
+ overflow: hidden;
515
+ }
516
+ .header {
517
+ background: #007acc;
518
+ padding: 16px 24px;
519
+ display: flex;
520
+ align-items: center;
521
+ gap: 12px;
522
+ }
523
+ .header h1 {
524
+ font-size: 18px;
525
+ font-weight: 600;
526
+ color: white;
527
+ }
528
+ .header .icon { font-size: 24px; }
529
+ .content { padding: 24px; }
530
+ .ai-message {
531
+ background: #2d2d2d;
532
+ border: 1px solid #3c3c3c;
533
+ border-radius: 8px;
534
+ padding: 16px;
535
+ margin-bottom: 20px;
536
+ max-height: 400px;
537
+ overflow-y: auto;
538
+ line-height: 1.6;
539
+ }
540
+ .label {
541
+ font-size: 12px;
542
+ color: #888;
543
+ margin-bottom: 8px;
544
+ display: flex;
545
+ align-items: center;
546
+ gap: 6px;
547
+ }
548
+ .quick-btns {
549
+ display: flex;
550
+ gap: 8px;
551
+ margin-bottom: 16px;
552
+ flex-wrap: wrap;
553
+ }
554
+ .quick-btn {
555
+ padding: 8px 16px;
556
+ background: transparent;
557
+ border: 1px solid #3c3c3c;
558
+ color: #d4d4d4;
559
+ border-radius: 20px;
560
+ cursor: pointer;
561
+ font-size: 13px;
562
+ transition: all 0.2s;
563
+ }
564
+ .quick-btn:hover {
565
+ background: #3c3c3c;
566
+ border-color: #569cd6;
567
+ }
568
+ textarea {
569
+ width: 100%;
570
+ min-height: 120px;
571
+ padding: 12px;
572
+ background: #2d2d2d;
573
+ border: 1px solid #3c3c3c;
574
+ border-radius: 8px;
575
+ color: #d4d4d4;
576
+ font-size: 14px;
577
+ font-family: inherit;
578
+ resize: vertical;
579
+ margin-bottom: 16px;
580
+ }
581
+ textarea:focus {
582
+ outline: none;
583
+ border-color: #007acc;
584
+ }
585
+ .submit-btn {
586
+ width: 100%;
587
+ padding: 14px;
588
+ background: #007acc;
589
+ border: none;
590
+ border-radius: 8px;
591
+ color: white;
592
+ font-size: 15px;
593
+ font-weight: 600;
594
+ cursor: pointer;
595
+ transition: background 0.2s;
596
+ }
597
+ .submit-btn:hover { background: #0098ff; }
598
+ .submit-btn:disabled {
599
+ background: #3c3c3c;
600
+ cursor: not-allowed;
601
+ }
602
+ .success {
603
+ text-align: center;
604
+ padding: 40px;
605
+ }
606
+ .success .icon { font-size: 48px; margin-bottom: 16px; }
607
+ .success h2 { color: #4ec9b0; margin-bottom: 8px; }
608
+ .success p { color: #888; }
609
+ </style>
610
+ </head>
611
+ <body>
612
+ <div class="container" id="feedbackForm">
613
+ <div class="header">
614
+ <span class="icon">💬</span>
615
+ <h1>MCP Feedback</h1>
616
+ </div>
617
+ <div class="content">
618
+ <div class="label">🤖 AI Summary</div>
619
+ <div class="ai-message">${escapedSummary}</div>
620
+
621
+ <div class="label">💬 Your Feedback</div>
622
+ <div class="quick-btns">
623
+ <button class="quick-btn" onclick="setFeedback('Continue')">▶️ Continue</button>
624
+ <button class="quick-btn" onclick="setFeedback('Looks good')">👍 Good</button>
625
+ <button class="quick-btn" onclick="setFeedback('Please fix it')">🔧 Fix</button>
626
+ <button class="quick-btn" onclick="setFeedback('Stop')">⏹️ Stop</button>
627
+ </div>
628
+ <textarea id="feedback" placeholder="Type your feedback here..."></textarea>
629
+ <button class="submit-btn" id="submitBtn" onclick="submitFeedback()">Send Feedback</button>
630
+ </div>
631
+ </div>
632
+
633
+ <div class="container" id="successMsg" style="display: none;">
634
+ <div class="success">
635
+ <div class="icon">✅</div>
636
+ <h2>Feedback Sent!</h2>
637
+ <p>You can close this window now.</p>
638
+ </div>
639
+ </div>
640
+
641
+ <script>
642
+ function setFeedback(text) {
643
+ document.getElementById('feedback').value = text;
644
+ }
645
+
646
+ async function submitFeedback() {
647
+ const feedback = document.getElementById('feedback').value.trim();
648
+ if (!feedback) {
649
+ alert('Please enter your feedback');
650
+ return;
651
+ }
652
+
653
+ const btn = document.getElementById('submitBtn');
654
+ btn.disabled = true;
655
+ btn.textContent = 'Sending...';
656
+
657
+ try {
658
+ const response = await fetch('/submit', {
659
+ method: 'POST',
660
+ headers: { 'Content-Type': 'application/json' },
661
+ body: JSON.stringify({ feedback })
662
+ });
663
+
664
+ if (response.ok) {
665
+ document.getElementById('feedbackForm').style.display = 'none';
666
+ document.getElementById('successMsg').style.display = 'block';
667
+ } else {
668
+ throw new Error('Submit failed');
669
+ }
670
+ } catch (e) {
671
+ btn.disabled = false;
672
+ btn.textContent = 'Send Feedback';
673
+ alert('Failed to send feedback. Please try again.');
674
+ }
675
+ }
676
+ </script>
677
+ </body>
678
+ </html>`;
679
+ }
680
+ /**
681
+ * Request feedback via browser (fallback when extension not available)
682
+ */
683
+ async function requestFeedbackViaBrowser(projectDirectory, summary, timeout) {
684
+ return new Promise((resolve, reject) => {
685
+ // Find available port
686
+ const server = http.createServer();
687
+ server.listen(0, '127.0.0.1', () => {
688
+ const address = server.address();
689
+ if (!address || typeof address === 'string') {
690
+ reject(new Error('Failed to start HTTP server'));
691
+ return;
692
+ }
693
+ const port = address.port;
694
+ debug(`Browser feedback server started on port ${port}`);
695
+ let feedbackReceived = false;
696
+ // Handle requests
697
+ server.on('request', (req, res) => {
698
+ if (req.method === 'GET' && req.url === '/') {
699
+ // Serve HTML page
700
+ res.writeHead(200, { 'Content-Type': 'text/html' });
701
+ res.end(generateBrowserHtml(summary, port));
702
+ }
703
+ else if (req.method === 'POST' && req.url === '/submit') {
704
+ // Handle feedback submission
705
+ let body = '';
706
+ req.on('data', chunk => body += chunk);
707
+ req.on('end', () => {
708
+ try {
709
+ const data = JSON.parse(body);
710
+ feedbackReceived = true;
711
+ res.writeHead(200, { 'Content-Type': 'application/json' });
712
+ res.end(JSON.stringify({ success: true }));
713
+ // Close server and resolve
714
+ setTimeout(() => {
715
+ server.close();
716
+ resolve({
717
+ feedback: data.feedback || '',
718
+ images: []
719
+ });
720
+ }, 1000);
721
+ }
722
+ catch (e) {
723
+ res.writeHead(400);
724
+ res.end('Invalid request');
725
+ }
726
+ });
727
+ }
728
+ else {
729
+ res.writeHead(404);
730
+ res.end('Not found');
731
+ }
732
+ });
733
+ // Open browser
734
+ const url = `http://127.0.0.1:${port}`;
735
+ debug(`Opening browser: ${url}`);
736
+ openBrowser(url);
737
+ // Timeout
738
+ const timeoutHandle = setTimeout(() => {
739
+ if (!feedbackReceived) {
740
+ server.close();
741
+ reject(new Error(`Browser feedback timeout after ${timeout} seconds`));
742
+ }
743
+ }, timeout * 1000);
744
+ server.on('close', () => {
745
+ clearTimeout(timeoutHandle);
746
+ });
747
+ });
748
+ server.on('error', (err) => {
749
+ reject(new Error(`Failed to start browser feedback server: ${err.message}`));
750
+ });
751
+ });
752
+ }
753
+ // ============================================================================
420
754
  // MCP Server Setup
421
755
  // ============================================================================
422
756
  const server = new McpServer({
@@ -428,7 +762,7 @@ server.tool('interactive_feedback', 'Collect feedback from user through VSCode e
428
762
  project_directory: z.string().describe('The project directory path for context'),
429
763
  summary: z.string().describe('Summary of AI work completed for user review'),
430
764
  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")')
765
+ agent_name: z.string().optional().describe('Unique identifier for this agent/chat session. Use a stable name like the chat title or task description (e.g. "Chat 1", "Pre-credit Implementation", "Bug Fix #123"). If not provided, defaults to "Agent" and all messages will appear in the same tab. Providing a unique name allows multiple agents to have separate conversation tabs.')
432
766
  }, async ({ project_directory, summary, timeout, agent_name }) => {
433
767
  debug(`interactive_feedback called: project=${project_directory}, agent=${agent_name || 'default'}`);
434
768
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-feedback-enhanced",
3
- "version": "0.1.55",
3
+ "version": "0.1.65",
4
4
  "description": "MCP Feedback Enhanced Server - Interactive feedback collection for AI assistants",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -17,11 +17,11 @@
17
17
  "vscode",
18
18
  "claude"
19
19
  ],
20
- "author": "Minidoracat",
20
+ "author": "yuanming.chen",
21
21
  "license": "MIT",
22
22
  "repository": {
23
23
  "type": "git",
24
- "url": "https://github.com/Minidoracat/mcp-feedback-enhanced"
24
+ "url": "https://github.com/xxx/mcp-feedback-enhanced"
25
25
  },
26
26
  "scripts": {
27
27
  "build": "tsc",