cursor-mcp-feedback 2.0.3 → 2.0.5

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/README.md CHANGED
@@ -32,7 +32,7 @@ When an AI agent calls the `interactive_feedback` tool, a feedback panel appears
32
32
  ### Cursor Integration
33
33
 
34
34
  - **Auto-install** — MCP server config, Cursor rules, and hooks are all auto-configured on first startup
35
- - **Subagent protection** — hooks prevent subagents from calling `interactive_feedback`
35
+ - **Subagent protection** — hooks use per-conversation reference counting (`subagentStart`/`subagentStop`) to block feedback calls when subagents are active
36
36
  - **Session lifecycle** — automatic session tracking via hooks (`sessionStart`, `preToolUse`, etc.)
37
37
  - **Event logging** — all interactions logged to `events.jsonl` per session for chat history
38
38
 
@@ -138,7 +138,7 @@ On first startup (or `npm install -g`), the MCP server **automatically configure
138
138
 
139
139
  - **Cursor rule** (`~/.cursor/rules/cursor-mcp-feedback.mdc`) — instructs the agent to call `interactive_feedback`
140
140
  - **Cursor hooks** (`~/.cursor/hooks/`) — subagent protection + pending message delivery + event logging
141
- - **hooks.json entries** — registers `sessionStart`, `subagentStart`, `subagentStop`, `beforeMCPExecution`, `preToolUse`, `afterMCPExecution` hooks
141
+ - **hooks.json entries** — registers `subagentStart`, `subagentStop`, `beforeMCPExecution`, `afterMCPExecution`, `preToolUse` hooks
142
142
 
143
143
  All auto-installed files are kept in sync on every startup (hash-based diffing, idempotent).
144
144
 
@@ -182,9 +182,9 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
182
182
 
183
183
  | Hook | Event | Purpose |
184
184
  |------|-------|---------|
185
- | `block-cursor-mcp-feedback.js` | `subagentStart` | Record subagent_id to negative list |
186
- | `block-cursor-mcp-feedback.js` | `subagentStop` | Remove subagent_id on completion |
187
- | `block-cursor-mcp-feedback.js` | `beforeMCPExecution` | Deny cursor-mcp-feedback calls from subagents |
185
+ | `block-cursor-mcp-feedback.js` | `subagentStart` | Increment active subagent counter for parent conversation |
186
+ | `block-cursor-mcp-feedback.js` | `subagentStop` | Decrement active subagent counter |
187
+ | `block-cursor-mcp-feedback.js` | `beforeMCPExecution` | Deny cursor-mcp-feedback MCP calls when subagents are active |
188
188
  | `block-cursor-mcp-feedback.js` | `afterMCPExecution` | Log feedback_request/response events to events.jsonl |
189
189
  | `consume-pending.js` | `preToolUse` | Consume pending messages and inject as agent feedback |
190
190
 
package/dist/main.js CHANGED
@@ -93,68 +93,41 @@ function installCursorHooks() {
93
93
  catch {
94
94
  config = { version: 1, hooks: {} };
95
95
  }
96
- const SOURCE_TAG = "cursor-mcp-feedback";
97
96
  const node = process.execPath;
98
97
  const blockHook = path.join(hooksDir, "block-cursor-mcp-feedback.js");
99
98
  const pendingHook = path.join(hooksDir, "consume-pending.js");
100
- const entries = {
101
- sessionStart: {
102
- command: `${node} ${blockHook} sessionStart`,
103
- _source: SOURCE_TAG,
104
- },
105
- sessionEnd: {
106
- command: `${node} ${blockHook} sessionEnd`,
107
- _source: SOURCE_TAG,
108
- },
109
- subagentStart: {
110
- command: `${node} ${blockHook} subagentStart`,
111
- _source: SOURCE_TAG,
112
- },
113
- subagentStop: {
114
- command: `${node} ${blockHook} subagentStop`,
115
- _source: SOURCE_TAG,
116
- },
117
- beforeMCPExecution: {
118
- command: `${node} ${blockHook} beforeMCPExecution`,
119
- failClosed: true,
120
- _source: SOURCE_TAG,
121
- },
122
- preToolUse: {
123
- command: `${node} ${pendingHook}`,
124
- _source: SOURCE_TAG,
125
- },
126
- afterMCPExecution: {
127
- command: `${node} ${blockHook} afterMCPExecution`,
128
- _source: SOURCE_TAG,
129
- },
130
- };
131
- let changed = false;
132
- // Remove stale entries from events no longer in `entries` (e.g. sessionStart → subagentStart migration)
133
- for (const [event, arr] of Object.entries(config.hooks)) {
134
- if (entries[event])
135
- continue;
136
- const idx = arr.findIndex((h) => h._source === SOURCE_TAG);
137
- if (idx >= 0) {
138
- arr.splice(idx, 1);
139
- changed = true;
99
+ // Each entry has a unique _source tag for idempotent upsert.
100
+ // Multiple entries per event are supported via distinct _source values.
101
+ const entries = [
102
+ { event: "subagentStart", hook: { command: `${node} ${blockHook} subagentStart`, _source: "cursor-mcp-feedback" } },
103
+ { event: "subagentStop", hook: { command: `${node} ${blockHook} subagentStop`, _source: "cursor-mcp-feedback" } },
104
+ { event: "beforeMCPExecution", hook: { command: `${node} ${blockHook} beforeMCPExecution`, failClosed: true, _source: "cursor-mcp-feedback" } },
105
+ { event: "afterMCPExecution", hook: { command: `${node} ${blockHook} afterMCPExecution`, _source: "cursor-mcp-feedback" } },
106
+ { event: "preToolUse", hook: { command: `${node} ${pendingHook}`, _source: "cursor-mcp-feedback" } },
107
+ ];
108
+ const allSources = new Set([...entries.map((e) => e.hook._source), "cursor-mcp-feedback-block"]);
109
+ const before = JSON.stringify(config);
110
+ // First pass: remove ALL our entries from every event (clean slate)
111
+ for (const arr of Object.values(config.hooks)) {
112
+ for (let i = arr.length - 1; i >= 0; i--) {
113
+ if (allSources.has(arr[i]._source)) {
114
+ arr.splice(i, 1);
115
+ }
140
116
  }
141
117
  }
142
- for (const [event, entry] of Object.entries(entries)) {
118
+ // Second pass: add desired entries
119
+ for (const { event, hook } of entries) {
143
120
  if (!config.hooks[event])
144
121
  config.hooks[event] = [];
145
- const arr = config.hooks[event];
146
- const idx = arr.findIndex((h) => h._source === SOURCE_TAG);
147
- if (idx >= 0) {
148
- if (JSON.stringify(arr[idx]) !== JSON.stringify(entry)) {
149
- arr[idx] = entry;
150
- changed = true;
151
- }
152
- }
153
- else {
154
- arr.push(entry);
155
- changed = true;
122
+ config.hooks[event].push(hook);
123
+ }
124
+ // Clean up empty arrays
125
+ for (const [event, arr] of Object.entries(config.hooks)) {
126
+ if (arr.length === 0) {
127
+ delete config.hooks[event];
156
128
  }
157
129
  }
130
+ const changed = JSON.stringify(config) !== before;
158
131
  if (changed) {
159
132
  fs.writeFileSync(hooksJsonPath, JSON.stringify(config, null, 2) + "\n");
160
133
  log("Cursor hooks.json updated for cursor-mcp-feedback");
@@ -1,12 +1,11 @@
1
1
  #!/usr/bin/env node
2
- // Cursor hook: session lifecycle + subagent protection.
2
+ // Cursor hook: subagent protection + feedback event logging.
3
3
  //
4
4
  // Events (argv[2]):
5
- // sessionStart create per-session directory & meta
6
- // sessionEnd mark session inactive
7
- // subagentStart record subagent to negative list
8
- // subagentStop remove subagent
9
- // beforeMCPExecution — deny cursor-mcp-feedback calls from subagents
5
+ // subagentStart increment active subagent counter
6
+ // subagentStop decrement active subagent counter
7
+ // beforeMCPExecution deny cursor-mcp-feedback calls when subagents active
8
+ // afterMCPExecution log feedback events to events.jsonl
10
9
 
11
10
  'use strict';
12
11
 
@@ -20,45 +19,20 @@ process.stdin.on('end', () => {
20
19
  let input;
21
20
  try { input = JSON.parse(raw); } catch { input = {}; }
22
21
 
23
- if (event === 'sessionStart') {
24
- su.cleanupStaleSessions();
25
- const convId = input.session_id || input.conversation_id;
26
- if (convId) {
27
- const workspace = (input.workspace_roots || [])[0] || '';
28
- su.createSession(convId, {
29
- workspace,
30
- mode: input.composer_mode || 'agent',
31
- model: input.model || '',
32
- is_background: input.is_background_agent || false,
33
- transcript_path: input.transcript_path || null,
34
- });
35
- }
36
- su.writeHookOutput({});
37
- return;
38
- }
39
-
40
- if (event === 'sessionEnd') {
41
- const convId = input.session_id || input.conversation_id;
42
- if (convId) {
43
- su.endSession(convId, input.reason || 'unknown');
44
- }
45
- su.writeHookOutput({});
46
- return;
47
- }
48
-
49
22
  if (event === 'subagentStart') {
50
23
  const subagentId = input.subagent_id;
51
24
  const parentConvId = input.parent_conversation_id || input.conversation_id;
52
- if (subagentId) {
53
- su.recordSubagent(subagentId, parentConvId);
54
- }
25
+ if (subagentId) su.recordSubagent(subagentId, parentConvId);
26
+ if (parentConvId) su.incrementActiveSubagents(parentConvId);
55
27
  su.writeHookOutput({});
56
28
  return;
57
29
  }
58
30
 
59
31
  if (event === 'subagentStop') {
60
32
  const subagentId = input.subagent_id || input.conversation_id;
33
+ const parentConvId = input.parent_conversation_id || input.conversation_id;
61
34
  if (subagentId) su.removeSubagent(subagentId);
35
+ if (parentConvId) su.decrementActiveSubagents(parentConvId);
62
36
  su.writeHookOutput({});
63
37
  return;
64
38
  }
@@ -75,13 +49,14 @@ process.stdin.on('end', () => {
75
49
  }
76
50
 
77
51
  const convId = input.conversation_id;
78
- if (convId && su.isSubagent(convId)) {
52
+ if (convId && su.hasActiveSubagents(convId)) {
79
53
  su.writeHookOutput({
80
54
  permission: 'deny',
81
- user_message: '[Hook] Blocked cursor-mcp-feedback call from subagent',
55
+ user_message: '[Hook] Blocked cursor-mcp-feedback subagent(s) active',
82
56
  agent_message:
83
57
  'DENIED by beforeMCPExecution hook. ' +
84
58
  'cursor-mcp-feedback tools are for the MAIN agent only. ' +
59
+ 'Active subagents detected for this conversation. ' +
85
60
  'Return your results as text in your final response instead.',
86
61
  });
87
62
  } else {
@@ -100,6 +75,11 @@ process.stdin.on('end', () => {
100
75
 
101
76
  const isFeedback = toolName === 'interactive_feedback' || toolName === 'submit_feedback';
102
77
  if (convId && isFeedback) {
78
+ su.ensureSession(convId, {
79
+ workspace: (input.workspace_roots || [])[0] || '',
80
+ model: input.model || '',
81
+ });
82
+
103
83
  const eventsFile = require('path').join(su.sessionDir(convId), 'events.jsonl');
104
84
  let toolInput = {};
105
85
  try { toolInput = typeof input.tool_input === 'string' ? JSON.parse(input.tool_input) : (input.tool_input || {}); } catch {}
@@ -223,6 +223,48 @@ function isSubagent(convId) {
223
223
  return !!data[convId];
224
224
  }
225
225
 
226
+ // ── Active subagent counter (per conversation) ──
227
+
228
+ const ACTIVE_COUNTER_FILE = path.join(BASE_DIR, "active-subagent-counts.json");
229
+
230
+ function incrementActiveSubagents(convId) {
231
+ const data = readJson(ACTIVE_COUNTER_FILE, {});
232
+ const entry = data[convId] || { count: 0, ts: Date.now() };
233
+ entry.count = Math.max(0, entry.count) + 1;
234
+ entry.ts = Date.now();
235
+ data[convId] = entry;
236
+ const FOUR_HOURS = 4 * 60 * 60 * 1000;
237
+ for (const [k, v] of Object.entries(data)) {
238
+ if (Date.now() - (v.ts || 0) > FOUR_HOURS) delete data[k];
239
+ }
240
+ writeJson(ACTIVE_COUNTER_FILE, data);
241
+ }
242
+
243
+ function decrementActiveSubagents(convId) {
244
+ const data = readJson(ACTIVE_COUNTER_FILE, {});
245
+ const entry = data[convId];
246
+ if (!entry) return;
247
+ entry.count = Math.max(0, entry.count - 1);
248
+ entry.ts = Date.now();
249
+ data[convId] = entry;
250
+ writeJson(ACTIVE_COUNTER_FILE, data);
251
+ }
252
+
253
+ function hasActiveSubagents(convId) {
254
+ const data = readJson(ACTIVE_COUNTER_FILE, {});
255
+ const entry = data[convId];
256
+ if (!entry) return false;
257
+ const FOUR_HOURS = 4 * 60 * 60 * 1000;
258
+ if (Date.now() - (entry.ts || 0) > FOUR_HOURS) return false;
259
+ return entry.count > 0;
260
+ }
261
+
262
+ function resetActiveSubagents(convId) {
263
+ const data = readJson(ACTIVE_COUNTER_FILE, {});
264
+ delete data[convId];
265
+ writeJson(ACTIVE_COUNTER_FILE, data);
266
+ }
267
+
226
268
  // ── Most recent active session ──
227
269
 
228
270
  function getMostRecentSession() {
@@ -259,6 +301,7 @@ module.exports = {
259
301
  updateActiveList, markSessionInactive, touchSessionActivity,
260
302
  readSessionPending, writeSessionPending, addSessionPending, consumeSessionPending,
261
303
  recordSubagent, removeSubagent, isSubagent,
304
+ incrementActiveSubagents, decrementActiveSubagents, hasActiveSubagents, resetActiveSubagents,
262
305
  getMostRecentSession, readGlobalPending, writeGlobalPending,
263
306
  readHookInput, writeHookOutput,
264
307
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursor-mcp-feedback",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
4
4
  "description": "MCP App for interactive feedback — renders a chat UI directly inside MCP hosts (Claude, Cursor, etc.)",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",