cursor-feedback 1.2.0 → 2.0.1

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/src/mcp-server.ts DELETED
@@ -1,768 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * MCP Server 独立入口文件
4
- *
5
- * 此文件用于作为独立进程运行 MCP Server
6
- * Cursor/VS Code 会通过 stdio 与此服务器通信
7
- *
8
- * 使用方法:
9
- * 在 Cursor 的 MCP 配置中添加:
10
- * {
11
- * "mcpServers": {
12
- * "cursor-feedback": {
13
- * "command": "node",
14
- * "args": ["/path/to/dist/mcp-server.js"]
15
- * }
16
- * }
17
- * }
18
- */
19
-
20
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
21
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
22
- import {
23
- CallToolRequestSchema,
24
- ListToolsRequestSchema,
25
- } from '@modelcontextprotocol/sdk/types.js';
26
- import * as http from 'http';
27
- import * as os from 'os';
28
-
29
- // 调试日志输出到 stderr(不影响 stdio 通信)
30
- function debugLog(message: string) {
31
- const timestamp = new Date().toISOString();
32
- console.error(`[${timestamp}] ${message}`);
33
- }
34
-
35
- /**
36
- * 反馈请求接口
37
- */
38
- interface FeedbackRequest {
39
- id: string;
40
- summary: string;
41
- projectDir: string;
42
- timeout: number;
43
- timestamp: number;
44
- }
45
-
46
- /**
47
- * 反馈响应接口
48
- */
49
- interface FeedbackResponse {
50
- interactive_feedback: string;
51
- images: Array<{
52
- name: string;
53
- data: string;
54
- size: number;
55
- }>;
56
- attachedFiles: string[];
57
- project_directory: string;
58
- }
59
-
60
- /**
61
- * MCP Feedback Server
62
- */
63
- class McpFeedbackServer {
64
- private server: Server;
65
- private httpServer: http.Server | null = null;
66
- private port: number;
67
-
68
- // 待处理的反馈请求
69
- private pendingRequests: Map<string, {
70
- resolve: (value: FeedbackResponse | null) => void;
71
- reject: (error: Error) => void;
72
- timeout: NodeJS.Timeout;
73
- }> = new Map();
74
-
75
- // 当前反馈请求
76
- private currentRequest: FeedbackRequest | null = null;
77
-
78
- // 所属工作区(只在 AI 调用 feedback 时设置)
79
- // 只有来自同一工作区的轮询才会更新活动时间
80
- private ownerWorkspace: string | null = null;
81
-
82
- // Server 启动时间
83
- private readonly startTime: number = Date.now();
84
-
85
- // 最近一次活动时间(任意 HTTP 轮询 / MCP 调用都会刷新)
86
- // 用于 watchdog 判定是否所有 Cursor 窗口都已关闭
87
- private lastActivityTime: number = Date.now();
88
-
89
- // 看门狗定时器(兜底退出,防止进程残留 / CPU 占满)
90
- private watchdogTimer: NodeJS.Timeout | null = null;
91
-
92
- constructor(port: number = 8766) {
93
- this.port = port;
94
-
95
- this.server = new Server(
96
- {
97
- name: 'cursor-feedback-server',
98
- version: '0.0.1',
99
- },
100
- {
101
- capabilities: {
102
- tools: {},
103
- },
104
- }
105
- );
106
-
107
- this.setupHandlers();
108
- }
109
-
110
- private setupHandlers() {
111
- // 列出可用工具
112
- this.server.setRequestHandler(ListToolsRequestSchema, async () => {
113
- return {
114
- tools: [
115
- {
116
- name: 'interactive_feedback',
117
- description: `交互式反馈收集工具。
118
-
119
- 【重要】这是你与用户沟通的主要渠道,所有回复都必须通过此工具。
120
-
121
- 使用规则:
122
- 1. 每次完成任务或需要与用户沟通时,必须调用此工具
123
- 2. 收到用户反馈后,必须再次调用此工具继续对话(这是一个持续的对话循环)
124
- 3. 不要直接回复用户,所有回复必须通过此工具的 summary 参数
125
- 4. 只有当用户明确提出结束对话时,才能停止调用此工具
126
- 5. 提供清晰的工作摘要,说明你做了什么以及需要什么反馈
127
-
128
- 对话流程:
129
- 用户消息 → 你工作 → 调用此工具(附带摘要)→ 用户反馈 → 你工作 → 再次调用此工具 → ...
130
-
131
- 参数:
132
- project_directory: 【必填】项目目录的绝对路径,用于正确匹配项目窗口
133
- summary: AI 工作摘要,供用户查看(支持 Markdown)
134
- timeout: 等待用户反馈的超时时间(秒),默认 300 秒(5 分钟)
135
-
136
- 返回:
137
- 用户反馈内容(文字/图片/文件路径),或 timeout/cancelled 状态`,
138
- inputSchema: {
139
- type: 'object',
140
- properties: {
141
- project_directory: {
142
- type: 'string',
143
- description: 'Project directory absolute path (REQUIRED - must be the absolute path of current workspace for correct project matching)',
144
- },
145
- summary: {
146
- type: 'string',
147
- description: 'Summary of AI work completed for user review (supports Markdown)',
148
- default: '我已完成您的请求。',
149
- },
150
- timeout: {
151
- type: 'number',
152
- description: 'Timeout in seconds for waiting user feedback (default: 300 seconds = 5 minutes)',
153
- default: 300,
154
- },
155
- },
156
- required: ['project_directory'],
157
- },
158
- },
159
- {
160
- name: 'get_system_info',
161
- description: 'Get system environment information',
162
- inputSchema: {
163
- type: 'object',
164
- properties: {},
165
- },
166
- },
167
- ],
168
- };
169
- });
170
-
171
- // 处理工具调用
172
- this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
173
- // AI 发起调用,刷新活动时间,避免被 watchdog 误判为闲置
174
- this.lastActivityTime = Date.now();
175
- const { name, arguments: args } = request.params;
176
-
177
- try {
178
- switch (name) {
179
- case 'interactive_feedback':
180
- return await this.handleInteractiveFeedback(args);
181
- case 'get_system_info':
182
- return this.handleGetSystemInfo();
183
- default:
184
- return {
185
- content: [{ type: 'text', text: `Unknown tool: ${name}` }],
186
- isError: true,
187
- };
188
- }
189
- } catch (error) {
190
- debugLog(`Error in tool ${name}: ${error}`);
191
- return {
192
- content: [{ type: 'text', text: `Tool error: ${error}` }],
193
- isError: true,
194
- };
195
- }
196
- });
197
- }
198
-
199
- /**
200
- * 处理交互式反馈请求
201
- */
202
- private async handleInteractiveFeedback(args: Record<string, unknown> | undefined): Promise<{
203
- content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
204
- isError?: boolean;
205
- }> {
206
- // 参数校验:project_directory 是必填项
207
- if (!args?.project_directory) {
208
- const receivedParams = JSON.stringify(args || {});
209
- return {
210
- content: [{
211
- type: 'text',
212
- text: `参数错误:缺少必填参数 project_directory。请查看 interactive_feedback 工具描述后使用正确参数重新调用。\n\n你传入的参数:${receivedParams}`,
213
- }],
214
- isError: true,
215
- };
216
- }
217
-
218
- const projectDir = args.project_directory as string;
219
- // summary 支持别名 message
220
- const summary = (args?.summary as string) || (args?.message as string) || '我已完成您的请求。';
221
- // 超时时间优先级:环境变量 > 工具参数 > 默认值(300秒)
222
- // 这样用户配置的环境变量永远生效,不会被 AI 覆盖
223
- const envTimeout = process.env.MCP_FEEDBACK_TIMEOUT ? parseInt(process.env.MCP_FEEDBACK_TIMEOUT, 10) : null;
224
- const timeout = envTimeout || (args?.timeout as number) || 300;
225
-
226
- const requestId = this.generateRequestId();
227
-
228
- // AI 调用 feedback 时设置 ownerWorkspace(这是唯一正确的时机)
229
- const normalize = (p: string) => p.replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase();
230
- this.ownerWorkspace = normalize(projectDir);
231
- debugLog(`Owner workspace set to: ${this.ownerWorkspace}`);
232
-
233
- // 创建反馈请求
234
- this.currentRequest = {
235
- id: requestId,
236
- summary,
237
- projectDir,
238
- timeout,
239
- timestamp: Date.now(),
240
- };
241
-
242
- debugLog(`Feedback request created: ${requestId}`);
243
- debugLog(`Summary: ${summary}`);
244
- debugLog(`Project: ${projectDir}`);
245
- debugLog(`Timeout: ${timeout}s`);
246
- debugLog(`Waiting for VS Code extension to collect feedback...`);
247
-
248
- try {
249
- // 等待用户反馈
250
- const result = await this.waitForFeedback(requestId, timeout * 1000);
251
-
252
- if (!result) {
253
- debugLog('Feedback request timed out or cancelled');
254
- // MCP_AUTO_RETRY 环境变量控制是否显示重试指示,默认 true
255
- const autoRetry = process.env.MCP_AUTO_RETRY !== 'false';
256
- const timeoutMessage = autoRetry
257
- ? 'User cancelled the feedback or timeout. 【必须立即重新调用此工具继续等待用户反馈】'
258
- : 'User cancelled the feedback or timeout.';
259
- return {
260
- content: [
261
- {
262
- type: 'text',
263
- text: timeoutMessage,
264
- },
265
- ],
266
- };
267
- }
268
-
269
- debugLog(`Received feedback: ${result.interactive_feedback?.substring(0, 100)}...`);
270
-
271
- const contentItems: Array<{ type: string; text?: string; data?: string; mimeType?: string }> = [];
272
-
273
- // 构建反馈文本
274
- let feedbackText = '';
275
-
276
- // 添加文字反馈
277
- if (result.interactive_feedback) {
278
- feedbackText += `=== User Feedback ===\n${result.interactive_feedback}`;
279
- }
280
-
281
- // 添加附加文件路径
282
- if (result.attachedFiles && result.attachedFiles.length > 0) {
283
- debugLog(`Processing ${result.attachedFiles.length} attached files`);
284
- feedbackText += `\n\n=== Attached Files ===\n`;
285
- for (const filePath of result.attachedFiles) {
286
- feedbackText += `${filePath}\n`;
287
- }
288
- feedbackText += `\nPlease read the above files to understand the context.`;
289
- }
290
-
291
- if (feedbackText) {
292
- contentItems.push({
293
- type: 'text',
294
- text: feedbackText,
295
- });
296
- }
297
-
298
- // 添加图片
299
- if (result.images && result.images.length > 0) {
300
- debugLog(`Processing ${result.images.length} images`);
301
- for (const img of result.images) {
302
- contentItems.push({
303
- type: 'image',
304
- data: img.data,
305
- mimeType: this.getMimeType(img.name),
306
- });
307
- }
308
- }
309
-
310
- if (contentItems.length === 0) {
311
- contentItems.push({
312
- type: 'text',
313
- text: 'User did not provide any feedback.',
314
- });
315
- }
316
-
317
- return { content: contentItems };
318
- } catch (error) {
319
- debugLog(`Error collecting feedback: ${error}`);
320
- return {
321
- content: [
322
- {
323
- type: 'text',
324
- text: `Error collecting feedback: ${error}`,
325
- },
326
- ],
327
- };
328
- } finally {
329
- this.currentRequest = null;
330
- }
331
- }
332
-
333
- /**
334
- * 等待用户反馈
335
- */
336
- private waitForFeedback(requestId: string, timeoutMs: number): Promise<FeedbackResponse | null> {
337
- return new Promise((resolve) => {
338
- const timeout = setTimeout(() => {
339
- debugLog(`Request ${requestId} timed out`);
340
- this.pendingRequests.delete(requestId);
341
- resolve(null);
342
- }, timeoutMs);
343
-
344
- this.pendingRequests.set(requestId, {
345
- resolve,
346
- reject: () => resolve(null),
347
- timeout
348
- });
349
- });
350
- }
351
-
352
- /**
353
- * 处理获取系统信息请求
354
- */
355
- private handleGetSystemInfo(): {
356
- content: Array<{ type: string; text: string }>;
357
- } {
358
- const systemInfo = {
359
- platform: process.platform,
360
- nodeVersion: process.version,
361
- arch: process.arch,
362
- hostname: os.hostname(),
363
- interfaceType: 'VS Code Extension',
364
- mcpServerPort: this.port,
365
- pid: process.pid,
366
- };
367
-
368
- return {
369
- content: [
370
- {
371
- type: 'text',
372
- text: JSON.stringify(systemInfo, null, 2),
373
- },
374
- ],
375
- };
376
- }
377
-
378
- /**
379
- * 根据文件名获取 MIME 类型
380
- */
381
- private getMimeType(filename: string): string {
382
- const ext = filename.toLowerCase().split('.').pop();
383
- switch (ext) {
384
- case 'jpg':
385
- case 'jpeg':
386
- return 'image/jpeg';
387
- case 'png':
388
- return 'image/png';
389
- case 'gif':
390
- return 'image/gif';
391
- case 'webp':
392
- return 'image/webp';
393
- default:
394
- return 'image/png';
395
- }
396
- }
397
-
398
- /**
399
- * 生成唯一的请求 ID
400
- */
401
- private generateRequestId(): string {
402
- return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
403
- }
404
-
405
- /**
406
- * 检查端口是否被我们的 MCP Server 占用,如果是则请求关闭
407
- */
408
- private async checkAndCleanPort(port: number): Promise<boolean> {
409
- return new Promise((resolve) => {
410
- const req = http.request(
411
- {
412
- hostname: '127.0.0.1',
413
- port: port,
414
- path: '/api/health',
415
- method: 'GET',
416
- timeout: 1000,
417
- },
418
- (res) => {
419
- let data = '';
420
- res.on('data', (chunk) => (data += chunk));
421
- res.on('end', () => {
422
- try {
423
- const health = JSON.parse(data);
424
- if (health.status === 'ok') {
425
- debugLog(`Found existing MCP Server on port ${port}, requesting shutdown...`);
426
- // 请求旧服务器关闭
427
- this.requestShutdown(port).then(() => {
428
- resolve(true);
429
- }).catch(() => {
430
- resolve(false);
431
- });
432
- } else {
433
- resolve(false);
434
- }
435
- } catch {
436
- resolve(false);
437
- }
438
- });
439
- }
440
- );
441
- req.on('error', () => resolve(false));
442
- req.on('timeout', () => {
443
- req.destroy();
444
- resolve(false);
445
- });
446
- req.end();
447
- });
448
- }
449
-
450
- /**
451
- * 请求旧的 MCP Server 关闭
452
- */
453
- private async requestShutdown(port: number): Promise<void> {
454
- return new Promise((resolve, reject) => {
455
- const req = http.request(
456
- {
457
- hostname: '127.0.0.1',
458
- port: port,
459
- path: '/api/shutdown',
460
- method: 'POST',
461
- timeout: 3000,
462
- },
463
- (res) => {
464
- res.on('data', () => {});
465
- res.on('end', () => {
466
- debugLog(`Shutdown request sent to port ${port}`);
467
- // 等待旧进程退出
468
- setTimeout(resolve, 500);
469
- });
470
- }
471
- );
472
- req.on('error', () => {
473
- // 旧服务器可能已经关闭
474
- setTimeout(resolve, 200);
475
- });
476
- req.on('timeout', () => {
477
- req.destroy();
478
- reject(new Error('Shutdown request timeout'));
479
- });
480
- req.end();
481
- });
482
- }
483
-
484
- /**
485
- * 启动 HTTP 服务器,用于与 VS Code 插件通信
486
- */
487
- private startHttpServer(): Promise<void> {
488
- return new Promise((resolve, reject) => {
489
- this.httpServer = http.createServer((req, res) => {
490
- // 包裹整个请求处理逻辑,防止异常导致进程崩溃
491
- try {
492
- // 任何来自插件的 HTTP 请求都视为 Cursor 仍存活的心跳(供 watchdog 判定)
493
- this.lastActivityTime = Date.now();
494
-
495
- // 设置 CORS 头
496
- res.setHeader('Access-Control-Allow-Origin', '*');
497
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
498
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
499
-
500
- if (req.method === 'OPTIONS') {
501
- res.writeHead(200);
502
- res.end();
503
- return;
504
- }
505
-
506
- // 获取当前反馈请求
507
- if (req.method === 'GET' && req.url?.startsWith('/api/feedback/current')) {
508
- res.writeHead(200, { 'Content-Type': 'application/json' });
509
- // 返回当前请求、ownerWorkspace 和 startTime
510
- res.end(JSON.stringify({
511
- request: this.currentRequest || null,
512
- ownerWorkspace: this.ownerWorkspace,
513
- startTime: this.startTime,
514
- }));
515
- return;
516
- }
517
-
518
- // 提交反馈
519
- if (req.method === 'POST' && req.url === '/api/feedback/submit') {
520
- let body = '';
521
- req.on('data', chunk => {
522
- body += chunk.toString();
523
- });
524
- req.on('end', () => {
525
- try {
526
- const data = JSON.parse(body) as { requestId: string; feedback: FeedbackResponse };
527
- const { requestId, feedback } = data;
528
-
529
- debugLog(`Received feedback submission for request: ${requestId}`);
530
-
531
- const pending = this.pendingRequests.get(requestId);
532
- if (pending) {
533
- clearTimeout(pending.timeout);
534
- pending.resolve(feedback);
535
- this.pendingRequests.delete(requestId);
536
-
537
- res.writeHead(200, { 'Content-Type': 'application/json' });
538
- res.end(JSON.stringify({ success: true }));
539
- } else {
540
- debugLog(`Request ${requestId} not found`);
541
- res.writeHead(404, { 'Content-Type': 'application/json' });
542
- res.end(JSON.stringify({ error: 'Request not found' }));
543
- }
544
- } catch (error) {
545
- debugLog(`Invalid request body: ${error}`);
546
- res.writeHead(400, { 'Content-Type': 'application/json' });
547
- res.end(JSON.stringify({ error: 'Invalid request body' }));
548
- }
549
- });
550
- return;
551
- }
552
-
553
- // 健康检查
554
- if (req.method === 'GET' && req.url === '/api/health') {
555
- res.writeHead(200, { 'Content-Type': 'application/json' });
556
- res.end(JSON.stringify({
557
- status: 'ok',
558
- version: '0.0.1',
559
- hasCurrentRequest: this.currentRequest !== null,
560
- pid: process.pid,
561
- }));
562
- return;
563
- }
564
-
565
- // 关闭服务器(用于新进程替换旧进程)
566
- if (req.method === 'POST' && req.url === '/api/shutdown') {
567
- debugLog('Received shutdown request from new MCP Server instance');
568
- res.writeHead(200, { 'Content-Type': 'application/json' });
569
- res.end(JSON.stringify({ success: true, message: 'Shutting down...' }));
570
-
571
- // 延迟关闭,确保响应已发送
572
- setTimeout(() => {
573
- this.stop();
574
- process.exit(0);
575
- }, 100);
576
- return;
577
- }
578
-
579
- res.writeHead(404);
580
- res.end('Not Found');
581
- } catch (error) {
582
- debugLog(`HTTP request error: ${error}`);
583
- try {
584
- res.writeHead(500, { 'Content-Type': 'application/json' });
585
- res.end(JSON.stringify({ error: 'Internal server error' }));
586
- } catch {
587
- // 响应可能已经发送,忽略
588
- }
589
- }
590
- });
591
-
592
- this.httpServer.on('error', async (err: NodeJS.ErrnoException) => {
593
- if (err.code === 'EADDRINUSE') {
594
- // 端口被占用,使用下一个端口(不关闭旧的 MCP Server,支持多项目独立运行)
595
- debugLog(`Port ${this.port} is already in use, trying next port...`);
596
- this.httpServer?.close();
597
- this.port++;
598
- this.startHttpServer().then(resolve).catch(reject);
599
- } else {
600
- reject(err);
601
- }
602
- });
603
-
604
- this.httpServer.listen(this.port, '127.0.0.1', () => {
605
- debugLog(`HTTP Server listening on http://127.0.0.1:${this.port}`);
606
- resolve();
607
- });
608
- });
609
- }
610
-
611
- /**
612
- * 启动服务器
613
- */
614
- async start(): Promise<void> {
615
- try {
616
- debugLog('Starting MCP Feedback Server...');
617
-
618
- // 启动 HTTP 服务器
619
- await this.startHttpServer();
620
-
621
- // 启动 MCP stdio 传输
622
- const transport = new StdioServerTransport();
623
- await this.server.connect(transport);
624
-
625
- // 启动看门狗兜底退出(防止 Cursor 关闭后进程残留 / CPU 占满)
626
- this.startWatchdog();
627
-
628
- debugLog('MCP Server started successfully');
629
- debugLog('Waiting for tool calls from AI agent...');
630
- } catch (error) {
631
- debugLog(`Failed to start server: ${error}`);
632
- throw error;
633
- }
634
- }
635
-
636
- /**
637
- * 看门狗:兜底退出机制
638
- *
639
- * 背景:经 npx / npm exec 启动时,进程链为 Cursor → npm exec → node。
640
- * Cursor 关闭后,stdin 的 EOF 在该中间层下不可靠,node 易变成孤儿进程残留;
641
- * 叠加 HTTP server 是常驻 active handle,进程无法自然退出。
642
- *
643
- * 两道防线:
644
- * 1) 父进程死亡(被 init/launchd 收养,ppid 变为 1)→ 立即退出;
645
- * 2) 超过 IDLE_TIMEOUT 没有任何插件轮询 / MCP 调用 → 判定所有 Cursor 窗口已关闭 → 退出。
646
- * (插件每秒轮询 HTTP,只要还有任意 Cursor 窗口存活就会持续刷新活动时间)
647
- */
648
- private startWatchdog(): void {
649
- const IDLE_TIMEOUT = process.env.MCP_FEEDBACK_IDLE_TIMEOUT
650
- ? parseInt(process.env.MCP_FEEDBACK_IDLE_TIMEOUT, 10)
651
- : 30000;
652
-
653
- this.watchdogTimer = setInterval(() => {
654
- // 防线 1:父进程已死,自己成了孤儿进程
655
- if (process.ppid === 1) {
656
- debugLog('Parent process gone (ppid=1), exiting...');
657
- this.stop();
658
- process.exit(0);
659
- }
660
-
661
- // 防线 2:长时间无任何活动 = Cursor 已全部关闭
662
- const idle = Date.now() - this.lastActivityTime;
663
- if (idle > IDLE_TIMEOUT) {
664
- debugLog(`No activity for ${idle}ms (> ${IDLE_TIMEOUT}ms), assuming Cursor closed, exiting...`);
665
- this.stop();
666
- process.exit(0);
667
- }
668
- }, 5000);
669
-
670
- // 不让 watchdog 自身阻止进程的自然退出
671
- this.watchdogTimer.unref();
672
- }
673
-
674
- /**
675
- * 停止服务器
676
- */
677
- stop(): void {
678
- debugLog('Stopping server...');
679
-
680
- // 关闭看门狗定时器
681
- if (this.watchdogTimer) {
682
- clearInterval(this.watchdogTimer);
683
- this.watchdogTimer = null;
684
- }
685
-
686
- // 关闭 HTTP 服务器
687
- if (this.httpServer) {
688
- this.httpServer.close();
689
- this.httpServer = null;
690
- }
691
-
692
- // 清理待处理的请求
693
- for (const [, pending] of this.pendingRequests) {
694
- clearTimeout(pending.timeout);
695
- pending.resolve(null);
696
- }
697
- this.pendingRequests.clear();
698
-
699
- // 关闭 MCP 服务器
700
- this.server.close();
701
- debugLog('Server stopped');
702
- }
703
- }
704
-
705
- // 主函数
706
- async function main() {
707
- const port = 61927;
708
- const server = new McpFeedbackServer(port);
709
-
710
- // 处理进程信号
711
- process.on('SIGINT', () => {
712
- debugLog('Received SIGINT');
713
- server.stop();
714
- process.exit(0);
715
- });
716
-
717
- process.on('SIGTERM', () => {
718
- debugLog('Received SIGTERM');
719
- server.stop();
720
- process.exit(0);
721
- });
722
-
723
- // 监听 stdin 关闭(Cursor 关闭时会触发)
724
- process.stdin.on('close', () => {
725
- debugLog('stdin closed, exiting...');
726
- server.stop();
727
- // 给 100ms 缓冲后强制退出
728
- setTimeout(() => process.exit(0), 100);
729
- });
730
-
731
- process.stdin.on('end', () => {
732
- debugLog('stdin ended, exiting...');
733
- server.stop();
734
- setTimeout(() => process.exit(0), 100);
735
- });
736
-
737
- // stdin 出错(父进程经 npx 中间层异常断开时可能触发)同样退出,避免残留
738
- process.stdin.on('error', (error) => {
739
- debugLog(`stdin error, exiting: ${error}`);
740
- server.stop();
741
- setTimeout(() => process.exit(0), 100);
742
- });
743
-
744
- // 捕获未处理的异常:记录日志后退出。
745
- // ⚠️ 绝不能“吞掉异常继续运行”——否则 stdin 在父进程断开后产生的反复错误会形成
746
- // busy-loop,导致 CPU 占满且进程永不退出(本次修复的核心症状之一)。
747
- process.on('uncaughtException', (error) => {
748
- debugLog(`Uncaught exception, exiting: ${error?.stack || error}`);
749
- try {
750
- server.stop();
751
- } catch {
752
- // ignore
753
- }
754
- process.exit(1);
755
- });
756
-
757
- // 捕获未处理的 Promise 拒绝(仅记录;Promise 拒绝本身不会造成 busy-loop)
758
- process.on('unhandledRejection', (reason) => {
759
- debugLog(`Unhandled rejection: ${reason}`);
760
- });
761
-
762
- await server.start();
763
- }
764
-
765
- main().catch((error) => {
766
- console.error('Failed to start MCP server:', error);
767
- process.exit(1);
768
- });