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.
@@ -0,0 +1,455 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import {
4
+ CallToolRequestSchema,
5
+ ListToolsRequestSchema,
6
+ } from '@modelcontextprotocol/sdk/types.js';
7
+ import * as http from 'http';
8
+ import * as os from 'os';
9
+
10
+ /**
11
+ * 反馈请求接口
12
+ */
13
+ interface FeedbackRequest {
14
+ id: string;
15
+ summary: string;
16
+ projectDir: string;
17
+ timeout: number;
18
+ timestamp: number;
19
+ }
20
+
21
+ /**
22
+ * 反馈响应接口
23
+ */
24
+ interface FeedbackResponse {
25
+ interactive_feedback: string;
26
+ images: Array<{
27
+ name: string;
28
+ data: string;
29
+ size: number;
30
+ }>;
31
+ project_directory: string;
32
+ }
33
+
34
+ /**
35
+ * MCP Server - 独立运行的 MCP 服务
36
+ *
37
+ * 架构说明:
38
+ * 1. MCP Server 通过 stdio 与 AI Agent (Cursor) 通信
39
+ * 2. 同时启动一个 HTTP Server 用于与 VS Code 插件通信
40
+ * 3. 当 AI 调用 interactive_feedback 工具时,通知 VS Code 插件显示反馈界面
41
+ * 4. VS Code 插件收集用户反馈后,通过 HTTP 接口返回结果
42
+ */
43
+ export class McpServer {
44
+ private server: Server;
45
+ private httpServer: http.Server | null = null;
46
+ private port: number;
47
+ private isRunning: boolean = false;
48
+
49
+ // 待处理的反馈请求
50
+ private pendingRequests: Map<string, {
51
+ resolve: (value: FeedbackResponse | null) => void;
52
+ reject: (error: Error) => void;
53
+ timeout: NodeJS.Timeout;
54
+ }> = new Map();
55
+
56
+ // 当前反馈请求
57
+ private currentRequest: FeedbackRequest | null = null;
58
+
59
+ constructor(port: number = 5678) {
60
+ this.port = port;
61
+
62
+ this.server = new Server(
63
+ {
64
+ name: 'cursor-feedback-server',
65
+ version: '0.0.1',
66
+ },
67
+ {
68
+ capabilities: {
69
+ tools: {},
70
+ },
71
+ }
72
+ );
73
+
74
+ this.setupHandlers();
75
+ }
76
+
77
+ private setupHandlers() {
78
+ // 列出可用工具
79
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => {
80
+ return {
81
+ tools: [
82
+ {
83
+ name: 'interactive_feedback',
84
+ description: `Interactive feedback collection tool for LLM agents.
85
+
86
+ USAGE RULES:
87
+ 1. During any process, task, or conversation, whether asking, replying, or completing phased tasks, you must call this tool to ask for feedback.
88
+ 2. Unless receiving termination instructions, all steps must repeatedly call this tool.
89
+ 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.
90
+ 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.
91
+ 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.
92
+ 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.
93
+
94
+ Args:
95
+ project_directory: Project directory path for context (MUST be the absolute path of current workspace)
96
+ summary: Summary of AI work completed for user review (supports Markdown)
97
+ timeout: Timeout in seconds for waiting user feedback (default: 300 seconds = 5 minutes)
98
+
99
+ Returns:
100
+ list: List containing TextContent and MCPImage objects representing user feedback`,
101
+ inputSchema: {
102
+ type: 'object',
103
+ properties: {
104
+ project_directory: {
105
+ type: 'string',
106
+ description: 'Project directory path for context (MUST be the absolute path of current workspace)',
107
+ default: '.',
108
+ },
109
+ summary: {
110
+ type: 'string',
111
+ description: 'Summary of AI work completed for user review (supports Markdown)',
112
+ default: 'I have completed the task you requested.',
113
+ },
114
+ timeout: {
115
+ type: 'number',
116
+ description: 'Timeout in seconds for waiting user feedback (default: 300 seconds = 5 minutes)',
117
+ default: 300,
118
+ },
119
+ },
120
+ },
121
+ },
122
+ {
123
+ name: 'get_system_info',
124
+ description: 'Get system environment information',
125
+ inputSchema: {
126
+ type: 'object',
127
+ properties: {},
128
+ },
129
+ },
130
+ ],
131
+ };
132
+ });
133
+
134
+ // 处理工具调用
135
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
136
+ const { name, arguments: args } = request.params;
137
+
138
+ switch (name) {
139
+ case 'interactive_feedback':
140
+ return this.handleInteractiveFeedback(args);
141
+ case 'get_system_info':
142
+ return this.handleGetSystemInfo();
143
+ default:
144
+ throw new Error(`Unknown tool: ${name}`);
145
+ }
146
+ });
147
+ }
148
+
149
+ /**
150
+ * 处理交互式反馈请求
151
+ */
152
+ private async handleInteractiveFeedback(args: Record<string, unknown> | undefined): Promise<{
153
+ content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
154
+ }> {
155
+ const projectDir = (args?.project_directory as string) || '.';
156
+ const summary = (args?.summary as string) || 'I have completed the task you requested.';
157
+ const timeout = (args?.timeout as number) || 300;
158
+
159
+ const requestId = this.generateRequestId();
160
+
161
+ // 创建反馈请求
162
+ this.currentRequest = {
163
+ id: requestId,
164
+ summary,
165
+ projectDir,
166
+ timeout,
167
+ timestamp: Date.now(),
168
+ };
169
+
170
+ console.error(`[MCP] Feedback request created: ${requestId}`);
171
+ console.error(`[MCP] Waiting for VS Code extension to collect feedback...`);
172
+
173
+ try {
174
+ // 等待用户反馈
175
+ const result = await this.waitForFeedback(requestId, timeout * 1000);
176
+
177
+ if (!result) {
178
+ return {
179
+ content: [
180
+ {
181
+ type: 'text',
182
+ text: 'User cancelled the feedback or timeout.',
183
+ },
184
+ ],
185
+ };
186
+ }
187
+
188
+ const contentItems: Array<{ type: string; text?: string; data?: string; mimeType?: string }> = [];
189
+
190
+ // 添加文字反馈
191
+ if (result.interactive_feedback) {
192
+ contentItems.push({
193
+ type: 'text',
194
+ text: `=== User Feedback ===\n${result.interactive_feedback}`,
195
+ });
196
+ }
197
+
198
+ // 添加图片
199
+ if (result.images && result.images.length > 0) {
200
+ for (const img of result.images) {
201
+ contentItems.push({
202
+ type: 'image',
203
+ data: img.data,
204
+ mimeType: this.getMimeType(img.name),
205
+ });
206
+ }
207
+ }
208
+
209
+ if (contentItems.length === 0) {
210
+ contentItems.push({
211
+ type: 'text',
212
+ text: 'User did not provide any feedback.',
213
+ });
214
+ }
215
+
216
+ return { content: contentItems };
217
+ } catch (error) {
218
+ return {
219
+ content: [
220
+ {
221
+ type: 'text',
222
+ text: `Error collecting feedback: ${error}`,
223
+ },
224
+ ],
225
+ };
226
+ } finally {
227
+ this.currentRequest = null;
228
+ }
229
+ }
230
+
231
+ /**
232
+ * 等待用户反馈
233
+ */
234
+ private waitForFeedback(requestId: string, timeoutMs: number): Promise<FeedbackResponse | null> {
235
+ return new Promise((resolve, reject) => {
236
+ const timeout = setTimeout(() => {
237
+ this.pendingRequests.delete(requestId);
238
+ resolve(null);
239
+ }, timeoutMs);
240
+
241
+ this.pendingRequests.set(requestId, { resolve, reject, timeout });
242
+ });
243
+ }
244
+
245
+ /**
246
+ * 处理获取系统信息请求
247
+ */
248
+ private handleGetSystemInfo(): {
249
+ content: Array<{ type: string; text: string }>;
250
+ } {
251
+ const systemInfo = {
252
+ platform: process.platform,
253
+ nodeVersion: process.version,
254
+ arch: process.arch,
255
+ hostname: os.hostname(),
256
+ interfaceType: 'VS Code Extension',
257
+ mcpServerPort: this.port,
258
+ };
259
+
260
+ return {
261
+ content: [
262
+ {
263
+ type: 'text',
264
+ text: JSON.stringify(systemInfo, null, 2),
265
+ },
266
+ ],
267
+ };
268
+ }
269
+
270
+ /**
271
+ * 根据文件名获取 MIME 类型
272
+ */
273
+ private getMimeType(filename: string): string {
274
+ const ext = filename.toLowerCase().split('.').pop();
275
+ switch (ext) {
276
+ case 'jpg':
277
+ case 'jpeg':
278
+ return 'image/jpeg';
279
+ case 'png':
280
+ return 'image/png';
281
+ case 'gif':
282
+ return 'image/gif';
283
+ case 'webp':
284
+ return 'image/webp';
285
+ default:
286
+ return 'image/png';
287
+ }
288
+ }
289
+
290
+ /**
291
+ * 生成唯一的请求 ID
292
+ */
293
+ private generateRequestId(): string {
294
+ return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
295
+ }
296
+
297
+ /**
298
+ * 启动 HTTP 服务器,用于与 VS Code 插件通信
299
+ */
300
+ private startHttpServer(): Promise<void> {
301
+ return new Promise((resolve, reject) => {
302
+ this.httpServer = http.createServer((req, res) => {
303
+ // 设置 CORS 头
304
+ res.setHeader('Access-Control-Allow-Origin', '*');
305
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
306
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
307
+
308
+ if (req.method === 'OPTIONS') {
309
+ res.writeHead(200);
310
+ res.end();
311
+ return;
312
+ }
313
+
314
+ // 获取当前反馈请求
315
+ if (req.method === 'GET' && req.url === '/api/feedback/current') {
316
+ res.writeHead(200, { 'Content-Type': 'application/json' });
317
+ res.end(JSON.stringify(this.currentRequest || null));
318
+ return;
319
+ }
320
+
321
+ // 提交反馈
322
+ if (req.method === 'POST' && req.url === '/api/feedback/submit') {
323
+ let body = '';
324
+ req.on('data', chunk => {
325
+ body += chunk.toString();
326
+ });
327
+ req.on('end', () => {
328
+ try {
329
+ const data = JSON.parse(body) as { requestId: string; feedback: FeedbackResponse };
330
+ const { requestId, feedback } = data;
331
+
332
+ const pending = this.pendingRequests.get(requestId);
333
+ if (pending) {
334
+ clearTimeout(pending.timeout);
335
+ pending.resolve(feedback);
336
+ this.pendingRequests.delete(requestId);
337
+
338
+ res.writeHead(200, { 'Content-Type': 'application/json' });
339
+ res.end(JSON.stringify({ success: true }));
340
+ } else {
341
+ res.writeHead(404, { 'Content-Type': 'application/json' });
342
+ res.end(JSON.stringify({ error: 'Request not found' }));
343
+ }
344
+ } catch (error) {
345
+ res.writeHead(400, { 'Content-Type': 'application/json' });
346
+ res.end(JSON.stringify({ error: 'Invalid request body' }));
347
+ }
348
+ });
349
+ return;
350
+ }
351
+
352
+ // 健康检查
353
+ if (req.method === 'GET' && req.url === '/api/health') {
354
+ res.writeHead(200, { 'Content-Type': 'application/json' });
355
+ res.end(JSON.stringify({ status: 'ok', version: '0.0.1' }));
356
+ return;
357
+ }
358
+
359
+ res.writeHead(404);
360
+ res.end('Not Found');
361
+ });
362
+
363
+ this.httpServer.on('error', (err: NodeJS.ErrnoException) => {
364
+ if (err.code === 'EADDRINUSE') {
365
+ console.error(`[MCP] Port ${this.port} is already in use`);
366
+ reject(err);
367
+ } else {
368
+ reject(err);
369
+ }
370
+ });
371
+
372
+ this.httpServer.listen(this.port, '127.0.0.1', () => {
373
+ console.error(`[MCP] HTTP Server listening on http://127.0.0.1:${this.port}`);
374
+ resolve();
375
+ });
376
+ });
377
+ }
378
+
379
+ /**
380
+ * 启动服务器
381
+ */
382
+ async start(): Promise<void> {
383
+ if (this.isRunning) {
384
+ console.error('[MCP] Server is already running');
385
+ return;
386
+ }
387
+
388
+ try {
389
+ // 启动 HTTP 服务器
390
+ await this.startHttpServer();
391
+
392
+ // 启动 MCP stdio 传输
393
+ const transport = new StdioServerTransport();
394
+ await this.server.connect(transport);
395
+
396
+ this.isRunning = true;
397
+ console.error('[MCP] MCP Server started successfully');
398
+ } catch (error) {
399
+ console.error('[MCP] Failed to start server:', error);
400
+ throw error;
401
+ }
402
+ }
403
+
404
+ /**
405
+ * 停止服务器
406
+ */
407
+ stop(): void {
408
+ if (!this.isRunning) {
409
+ return;
410
+ }
411
+
412
+ // 关闭 HTTP 服务器
413
+ if (this.httpServer) {
414
+ this.httpServer.close();
415
+ this.httpServer = null;
416
+ }
417
+
418
+ // 清理待处理的请求
419
+ for (const [, pending] of this.pendingRequests) {
420
+ clearTimeout(pending.timeout);
421
+ pending.resolve(null);
422
+ }
423
+ this.pendingRequests.clear();
424
+
425
+ // 关闭 MCP 服务器
426
+ this.server.close();
427
+ this.isRunning = false;
428
+ console.error('[MCP] Server stopped');
429
+ }
430
+ }
431
+
432
+ /**
433
+ * 独立运行入口
434
+ */
435
+ async function main() {
436
+ const port = parseInt(process.env.MCP_PORT || '8766', 10);
437
+ const server = new McpServer(port);
438
+
439
+ process.on('SIGINT', () => {
440
+ server.stop();
441
+ process.exit(0);
442
+ });
443
+
444
+ process.on('SIGTERM', () => {
445
+ server.stop();
446
+ process.exit(0);
447
+ });
448
+
449
+ await server.start();
450
+ }
451
+
452
+ // 如果直接运行此文件
453
+ if (require.main === module) {
454
+ main().catch(console.error);
455
+ }