cursor-feedback 0.1.0 → 0.1.3
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/.vscode/launch.json +28 -0
- package/.vscode/tasks.json +22 -0
- package/.vscodeignore +10 -0
- package/LICENSE +21 -0
- package/README.md +80 -26
- package/dist/extension.js +71 -92
- package/dist/extension.js.map +1 -1
- package/dist/mcp/McpServer.js +9 -8
- package/dist/mcp/McpServer.js.map +1 -1
- package/dist/mcp-server.js +110 -12
- package/dist/mcp-server.js.map +1 -1
- package/package.json +2 -17
- package/src/extension.ts +1071 -0
- package/src/mcp/McpServer.ts +455 -0
- package/src/mcp-server.ts +603 -0
- package/src/webview/FeedbackPanel.ts +134 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,603 @@
|
|
|
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
|
+
project_directory: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* MCP Feedback Server
|
|
61
|
+
*/
|
|
62
|
+
class McpFeedbackServer {
|
|
63
|
+
private server: Server;
|
|
64
|
+
private httpServer: http.Server | null = null;
|
|
65
|
+
private port: number;
|
|
66
|
+
|
|
67
|
+
// 待处理的反馈请求
|
|
68
|
+
private pendingRequests: Map<string, {
|
|
69
|
+
resolve: (value: FeedbackResponse | null) => void;
|
|
70
|
+
reject: (error: Error) => void;
|
|
71
|
+
timeout: NodeJS.Timeout;
|
|
72
|
+
}> = new Map();
|
|
73
|
+
|
|
74
|
+
// 当前反馈请求
|
|
75
|
+
private currentRequest: FeedbackRequest | null = null;
|
|
76
|
+
|
|
77
|
+
constructor(port: number = 8766) {
|
|
78
|
+
this.port = port;
|
|
79
|
+
|
|
80
|
+
this.server = new Server(
|
|
81
|
+
{
|
|
82
|
+
name: 'cursor-feedback-server',
|
|
83
|
+
version: '0.0.1',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
capabilities: {
|
|
87
|
+
tools: {},
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
this.setupHandlers();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private setupHandlers() {
|
|
96
|
+
// 列出可用工具
|
|
97
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
98
|
+
return {
|
|
99
|
+
tools: [
|
|
100
|
+
{
|
|
101
|
+
name: 'interactive_feedback',
|
|
102
|
+
description: `Interactive feedback collection tool for LLM agents.
|
|
103
|
+
|
|
104
|
+
USAGE RULES:
|
|
105
|
+
1. During any process, task, or conversation, whether asking, replying, or completing phased tasks, you must call this tool to ask for feedback.
|
|
106
|
+
2. Unless receiving termination instructions, all steps must repeatedly call this tool.
|
|
107
|
+
3. Whenever user feedback is received, if the feedback content is not empty, you must call this tool again and adjust behavior based on the feedback content.
|
|
108
|
+
4. Only when the user explicitly indicates "end" or "no more interaction needed" can you stop calling this tool, and the process is considered complete.
|
|
109
|
+
5. You should summarize what have done, and provide project directory through args to let user know what you have done to provide feedback for next step.
|
|
110
|
+
6. IMPORTANT: If feedback request times out, you MUST call this tool again to continue waiting for user feedback. Do not give up after timeout.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
project_directory: Project directory path for context (MUST be the absolute path of current workspace)
|
|
114
|
+
summary: Summary of AI work completed for user review (supports Markdown)
|
|
115
|
+
timeout: Timeout in seconds for waiting user feedback (default: 300 seconds = 5 minutes)
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
list: List containing TextContent and MCPImage objects representing user feedback`,
|
|
119
|
+
inputSchema: {
|
|
120
|
+
type: 'object',
|
|
121
|
+
properties: {
|
|
122
|
+
project_directory: {
|
|
123
|
+
type: 'string',
|
|
124
|
+
description: 'Project directory path for context (MUST be the absolute path of current workspace)',
|
|
125
|
+
default: '.',
|
|
126
|
+
},
|
|
127
|
+
summary: {
|
|
128
|
+
type: 'string',
|
|
129
|
+
description: 'Summary of AI work completed for user review (supports Markdown)',
|
|
130
|
+
default: 'I have completed the task you requested.',
|
|
131
|
+
},
|
|
132
|
+
timeout: {
|
|
133
|
+
type: 'number',
|
|
134
|
+
description: 'Timeout in seconds for waiting user feedback (default: 300 seconds = 5 minutes)',
|
|
135
|
+
default: 300,
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: 'get_system_info',
|
|
142
|
+
description: 'Get system environment information',
|
|
143
|
+
inputSchema: {
|
|
144
|
+
type: 'object',
|
|
145
|
+
properties: {},
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
};
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// 处理工具调用
|
|
153
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
154
|
+
const { name, arguments: args } = request.params;
|
|
155
|
+
|
|
156
|
+
switch (name) {
|
|
157
|
+
case 'interactive_feedback':
|
|
158
|
+
return this.handleInteractiveFeedback(args);
|
|
159
|
+
case 'get_system_info':
|
|
160
|
+
return this.handleGetSystemInfo();
|
|
161
|
+
default:
|
|
162
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* 处理交互式反馈请求
|
|
169
|
+
*/
|
|
170
|
+
private async handleInteractiveFeedback(args: Record<string, unknown> | undefined): Promise<{
|
|
171
|
+
content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
|
|
172
|
+
}> {
|
|
173
|
+
const projectDir = (args?.project_directory as string) || '.';
|
|
174
|
+
const summary = (args?.summary as string) || 'I have completed the task you requested.';
|
|
175
|
+
const timeout = (args?.timeout as number) || 300;
|
|
176
|
+
|
|
177
|
+
const requestId = this.generateRequestId();
|
|
178
|
+
|
|
179
|
+
// 创建反馈请求
|
|
180
|
+
this.currentRequest = {
|
|
181
|
+
id: requestId,
|
|
182
|
+
summary,
|
|
183
|
+
projectDir,
|
|
184
|
+
timeout,
|
|
185
|
+
timestamp: Date.now(),
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
debugLog(`Feedback request created: ${requestId}`);
|
|
189
|
+
debugLog(`Summary: ${summary}`);
|
|
190
|
+
debugLog(`Project: ${projectDir}`);
|
|
191
|
+
debugLog(`Timeout: ${timeout}s`);
|
|
192
|
+
debugLog(`Waiting for VS Code extension to collect feedback...`);
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
// 等待用户反馈
|
|
196
|
+
const result = await this.waitForFeedback(requestId, timeout * 1000);
|
|
197
|
+
|
|
198
|
+
if (!result) {
|
|
199
|
+
debugLog('Feedback request timed out or cancelled');
|
|
200
|
+
return {
|
|
201
|
+
content: [
|
|
202
|
+
{
|
|
203
|
+
type: 'text',
|
|
204
|
+
text: 'User cancelled the feedback or timeout.',
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
debugLog(`Received feedback: ${result.interactive_feedback?.substring(0, 100)}...`);
|
|
211
|
+
|
|
212
|
+
const contentItems: Array<{ type: string; text?: string; data?: string; mimeType?: string }> = [];
|
|
213
|
+
|
|
214
|
+
// 添加文字反馈
|
|
215
|
+
if (result.interactive_feedback) {
|
|
216
|
+
contentItems.push({
|
|
217
|
+
type: 'text',
|
|
218
|
+
text: `=== User Feedback ===\n${result.interactive_feedback}`,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 添加图片
|
|
223
|
+
if (result.images && result.images.length > 0) {
|
|
224
|
+
debugLog(`Processing ${result.images.length} images`);
|
|
225
|
+
for (const img of result.images) {
|
|
226
|
+
contentItems.push({
|
|
227
|
+
type: 'image',
|
|
228
|
+
data: img.data,
|
|
229
|
+
mimeType: this.getMimeType(img.name),
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (contentItems.length === 0) {
|
|
235
|
+
contentItems.push({
|
|
236
|
+
type: 'text',
|
|
237
|
+
text: 'User did not provide any feedback.',
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return { content: contentItems };
|
|
242
|
+
} catch (error) {
|
|
243
|
+
debugLog(`Error collecting feedback: ${error}`);
|
|
244
|
+
return {
|
|
245
|
+
content: [
|
|
246
|
+
{
|
|
247
|
+
type: 'text',
|
|
248
|
+
text: `Error collecting feedback: ${error}`,
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
};
|
|
252
|
+
} finally {
|
|
253
|
+
this.currentRequest = null;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* 等待用户反馈
|
|
259
|
+
*/
|
|
260
|
+
private waitForFeedback(requestId: string, timeoutMs: number): Promise<FeedbackResponse | null> {
|
|
261
|
+
return new Promise((resolve) => {
|
|
262
|
+
const timeout = setTimeout(() => {
|
|
263
|
+
debugLog(`Request ${requestId} timed out`);
|
|
264
|
+
this.pendingRequests.delete(requestId);
|
|
265
|
+
resolve(null);
|
|
266
|
+
}, timeoutMs);
|
|
267
|
+
|
|
268
|
+
this.pendingRequests.set(requestId, {
|
|
269
|
+
resolve,
|
|
270
|
+
reject: () => resolve(null),
|
|
271
|
+
timeout
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* 处理获取系统信息请求
|
|
278
|
+
*/
|
|
279
|
+
private handleGetSystemInfo(): {
|
|
280
|
+
content: Array<{ type: string; text: string }>;
|
|
281
|
+
} {
|
|
282
|
+
const systemInfo = {
|
|
283
|
+
platform: process.platform,
|
|
284
|
+
nodeVersion: process.version,
|
|
285
|
+
arch: process.arch,
|
|
286
|
+
hostname: os.hostname(),
|
|
287
|
+
interfaceType: 'VS Code Extension',
|
|
288
|
+
mcpServerPort: this.port,
|
|
289
|
+
pid: process.pid,
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
content: [
|
|
294
|
+
{
|
|
295
|
+
type: 'text',
|
|
296
|
+
text: JSON.stringify(systemInfo, null, 2),
|
|
297
|
+
},
|
|
298
|
+
],
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* 根据文件名获取 MIME 类型
|
|
304
|
+
*/
|
|
305
|
+
private getMimeType(filename: string): string {
|
|
306
|
+
const ext = filename.toLowerCase().split('.').pop();
|
|
307
|
+
switch (ext) {
|
|
308
|
+
case 'jpg':
|
|
309
|
+
case 'jpeg':
|
|
310
|
+
return 'image/jpeg';
|
|
311
|
+
case 'png':
|
|
312
|
+
return 'image/png';
|
|
313
|
+
case 'gif':
|
|
314
|
+
return 'image/gif';
|
|
315
|
+
case 'webp':
|
|
316
|
+
return 'image/webp';
|
|
317
|
+
default:
|
|
318
|
+
return 'image/png';
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* 生成唯一的请求 ID
|
|
324
|
+
*/
|
|
325
|
+
private generateRequestId(): string {
|
|
326
|
+
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* 检查端口是否被我们的 MCP Server 占用,如果是则请求关闭
|
|
331
|
+
*/
|
|
332
|
+
private async checkAndCleanPort(port: number): Promise<boolean> {
|
|
333
|
+
return new Promise((resolve) => {
|
|
334
|
+
const req = http.request(
|
|
335
|
+
{
|
|
336
|
+
hostname: '127.0.0.1',
|
|
337
|
+
port: port,
|
|
338
|
+
path: '/api/health',
|
|
339
|
+
method: 'GET',
|
|
340
|
+
timeout: 1000,
|
|
341
|
+
},
|
|
342
|
+
(res) => {
|
|
343
|
+
let data = '';
|
|
344
|
+
res.on('data', (chunk) => (data += chunk));
|
|
345
|
+
res.on('end', () => {
|
|
346
|
+
try {
|
|
347
|
+
const health = JSON.parse(data);
|
|
348
|
+
if (health.status === 'ok') {
|
|
349
|
+
debugLog(`Found existing MCP Server on port ${port}, requesting shutdown...`);
|
|
350
|
+
// 请求旧服务器关闭
|
|
351
|
+
this.requestShutdown(port).then(() => {
|
|
352
|
+
resolve(true);
|
|
353
|
+
}).catch(() => {
|
|
354
|
+
resolve(false);
|
|
355
|
+
});
|
|
356
|
+
} else {
|
|
357
|
+
resolve(false);
|
|
358
|
+
}
|
|
359
|
+
} catch {
|
|
360
|
+
resolve(false);
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
);
|
|
365
|
+
req.on('error', () => resolve(false));
|
|
366
|
+
req.on('timeout', () => {
|
|
367
|
+
req.destroy();
|
|
368
|
+
resolve(false);
|
|
369
|
+
});
|
|
370
|
+
req.end();
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* 请求旧的 MCP Server 关闭
|
|
376
|
+
*/
|
|
377
|
+
private async requestShutdown(port: number): Promise<void> {
|
|
378
|
+
return new Promise((resolve, reject) => {
|
|
379
|
+
const req = http.request(
|
|
380
|
+
{
|
|
381
|
+
hostname: '127.0.0.1',
|
|
382
|
+
port: port,
|
|
383
|
+
path: '/api/shutdown',
|
|
384
|
+
method: 'POST',
|
|
385
|
+
timeout: 3000,
|
|
386
|
+
},
|
|
387
|
+
(res) => {
|
|
388
|
+
res.on('data', () => {});
|
|
389
|
+
res.on('end', () => {
|
|
390
|
+
debugLog(`Shutdown request sent to port ${port}`);
|
|
391
|
+
// 等待旧进程退出
|
|
392
|
+
setTimeout(resolve, 500);
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
);
|
|
396
|
+
req.on('error', () => {
|
|
397
|
+
// 旧服务器可能已经关闭
|
|
398
|
+
setTimeout(resolve, 200);
|
|
399
|
+
});
|
|
400
|
+
req.on('timeout', () => {
|
|
401
|
+
req.destroy();
|
|
402
|
+
reject(new Error('Shutdown request timeout'));
|
|
403
|
+
});
|
|
404
|
+
req.end();
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* 启动 HTTP 服务器,用于与 VS Code 插件通信
|
|
410
|
+
*/
|
|
411
|
+
private startHttpServer(): Promise<void> {
|
|
412
|
+
return new Promise((resolve, reject) => {
|
|
413
|
+
this.httpServer = http.createServer((req, res) => {
|
|
414
|
+
// 设置 CORS 头
|
|
415
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
416
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
417
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
418
|
+
|
|
419
|
+
if (req.method === 'OPTIONS') {
|
|
420
|
+
res.writeHead(200);
|
|
421
|
+
res.end();
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// 获取当前反馈请求
|
|
426
|
+
if (req.method === 'GET' && req.url === '/api/feedback/current') {
|
|
427
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
428
|
+
res.end(JSON.stringify(this.currentRequest || null));
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// 提交反馈
|
|
433
|
+
if (req.method === 'POST' && req.url === '/api/feedback/submit') {
|
|
434
|
+
let body = '';
|
|
435
|
+
req.on('data', chunk => {
|
|
436
|
+
body += chunk.toString();
|
|
437
|
+
});
|
|
438
|
+
req.on('end', () => {
|
|
439
|
+
try {
|
|
440
|
+
const data = JSON.parse(body) as { requestId: string; feedback: FeedbackResponse };
|
|
441
|
+
const { requestId, feedback } = data;
|
|
442
|
+
|
|
443
|
+
debugLog(`Received feedback submission for request: ${requestId}`);
|
|
444
|
+
|
|
445
|
+
const pending = this.pendingRequests.get(requestId);
|
|
446
|
+
if (pending) {
|
|
447
|
+
clearTimeout(pending.timeout);
|
|
448
|
+
pending.resolve(feedback);
|
|
449
|
+
this.pendingRequests.delete(requestId);
|
|
450
|
+
|
|
451
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
452
|
+
res.end(JSON.stringify({ success: true }));
|
|
453
|
+
} else {
|
|
454
|
+
debugLog(`Request ${requestId} not found`);
|
|
455
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
456
|
+
res.end(JSON.stringify({ error: 'Request not found' }));
|
|
457
|
+
}
|
|
458
|
+
} catch (error) {
|
|
459
|
+
debugLog(`Invalid request body: ${error}`);
|
|
460
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
461
|
+
res.end(JSON.stringify({ error: 'Invalid request body' }));
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// 健康检查
|
|
468
|
+
if (req.method === 'GET' && req.url === '/api/health') {
|
|
469
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
470
|
+
res.end(JSON.stringify({
|
|
471
|
+
status: 'ok',
|
|
472
|
+
version: '0.0.1',
|
|
473
|
+
hasCurrentRequest: this.currentRequest !== null,
|
|
474
|
+
pid: process.pid,
|
|
475
|
+
}));
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// 关闭服务器(用于新进程替换旧进程)
|
|
480
|
+
if (req.method === 'POST' && req.url === '/api/shutdown') {
|
|
481
|
+
debugLog('Received shutdown request from new MCP Server instance');
|
|
482
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
483
|
+
res.end(JSON.stringify({ success: true, message: 'Shutting down...' }));
|
|
484
|
+
|
|
485
|
+
// 延迟关闭,确保响应已发送
|
|
486
|
+
setTimeout(() => {
|
|
487
|
+
this.stop();
|
|
488
|
+
process.exit(0);
|
|
489
|
+
}, 100);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
res.writeHead(404);
|
|
494
|
+
res.end('Not Found');
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
this.httpServer.on('error', async (err: NodeJS.ErrnoException) => {
|
|
498
|
+
if (err.code === 'EADDRINUSE') {
|
|
499
|
+
debugLog(`Port ${this.port} is already in use, checking if it's our MCP Server...`);
|
|
500
|
+
this.httpServer?.close();
|
|
501
|
+
|
|
502
|
+
// 尝试关闭旧的 MCP Server
|
|
503
|
+
const cleaned = await this.checkAndCleanPort(this.port);
|
|
504
|
+
if (cleaned) {
|
|
505
|
+
debugLog(`Old MCP Server closed, retrying on port ${this.port}...`);
|
|
506
|
+
// 等待端口释放
|
|
507
|
+
await new Promise(r => setTimeout(r, 300));
|
|
508
|
+
this.startHttpServer().then(resolve).catch(reject);
|
|
509
|
+
} else {
|
|
510
|
+
debugLog(`Port ${this.port} is used by another process, trying next port...`);
|
|
511
|
+
this.port++;
|
|
512
|
+
this.startHttpServer().then(resolve).catch(reject);
|
|
513
|
+
}
|
|
514
|
+
} else {
|
|
515
|
+
reject(err);
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
this.httpServer.listen(this.port, '127.0.0.1', () => {
|
|
520
|
+
debugLog(`HTTP Server listening on http://127.0.0.1:${this.port}`);
|
|
521
|
+
resolve();
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* 启动服务器
|
|
528
|
+
*/
|
|
529
|
+
async start(): Promise<void> {
|
|
530
|
+
try {
|
|
531
|
+
debugLog('Starting MCP Feedback Server...');
|
|
532
|
+
|
|
533
|
+
// 启动 HTTP 服务器
|
|
534
|
+
await this.startHttpServer();
|
|
535
|
+
|
|
536
|
+
// 启动 MCP stdio 传输
|
|
537
|
+
const transport = new StdioServerTransport();
|
|
538
|
+
await this.server.connect(transport);
|
|
539
|
+
|
|
540
|
+
debugLog('MCP Server started successfully');
|
|
541
|
+
debugLog('Waiting for tool calls from AI agent...');
|
|
542
|
+
} catch (error) {
|
|
543
|
+
debugLog(`Failed to start server: ${error}`);
|
|
544
|
+
throw error;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* 停止服务器
|
|
550
|
+
*/
|
|
551
|
+
stop(): void {
|
|
552
|
+
debugLog('Stopping server...');
|
|
553
|
+
|
|
554
|
+
// 关闭 HTTP 服务器
|
|
555
|
+
if (this.httpServer) {
|
|
556
|
+
this.httpServer.close();
|
|
557
|
+
this.httpServer = null;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// 清理待处理的请求
|
|
561
|
+
for (const [, pending] of this.pendingRequests) {
|
|
562
|
+
clearTimeout(pending.timeout);
|
|
563
|
+
pending.resolve(null);
|
|
564
|
+
}
|
|
565
|
+
this.pendingRequests.clear();
|
|
566
|
+
|
|
567
|
+
// 关闭 MCP 服务器
|
|
568
|
+
this.server.close();
|
|
569
|
+
debugLog('Server stopped');
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// 主函数
|
|
574
|
+
async function main() {
|
|
575
|
+
const port = parseInt(process.env.MCP_FEEDBACK_PORT || '5678', 10);
|
|
576
|
+
const server = new McpFeedbackServer(port);
|
|
577
|
+
|
|
578
|
+
// 处理进程信号
|
|
579
|
+
process.on('SIGINT', () => {
|
|
580
|
+
debugLog('Received SIGINT');
|
|
581
|
+
server.stop();
|
|
582
|
+
process.exit(0);
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
process.on('SIGTERM', () => {
|
|
586
|
+
debugLog('Received SIGTERM');
|
|
587
|
+
server.stop();
|
|
588
|
+
process.exit(0);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
process.on('uncaughtException', (error) => {
|
|
592
|
+
debugLog(`Uncaught exception: ${error}`);
|
|
593
|
+
server.stop();
|
|
594
|
+
process.exit(1);
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
await server.start();
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
main().catch((error) => {
|
|
601
|
+
console.error('Failed to start MCP server:', error);
|
|
602
|
+
process.exit(1);
|
|
603
|
+
});
|