@getmarrow/mcp 3.0.7 → 3.0.9
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/cli.js +297 -256
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +73 -28
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
13
|
const index_1 = require("./index");
|
|
14
|
-
// Parse CLI args
|
|
14
|
+
// Parse CLI args
|
|
15
15
|
function parseArgs() {
|
|
16
16
|
const args = process.argv.slice(2);
|
|
17
17
|
const result = {};
|
|
@@ -20,14 +20,85 @@ function parseArgs() {
|
|
|
20
20
|
result.apiKey = args[i + 1];
|
|
21
21
|
i++;
|
|
22
22
|
}
|
|
23
|
+
if (args[i] === 'setup' || args[i] === '--setup') {
|
|
24
|
+
result.setup = true;
|
|
25
|
+
}
|
|
23
26
|
}
|
|
24
27
|
return result;
|
|
25
28
|
}
|
|
29
|
+
// ─── Setup command: inject Marrow instructions into CLAUDE.md ───
|
|
30
|
+
function runSetup() {
|
|
31
|
+
const fs = require('fs');
|
|
32
|
+
const path = require('path');
|
|
33
|
+
const MARROW_BLOCK_START = '<!-- marrow:start -->';
|
|
34
|
+
const MARROW_BLOCK_END = '<!-- marrow:end -->';
|
|
35
|
+
const marrowInstructions = `${MARROW_BLOCK_START}
|
|
36
|
+
## Marrow — Decision Memory (auto-injected by @getmarrow/mcp setup)
|
|
37
|
+
|
|
38
|
+
You have Marrow MCP connected. Use it on EVERY session automatically:
|
|
39
|
+
|
|
40
|
+
1. **Session start:** Call \`marrow_orient\` FIRST before any work. It warns you about past failures.
|
|
41
|
+
2. **Before meaningful actions:** Call \`marrow_auto({ action: "what you're about to do" })\` — deploys, file changes, config changes, publishes.
|
|
42
|
+
3. **After actions:** Call \`marrow_auto({ action: "what you did", outcome: "what happened", success: true/false })\`.
|
|
43
|
+
4. **To query history:** Call \`marrow_ask("plain english question")\` — ask what failed, what worked, what to avoid.
|
|
44
|
+
|
|
45
|
+
Do NOT skip logging. Every decision you log compounds — failure rates drop, patterns emerge, the next session starts smarter. This is not optional overhead.
|
|
46
|
+
|
|
47
|
+
For one-shot logging: \`marrow_auto({ action: "did X", outcome: "result Y", success: true })\` — one call, done.
|
|
48
|
+
${MARROW_BLOCK_END}`;
|
|
49
|
+
// Find CLAUDE.md — check current dir, then walk up
|
|
50
|
+
let dir = process.cwd();
|
|
51
|
+
let claudeMdPath = null;
|
|
52
|
+
for (let i = 0; i < 5; i++) {
|
|
53
|
+
const candidate = path.join(dir, 'CLAUDE.md');
|
|
54
|
+
if (fs.existsSync(candidate)) {
|
|
55
|
+
claudeMdPath = candidate;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
const parent = path.dirname(dir);
|
|
59
|
+
if (parent === dir)
|
|
60
|
+
break;
|
|
61
|
+
dir = parent;
|
|
62
|
+
}
|
|
63
|
+
if (!claudeMdPath) {
|
|
64
|
+
claudeMdPath = path.join(process.cwd(), 'CLAUDE.md');
|
|
65
|
+
process.stdout.write(`Creating ${claudeMdPath}\n`);
|
|
66
|
+
}
|
|
67
|
+
let content = '';
|
|
68
|
+
if (fs.existsSync(claudeMdPath)) {
|
|
69
|
+
content = fs.readFileSync(claudeMdPath, 'utf8');
|
|
70
|
+
// Check if already present
|
|
71
|
+
if (content.includes(MARROW_BLOCK_START)) {
|
|
72
|
+
// Replace existing block
|
|
73
|
+
const startIdx = content.indexOf(MARROW_BLOCK_START);
|
|
74
|
+
const endIdx = content.indexOf(MARROW_BLOCK_END);
|
|
75
|
+
if (endIdx > startIdx) {
|
|
76
|
+
content = content.slice(0, startIdx) + marrowInstructions + content.slice(endIdx + MARROW_BLOCK_END.length);
|
|
77
|
+
fs.writeFileSync(claudeMdPath, content);
|
|
78
|
+
process.stdout.write(`Updated Marrow instructions in ${claudeMdPath}\n`);
|
|
79
|
+
process.exit(0);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Append
|
|
85
|
+
const separator = content.length > 0 && !content.endsWith('\n') ? '\n\n' : content.length > 0 ? '\n' : '';
|
|
86
|
+
fs.writeFileSync(claudeMdPath, content + separator + marrowInstructions + '\n');
|
|
87
|
+
process.stdout.write(`Added Marrow instructions to ${claudeMdPath}\n`);
|
|
88
|
+
process.stdout.write(`Your agent will now use Marrow automatically in every session.\n`);
|
|
89
|
+
process.exit(0);
|
|
90
|
+
}
|
|
26
91
|
const cliArgs = parseArgs();
|
|
92
|
+
// Handle setup command before anything else
|
|
93
|
+
if (cliArgs.setup) {
|
|
94
|
+
runSetup();
|
|
95
|
+
}
|
|
27
96
|
const API_KEY = cliArgs.apiKey || process.env.MARROW_API_KEY || '';
|
|
28
|
-
|
|
97
|
+
// [SECURITY #3] Validate BASE_URL — require HTTPS to prevent SSRF / credential leakage
|
|
98
|
+
const rawBaseUrl = process.env.MARROW_BASE_URL || 'https://api.getmarrow.ai';
|
|
99
|
+
const BASE_URL = (0, index_1.validateBaseUrl)(rawBaseUrl);
|
|
29
100
|
const SESSION_ID = process.env.MARROW_SESSION_ID || undefined;
|
|
30
|
-
const AUTO_ENROLL = process.env.MARROW_AUTO_ENROLL
|
|
101
|
+
const AUTO_ENROLL = process.env.MARROW_AUTO_ENROLL !== 'false'; // on by default
|
|
31
102
|
const AGENT_ID = process.env.MARROW_AGENT_ID || `${require('os').hostname()}-${Date.now().toString(36)}`;
|
|
32
103
|
if (!API_KEY) {
|
|
33
104
|
process.stderr.write('Error: MARROW_API_KEY environment variable is required\n');
|
|
@@ -35,16 +106,19 @@ if (!API_KEY) {
|
|
|
35
106
|
process.stderr.write(' or: npx @getmarrow/mcp --key mrw_yourkey\n');
|
|
36
107
|
process.exit(1);
|
|
37
108
|
}
|
|
109
|
+
// [SECURITY #12] Warn if API key is visible in process args
|
|
110
|
+
if (cliArgs.apiKey) {
|
|
111
|
+
process.stderr.write('[marrow] Warning: --key flag exposes API key in process list. Use MARROW_API_KEY env var for production.\n');
|
|
112
|
+
}
|
|
38
113
|
// Auto-orient on startup — cache warnings, inject into EVERY marrow_think response
|
|
39
114
|
let cachedOrientWarnings = [];
|
|
40
115
|
let thinkCallCount = 0;
|
|
41
|
-
let orientCallCount = 0;
|
|
42
|
-
let initialized = false;
|
|
116
|
+
let orientCallCount = 0;
|
|
117
|
+
let initialized = false;
|
|
43
118
|
const pendingDecisions = new Map();
|
|
44
119
|
const PENDING_TTL_MS = 30 * 60 * 1000; // 30 min TTL
|
|
45
120
|
function actionHash(action) {
|
|
46
121
|
const normalized = action.toLowerCase().trim().replace(/\s+/g, ' ');
|
|
47
|
-
// djb2 hash to prevent decision_id mismatches from normalization-only collisions
|
|
48
122
|
let h = 5381;
|
|
49
123
|
for (let i = 0; i < normalized.length; i++) {
|
|
50
124
|
h = ((h << 5) + h) ^ normalized.charCodeAt(i);
|
|
@@ -52,6 +126,7 @@ function actionHash(action) {
|
|
|
52
126
|
}
|
|
53
127
|
return h.toString(36) + '_' + normalized.slice(0, 32);
|
|
54
128
|
}
|
|
129
|
+
// [FIX #11] Actually call cleanupPending to prevent unbounded map growth
|
|
55
130
|
function cleanupPending() {
|
|
56
131
|
const now = Date.now();
|
|
57
132
|
for (const [key, val] of pendingDecisions) {
|
|
@@ -64,13 +139,15 @@ function formatWarningActionably(w) {
|
|
|
64
139
|
const pct = Math.round(w.failureRate * 100);
|
|
65
140
|
return `⚠️ ${w.type} has ${pct}% failure rate — check what went wrong last time before proceeding`;
|
|
66
141
|
}
|
|
142
|
+
// [FIX #4] Log orient refresh failures instead of silently ignoring
|
|
67
143
|
async function refreshOrientWarnings() {
|
|
68
144
|
try {
|
|
69
145
|
const r = await (0, index_1.marrowOrient)(API_KEY, BASE_URL, undefined, SESSION_ID);
|
|
70
146
|
cachedOrientWarnings = r.warnings;
|
|
71
147
|
}
|
|
72
|
-
catch {
|
|
73
|
-
|
|
148
|
+
catch (err) {
|
|
149
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
150
|
+
process.stderr.write(`[marrow] Warning: failed to refresh orient warnings: ${msg}\n`);
|
|
74
151
|
}
|
|
75
152
|
}
|
|
76
153
|
// Initial orient
|
|
@@ -82,29 +159,31 @@ refreshOrientWarnings().then(() => {
|
|
|
82
159
|
// Auto-commit tracking for session close
|
|
83
160
|
let lastDecisionId = null;
|
|
84
161
|
let lastCommitted = false;
|
|
162
|
+
// [FIX #5] Log auto-commit failures instead of silently ignoring; remove broken AbortController
|
|
85
163
|
async function autoCommitOnClose() {
|
|
86
164
|
if (lastDecisionId && !lastCommitted) {
|
|
87
165
|
try {
|
|
88
|
-
const controller = new AbortController();
|
|
89
|
-
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
90
166
|
await (0, index_1.marrowCommit)(API_KEY, BASE_URL, {
|
|
91
167
|
decision_id: lastDecisionId,
|
|
92
168
|
success: false,
|
|
93
169
|
outcome: 'Session ended without explicit commit',
|
|
94
170
|
}, SESSION_ID);
|
|
95
|
-
clearTimeout(timeout);
|
|
96
171
|
}
|
|
97
|
-
catch {
|
|
98
|
-
|
|
172
|
+
catch (err) {
|
|
173
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
174
|
+
process.stderr.write(`[marrow] Warning: auto-commit on close failed: ${msg}\n`);
|
|
99
175
|
}
|
|
100
176
|
}
|
|
101
177
|
}
|
|
102
|
-
|
|
178
|
+
// [FIX #10] Handle both SIGTERM and SIGINT for clean shutdown
|
|
179
|
+
async function gracefulShutdown() {
|
|
103
180
|
const forceExit = setTimeout(() => process.exit(0), 5000);
|
|
104
181
|
forceExit.unref();
|
|
105
182
|
await autoCommitOnClose();
|
|
106
183
|
process.exit(0);
|
|
107
|
-
}
|
|
184
|
+
}
|
|
185
|
+
process.on('SIGTERM', gracefulShutdown);
|
|
186
|
+
process.on('SIGINT', gracefulShutdown);
|
|
108
187
|
function send(response) {
|
|
109
188
|
process.stdout.write(JSON.stringify(response) + '\n');
|
|
110
189
|
}
|
|
@@ -114,7 +193,31 @@ function success(id, result) {
|
|
|
114
193
|
function error(id, code, message) {
|
|
115
194
|
send({ jsonrpc: '2.0', id, error: { code, message } });
|
|
116
195
|
}
|
|
117
|
-
//
|
|
196
|
+
// [FIX #9] Runtime validation helper for required string params
|
|
197
|
+
function requireString(args, name) {
|
|
198
|
+
const val = args[name];
|
|
199
|
+
if (typeof val !== 'string' || !val.trim()) {
|
|
200
|
+
throw new Error(`"${name}" is required and must be a non-empty string`);
|
|
201
|
+
}
|
|
202
|
+
return val;
|
|
203
|
+
}
|
|
204
|
+
// [FIX #6 & #7] Safe JSON response helper for memory API functions
|
|
205
|
+
async function safeMemoryResponse(res) {
|
|
206
|
+
if (!res.ok) {
|
|
207
|
+
let detail = '';
|
|
208
|
+
try {
|
|
209
|
+
detail = await res.text();
|
|
210
|
+
}
|
|
211
|
+
catch { /* ignore */ }
|
|
212
|
+
throw new Error(`API error ${res.status}: ${detail.slice(0, 200)}`);
|
|
213
|
+
}
|
|
214
|
+
const json = await res.json();
|
|
215
|
+
if (json.error) {
|
|
216
|
+
throw new Error(json.error);
|
|
217
|
+
}
|
|
218
|
+
return json;
|
|
219
|
+
}
|
|
220
|
+
// Memory API functions — all patched with safeMemoryResponse and validatePathParam
|
|
118
221
|
async function marrowListMemories(apiKey, baseUrl, params, sessionId) {
|
|
119
222
|
const qs = new URLSearchParams();
|
|
120
223
|
if (params?.status)
|
|
@@ -131,21 +234,23 @@ async function marrowListMemories(apiKey, baseUrl, params, sessionId) {
|
|
|
131
234
|
...(sessionId ? { 'X-Marrow-Session-Id': sessionId } : {}),
|
|
132
235
|
},
|
|
133
236
|
});
|
|
134
|
-
const json = await res
|
|
237
|
+
const json = await safeMemoryResponse(res);
|
|
135
238
|
return json.data?.memories || [];
|
|
136
239
|
}
|
|
137
240
|
async function marrowGetMemory(apiKey, baseUrl, id, sessionId) {
|
|
138
|
-
const
|
|
241
|
+
const safeId = (0, index_1.validatePathParam)(id, 'id');
|
|
242
|
+
const res = await fetch(`${baseUrl}/v1/memories/${safeId}`, {
|
|
139
243
|
headers: {
|
|
140
244
|
Authorization: `Bearer ${apiKey}`,
|
|
141
245
|
...(sessionId ? { 'X-Marrow-Session-Id': sessionId } : {}),
|
|
142
246
|
},
|
|
143
247
|
});
|
|
144
|
-
const json = await res
|
|
248
|
+
const json = await safeMemoryResponse(res);
|
|
145
249
|
return json.data?.memory || null;
|
|
146
250
|
}
|
|
147
251
|
async function marrowUpdateMemory(apiKey, baseUrl, id, patch, sessionId) {
|
|
148
|
-
const
|
|
252
|
+
const safeId = (0, index_1.validatePathParam)(id, 'id');
|
|
253
|
+
const res = await fetch(`${baseUrl}/v1/memories/${safeId}`, {
|
|
149
254
|
method: 'PATCH',
|
|
150
255
|
headers: {
|
|
151
256
|
Authorization: `Bearer ${apiKey}`,
|
|
@@ -154,11 +259,12 @@ async function marrowUpdateMemory(apiKey, baseUrl, id, patch, sessionId) {
|
|
|
154
259
|
},
|
|
155
260
|
body: JSON.stringify(patch),
|
|
156
261
|
});
|
|
157
|
-
const json = await res
|
|
158
|
-
return json.data
|
|
262
|
+
const json = await safeMemoryResponse(res);
|
|
263
|
+
return json.data?.memory;
|
|
159
264
|
}
|
|
160
265
|
async function marrowDeleteMemory(apiKey, baseUrl, id, meta, sessionId) {
|
|
161
|
-
const
|
|
266
|
+
const safeId = (0, index_1.validatePathParam)(id, 'id');
|
|
267
|
+
const res = await fetch(`${baseUrl}/v1/memories/${safeId}`, {
|
|
162
268
|
method: 'DELETE',
|
|
163
269
|
headers: {
|
|
164
270
|
Authorization: `Bearer ${apiKey}`,
|
|
@@ -167,11 +273,12 @@ async function marrowDeleteMemory(apiKey, baseUrl, id, meta, sessionId) {
|
|
|
167
273
|
},
|
|
168
274
|
body: JSON.stringify(meta || {}),
|
|
169
275
|
});
|
|
170
|
-
const json = await res
|
|
171
|
-
return json.data
|
|
276
|
+
const json = await safeMemoryResponse(res);
|
|
277
|
+
return json.data?.memory;
|
|
172
278
|
}
|
|
173
279
|
async function marrowMarkOutdated(apiKey, baseUrl, id, meta, sessionId) {
|
|
174
|
-
const
|
|
280
|
+
const safeId = (0, index_1.validatePathParam)(id, 'id');
|
|
281
|
+
const res = await fetch(`${baseUrl}/v1/memories/${safeId}/outdated`, {
|
|
175
282
|
method: 'POST',
|
|
176
283
|
headers: {
|
|
177
284
|
Authorization: `Bearer ${apiKey}`,
|
|
@@ -180,11 +287,12 @@ async function marrowMarkOutdated(apiKey, baseUrl, id, meta, sessionId) {
|
|
|
180
287
|
},
|
|
181
288
|
body: JSON.stringify(meta || {}),
|
|
182
289
|
});
|
|
183
|
-
const json = await res
|
|
184
|
-
return json.data
|
|
290
|
+
const json = await safeMemoryResponse(res);
|
|
291
|
+
return json.data?.memory;
|
|
185
292
|
}
|
|
186
293
|
async function marrowSupersedeMemory(apiKey, baseUrl, id, replacement, sessionId) {
|
|
187
|
-
const
|
|
294
|
+
const safeId = (0, index_1.validatePathParam)(id, 'id');
|
|
295
|
+
const res = await fetch(`${baseUrl}/v1/memories/${safeId}/supersede`, {
|
|
188
296
|
method: 'POST',
|
|
189
297
|
headers: {
|
|
190
298
|
Authorization: `Bearer ${apiKey}`,
|
|
@@ -193,11 +301,12 @@ async function marrowSupersedeMemory(apiKey, baseUrl, id, replacement, sessionId
|
|
|
193
301
|
},
|
|
194
302
|
body: JSON.stringify(replacement),
|
|
195
303
|
});
|
|
196
|
-
const json = await res
|
|
304
|
+
const json = await safeMemoryResponse(res);
|
|
197
305
|
return json.data;
|
|
198
306
|
}
|
|
199
307
|
async function marrowShareMemory(apiKey, baseUrl, id, agentIds, actor, sessionId) {
|
|
200
|
-
const
|
|
308
|
+
const safeId = (0, index_1.validatePathParam)(id, 'id');
|
|
309
|
+
const res = await fetch(`${baseUrl}/v1/memories/${safeId}/share`, {
|
|
201
310
|
method: 'POST',
|
|
202
311
|
headers: {
|
|
203
312
|
Authorization: `Bearer ${apiKey}`,
|
|
@@ -206,8 +315,8 @@ async function marrowShareMemory(apiKey, baseUrl, id, agentIds, actor, sessionId
|
|
|
206
315
|
},
|
|
207
316
|
body: JSON.stringify({ agent_ids: agentIds, actor }),
|
|
208
317
|
});
|
|
209
|
-
const json = await res
|
|
210
|
-
return json.data
|
|
318
|
+
const json = await safeMemoryResponse(res);
|
|
319
|
+
return json.data?.memory;
|
|
211
320
|
}
|
|
212
321
|
async function marrowExportMemories(apiKey, baseUrl, params, sessionId) {
|
|
213
322
|
const qs = new URLSearchParams();
|
|
@@ -223,7 +332,7 @@ async function marrowExportMemories(apiKey, baseUrl, params, sessionId) {
|
|
|
223
332
|
...(sessionId ? { 'X-Marrow-Session-Id': sessionId } : {}),
|
|
224
333
|
},
|
|
225
334
|
});
|
|
226
|
-
const json = await res
|
|
335
|
+
const json = await safeMemoryResponse(res);
|
|
227
336
|
return json.data;
|
|
228
337
|
}
|
|
229
338
|
async function marrowImportMemories(apiKey, baseUrl, memories, mode, sessionId) {
|
|
@@ -236,7 +345,7 @@ async function marrowImportMemories(apiKey, baseUrl, memories, mode, sessionId)
|
|
|
236
345
|
},
|
|
237
346
|
body: JSON.stringify({ memories, mode }),
|
|
238
347
|
});
|
|
239
|
-
const json = await res
|
|
348
|
+
const json = await safeMemoryResponse(res);
|
|
240
349
|
return json.data;
|
|
241
350
|
}
|
|
242
351
|
async function marrowRetrieveMemories(apiKey, baseUrl, query, params, sessionId) {
|
|
@@ -262,10 +371,10 @@ async function marrowRetrieveMemories(apiKey, baseUrl, query, params, sessionId)
|
|
|
262
371
|
...(sessionId ? { 'X-Marrow-Session-Id': sessionId } : {}),
|
|
263
372
|
},
|
|
264
373
|
});
|
|
265
|
-
const json = await res
|
|
374
|
+
const json = await safeMemoryResponse(res);
|
|
266
375
|
return json.data;
|
|
267
376
|
}
|
|
268
|
-
// Tool definitions
|
|
377
|
+
// Tool definitions (unchanged)
|
|
269
378
|
const TOOLS = [
|
|
270
379
|
{
|
|
271
380
|
name: 'marrow_orient',
|
|
@@ -298,35 +407,17 @@ const TOOLS = [
|
|
|
298
407
|
inputSchema: {
|
|
299
408
|
type: 'object',
|
|
300
409
|
properties: {
|
|
301
|
-
action: {
|
|
302
|
-
type: 'string',
|
|
303
|
-
description: 'What the agent is about to do',
|
|
304
|
-
},
|
|
410
|
+
action: { type: 'string', description: 'What the agent is about to do' },
|
|
305
411
|
type: {
|
|
306
412
|
type: 'string',
|
|
307
413
|
enum: ['implementation', 'security', 'architecture', 'process', 'general'],
|
|
308
414
|
description: 'Type of action (default: general)',
|
|
309
415
|
},
|
|
310
|
-
context: {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
},
|
|
314
|
-
|
|
315
|
-
type: 'string',
|
|
316
|
-
description: 'decision_id from previous think() call — auto-commits that session',
|
|
317
|
-
},
|
|
318
|
-
previous_success: {
|
|
319
|
-
type: 'boolean',
|
|
320
|
-
description: 'Did the previous action succeed?',
|
|
321
|
-
},
|
|
322
|
-
previous_outcome: {
|
|
323
|
-
type: 'string',
|
|
324
|
-
description: 'What happened in the previous action (required if previous_decision_id provided)',
|
|
325
|
-
},
|
|
326
|
-
checkLoop: {
|
|
327
|
-
type: 'boolean',
|
|
328
|
-
description: 'Enable loop detection: warns if you are about to retry a failed approach. Recommended: true.',
|
|
329
|
-
},
|
|
416
|
+
context: { type: 'object', description: 'Optional metadata about the current situation' },
|
|
417
|
+
previous_decision_id: { type: 'string', description: 'decision_id from previous think() call — auto-commits that session' },
|
|
418
|
+
previous_success: { type: 'boolean', description: 'Did the previous action succeed?' },
|
|
419
|
+
previous_outcome: { type: 'string', description: 'What happened in the previous action (required if previous_decision_id provided)' },
|
|
420
|
+
checkLoop: { type: 'boolean', description: 'Enable loop detection: warns if you are about to retry a failed approach. Recommended: true.' },
|
|
330
421
|
},
|
|
331
422
|
required: ['action'],
|
|
332
423
|
},
|
|
@@ -339,22 +430,10 @@ const TOOLS = [
|
|
|
339
430
|
inputSchema: {
|
|
340
431
|
type: 'object',
|
|
341
432
|
properties: {
|
|
342
|
-
decision_id: {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
},
|
|
346
|
-
success: {
|
|
347
|
-
type: 'boolean',
|
|
348
|
-
description: 'Did the action succeed?',
|
|
349
|
-
},
|
|
350
|
-
outcome: {
|
|
351
|
-
type: 'string',
|
|
352
|
-
description: 'What happened — be specific, this trains the hive',
|
|
353
|
-
},
|
|
354
|
-
caused_by: {
|
|
355
|
-
type: 'string',
|
|
356
|
-
description: 'Optional: what caused this action',
|
|
357
|
-
},
|
|
433
|
+
decision_id: { type: 'string', description: 'decision_id from the marrow_think call' },
|
|
434
|
+
success: { type: 'boolean', description: 'Did the action succeed?' },
|
|
435
|
+
outcome: { type: 'string', description: 'What happened — be specific, this trains the hive' },
|
|
436
|
+
caused_by: { type: 'string', description: 'Optional: what caused this action' },
|
|
358
437
|
},
|
|
359
438
|
required: ['decision_id', 'success', 'outcome'],
|
|
360
439
|
},
|
|
@@ -366,18 +445,9 @@ const TOOLS = [
|
|
|
366
445
|
inputSchema: {
|
|
367
446
|
type: 'object',
|
|
368
447
|
properties: {
|
|
369
|
-
description: {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
},
|
|
373
|
-
success: {
|
|
374
|
-
type: 'boolean',
|
|
375
|
-
description: 'Whether it succeeded',
|
|
376
|
-
},
|
|
377
|
-
outcome: {
|
|
378
|
-
type: 'string',
|
|
379
|
-
description: 'One-line summary of what happened',
|
|
380
|
-
},
|
|
448
|
+
description: { type: 'string', description: 'What the agent did' },
|
|
449
|
+
success: { type: 'boolean', description: 'Whether it succeeded' },
|
|
450
|
+
outcome: { type: 'string', description: 'One-line summary of what happened' },
|
|
381
451
|
type: {
|
|
382
452
|
type: 'string',
|
|
383
453
|
enum: ['implementation', 'security', 'architecture', 'process', 'general'],
|
|
@@ -396,18 +466,9 @@ const TOOLS = [
|
|
|
396
466
|
inputSchema: {
|
|
397
467
|
type: 'object',
|
|
398
468
|
properties: {
|
|
399
|
-
action: {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
},
|
|
403
|
-
outcome: {
|
|
404
|
-
type: 'string',
|
|
405
|
-
description: 'What happened (if already done). Omit to log intent only.',
|
|
406
|
-
},
|
|
407
|
-
success: {
|
|
408
|
-
type: 'boolean',
|
|
409
|
-
description: 'Did it succeed (default: true)',
|
|
410
|
-
},
|
|
469
|
+
action: { type: 'string', description: 'What you are about to do or just did' },
|
|
470
|
+
outcome: { type: 'string', description: 'What happened (if already done). Omit to log intent only.' },
|
|
471
|
+
success: { type: 'boolean', description: 'Did it succeed (default: true)' },
|
|
411
472
|
type: {
|
|
412
473
|
type: 'string',
|
|
413
474
|
enum: ['implementation', 'security', 'architecture', 'process', 'general'],
|
|
@@ -425,10 +486,7 @@ const TOOLS = [
|
|
|
425
486
|
inputSchema: {
|
|
426
487
|
type: 'object',
|
|
427
488
|
properties: {
|
|
428
|
-
query: {
|
|
429
|
-
type: 'string',
|
|
430
|
-
description: 'Plain English question about your decision history',
|
|
431
|
-
},
|
|
489
|
+
query: { type: 'string', description: 'Plain English question about your decision history' },
|
|
432
490
|
},
|
|
433
491
|
required: ['query'],
|
|
434
492
|
},
|
|
@@ -436,11 +494,7 @@ const TOOLS = [
|
|
|
436
494
|
{
|
|
437
495
|
name: 'marrow_status',
|
|
438
496
|
description: 'Check Marrow platform health and status.',
|
|
439
|
-
inputSchema: {
|
|
440
|
-
type: 'object',
|
|
441
|
-
properties: {},
|
|
442
|
-
required: [],
|
|
443
|
-
},
|
|
497
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
444
498
|
},
|
|
445
499
|
{
|
|
446
500
|
name: 'marrow_list_memories',
|
|
@@ -459,13 +513,7 @@ const TOOLS = [
|
|
|
459
513
|
{
|
|
460
514
|
name: 'marrow_get_memory',
|
|
461
515
|
description: 'Get a single memory by ID.',
|
|
462
|
-
inputSchema: {
|
|
463
|
-
type: 'object',
|
|
464
|
-
properties: {
|
|
465
|
-
id: { type: 'string', description: 'Memory ID' },
|
|
466
|
-
},
|
|
467
|
-
required: ['id'],
|
|
468
|
-
},
|
|
516
|
+
inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Memory ID' } }, required: ['id'] },
|
|
469
517
|
},
|
|
470
518
|
{
|
|
471
519
|
name: 'marrow_update_memory',
|
|
@@ -589,29 +637,12 @@ const TOOLS = [
|
|
|
589
637
|
inputSchema: {
|
|
590
638
|
type: 'object',
|
|
591
639
|
properties: {
|
|
592
|
-
action: {
|
|
593
|
-
type: 'string',
|
|
594
|
-
enum: ['register', 'list', 'get', 'update', 'start', 'advance', 'instances'],
|
|
595
|
-
description: 'Workflow action to perform',
|
|
596
|
-
},
|
|
640
|
+
action: { type: 'string', enum: ['register', 'list', 'get', 'update', 'start', 'advance', 'instances'], description: 'Workflow action to perform' },
|
|
597
641
|
workflowId: { type: 'string', description: 'Workflow ID (required for get/start/advance/instances)' },
|
|
598
642
|
instanceId: { type: 'string', description: 'Instance ID (required for advance)' },
|
|
599
643
|
name: { type: 'string', description: 'Workflow name (for register)' },
|
|
600
644
|
description: { type: 'string', description: 'Workflow description (for register/update)' },
|
|
601
|
-
steps: {
|
|
602
|
-
type: 'array',
|
|
603
|
-
description: 'Step definitions (for register)',
|
|
604
|
-
items: {
|
|
605
|
-
type: 'object',
|
|
606
|
-
properties: {
|
|
607
|
-
step: { type: 'number', description: 'Step order (1, 2, 3...)' },
|
|
608
|
-
agent_role: { type: 'string', description: 'Expected agent role (e.g., "builder", "auditor")' },
|
|
609
|
-
action_type: { type: 'string', description: 'Action type (e.g., "build", "audit", "patch")' },
|
|
610
|
-
description: { type: 'string', description: 'Step description' },
|
|
611
|
-
},
|
|
612
|
-
required: ['step', 'description'],
|
|
613
|
-
},
|
|
614
|
-
},
|
|
645
|
+
steps: { type: 'array', description: 'Step definitions (for register)', items: { type: 'object', properties: { step: { type: 'number', description: 'Step order (1, 2, 3...)' }, agent_role: { type: 'string', description: 'Expected agent role (e.g., "builder", "auditor")' }, action_type: { type: 'string', description: 'Action type (e.g., "build", "audit", "patch")' }, description: { type: 'string', description: 'Step description' } }, required: ['step', 'description'] } },
|
|
615
646
|
tags: { type: 'array', items: { type: 'string' }, description: 'Tags (for register)' },
|
|
616
647
|
agentId: { type: 'string', description: 'Agent ID starting the workflow (for start)' },
|
|
617
648
|
context: { type: 'object', description: 'Workflow context (for start)' },
|
|
@@ -629,13 +660,18 @@ const TOOLS = [
|
|
|
629
660
|
// Request handler
|
|
630
661
|
async function handleRequest(req) {
|
|
631
662
|
const { id, method, params } = req;
|
|
663
|
+
// [FIX #15] Enforce initialize-first per MCP spec
|
|
664
|
+
if (!initialized && method !== 'initialize') {
|
|
665
|
+
error(id, -32002, 'Server not initialized. Send initialize first.');
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
632
668
|
try {
|
|
633
669
|
if (method === 'initialize') {
|
|
634
670
|
initialized = true;
|
|
635
671
|
success(id, {
|
|
636
672
|
protocolVersion: '2024-11-05',
|
|
637
673
|
capabilities: { tools: {}, prompts: {} },
|
|
638
|
-
serverInfo: { name: 'marrow', version: '
|
|
674
|
+
serverInfo: { name: 'marrow', version: '3.0.9' },
|
|
639
675
|
});
|
|
640
676
|
// Auto-enroll: emit enrollment notification on connection
|
|
641
677
|
if (AUTO_ENROLL) {
|
|
@@ -656,7 +692,6 @@ async function handleRequest(req) {
|
|
|
656
692
|
return;
|
|
657
693
|
}
|
|
658
694
|
if (method === 'prompts/list') {
|
|
659
|
-
// Auto-enroll: only expose marrow-always-on prompt when MARROW_AUTO_ENROLL is set
|
|
660
695
|
if (AUTO_ENROLL) {
|
|
661
696
|
success(id, {
|
|
662
697
|
prompts: [
|
|
@@ -675,7 +710,6 @@ async function handleRequest(req) {
|
|
|
675
710
|
}
|
|
676
711
|
if (method === 'prompts/get') {
|
|
677
712
|
const promptName = params?.name;
|
|
678
|
-
// Auto-enroll gate: only serve marrow-always-on when enabled
|
|
679
713
|
if (promptName !== 'marrow-always-on' || !AUTO_ENROLL) {
|
|
680
714
|
error(id, -32602, 'Unknown prompt');
|
|
681
715
|
return;
|
|
@@ -744,7 +778,6 @@ This is not optional overhead — it's how you stop repeating the same failures.
|
|
|
744
778
|
if (toolName === 'marrow_orient') {
|
|
745
779
|
orientCallCount++;
|
|
746
780
|
const result = await (0, index_1.marrowOrient)(API_KEY, BASE_URL, { taskType: args.taskType, autoWarn: args.autoWarn ?? true }, SESSION_ID);
|
|
747
|
-
// Auto-enroll: on first orient call, prepend enrollment instructions
|
|
748
781
|
if (AUTO_ENROLL && orientCallCount === 1) {
|
|
749
782
|
const enrollmentText = `\n\n📋 **Marrow Auto-Enroll Active**\n\n` +
|
|
750
783
|
`You have Marrow — a collective memory that tells you what went wrong last time before you do it again.\n\n` +
|
|
@@ -771,8 +804,10 @@ This is not optional overhead — it's how you stop repeating the same failures.
|
|
|
771
804
|
return;
|
|
772
805
|
}
|
|
773
806
|
if (toolName === 'marrow_think') {
|
|
807
|
+
// [FIX #9] Validate required param
|
|
808
|
+
const action = requireString(args, 'action');
|
|
774
809
|
const result = await (0, index_1.marrowThink)(API_KEY, BASE_URL, {
|
|
775
|
-
action
|
|
810
|
+
action,
|
|
776
811
|
type: args.type,
|
|
777
812
|
context: args.context,
|
|
778
813
|
previous_decision_id: args.previous_decision_id,
|
|
@@ -799,7 +834,6 @@ This is not optional overhead — it's how you stop repeating the same failures.
|
|
|
799
834
|
...existingInsights,
|
|
800
835
|
];
|
|
801
836
|
}
|
|
802
|
-
// Track for auto-commit
|
|
803
837
|
lastDecisionId = result.decision_id;
|
|
804
838
|
lastCommitted = false;
|
|
805
839
|
success(id, {
|
|
@@ -808,10 +842,16 @@ This is not optional overhead — it's how you stop repeating the same failures.
|
|
|
808
842
|
return;
|
|
809
843
|
}
|
|
810
844
|
if (toolName === 'marrow_commit') {
|
|
845
|
+
// [FIX #9] Validate required params
|
|
846
|
+
const decision_id = requireString(args, 'decision_id');
|
|
847
|
+
const outcome = requireString(args, 'outcome');
|
|
848
|
+
if (typeof args.success !== 'boolean') {
|
|
849
|
+
throw new Error('"success" is required and must be a boolean');
|
|
850
|
+
}
|
|
811
851
|
const result = await (0, index_1.marrowCommit)(API_KEY, BASE_URL, {
|
|
812
|
-
decision_id
|
|
852
|
+
decision_id,
|
|
813
853
|
success: args.success,
|
|
814
|
-
outcome
|
|
854
|
+
outcome,
|
|
815
855
|
caused_by: args.caused_by,
|
|
816
856
|
}, SESSION_ID);
|
|
817
857
|
lastCommitted = true;
|
|
@@ -822,61 +862,83 @@ This is not optional overhead — it's how you stop repeating the same failures.
|
|
|
822
862
|
return;
|
|
823
863
|
}
|
|
824
864
|
if (toolName === 'marrow_run') {
|
|
825
|
-
//
|
|
826
|
-
|
|
827
|
-
const
|
|
828
|
-
|
|
865
|
+
// [FIX #9] Validate required params
|
|
866
|
+
const description = requireString(args, 'description');
|
|
867
|
+
const outcome = requireString(args, 'outcome');
|
|
868
|
+
// [FIX #16] Handle partial failures — return think result even if commit fails
|
|
869
|
+
let thinkResult = null;
|
|
870
|
+
try {
|
|
871
|
+
await (0, index_1.marrowOrient)(API_KEY, BASE_URL, undefined, SESSION_ID);
|
|
872
|
+
}
|
|
873
|
+
catch (err) {
|
|
874
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
875
|
+
process.stderr.write(`[marrow] marrow_run orient failed (continuing): ${msg}\n`);
|
|
876
|
+
}
|
|
877
|
+
thinkResult = await (0, index_1.marrowThink)(API_KEY, BASE_URL, {
|
|
878
|
+
action: description,
|
|
829
879
|
type: args.type || 'general',
|
|
830
880
|
}, SESSION_ID);
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
881
|
+
let commitResult = null;
|
|
882
|
+
try {
|
|
883
|
+
commitResult = await (0, index_1.marrowCommit)(API_KEY, BASE_URL, {
|
|
884
|
+
decision_id: thinkResult.decision_id,
|
|
885
|
+
success: args.success ?? true,
|
|
886
|
+
outcome,
|
|
887
|
+
}, SESSION_ID);
|
|
888
|
+
}
|
|
889
|
+
catch (err) {
|
|
890
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
891
|
+
process.stderr.write(`[marrow] marrow_run commit failed: ${msg}\n`);
|
|
892
|
+
success(id, {
|
|
893
|
+
content: [{
|
|
894
|
+
type: 'text',
|
|
895
|
+
text: JSON.stringify({
|
|
896
|
+
think: thinkResult,
|
|
897
|
+
commit: null,
|
|
898
|
+
commit_error: msg,
|
|
899
|
+
decision_id: thinkResult.decision_id,
|
|
900
|
+
}, null, 2),
|
|
901
|
+
}],
|
|
902
|
+
});
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
836
905
|
success(id, {
|
|
837
|
-
content: [
|
|
838
|
-
{
|
|
906
|
+
content: [{
|
|
839
907
|
type: 'text',
|
|
840
908
|
text: JSON.stringify({ think: thinkResult, commit: commitResult }, null, 2),
|
|
841
|
-
},
|
|
842
|
-
],
|
|
909
|
+
}],
|
|
843
910
|
});
|
|
844
911
|
return;
|
|
845
912
|
}
|
|
846
913
|
if (toolName === 'marrow_auto') {
|
|
847
|
-
//
|
|
848
|
-
|
|
849
|
-
const action = args.action;
|
|
914
|
+
// [FIX #9] Validate required param
|
|
915
|
+
const action = requireString(args, 'action');
|
|
850
916
|
const outcome = args.outcome;
|
|
851
917
|
const outcomeSuccess = args.success ?? true;
|
|
852
918
|
const type = args.type || 'general';
|
|
853
|
-
//
|
|
919
|
+
// [FIX #11] Cleanup pending decisions on each auto call
|
|
920
|
+
cleanupPending();
|
|
921
|
+
// [FIX #8] Include pending flag so agent knows logging is deferred
|
|
854
922
|
const response = {
|
|
855
923
|
action,
|
|
856
924
|
outcome: outcome || 'pending',
|
|
857
925
|
warnings: cachedOrientWarnings.map(formatWarningActionably),
|
|
926
|
+
logging: 'deferred',
|
|
858
927
|
};
|
|
859
928
|
// Fire-and-forget the actual API calls
|
|
860
929
|
(async () => {
|
|
861
930
|
try {
|
|
862
931
|
if (!outcome) {
|
|
863
|
-
// Intent only
|
|
864
932
|
await (0, index_1.marrowThink)(API_KEY, BASE_URL, { action, type }, SESSION_ID);
|
|
865
933
|
}
|
|
866
934
|
else {
|
|
867
|
-
// Full loop
|
|
868
935
|
const thinkResult = await (0, index_1.marrowThink)(API_KEY, BASE_URL, { action, type }, SESSION_ID);
|
|
869
|
-
await (0, index_1.marrowCommit)(API_KEY, BASE_URL, {
|
|
870
|
-
decision_id: thinkResult.decision_id,
|
|
871
|
-
success: outcomeSuccess,
|
|
872
|
-
outcome,
|
|
873
|
-
}, SESSION_ID);
|
|
936
|
+
await (0, index_1.marrowCommit)(API_KEY, BASE_URL, { decision_id: thinkResult.decision_id, success: outcomeSuccess, outcome }, SESSION_ID);
|
|
874
937
|
}
|
|
875
938
|
}
|
|
876
939
|
catch (err) {
|
|
877
|
-
// Log to stderr so agent can see it in logs
|
|
878
940
|
const msg = err instanceof Error ? err.message : String(err);
|
|
879
|
-
process.stderr.write(`[marrow] marrow_auto failed: ${msg}\n`);
|
|
941
|
+
process.stderr.write(`[marrow] marrow_auto background logging failed: ${msg}\n`);
|
|
880
942
|
}
|
|
881
943
|
})();
|
|
882
944
|
success(id, {
|
|
@@ -885,7 +947,8 @@ This is not optional overhead — it's how you stop repeating the same failures.
|
|
|
885
947
|
return;
|
|
886
948
|
}
|
|
887
949
|
if (toolName === 'marrow_ask') {
|
|
888
|
-
const
|
|
950
|
+
const query = requireString(args, 'query');
|
|
951
|
+
const result = await (0, index_1.marrowAsk)(API_KEY, BASE_URL, { query }, SESSION_ID);
|
|
889
952
|
success(id, {
|
|
890
953
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
891
954
|
});
|
|
@@ -898,104 +961,63 @@ This is not optional overhead — it's how you stop repeating the same failures.
|
|
|
898
961
|
});
|
|
899
962
|
return;
|
|
900
963
|
}
|
|
901
|
-
// Memory control tools
|
|
964
|
+
// Memory control tools — all use requireString for id validation
|
|
902
965
|
if (toolName === 'marrow_list_memories') {
|
|
903
|
-
const result = await marrowListMemories(API_KEY, BASE_URL, {
|
|
904
|
-
|
|
905
|
-
query: args.query,
|
|
906
|
-
limit: args.limit,
|
|
907
|
-
agentId: args.agentId,
|
|
908
|
-
}, SESSION_ID);
|
|
909
|
-
success(id, {
|
|
910
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
911
|
-
});
|
|
966
|
+
const result = await marrowListMemories(API_KEY, BASE_URL, { status: args.status, query: args.query, limit: args.limit, agentId: args.agentId }, SESSION_ID);
|
|
967
|
+
success(id, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] });
|
|
912
968
|
return;
|
|
913
969
|
}
|
|
914
970
|
if (toolName === 'marrow_get_memory') {
|
|
915
|
-
const
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
});
|
|
971
|
+
const memId = requireString(args, 'id');
|
|
972
|
+
const result = await marrowGetMemory(API_KEY, BASE_URL, memId, SESSION_ID);
|
|
973
|
+
success(id, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] });
|
|
919
974
|
return;
|
|
920
975
|
}
|
|
921
976
|
if (toolName === 'marrow_update_memory') {
|
|
922
|
-
const
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
tags: args.tags,
|
|
926
|
-
actor: args.actor,
|
|
927
|
-
note: args.note,
|
|
928
|
-
}, SESSION_ID);
|
|
929
|
-
success(id, {
|
|
930
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
931
|
-
});
|
|
977
|
+
const memId = requireString(args, 'id');
|
|
978
|
+
const result = await marrowUpdateMemory(API_KEY, BASE_URL, memId, { text: args.text, source: args.source, tags: args.tags, actor: args.actor, note: args.note }, SESSION_ID);
|
|
979
|
+
success(id, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] });
|
|
932
980
|
return;
|
|
933
981
|
}
|
|
934
982
|
if (toolName === 'marrow_delete_memory') {
|
|
935
|
-
const
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
});
|
|
983
|
+
const memId = requireString(args, 'id');
|
|
984
|
+
const result = await marrowDeleteMemory(API_KEY, BASE_URL, memId, { actor: args.actor, note: args.note }, SESSION_ID);
|
|
985
|
+
success(id, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] });
|
|
939
986
|
return;
|
|
940
987
|
}
|
|
941
988
|
if (toolName === 'marrow_mark_outdated') {
|
|
942
|
-
const
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
});
|
|
989
|
+
const memId = requireString(args, 'id');
|
|
990
|
+
const result = await marrowMarkOutdated(API_KEY, BASE_URL, memId, { actor: args.actor, note: args.note }, SESSION_ID);
|
|
991
|
+
success(id, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] });
|
|
946
992
|
return;
|
|
947
993
|
}
|
|
948
994
|
if (toolName === 'marrow_supersede_memory') {
|
|
949
|
-
const
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
actor: args.actor,
|
|
954
|
-
note: args.note,
|
|
955
|
-
}, SESSION_ID);
|
|
956
|
-
success(id, {
|
|
957
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
958
|
-
});
|
|
995
|
+
const memId = requireString(args, 'id');
|
|
996
|
+
const newText = requireString(args, 'text');
|
|
997
|
+
const result = await marrowSupersedeMemory(API_KEY, BASE_URL, memId, { text: newText, source: args.source, tags: args.tags, actor: args.actor, note: args.note }, SESSION_ID);
|
|
998
|
+
success(id, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] });
|
|
959
999
|
return;
|
|
960
1000
|
}
|
|
961
1001
|
if (toolName === 'marrow_share_memory') {
|
|
962
|
-
const
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
});
|
|
1002
|
+
const memId = requireString(args, 'id');
|
|
1003
|
+
const result = await marrowShareMemory(API_KEY, BASE_URL, memId, args.agentIds || [], args.actor, SESSION_ID);
|
|
1004
|
+
success(id, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] });
|
|
966
1005
|
return;
|
|
967
1006
|
}
|
|
968
1007
|
if (toolName === 'marrow_export_memories') {
|
|
969
|
-
const result = await marrowExportMemories(API_KEY, BASE_URL, {
|
|
970
|
-
|
|
971
|
-
status: args.status,
|
|
972
|
-
tags: args.tags,
|
|
973
|
-
}, SESSION_ID);
|
|
974
|
-
success(id, {
|
|
975
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
976
|
-
});
|
|
1008
|
+
const result = await marrowExportMemories(API_KEY, BASE_URL, { format: args.format, status: args.status, tags: args.tags }, SESSION_ID);
|
|
1009
|
+
success(id, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] });
|
|
977
1010
|
return;
|
|
978
1011
|
}
|
|
979
1012
|
if (toolName === 'marrow_import_memories') {
|
|
980
1013
|
const result = await marrowImportMemories(API_KEY, BASE_URL, args.memories || [], args.mode || 'merge', SESSION_ID);
|
|
981
|
-
success(id, {
|
|
982
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
983
|
-
});
|
|
1014
|
+
success(id, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] });
|
|
984
1015
|
return;
|
|
985
1016
|
}
|
|
986
1017
|
if (toolName === 'marrow_retrieve_memories') {
|
|
987
|
-
const
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
to: args.to,
|
|
991
|
-
tags: args.tags,
|
|
992
|
-
source: args.source,
|
|
993
|
-
status: args.status,
|
|
994
|
-
shared: args.shared,
|
|
995
|
-
}, SESSION_ID);
|
|
996
|
-
success(id, {
|
|
997
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
998
|
-
});
|
|
1018
|
+
const query = requireString(args, 'query');
|
|
1019
|
+
const result = await marrowRetrieveMemories(API_KEY, BASE_URL, query, { limit: args.limit, from: args.from, to: args.to, tags: args.tags, source: args.source, status: args.status, shared: args.shared }, SESSION_ID);
|
|
1020
|
+
success(id, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] });
|
|
999
1021
|
return;
|
|
1000
1022
|
}
|
|
1001
1023
|
if (toolName === 'marrow_workflow') {
|
|
@@ -1016,9 +1038,7 @@ This is not optional overhead — it's how you stop repeating the same failures.
|
|
|
1016
1038
|
contextUpdate: args.contextUpdate,
|
|
1017
1039
|
status: args.status,
|
|
1018
1040
|
}, SESSION_ID);
|
|
1019
|
-
success(id, {
|
|
1020
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
1021
|
-
});
|
|
1041
|
+
success(id, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] });
|
|
1022
1042
|
return;
|
|
1023
1043
|
}
|
|
1024
1044
|
error(id, -32601, `Method not found: ${toolName}`);
|
|
@@ -1034,8 +1054,9 @@ This is not optional overhead — it's how you stop repeating the same failures.
|
|
|
1034
1054
|
// MCP stdio loop — raw stdin, no readline (readline writes prompts to stdout which breaks MCP)
|
|
1035
1055
|
let buffer = '';
|
|
1036
1056
|
let pendingRequests = 0;
|
|
1057
|
+
let stdinEnded = false;
|
|
1037
1058
|
function checkExit() {
|
|
1038
|
-
if (pendingRequests === 0) {
|
|
1059
|
+
if (stdinEnded && pendingRequests === 0) {
|
|
1039
1060
|
autoCommitOnClose().then(() => process.exit(0));
|
|
1040
1061
|
}
|
|
1041
1062
|
}
|
|
@@ -1048,8 +1069,21 @@ process.stdin.on('data', (chunk) => {
|
|
|
1048
1069
|
const trimmed = line.trim();
|
|
1049
1070
|
if (!trimmed)
|
|
1050
1071
|
continue;
|
|
1072
|
+
// [FIX #1] Wrap JSON.parse in try-catch to prevent crash on malformed input
|
|
1073
|
+
let msg;
|
|
1074
|
+
try {
|
|
1075
|
+
msg = JSON.parse(trimmed);
|
|
1076
|
+
}
|
|
1077
|
+
catch (parseErr) {
|
|
1078
|
+
process.stderr.write(`[marrow] JSON parse error: ${parseErr}\n`);
|
|
1079
|
+
send({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } });
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
// MCP notifications (no id) must be silently ignored per spec
|
|
1083
|
+
if (msg.id === undefined || msg.id === null)
|
|
1084
|
+
continue;
|
|
1051
1085
|
pendingRequests++;
|
|
1052
|
-
handleRequest(
|
|
1086
|
+
handleRequest(msg)
|
|
1053
1087
|
.catch((err) => {
|
|
1054
1088
|
process.stderr.write(`[marrow] Handler error: ${err}\n`);
|
|
1055
1089
|
})
|
|
@@ -1060,23 +1094,30 @@ process.stdin.on('data', (chunk) => {
|
|
|
1060
1094
|
}
|
|
1061
1095
|
});
|
|
1062
1096
|
process.stdin.on('end', () => {
|
|
1063
|
-
|
|
1097
|
+
stdinEnded = true;
|
|
1064
1098
|
if (buffer.trim()) {
|
|
1099
|
+
let msg;
|
|
1065
1100
|
try {
|
|
1066
|
-
|
|
1067
|
-
handleRequest(JSON.parse(buffer.trim()))
|
|
1068
|
-
.catch((err) => {
|
|
1069
|
-
process.stderr.write(`[marrow] Parse error on remaining: ${err}\n`);
|
|
1070
|
-
})
|
|
1071
|
-
.finally(() => {
|
|
1072
|
-
pendingRequests--;
|
|
1073
|
-
checkExit();
|
|
1074
|
-
});
|
|
1101
|
+
msg = JSON.parse(buffer.trim());
|
|
1075
1102
|
}
|
|
1076
1103
|
catch (err) {
|
|
1077
|
-
process.stderr.write(`[marrow]
|
|
1104
|
+
process.stderr.write(`[marrow] JSON parse error on remaining buffer: ${err}\n`);
|
|
1105
|
+
checkExit();
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
if (msg.id === undefined || msg.id === null) {
|
|
1078
1109
|
checkExit();
|
|
1110
|
+
return;
|
|
1079
1111
|
}
|
|
1112
|
+
pendingRequests++;
|
|
1113
|
+
handleRequest(msg)
|
|
1114
|
+
.catch((err) => {
|
|
1115
|
+
process.stderr.write(`[marrow] Handler error on remaining: ${err}\n`);
|
|
1116
|
+
})
|
|
1117
|
+
.finally(() => {
|
|
1118
|
+
pendingRequests--;
|
|
1119
|
+
checkExit();
|
|
1120
|
+
});
|
|
1080
1121
|
}
|
|
1081
1122
|
else {
|
|
1082
1123
|
checkExit();
|