@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 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 for --key flag
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
- const BASE_URL = process.env.MARROW_BASE_URL || 'https://api.getmarrow.ai';
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 === 'true';
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; // Track if this is first-time orient (for autoEnroll)
42
- let initialized = false; // Track if initialize() has been called
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
- // ignore
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
- // ignore
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
- process.on('SIGTERM', async () => {
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
- // Memory API functions
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.json();
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 res = await fetch(`${baseUrl}/v1/memories/${id}`, {
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.json();
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 res = await fetch(`${baseUrl}/v1/memories/${id}`, {
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.json();
158
- return json.data.memory;
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 res = await fetch(`${baseUrl}/v1/memories/${id}`, {
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.json();
171
- return json.data.memory;
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 res = await fetch(`${baseUrl}/v1/memories/${id}/outdated`, {
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.json();
184
- return json.data.memory;
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 res = await fetch(`${baseUrl}/v1/memories/${id}/supersede`, {
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.json();
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 res = await fetch(`${baseUrl}/v1/memories/${id}/share`, {
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.json();
210
- return json.data.memory;
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.json();
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.json();
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.json();
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
- type: 'object',
312
- description: 'Optional metadata about the current situation',
313
- },
314
- previous_decision_id: {
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
- type: 'string',
344
- description: 'decision_id from the marrow_think call',
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
- type: 'string',
371
- description: 'What the agent did',
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
- type: 'string',
401
- description: 'What you are about to do or just did',
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: '2.8.0' },
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: args.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: args.decision_id,
852
+ decision_id,
813
853
  success: args.success,
814
- outcome: args.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
- // marrow_run = orient + think + commit in one call
826
- await (0, index_1.marrowOrient)(API_KEY, BASE_URL, undefined, SESSION_ID);
827
- const thinkResult = await (0, index_1.marrowThink)(API_KEY, BASE_URL, {
828
- action: args.description,
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
- const commitResult = await (0, index_1.marrowCommit)(API_KEY, BASE_URL, {
832
- decision_id: thinkResult.decision_id,
833
- success: args.success ?? true,
834
- outcome: args.outcome,
835
- }, SESSION_ID);
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
- // marrow_auto = fire-and-forget background logging
848
- // Return immediately with cached orient warnings, API calls happen in background
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
- // Return cached warnings immediately
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 result = await (0, index_1.marrowAsk)(API_KEY, BASE_URL, { query: args.query }, SESSION_ID);
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
- status: args.status,
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 result = await marrowGetMemory(API_KEY, BASE_URL, args.id, SESSION_ID);
916
- success(id, {
917
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
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 result = await marrowUpdateMemory(API_KEY, BASE_URL, args.id, {
923
- text: args.text,
924
- source: args.source,
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 result = await marrowDeleteMemory(API_KEY, BASE_URL, args.id, { actor: args.actor, note: args.note }, SESSION_ID);
936
- success(id, {
937
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
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 result = await marrowMarkOutdated(API_KEY, BASE_URL, args.id, { actor: args.actor, note: args.note }, SESSION_ID);
943
- success(id, {
944
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
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 result = await marrowSupersedeMemory(API_KEY, BASE_URL, args.id, {
950
- text: args.text,
951
- source: args.source,
952
- tags: args.tags,
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 result = await marrowShareMemory(API_KEY, BASE_URL, args.id, args.agentIds || [], args.actor, SESSION_ID);
963
- success(id, {
964
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
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
- format: args.format,
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 result = await marrowRetrieveMemories(API_KEY, BASE_URL, args.query, {
988
- limit: args.limit,
989
- from: args.from,
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(JSON.parse(trimmed))
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
- // Process any remaining buffered line
1097
+ stdinEnded = true;
1064
1098
  if (buffer.trim()) {
1099
+ let msg;
1065
1100
  try {
1066
- pendingRequests++;
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] Parse error on remaining: ${err}\n`);
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();