cursor-mcp-feedback 2.0.2 → 2.0.4
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 +16 -7
- package/dist/main.js +29 -53
- package/dist/session-store.js +11 -2
- package/hooks/block-cursor-mcp-feedback.js +43 -2
- package/hooks/session-utils.js +43 -0
- package/package.json +1 -1
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
|
|
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
|
|
|
@@ -99,11 +99,17 @@ npm run build
|
|
|
99
99
|
|
|
100
100
|
```bash
|
|
101
101
|
cd floating-app
|
|
102
|
-
|
|
103
|
-
#
|
|
102
|
+
|
|
103
|
+
# Option A: Build DMG for distribution
|
|
104
|
+
./build-dmg.sh # Creates CursorMCPFeedback.dmg (~800KB)
|
|
105
|
+
# DMG contains: .app + Install.command (handles xattr) + /Applications shortcut
|
|
106
|
+
|
|
107
|
+
# Option B: Build .app directly
|
|
108
|
+
./build-app.sh # Creates CursorMCPFeedback.app
|
|
109
|
+
cp -r CursorMCPFeedback.app /Applications/
|
|
104
110
|
```
|
|
105
111
|
|
|
106
|
-
To auto-launch on login, add
|
|
112
|
+
To auto-launch on login, add `CursorMCPFeedback` to System Settings > General > Login Items.
|
|
107
113
|
|
|
108
114
|
### CLI: Queue Pending Messages
|
|
109
115
|
|
|
@@ -176,9 +182,12 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
|
|
|
176
182
|
|
|
177
183
|
| Hook | Event | Purpose |
|
|
178
184
|
|------|-------|---------|
|
|
179
|
-
| `block-cursor-mcp-feedback.js` | `
|
|
180
|
-
| `block-cursor-mcp-feedback.js` | `
|
|
181
|
-
| `block-cursor-mcp-feedback.js` | `
|
|
185
|
+
| `block-cursor-mcp-feedback.js` | `sessionStart` | Create per-session directory & metadata |
|
|
186
|
+
| `block-cursor-mcp-feedback.js` | `sessionEnd` | Mark session inactive, reset subagent counter |
|
|
187
|
+
| `block-cursor-mcp-feedback.js` | `subagentStart` | Increment active subagent counter for parent conversation |
|
|
188
|
+
| `block-cursor-mcp-feedback.js` | `subagentStop` | Decrement active subagent counter |
|
|
189
|
+
| `block-cursor-mcp-feedback.js` | `preToolUse` | Deny feedback tool calls when subagents are active (matcher: `MCP:interactive_feedback\|MCP:submit_feedback`) |
|
|
190
|
+
| `block-cursor-mcp-feedback.js` | `beforeMCPExecution` | Deny cursor-mcp-feedback MCP calls when subagents are active |
|
|
182
191
|
| `block-cursor-mcp-feedback.js` | `afterMCPExecution` | Log feedback_request/response events to events.jsonl |
|
|
183
192
|
| `consume-pending.js` | `preToolUse` | Consume pending messages and inject as agent feedback |
|
|
184
193
|
|
package/dist/main.js
CHANGED
|
@@ -93,68 +93,44 @@ 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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
},
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
},
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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: "sessionStart", hook: { command: `${node} ${blockHook} sessionStart`, _source: "cursor-mcp-feedback" } },
|
|
103
|
+
{ event: "sessionEnd", hook: { command: `${node} ${blockHook} sessionEnd`, _source: "cursor-mcp-feedback" } },
|
|
104
|
+
{ event: "subagentStart", hook: { command: `${node} ${blockHook} subagentStart`, _source: "cursor-mcp-feedback" } },
|
|
105
|
+
{ event: "subagentStop", hook: { command: `${node} ${blockHook} subagentStop`, _source: "cursor-mcp-feedback" } },
|
|
106
|
+
{ event: "beforeMCPExecution", hook: { command: `${node} ${blockHook} beforeMCPExecution`, failClosed: true, _source: "cursor-mcp-feedback" } },
|
|
107
|
+
{ event: "afterMCPExecution", hook: { command: `${node} ${blockHook} afterMCPExecution`, _source: "cursor-mcp-feedback" } },
|
|
108
|
+
{ event: "preToolUse", hook: { command: `${node} ${pendingHook}`, _source: "cursor-mcp-feedback" } },
|
|
109
|
+
{ event: "preToolUse", hook: { command: `${node} ${blockHook} preToolUse`, matcher: "MCP:interactive_feedback|MCP:submit_feedback", failClosed: true, _source: "cursor-mcp-feedback-block" } },
|
|
110
|
+
];
|
|
111
|
+
const allSources = new Set(entries.map((e) => e.hook._source));
|
|
112
|
+
const before = JSON.stringify(config);
|
|
113
|
+
// First pass: remove ALL our entries from every event (clean slate)
|
|
114
|
+
for (const arr of Object.values(config.hooks)) {
|
|
115
|
+
for (let i = arr.length - 1; i >= 0; i--) {
|
|
116
|
+
if (allSources.has(arr[i]._source)) {
|
|
117
|
+
arr.splice(i, 1);
|
|
118
|
+
}
|
|
140
119
|
}
|
|
141
120
|
}
|
|
142
|
-
|
|
121
|
+
// Second pass: add desired entries
|
|
122
|
+
for (const { event, hook } of entries) {
|
|
143
123
|
if (!config.hooks[event])
|
|
144
124
|
config.hooks[event] = [];
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
else {
|
|
154
|
-
arr.push(entry);
|
|
155
|
-
changed = true;
|
|
125
|
+
config.hooks[event].push(hook);
|
|
126
|
+
}
|
|
127
|
+
// Clean up empty arrays
|
|
128
|
+
for (const [event, arr] of Object.entries(config.hooks)) {
|
|
129
|
+
if (arr.length === 0) {
|
|
130
|
+
delete config.hooks[event];
|
|
156
131
|
}
|
|
157
132
|
}
|
|
133
|
+
const changed = JSON.stringify(config) !== before;
|
|
158
134
|
if (changed) {
|
|
159
135
|
fs.writeFileSync(hooksJsonPath, JSON.stringify(config, null, 2) + "\n");
|
|
160
136
|
log("Cursor hooks.json updated for cursor-mcp-feedback");
|
package/dist/session-store.js
CHANGED
|
@@ -54,8 +54,17 @@ function cleanupSession(sessionId) {
|
|
|
54
54
|
export function createFreshSession(summary, sessionId) {
|
|
55
55
|
ensureDir();
|
|
56
56
|
cleanExpiredSessions();
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
let conversationId;
|
|
58
|
+
try {
|
|
59
|
+
const callerFile = path.join(os.homedir(), ".cursor-mcp-feedback", "current-feedback-caller.json");
|
|
60
|
+
const caller = JSON.parse(fs.readFileSync(callerFile, "utf8"));
|
|
61
|
+
if (caller.conversation_id && Date.now() - caller.ts < 10000) {
|
|
62
|
+
conversationId = caller.conversation_id;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch { /* ignore */ }
|
|
66
|
+
writeSession({ sessionId, summary, createdAt: Date.now(), status: "pending", conversation_id: conversationId });
|
|
67
|
+
logObj("session-store: created:fresh", { sessionId, summaryLen: summary.length, conversationId: conversationId || "unknown" });
|
|
59
68
|
return sessionId;
|
|
60
69
|
}
|
|
61
70
|
export function resolveSession(sessionId, feedback, images, userInput) {
|
|
@@ -41,6 +41,7 @@ process.stdin.on('end', () => {
|
|
|
41
41
|
const convId = input.session_id || input.conversation_id;
|
|
42
42
|
if (convId) {
|
|
43
43
|
su.endSession(convId, input.reason || 'unknown');
|
|
44
|
+
su.resetActiveSubagents(convId);
|
|
44
45
|
}
|
|
45
46
|
su.writeHookOutput({});
|
|
46
47
|
return;
|
|
@@ -52,17 +53,52 @@ process.stdin.on('end', () => {
|
|
|
52
53
|
if (subagentId) {
|
|
53
54
|
su.recordSubagent(subagentId, parentConvId);
|
|
54
55
|
}
|
|
56
|
+
if (parentConvId) {
|
|
57
|
+
su.incrementActiveSubagents(parentConvId);
|
|
58
|
+
}
|
|
55
59
|
su.writeHookOutput({});
|
|
56
60
|
return;
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
if (event === 'subagentStop') {
|
|
60
64
|
const subagentId = input.subagent_id || input.conversation_id;
|
|
65
|
+
const parentConvId = input.parent_conversation_id || input.conversation_id;
|
|
61
66
|
if (subagentId) su.removeSubagent(subagentId);
|
|
67
|
+
if (parentConvId) {
|
|
68
|
+
su.decrementActiveSubagents(parentConvId);
|
|
69
|
+
}
|
|
62
70
|
su.writeHookOutput({});
|
|
63
71
|
return;
|
|
64
72
|
}
|
|
65
73
|
|
|
74
|
+
if (event === 'preToolUse') {
|
|
75
|
+
const toolName = (input.tool_name || '').toLowerCase();
|
|
76
|
+
const isFeedbackTool =
|
|
77
|
+
toolName.includes('interactive_feedback') ||
|
|
78
|
+
toolName.includes('submit_feedback');
|
|
79
|
+
|
|
80
|
+
if (!isFeedbackTool) {
|
|
81
|
+
su.writeHookOutput({});
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const convId = input.conversation_id;
|
|
86
|
+
if (convId && su.hasActiveSubagents(convId)) {
|
|
87
|
+
su.writeHookOutput({
|
|
88
|
+
permission: 'deny',
|
|
89
|
+
user_message: '[Hook] Blocked feedback tool call — subagent(s) active',
|
|
90
|
+
agent_message:
|
|
91
|
+
'DENIED by preToolUse hook. ' +
|
|
92
|
+
'cursor-mcp-feedback tools are for the MAIN agent only. ' +
|
|
93
|
+
'Active subagents detected for this conversation. ' +
|
|
94
|
+
'Return your results as text in your final response instead.',
|
|
95
|
+
});
|
|
96
|
+
} else {
|
|
97
|
+
su.writeHookOutput({});
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
66
102
|
if (event === 'beforeMCPExecution') {
|
|
67
103
|
const command = (input.command || '').toLowerCase();
|
|
68
104
|
const isFeedbackApp =
|
|
@@ -75,16 +111,21 @@ process.stdin.on('end', () => {
|
|
|
75
111
|
}
|
|
76
112
|
|
|
77
113
|
const convId = input.conversation_id;
|
|
78
|
-
if (convId && su.
|
|
114
|
+
if (convId && su.hasActiveSubagents(convId)) {
|
|
79
115
|
su.writeHookOutput({
|
|
80
116
|
permission: 'deny',
|
|
81
|
-
user_message: '[Hook] Blocked cursor-mcp-feedback call
|
|
117
|
+
user_message: '[Hook] Blocked cursor-mcp-feedback call — subagent(s) active',
|
|
82
118
|
agent_message:
|
|
83
119
|
'DENIED by beforeMCPExecution hook. ' +
|
|
84
120
|
'cursor-mcp-feedback tools are for the MAIN agent only. ' +
|
|
121
|
+
'Active subagents detected for this conversation. ' +
|
|
85
122
|
'Return your results as text in your final response instead.',
|
|
86
123
|
});
|
|
87
124
|
} else {
|
|
125
|
+
if (convId) {
|
|
126
|
+
const callerFile = require('path').join(su.BASE_DIR, 'current-feedback-caller.json');
|
|
127
|
+
su.writeJson(callerFile, { conversation_id: convId, ts: Date.now() });
|
|
128
|
+
}
|
|
88
129
|
su.writeHookOutput({});
|
|
89
130
|
}
|
|
90
131
|
return;
|
package/hooks/session-utils.js
CHANGED
|
@@ -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