cursor-mcp-feedback 2.0.0

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/main.js ADDED
@@ -0,0 +1,438 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MCP Feedback App entry point.
4
+ *
5
+ * Modes:
6
+ * --stdio : stdio transport (default, for Claude Desktop / Cursor)
7
+ * --http : HTTP transport on port 3001 (for Claude Web / remote testing)
8
+ *
9
+ * The blocking feedback pattern (interactive_feedback waits for submit_feedback)
10
+ * requires a persistent connection, so stdio is the primary transport.
11
+ * HTTP mode creates a new server per request for stateless tool calls.
12
+ */
13
+ import { createServer } from "./server.js";
14
+ import * as fs from "node:fs";
15
+ import * as path from "node:path";
16
+ import * as os from "node:os";
17
+ import * as crypto from "node:crypto";
18
+ import { log, logError } from "./logger.js";
19
+ function getSrcDir() {
20
+ return import.meta.filename.endsWith(".ts")
21
+ ? import.meta.dirname
22
+ : path.dirname(import.meta.filename);
23
+ }
24
+ function syncFile(sourceFile, targetFile, label) {
25
+ if (!fs.existsSync(sourceFile))
26
+ return;
27
+ const srcHash = crypto.createHash("md5").update(fs.readFileSync(sourceFile)).digest("hex");
28
+ if (fs.existsSync(targetFile)) {
29
+ const tgtHash = crypto.createHash("md5").update(fs.readFileSync(targetFile)).digest("hex");
30
+ if (srcHash === tgtHash)
31
+ return;
32
+ fs.copyFileSync(sourceFile, targetFile);
33
+ log(`${label} updated (hash changed) at ${targetFile}`);
34
+ }
35
+ else {
36
+ fs.mkdirSync(path.dirname(targetFile), { recursive: true });
37
+ fs.copyFileSync(sourceFile, targetFile);
38
+ log(`${label} installed to ${targetFile}`);
39
+ }
40
+ }
41
+ function installCursorRule() {
42
+ try {
43
+ const src = path.join(getSrcDir(), "..", "rules", "cursor-mcp-feedback.mdc");
44
+ const dst = path.join(os.homedir(), ".cursor", "rules", "cursor-mcp-feedback.mdc");
45
+ syncFile(src, dst, "Cursor rule");
46
+ }
47
+ catch { /* non-critical */ }
48
+ }
49
+ function installCursorMcpConfig() {
50
+ try {
51
+ const mcpJsonPath = path.join(os.homedir(), ".cursor", "mcp.json");
52
+ let config;
53
+ try {
54
+ config = JSON.parse(fs.readFileSync(mcpJsonPath, "utf8"));
55
+ }
56
+ catch {
57
+ config = { mcpServers: {} };
58
+ }
59
+ const mainJs = path.join(getSrcDir(), "..", "dist", "main.js");
60
+ const resolvedMain = fs.realpathSync(mainJs);
61
+ const desired = {
62
+ command: process.execPath,
63
+ args: [resolvedMain],
64
+ timeout: 86400,
65
+ env: { MCP_FEEDBACK_TIMEOUT: "86400" },
66
+ };
67
+ const existing = config.mcpServers["cursor-mcp-feedback"];
68
+ if (existing && JSON.stringify(existing) === JSON.stringify(desired))
69
+ return;
70
+ config.mcpServers["cursor-mcp-feedback"] = desired;
71
+ fs.writeFileSync(mcpJsonPath, JSON.stringify(config, null, 2) + "\n");
72
+ log("Cursor mcp.json updated for cursor-mcp-feedback");
73
+ }
74
+ catch (err) {
75
+ logError("mcp.json install failed: " + err);
76
+ }
77
+ }
78
+ function installCursorHooks() {
79
+ try {
80
+ const hooksDir = path.join(os.homedir(), ".cursor", "hooks");
81
+ const hookScripts = ["session-utils.js", "block-cursor-mcp-feedback.js", "consume-pending.js"];
82
+ for (const script of hookScripts) {
83
+ const src = path.join(getSrcDir(), "..", "hooks", script);
84
+ const dst = path.join(hooksDir, script);
85
+ syncFile(src, dst, `Cursor hook (${script})`);
86
+ try {
87
+ fs.chmodSync(dst, 0o755);
88
+ }
89
+ catch { /* ignore */ }
90
+ }
91
+ const hooksJsonPath = path.join(os.homedir(), ".cursor", "hooks.json");
92
+ let config;
93
+ try {
94
+ config = JSON.parse(fs.readFileSync(hooksJsonPath, "utf8"));
95
+ }
96
+ catch {
97
+ config = { version: 1, hooks: {} };
98
+ }
99
+ const SOURCE_TAG = "cursor-mcp-feedback";
100
+ const node = process.execPath;
101
+ const blockHook = path.join(hooksDir, "block-cursor-mcp-feedback.js");
102
+ const pendingHook = path.join(hooksDir, "consume-pending.js");
103
+ const entries = {
104
+ sessionStart: {
105
+ command: `${node} ${blockHook} sessionStart`,
106
+ _source: SOURCE_TAG,
107
+ },
108
+ sessionEnd: {
109
+ command: `${node} ${blockHook} sessionEnd`,
110
+ _source: SOURCE_TAG,
111
+ },
112
+ subagentStart: {
113
+ command: `${node} ${blockHook} subagentStart`,
114
+ _source: SOURCE_TAG,
115
+ },
116
+ subagentStop: {
117
+ command: `${node} ${blockHook} subagentStop`,
118
+ _source: SOURCE_TAG,
119
+ },
120
+ beforeMCPExecution: {
121
+ command: `${node} ${blockHook} beforeMCPExecution`,
122
+ failClosed: true,
123
+ _source: SOURCE_TAG,
124
+ },
125
+ preToolUse: {
126
+ command: `${node} ${pendingHook}`,
127
+ _source: SOURCE_TAG,
128
+ },
129
+ afterMCPExecution: {
130
+ command: `${node} ${blockHook} afterMCPExecution`,
131
+ _source: SOURCE_TAG,
132
+ },
133
+ };
134
+ let changed = false;
135
+ // Remove stale entries from events no longer in `entries` (e.g. sessionStart → subagentStart migration)
136
+ for (const [event, arr] of Object.entries(config.hooks)) {
137
+ if (entries[event])
138
+ continue;
139
+ const idx = arr.findIndex((h) => h._source === SOURCE_TAG);
140
+ if (idx >= 0) {
141
+ arr.splice(idx, 1);
142
+ changed = true;
143
+ }
144
+ }
145
+ for (const [event, entry] of Object.entries(entries)) {
146
+ if (!config.hooks[event])
147
+ config.hooks[event] = [];
148
+ const arr = config.hooks[event];
149
+ const idx = arr.findIndex((h) => h._source === SOURCE_TAG);
150
+ if (idx >= 0) {
151
+ if (JSON.stringify(arr[idx]) !== JSON.stringify(entry)) {
152
+ arr[idx] = entry;
153
+ changed = true;
154
+ }
155
+ }
156
+ else {
157
+ arr.push(entry);
158
+ changed = true;
159
+ }
160
+ }
161
+ if (changed) {
162
+ fs.writeFileSync(hooksJsonPath, JSON.stringify(config, null, 2) + "\n");
163
+ log("Cursor hooks.json updated for cursor-mcp-feedback");
164
+ }
165
+ }
166
+ catch (err) {
167
+ logError("Hook install failed: " + err);
168
+ }
169
+ }
170
+ function migrateOldData() {
171
+ try {
172
+ const oldDir = path.join(os.homedir(), ".mcp-feedback-app");
173
+ const newDir = path.join(os.homedir(), ".cursor-mcp-feedback");
174
+ if (fs.existsSync(oldDir) && !fs.existsSync(newDir)) {
175
+ fs.renameSync(oldDir, newDir);
176
+ log("Migrated data directory from .mcp-feedback-app to .cursor-mcp-feedback");
177
+ }
178
+ const oldLog = path.join(os.homedir(), ".mcp-feedback-app.log");
179
+ const newLog = path.join(os.homedir(), ".cursor-mcp-feedback.log");
180
+ if (fs.existsSync(oldLog) && !fs.existsSync(newLog)) {
181
+ fs.renameSync(oldLog, newLog);
182
+ }
183
+ const oldSettings = path.join(os.homedir(), ".mcp-feedback-app-settings.json");
184
+ const newSettings = path.join(os.homedir(), ".cursor-mcp-feedback-settings.json");
185
+ if (fs.existsSync(oldSettings) && !fs.existsSync(newSettings)) {
186
+ fs.renameSync(oldSettings, newSettings);
187
+ }
188
+ const oldRule = path.join(os.homedir(), ".cursor", "rules", "mcp-feedback-app.mdc");
189
+ if (fs.existsSync(oldRule))
190
+ fs.unlinkSync(oldRule);
191
+ const oldHook = path.join(os.homedir(), ".cursor", "hooks", "block-mcp-feedback-app.js");
192
+ if (fs.existsSync(oldHook))
193
+ fs.unlinkSync(oldHook);
194
+ const hooksJsonPath = path.join(os.homedir(), ".cursor", "hooks.json");
195
+ try {
196
+ const hc = JSON.parse(fs.readFileSync(hooksJsonPath, "utf8"));
197
+ let hChanged = false;
198
+ for (const arr of Object.values(hc.hooks || {})) {
199
+ const idx = arr.findIndex((h) => h._source === "mcp-feedback-app");
200
+ if (idx >= 0) {
201
+ arr.splice(idx, 1);
202
+ hChanged = true;
203
+ }
204
+ }
205
+ if (hChanged)
206
+ fs.writeFileSync(hooksJsonPath, JSON.stringify(hc, null, 2) + "\n");
207
+ }
208
+ catch { /* ignore */ }
209
+ const mcpJsonPath = path.join(os.homedir(), ".cursor", "mcp.json");
210
+ try {
211
+ const mc = JSON.parse(fs.readFileSync(mcpJsonPath, "utf8"));
212
+ if (mc.mcpServers?.["mcp-feedback-app"]) {
213
+ delete mc.mcpServers["mcp-feedback-app"];
214
+ fs.writeFileSync(mcpJsonPath, JSON.stringify(mc, null, 2) + "\n");
215
+ }
216
+ }
217
+ catch { /* ignore */ }
218
+ }
219
+ catch { /* non-critical */ }
220
+ }
221
+ const BASE_DIR = path.join(os.homedir(), ".cursor-mcp-feedback");
222
+ const SESSIONS_DIR = path.join(BASE_DIR, "sessions");
223
+ const ACTIVE_FILE = path.join(BASE_DIR, "active-sessions.json");
224
+ const GLOBAL_PENDING = path.join(BASE_DIR, "pending.json");
225
+ function readJsonCli(file, fallback) {
226
+ try {
227
+ return JSON.parse(fs.readFileSync(file, "utf8"));
228
+ }
229
+ catch {
230
+ return fallback;
231
+ }
232
+ }
233
+ function writeJsonCli(file, data) {
234
+ fs.mkdirSync(path.dirname(file), { recursive: true });
235
+ fs.writeFileSync(file, JSON.stringify(data, null, 2));
236
+ }
237
+ function formatAgo(ms) {
238
+ const s = Math.floor(ms / 1000);
239
+ if (s < 60)
240
+ return `${s}s`;
241
+ if (s < 3600)
242
+ return `${Math.floor(s / 60)}m`;
243
+ return `${Math.floor(s / 3600)}h`;
244
+ }
245
+ function resolveSessionId(explicit) {
246
+ if (explicit)
247
+ return explicit;
248
+ const sessions = readJsonCli(ACTIVE_FILE, []);
249
+ if (sessions.length === 0)
250
+ return null;
251
+ sessions.sort((a, b) => (b.last_activity || 0) - (a.last_activity || 0));
252
+ return sessions[0].id;
253
+ }
254
+ function pendingFile(sessionId) {
255
+ return path.join(SESSIONS_DIR, sessionId, "pending.json");
256
+ }
257
+ const args = process.argv.slice(2);
258
+ // ── CLI: queue subcommand ──
259
+ if (args[0] === "queue") {
260
+ const sub = args[1];
261
+ const sessionFlag = args.indexOf("--session");
262
+ const explicitSession = sessionFlag >= 0 ? args[sessionFlag + 1] : undefined;
263
+ if (sub === "sessions") {
264
+ const sessions = readJsonCli(ACTIVE_FILE, []);
265
+ if (sessions.length === 0) {
266
+ console.log("No active sessions.");
267
+ }
268
+ else {
269
+ console.log(`${sessions.length} active session(s):`);
270
+ for (const s of sessions) {
271
+ const dir = s.workspace ? path.basename(s.workspace) : "?";
272
+ const ago = formatAgo(Date.now() - (s.last_activity || 0));
273
+ console.log(` ${s.id.slice(0, 8)} ${dir} (${s.mode}, ${ago} ago)`);
274
+ }
275
+ }
276
+ process.exit(0);
277
+ }
278
+ if (!sub || sub === "list") {
279
+ const sid = resolveSessionId(explicitSession);
280
+ if (!sid) {
281
+ const global = readJsonCli(GLOBAL_PENDING, []);
282
+ if (global.length === 0)
283
+ console.log("No active sessions and no global pending messages.");
284
+ else {
285
+ console.log(`${global.length} global pending message(s):`);
286
+ for (const m of global)
287
+ console.log(` [${formatAgo(Date.now() - m.createdAt)} ago] ${m.text}`);
288
+ }
289
+ }
290
+ else {
291
+ const msgs = readJsonCli(pendingFile(sid), []);
292
+ console.log(`Session ${sid.slice(0, 8)} — ${msgs.length} pending message(s):`);
293
+ for (const m of msgs)
294
+ console.log(` [${formatAgo(Date.now() - m.createdAt)} ago] ${m.text}`);
295
+ }
296
+ process.exit(0);
297
+ }
298
+ if (sub === "clear") {
299
+ const sid = resolveSessionId(explicitSession);
300
+ if (sid) {
301
+ writeJsonCli(pendingFile(sid), []);
302
+ console.log(`Session ${sid.slice(0, 8)} pending messages cleared.`);
303
+ }
304
+ else {
305
+ writeJsonCli(GLOBAL_PENDING, []);
306
+ console.log("Global pending messages cleared.");
307
+ }
308
+ process.exit(0);
309
+ }
310
+ // Default: add message
311
+ const filteredArgs = args.slice(1).filter((_, i) => {
312
+ const absI = i + 1;
313
+ return absI !== args.indexOf("--session") && absI !== args.indexOf("--session") + 1;
314
+ });
315
+ const text = filteredArgs.join(" ");
316
+ if (!text) {
317
+ console.error("Usage: cursor-mcp-feedback queue <message> [--session <id>]");
318
+ process.exit(1);
319
+ }
320
+ const sid = resolveSessionId(explicitSession);
321
+ if (sid) {
322
+ const msgs = readJsonCli(pendingFile(sid), []);
323
+ msgs.push({ id: crypto.randomUUID(), text, createdAt: Date.now() });
324
+ writeJsonCli(pendingFile(sid), msgs);
325
+ console.log(`Queued to session ${sid.slice(0, 8)}: "${text}" (${msgs.length} total)`);
326
+ }
327
+ else {
328
+ const msgs = readJsonCli(GLOBAL_PENDING, []);
329
+ msgs.push({ id: crypto.randomUUID(), text, createdAt: Date.now() });
330
+ writeJsonCli(GLOBAL_PENDING, msgs);
331
+ console.log(`Queued (global): "${text}" (${msgs.length} total)`);
332
+ }
333
+ process.exit(0);
334
+ }
335
+ if (args.includes("--install-only")) {
336
+ try {
337
+ migrateOldData();
338
+ installCursorRule();
339
+ installCursorMcpConfig();
340
+ installCursorHooks();
341
+ log("install-only: auto-configuration completed");
342
+ }
343
+ catch { /* non-critical */ }
344
+ process.exit(0);
345
+ }
346
+ const useHttp = args.includes("--http");
347
+ if (useHttp) {
348
+ const { StreamableHTTPServerTransport } = await import("@modelcontextprotocol/sdk/server/streamableHttp.js");
349
+ const express = (await import("express")).default;
350
+ const cors = (await import("cors")).default;
351
+ const app = express();
352
+ app.use(cors());
353
+ app.use(express.json());
354
+ // Persist transport sessions so each MCP client gets its own server instance.
355
+ // This isolates pendingSessions per client — submit_feedback from client A
356
+ // cannot accidentally resolve client B's interactive_feedback.
357
+ const sessions = new Map();
358
+ let seenNewIds = new Set();
359
+ app.all("/mcp", async (req, res) => {
360
+ const sessionId = req.headers["mcp-session-id"];
361
+ // Route to existing session (non-initialization requests)
362
+ if (sessionId && sessions.has(sessionId)) {
363
+ if (!seenNewIds.has(sessionId)) {
364
+ seenNewIds.add(sessionId);
365
+ log(`http: routing established session ${sessionId} (${sessions.size} active)`);
366
+ }
367
+ const transport = sessions.get(sessionId);
368
+ try {
369
+ await transport.handleRequest(req, res, req.body);
370
+ }
371
+ catch (error) {
372
+ logError(`http: request error on session ${sessionId}: ${error}`);
373
+ if (!res.headersSent) {
374
+ res.status(500).json({
375
+ jsonrpc: "2.0",
376
+ error: { code: -32603, message: "Internal server error" },
377
+ id: null,
378
+ });
379
+ }
380
+ }
381
+ return;
382
+ }
383
+ // New session: create a fresh server (with isolated pendingSessions)
384
+ const server = createServer();
385
+ const transport = new StreamableHTTPServerTransport({
386
+ sessionIdGenerator: () => crypto.randomUUID(),
387
+ onsessionclosed: (sid) => {
388
+ if (sid) {
389
+ log("http: session closed " + sid);
390
+ sessions.delete(sid);
391
+ }
392
+ },
393
+ });
394
+ try {
395
+ await server.connect(transport);
396
+ await transport.handleRequest(req, res, req.body);
397
+ // After initialization, the transport will have a sessionId — persist it
398
+ if (transport.sessionId) {
399
+ sessions.set(transport.sessionId, transport);
400
+ log(`http: new session ${transport.sessionId} (${sessions.size} active)`);
401
+ }
402
+ }
403
+ catch (error) {
404
+ logError("http: initialization error: " + error);
405
+ if (!res.headersSent) {
406
+ res.status(500).json({
407
+ jsonrpc: "2.0",
408
+ error: { code: -32603, message: "Internal server error" },
409
+ id: null,
410
+ });
411
+ }
412
+ }
413
+ });
414
+ const port = parseInt(process.env.PORT || "3001", 10);
415
+ app.listen(port, () => {
416
+ log(`http: server listening on http://localhost:${port}/mcp`);
417
+ });
418
+ }
419
+ else {
420
+ const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
421
+ try {
422
+ migrateOldData();
423
+ installCursorRule();
424
+ installCursorMcpConfig();
425
+ installCursorHooks();
426
+ const server = createServer();
427
+ const transport = new StdioServerTransport();
428
+ transport.onclose = () => {
429
+ log("stdio: transport closed");
430
+ };
431
+ await server.connect(transport);
432
+ log("stdio: server started");
433
+ }
434
+ catch (err) {
435
+ logError("stdio: fatal error: " + err);
436
+ process.exit(1);
437
+ }
438
+ }