@theronap/cortex-mcp 0.9.59 → 0.9.60

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/lib/server.mjs CHANGED
@@ -28,6 +28,18 @@ async function redLinkTriage(BASE, TOKEN, name) {
28
28
  }
29
29
  }
30
30
 
31
+ // MCP tool results carry an `isError` flag, and a failure that omits it is indistinguishable from a
32
+ // success at the protocol level — the caller just sees a normal result whose text happens to begin
33
+ // "Could not". This file had 41 tools, 64 failure returns and ZERO uses of isError, so nothing
34
+ // downstream could tell a rejected write from a completed one. Measured 2026-07-31: a walker over
35
+ // 397 transcripts scored 18 REJECTED `author` calls as successful writes for exactly this reason,
36
+ // and an agent skimming its own tool result is exposed the same way. Route every failure through
37
+ // here so the flag cannot be forgotten at a new call site.
38
+ //
39
+ // NAME: deliberately not `fail` — runServer already has a local `fail(verb, res)` for the
40
+ // file-request tools, and a module-level `fail` would be silently shadowed by it from that point on.
41
+ const toolError = (text) => ({ content: [{ type: 'text', text }], isError: true })
42
+
31
43
  // The Cortex MCP server (stdio). Serves the signed-in employee's scoped org
32
44
  // context to their AI assistant. CORTEX_TOKEN identifies the user + org.
33
45
 
@@ -406,12 +418,12 @@ export async function runServer(version) {
406
418
  if (vr.status === 404) return { content: [{ type: 'text', text: `No version "${version}" for "${name}" (or you can't see it). Use page_history "${name}" to list its versions.` }] }
407
419
  if (!vr.ok) {
408
420
  const d = classify(vr.status, vr.headers.get('content-type'), await vr.text(), vr.headers.get('x-vercel-id'))
409
- return { content: [{ type: 'text', text: `Could not read version "${version}" of "${name}": ${d.message}` }] }
421
+ return toolError(`Could not read version "${version}" of "${name}": ${d.message}`)
410
422
  }
411
423
  const v = await vr.json()
412
424
  return { content: [{ type: 'text', text: `# ${name} — historical version (rev ${v.revNo} · ${v.op} · ${String(v.createdAt).slice(0, 10)} · ${v.tier})\nversion: ${v.version}\n\n${v.body}\n\n— This is a HISTORICAL snapshot, not the current page. \`read_page "${name}"\` (no version) shows what's live; \`rollback_page\` restores this one as a new version.` }] }
413
425
  } catch (e) {
414
- return { content: [{ type: 'text', text: `Could not read version "${version}" of "${name}": ${e.message}` }] }
426
+ return toolError(`Could not read version "${version}" of "${name}": ${e.message}`)
415
427
  }
416
428
  }
417
429
  // PER-NODE TIMELINE (slice 4): history = the projection over the node's identifier stamps.
@@ -422,7 +434,7 @@ export async function runServer(version) {
422
434
  if (r.status === 404) return { content: [{ type: 'text', text: `No ${k} named "${name}" — cannot project a timeline.` }] }
423
435
  if (!r.ok) {
424
436
  const d = classify(r.status, r.headers.get('content-type'), await r.text(), r.headers.get('x-vercel-id'))
425
- return { content: [{ type: 'text', text: `Could not read the timeline for "${name}": ${d.message}` }] }
437
+ return toolError(`Could not read the timeline for "${name}": ${d.message}`)
426
438
  }
427
439
  const t = await r.json()
428
440
  if (!t.identifiers?.length) {
@@ -435,7 +447,7 @@ export async function runServer(version) {
435
447
  if (t.siblings?.length) lines.push(`Sibling homes (share a stamp — bridges, not history): ${t.siblings.map((s) => `"${s.title}"`).join(', ')}`)
436
448
  return { content: [{ type: 'text', text: lines.join('\n') }] }
437
449
  } catch (e) {
438
- return { content: [{ type: 'text', text: `Could not read the timeline for "${name}": ${e.message}` }] }
450
+ return toolError(`Could not read the timeline for "${name}": ${e.message}`)
439
451
  }
440
452
  }
441
453
  // IDENTIFIER RESOLUTION (slice 3): an identifier-shaped name is a JOIN KEY, not a page — resolve
@@ -474,14 +486,14 @@ export async function runServer(version) {
474
486
  res = await fetchCortex(`${BASE}/api/brain/page?kind=${k}&key=${encodeURIComponent(name)}`,
475
487
  { headers: { Authorization: `Bearer ${TOKEN}` } })
476
488
  } catch (e) {
477
- return { content: [{ type: 'text', text: `Could not read "${name}": ${e.message}` }] }
489
+ return toolError(`Could not read "${name}": ${e.message}`)
478
490
  }
479
491
  if (res.status === 404) {
480
492
  return { content: [{ type: 'text', text: `No ${k} page named "${name}". If it's a person or team, pass kind (person/org).${await redLinkTriage(BASE, TOKEN, name)}` }] }
481
493
  }
482
494
  if (!res.ok) {
483
495
  const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
484
- return { content: [{ type: 'text', text: `Could not read "${name}": ${d.message}` }] }
496
+ return toolError(`Could not read "${name}": ${d.message}`)
485
497
  }
486
498
  const page = await res.json()
487
499
  // Multi-brain (decision 3A): /api/brain/page returns matches[] — one entry per brain that has a
@@ -559,12 +571,12 @@ export async function runServer(version) {
559
571
  const qs = new URLSearchParams({ kind: k, key: name, ...(limit ? { limit: String(limit) } : {}) })
560
572
  res = await fetchCortex(`${BASE}/api/brain/page-history?${qs}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
561
573
  } catch (e) {
562
- return { content: [{ type: 'text', text: `Could not read history for "${name}": ${e.message}` }] }
574
+ return toolError(`Could not read history for "${name}": ${e.message}`)
563
575
  }
564
576
  if (res.status === 404) return { content: [{ type: 'text', text: `No ${k} named "${name}". Pass kind (person/org) if it isn't a project.` }] }
565
577
  if (!res.ok) {
566
578
  const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
567
- return { content: [{ type: 'text', text: `Could not read history for "${name}": ${d.message}` }] }
579
+ return toolError(`Could not read history for "${name}": ${d.message}`)
568
580
  }
569
581
  const out = await res.json()
570
582
  const revs = out.revisions ?? []
@@ -612,12 +624,12 @@ export async function runServer(version) {
612
624
  })
613
625
  res = await fetchCortex(`${BASE}/api/brain/page-diff?${qs}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
614
626
  } catch (e) {
615
- return { content: [{ type: 'text', text: `Could not diff "${name}": ${e.message}` }] }
627
+ return toolError(`Could not diff "${name}": ${e.message}`)
616
628
  }
617
629
  if (res.status === 404) return { content: [{ type: 'text', text: `No ${k} named "${name}", or it has no version "${to}". Run \`page_history "${name}"\` to list its versions.` }] }
618
630
  if (!res.ok) {
619
631
  const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
620
- return { content: [{ type: 'text', text: `Could not diff "${name}": ${d.message}` }] }
632
+ return toolError(`Could not diff "${name}": ${d.message}`)
621
633
  }
622
634
  const out = await res.json()
623
635
  const d = out.diff
@@ -669,10 +681,10 @@ export async function runServer(version) {
669
681
  body: JSON.stringify({ kind: k, name, to_version, ...(tier ? { tier } : {}), ...(reason ? { reason } : {}) }),
670
682
  })
671
683
  } catch (e) {
672
- return { content: [{ type: 'text', text: `Could not roll back "${name}": ${e.message}` }] }
684
+ return toolError(`Could not roll back "${name}": ${e.message}`)
673
685
  }
674
686
  const out = await res.json().catch(() => null)
675
- if (!res.ok) return { content: [{ type: 'text', text: `Could not roll back "${name}": ${out?.error ?? res.status}` }] }
687
+ if (!res.ok) return toolError(`Could not roll back "${name}": ${out?.error ?? res.status}`)
676
688
  return { content: [{ type: 'text', text: `Done — "${name}" ${out.note}. \`read_page "${name}"\` to confirm the current content.` }] }
677
689
  },
678
690
  )
@@ -705,7 +717,7 @@ export async function runServer(version) {
705
717
  }),
706
718
  })
707
719
  } catch (e) {
708
- return { content: [{ type: 'text', text: `Could not rename the section: ${e.message}` }] }
720
+ return toolError(`Could not rename the section: ${e.message}`)
709
721
  }
710
722
  const out = await res.json().catch(() => null)
711
723
  if (!res.ok) {
@@ -718,7 +730,7 @@ export async function runServer(version) {
718
730
  out?.currentVersion ? `current version: ${out.currentVersion}` : '',
719
731
  ].filter(Boolean).join(' · ')
720
732
  const hint = out?.hint ? `\n${out.hint}` : ''
721
- return { content: [{ type: 'text', text: `Could not rename "${from}" on "${name}": ${out?.error ?? res.status}${extra ? `\n${extra}` : ''}${hint}` }] }
733
+ return toolError(`Could not rename "${from}" on "${name}": ${out?.error ?? res.status}${extra ? `\n${extra}` : ''}${hint}`)
722
734
  }
723
735
  return { content: [{ type: 'text', text: `Renamed on "${name}" (${out.brain} · ${out.tier} tier): "${out.from}" -> "${out.to}". The section kept its body, position and as-of date. New version: ${out.version}` }] }
724
736
  },
@@ -753,7 +765,7 @@ export async function runServer(version) {
753
765
  }),
754
766
  })
755
767
  } catch (e) {
756
- return { content: [{ type: 'text', text: `Could not edit "${name}": ${e.message}` }] }
768
+ return toolError(`Could not edit "${name}": ${e.message}`)
757
769
  }
758
770
  const out = await res.json().catch(() => null)
759
771
  if (!res.ok) {
@@ -784,7 +796,7 @@ export async function runServer(version) {
784
796
  Array.isArray(out?.concurrentRemoved) && out.concurrentRemoved.length ? `sections REMOVED elsewhere: ${out.concurrentRemoved.join(', ')}` : '',
785
797
  Array.isArray(out?.concurrentTruncated) && out.concurrentTruncated.length ? `(truncated, see page_diff for all of: ${out.concurrentTruncated.join(', ')})` : '',
786
798
  ].filter(Boolean).join('\n')
787
- return { content: [{ type: 'text', text: `Could not edit "${name}": ${out?.error ?? res.status}${out?.hint ? `\n${out.hint}` : ''}${bits ? `\n${bits}` : ''}${ccMeta ? `\n${ccMeta}` : ''}${cc}${cur}` }] }
799
+ return toolError(`Could not edit "${name}": ${out?.error ?? res.status}${out?.hint ? `\n${out.hint}` : ''}${bits ? `\n${bits}` : ''}${ccMeta ? `\n${ccMeta}` : ''}${cc}${cur}`)
788
800
  }
789
801
  const red = Array.isArray(out.redLinks) && out.redLinks.length
790
802
  ? `\nRed-links now on this page: ${out.redLinks.map((r) => `[[${r}]]`).join(', ')}` : ''
@@ -804,11 +816,11 @@ export async function runServer(version) {
804
816
  try {
805
817
  res = await fetchCortex(`${BASE}/api/style`, { headers: { Authorization: `Bearer ${TOKEN}` } })
806
818
  } catch (e) {
807
- return { content: [{ type: 'text', text: `Could not load writing style: ${e.message}` }] }
819
+ return toolError(`Could not load writing style: ${e.message}`)
808
820
  }
809
821
  if (!res.ok) {
810
822
  const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
811
- return { content: [{ type: 'text', text: `Could not load writing style: ${d.message}` }] }
823
+ return toolError(`Could not load writing style: ${d.message}`)
812
824
  }
813
825
  const { style } = await res.json()
814
826
  return { content: [{ type: 'text', text: style }] }
@@ -831,11 +843,11 @@ export async function runServer(version) {
831
843
  body: JSON.stringify({ style_md }),
832
844
  })
833
845
  } catch (e) {
834
- return { content: [{ type: 'text', text: `Could not save writing style: ${e.message}` }] }
846
+ return toolError(`Could not save writing style: ${e.message}`)
835
847
  }
836
848
  if (!res.ok) {
837
849
  const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
838
- return { content: [{ type: 'text', text: `Could not save writing style: ${d.message}` }] }
850
+ return toolError(`Could not save writing style: ${d.message}`)
839
851
  }
840
852
  const r = await res.json()
841
853
  return { content: [{ type: 'text', text: r.saved ? `Saved your writing-style profile (${r.chars} chars).` : 'Cleared your writing-style profile.' }] }
@@ -854,11 +866,11 @@ export async function runServer(version) {
854
866
  try {
855
867
  res = await fetchCortex(`${BASE}/api/brains`, { headers: { Authorization: `Bearer ${TOKEN}` } })
856
868
  } catch (e) {
857
- return { content: [{ type: 'text', text: `Could not list brains: ${e.message}` }] }
869
+ return toolError(`Could not list brains: ${e.message}`)
858
870
  }
859
871
  if (!res.ok) {
860
872
  const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
861
- return { content: [{ type: 'text', text: `Could not list brains: ${d.message}` }] }
873
+ return toolError(`Could not list brains: ${d.message}`)
862
874
  }
863
875
  const { brains, activeIsExplicit, activeSource, sessionOrgId, accountOrgId } = await res.json()
864
876
  if (!brains?.length) return { content: [{ type: 'text', text: 'You have no brains.' }] }
@@ -898,11 +910,11 @@ export async function runServer(version) {
898
910
  try {
899
911
  res = await fetchCortex(`${BASE}/api/brains/pages?orgId=${encodeURIComponent(org_id)}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
900
912
  } catch (e) {
901
- return { content: [{ type: 'text', text: `Could not list pages: ${e.message}` }] }
913
+ return toolError(`Could not list pages: ${e.message}`)
902
914
  }
903
915
  if (!res.ok) {
904
916
  const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
905
- return { content: [{ type: 'text', text: `Could not list pages: ${d.message}` }] }
917
+ return toolError(`Could not list pages: ${d.message}`)
906
918
  }
907
919
  const { nodeCount, rowCount, pages } = await res.json()
908
920
  if (!pages?.length) return { content: [{ type: 'text', text: `Brain ${org_id} has no authored pages.` }] }
@@ -939,11 +951,11 @@ export async function runServer(version) {
939
951
  body: JSON.stringify({ orgId: org_id ?? null, scope: scope ?? 'session' }),
940
952
  })
941
953
  } catch (e) {
942
- return { content: [{ type: 'text', text: `Could not set active brain: ${e.message}` }] }
954
+ return toolError(`Could not set active brain: ${e.message}`)
943
955
  }
944
956
  if (!res.ok) {
945
957
  const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
946
- return { content: [{ type: 'text', text: `Could not set active brain: ${d.message}` }] }
958
+ return toolError(`Could not set active brain: ${d.message}`)
947
959
  }
948
960
  const r = await res.json()
949
961
  // Always say WHICH scope changed. The default is session-only, so a caller expecting the old
@@ -974,11 +986,11 @@ export async function runServer(version) {
974
986
  body: JSON.stringify({ name }),
975
987
  })
976
988
  } catch (e) {
977
- return { content: [{ type: 'text', text: `Could not create brain: ${e.message}` }] }
989
+ return toolError(`Could not create brain: ${e.message}`)
978
990
  }
979
991
  if (!res.ok) {
980
992
  const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
981
- return { content: [{ type: 'text', text: `Could not create brain: ${d.message}` }] }
993
+ return toolError(`Could not create brain: ${d.message}`)
982
994
  }
983
995
  const r = await res.json()
984
996
  return { content: [{ type: 'text', text: `Created brain "${name}" [${r.orgId}]. Reads already span it. Edits to pages in it route themselves; set_active_brain only if you want NEW pages to default here.` }] }
@@ -1007,11 +1019,11 @@ export async function runServer(version) {
1007
1019
  try {
1008
1020
  res = await fetchCortex(`${BASE}/api/records?${qs}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
1009
1021
  } catch (e) {
1010
- return { content: [{ type: 'text', text: `Could not list records: ${e.message}` }] }
1022
+ return toolError(`Could not list records: ${e.message}`)
1011
1023
  }
1012
1024
  if (!res.ok) {
1013
1025
  const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
1014
- return { content: [{ type: 'text', text: `Could not list records: ${d.message}` }] }
1026
+ return toolError(`Could not list records: ${d.message}`)
1015
1027
  }
1016
1028
  const { text } = await res.json()
1017
1029
  return { content: [{ type: 'text', text }] }
@@ -1039,11 +1051,11 @@ export async function runServer(version) {
1039
1051
  try {
1040
1052
  res = await fetchCortex(`${BASE}/api/records/daily?${qs}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
1041
1053
  } catch (e) {
1042
- return { content: [{ type: 'text', text: `Could not build daily log: ${e.message}` }] }
1054
+ return toolError(`Could not build daily log: ${e.message}`)
1043
1055
  }
1044
1056
  if (!res.ok) {
1045
1057
  const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
1046
- return { content: [{ type: 'text', text: `Could not build daily log: ${d.message}` }] }
1058
+ return toolError(`Could not build daily log: ${d.message}`)
1047
1059
  }
1048
1060
  const { text } = await res.json()
1049
1061
  return { content: [{ type: 'text', text }] }
@@ -1064,11 +1076,11 @@ export async function runServer(version) {
1064
1076
  headers: { Authorization: `Bearer ${TOKEN}` },
1065
1077
  })
1066
1078
  } catch (e) {
1067
- return { content: [{ type: 'text', text: `Could not list your sessions: ${e.message}` }] }
1079
+ return toolError(`Could not list your sessions: ${e.message}`)
1068
1080
  }
1069
1081
  if (!res.ok) {
1070
1082
  const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
1071
- return { content: [{ type: 'text', text: `Could not list your sessions: ${d.message}` }] }
1083
+ return toolError(`Could not list your sessions: ${d.message}`)
1072
1084
  }
1073
1085
  const { text } = await res.json()
1074
1086
  return { content: [{ type: 'text', text }] }
@@ -1094,11 +1106,11 @@ export async function runServer(version) {
1094
1106
  body: JSON.stringify({ privacy }),
1095
1107
  })
1096
1108
  } catch (e) {
1097
- return { content: [{ type: 'text', text: `Could not set privacy: ${e.message}` }] }
1109
+ return toolError(`Could not set privacy: ${e.message}`)
1098
1110
  }
1099
1111
  if (!res.ok) {
1100
1112
  const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
1101
- return { content: [{ type: 'text', text: `Could not set privacy: ${d.message}` }] }
1113
+ return toolError(`Could not set privacy: ${d.message}`)
1102
1114
  }
1103
1115
  const out = await res.json()
1104
1116
  return { content: [{ type: 'text', text: `Done — record ${out.id} is now "${out.privacy}".` }] }
@@ -1127,11 +1139,11 @@ export async function runServer(version) {
1127
1139
  body: JSON.stringify({ kind, name, validity, modality, superseded_by }),
1128
1140
  })
1129
1141
  } catch (e) {
1130
- return { content: [{ type: 'text', text: `Could not set validity: ${e.message}` }] }
1142
+ return toolError(`Could not set validity: ${e.message}`)
1131
1143
  }
1132
1144
  if (!res.ok) {
1133
1145
  const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
1134
- return { content: [{ type: 'text', text: `Could not set validity: ${d.message}` }] }
1146
+ return toolError(`Could not set validity: ${d.message}`)
1135
1147
  }
1136
1148
  const out = await res.json()
1137
1149
  return { content: [{ type: 'text', text: `Done — "${out.name}" is now ${out.validity}${out.modality ? ` / ${out.modality}` : ''} (${out.docs_updated} tier-doc(s) updated).` }] }
@@ -1158,10 +1170,10 @@ export async function runServer(version) {
1158
1170
  body: JSON.stringify({ name, target_name, ...(target_kind ? { target_kind } : {}) }),
1159
1171
  })
1160
1172
  } catch (e) {
1161
- return { content: [{ type: 'text', text: `Could not alias: ${e.message}` }] }
1173
+ return toolError(`Could not alias: ${e.message}`)
1162
1174
  }
1163
1175
  const out = await res.json().catch(() => null)
1164
- if (!res.ok) return { content: [{ type: 'text', text: `Could not alias "${name}": ${out?.error ?? res.status}` }] }
1176
+ if (!res.ok) return toolError(`Could not alias "${name}": ${out?.error ?? res.status}`)
1165
1177
  if (!out) return { content: [{ type: 'text', text: `Aliased "${name}", but the server returned no body — re-read the page to confirm.` }] }
1166
1178
  return { content: [{ type: 'text', text: `Done — [[${out.alias}]] now resolves to "${out.target}" (${out.target_kind}). It's out of the wanted-page backlog.` }] }
1167
1179
  },
@@ -1186,10 +1198,10 @@ export async function runServer(version) {
1186
1198
  body: JSON.stringify({ name, ...(days ? { days } : {}) }),
1187
1199
  })
1188
1200
  } catch (e) {
1189
- return { content: [{ type: 'text', text: `Could not snooze: ${e.message}` }] }
1201
+ return toolError(`Could not snooze: ${e.message}`)
1190
1202
  }
1191
1203
  const out = await res.json().catch(() => null)
1192
- if (!res.ok) return { content: [{ type: 'text', text: `Could not snooze "${name}": ${out?.error ?? res.status}` }] }
1204
+ if (!res.ok) return toolError(`Could not snooze "${name}": ${out?.error ?? res.status}`)
1193
1205
  if (!out) return { content: [{ type: 'text', text: `Snoozed "${name}", but the server returned no body.` }] }
1194
1206
  return { content: [{ type: 'text', text: `Snoozed "${out.name}" for ${out.days} day${out.days === 1 ? '' : 's'} — it won't surface until then.` }] }
1195
1207
  },
@@ -1218,7 +1230,7 @@ export async function runServer(version) {
1218
1230
  body: JSON.stringify({ kind, name, tier, ...(source_tier ? { source_tier } : {}), ...(absorb ? { absorb: true } : {}), ...(target_version ? { target_version } : {}) }),
1219
1231
  })
1220
1232
  } catch (e) {
1221
- return { content: [{ type: 'text', text: `Could not set page privacy: ${e.message}` }] }
1233
+ return toolError(`Could not set page privacy: ${e.message}`)
1222
1234
  }
1223
1235
  const out = await res.json().catch(() => null)
1224
1236
  if (!res.ok) {
@@ -1228,10 +1240,10 @@ export async function runServer(version) {
1228
1240
  const extra = out.collision === 'readable' && out.blocking
1229
1241
  ? `\nYour ${out.source?.tier} page: ${out.source?.summary ?? out.source?.title}\nExisting ${out.blocking.tier} page (current version: ${out.blocking.version ?? 'none — this page predates content-hash tracking and cannot be re-authored via base_version; ask an admin about a backfill'}): ${out.blocking.summary ?? out.blocking.title}`
1230
1242
  : ''
1231
- return { content: [{ type: 'text', text: `Could not set page privacy: ${out.error}${extra}` }] }
1243
+ return toolError(`Could not set page privacy: ${out.error}${extra}`)
1232
1244
  }
1233
1245
  const d = classify(res.status, res.headers.get('content-type'), '', res.headers.get('x-vercel-id'))
1234
- return { content: [{ type: 'text', text: `Could not set page privacy: ${d.message}` }] }
1246
+ return toolError(`Could not set page privacy: ${d.message}`)
1235
1247
  }
1236
1248
  const g = out.live_grants?.length
1237
1249
  ? ` Grants still active for: ${out.live_grants.map((x) => x.grantee_name ?? x.grantee_user_id).join(', ')}.`
@@ -1262,10 +1274,10 @@ export async function runServer(version) {
1262
1274
  body: JSON.stringify({ kind, name, grantee, action, ...(tier ? { tier } : {}) }),
1263
1275
  })
1264
1276
  } catch (e) {
1265
- return { content: [{ type: 'text', text: `Could not ${action}: ${e.message}` }] }
1277
+ return toolError(`Could not ${action}: ${e.message}`)
1266
1278
  }
1267
1279
  const out = await res.json().catch(() => null)
1268
- if (!res.ok) return { content: [{ type: 'text', text: `Could not ${action}: ${out?.error ?? res.status}` }] }
1280
+ if (!res.ok) return toolError(`Could not ${action}: ${out?.error ?? res.status}`)
1269
1281
  const verb = { granted: 'now has access to', already_granted: 'already had access to', revoked: 'no longer has access to', not_granted: 'had no grant on' }[out.action]
1270
1282
  return { content: [{ type: 'text', text: `Done — ${out.grantee} ${verb} the ${out.tier} page.` }] }
1271
1283
  },
@@ -1288,10 +1300,10 @@ export async function runServer(version) {
1288
1300
  headers: { Authorization: `Bearer ${TOKEN}` },
1289
1301
  })
1290
1302
  } catch (e) {
1291
- return { content: [{ type: 'text', text: `Could not list grants: ${e.message}` }] }
1303
+ return toolError(`Could not list grants: ${e.message}`)
1292
1304
  }
1293
1305
  const out = await res.json().catch(() => null)
1294
- if (!res.ok) return { content: [{ type: 'text', text: `Could not list grants: ${out?.error ?? res.status}` }] }
1306
+ if (!res.ok) return toolError(`Could not list grants: ${out?.error ?? res.status}`)
1295
1307
  if (!out.grants?.length) return { content: [{ type: 'text', text: 'No grants on this page.' }] }
1296
1308
  const lines = out.grants.map((g) => `- ${g.grantee_name ?? g.grantee_user_id} → ${g.tier} variant (since ${String(g.created_at).slice(0, 10)})`)
1297
1309
  return { content: [{ type: 'text', text: `Grants (${out.grants.length}):\n${lines.join('\n')}` }] }
@@ -1310,11 +1322,11 @@ export async function runServer(version) {
1310
1322
  try {
1311
1323
  res = await fetchCortex(`${BASE}/api/brain/retier-notices`, { headers: { Authorization: `Bearer ${TOKEN}` } })
1312
1324
  } catch (e) {
1313
- return { content: [{ type: 'text', text: `Could not list notices: ${e.message}` }] }
1325
+ return toolError(`Could not list notices: ${e.message}`)
1314
1326
  }
1315
1327
  if (!res.ok) {
1316
1328
  const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
1317
- return { content: [{ type: 'text', text: `Could not list notices: ${d.message}` }] }
1329
+ return toolError(`Could not list notices: ${d.message}`)
1318
1330
  }
1319
1331
  const { notices } = await res.json()
1320
1332
  if (!notices?.length) return { content: [{ type: 'text', text: 'No re-tier notices.' }] }
@@ -1336,11 +1348,11 @@ export async function runServer(version) {
1336
1348
  try {
1337
1349
  res = await fetchCortex(`${BASE}/api/brain/page-merge-requests`, { headers: { Authorization: `Bearer ${TOKEN}` } })
1338
1350
  } catch (e) {
1339
- return { content: [{ type: 'text', text: `Could not list merge requests: ${e.message}` }] }
1351
+ return toolError(`Could not list merge requests: ${e.message}`)
1340
1352
  }
1341
1353
  if (!res.ok) {
1342
1354
  const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
1343
- return { content: [{ type: 'text', text: `Could not list merge requests: ${d.message}` }] }
1355
+ return toolError(`Could not list merge requests: ${d.message}`)
1344
1356
  }
1345
1357
  const { requests } = await res.json()
1346
1358
  if (!requests?.length) return { content: [{ type: 'text', text: 'No pending page merge requests.' }] }
@@ -1369,17 +1381,17 @@ export async function runServer(version) {
1369
1381
  body: JSON.stringify({ id, decision }),
1370
1382
  })
1371
1383
  } catch (e) {
1372
- return { content: [{ type: 'text', text: `Could not decide: ${e.message}` }] }
1384
+ return toolError(`Could not decide: ${e.message}`)
1373
1385
  }
1374
1386
  const out = await res.json().catch(() => null)
1375
- if (!res.ok) return { content: [{ type: 'text', text: `Could not decide: ${out?.error ?? res.status}` }] }
1387
+ if (!res.ok) return toolError(`Could not decide: ${out?.error ?? res.status}`)
1376
1388
  return { content: [{ type: 'text', text: out.decision === 'denied' ? 'Denied — nothing moved.' : `Approved — removed variant(s): ${out.removed_variants.join(', ') || 'none remained'}. ${out.note}` }] }
1377
1389
  },
1378
1390
  )
1379
1391
 
1380
1392
  // ── File requests: ask the owner for a record's FULL original (lives on their machine) ──
1381
1393
  const fail = (verb, res) => async () =>
1382
- ({ content: [{ type: 'text', text: `Could not ${verb}: ${classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id')).message}` }] })
1394
+ toolError(`Could not ${verb}: ${classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id')).message}`)
1383
1395
 
1384
1396
  server.registerTool(
1385
1397
  'request_file',
@@ -1395,7 +1407,7 @@ export async function runServer(version) {
1395
1407
  method: 'POST', headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
1396
1408
  body: JSON.stringify({ recordId: record_id }),
1397
1409
  })
1398
- } catch (e) { return { content: [{ type: 'text', text: `Could not request: ${e.message}` }] } }
1410
+ } catch (e) { return toolError(`Could not request: ${e.message}`) }
1399
1411
  if (res.status === 409) {
1400
1412
  const j = await res.json().catch(() => ({}))
1401
1413
  if (j.error === 'you_own_it') return { content: [{ type: 'text', text: `You own this record — open the original directly:\n${j.uri}` }] }
@@ -1417,7 +1429,7 @@ export async function runServer(version) {
1417
1429
  async () => {
1418
1430
  let res
1419
1431
  try { res = await fetchCortex(`${BASE}/api/file-requests`, { headers: { Authorization: `Bearer ${TOKEN}` } }) }
1420
- catch (e) { return { content: [{ type: 'text', text: `Could not load file requests: ${e.message}` }] } }
1432
+ catch (e) { return toolError(`Could not load file requests: ${e.message}`) }
1421
1433
  if (!res.ok) return (await fail('load file requests', res))()
1422
1434
  const { mine, inbox } = await res.json()
1423
1435
  const parts = []
@@ -1441,7 +1453,7 @@ export async function runServer(version) {
1441
1453
  method: 'POST', headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
1442
1454
  body: JSON.stringify({ action: decision }),
1443
1455
  })
1444
- } catch (e) { return { content: [{ type: 'text', text: `Could not decide: ${e.message}` }] } }
1456
+ } catch (e) { return toolError(`Could not decide: ${e.message}`) }
1445
1457
  if (!res.ok) return (await fail('decide', res))()
1446
1458
  return { content: [{ type: 'text', text: `Request ${decision === 'approve' ? 'approved' : 'denied'}.` }] }
1447
1459
  },
@@ -1457,7 +1469,7 @@ export async function runServer(version) {
1457
1469
  async ({ request_id }) => {
1458
1470
  let res
1459
1471
  try { res = await fetchCortex(`${BASE}/api/file-requests/${request_id}/download`, { headers: { Authorization: `Bearer ${TOKEN}` } }) }
1460
- catch (e) { return { content: [{ type: 'text', text: `Could not download: ${e.message}` }] } }
1472
+ catch (e) { return toolError(`Could not download: ${e.message}`) }
1461
1473
  if (res.status === 409) return { content: [{ type: 'text', text: 'Not ready — the owner hasn\'t fulfilled this yet. Try again after they approve.' }] }
1462
1474
  if (res.status === 410) return { content: [{ type: 'text', text: 'This file has expired (downloads are available for 7 days). Request it again.' }] }
1463
1475
  if (!res.ok) return (await fail('download', res))()
@@ -1516,11 +1528,11 @@ export async function runServer(version) {
1516
1528
  try {
1517
1529
  res = await fetchCortex(`${BASE}/api/brain/authoring-context?kind=${k}`, { headers: { Authorization: `Bearer ${TOKEN}` } })
1518
1530
  } catch (e) {
1519
- return { content: [{ type: 'text', text: `Could not fetch authoring context: ${e.message}` }] }
1531
+ return toolError(`Could not fetch authoring context: ${e.message}`)
1520
1532
  }
1521
1533
  if (!res.ok) {
1522
1534
  const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
1523
- return { content: [{ type: 'text', text: `Could not fetch authoring context: ${d.message}` }] }
1535
+ return toolError(`Could not fetch authoring context: ${d.message}`)
1524
1536
  }
1525
1537
  const { connectionRules, namespace, retiredLinks } = await res.json()
1526
1538
  const ns = Array.isArray(namespace) ? namespace : []
@@ -1571,11 +1583,11 @@ export async function runServer(version) {
1571
1583
  body: JSON.stringify({ kind, name, pages, reason, change_kind }),
1572
1584
  })
1573
1585
  } catch (e) {
1574
- return { content: [{ type: 'text', text: `Could not author "${name}": ${e.message}` }] }
1586
+ return toolError(`Could not author "${name}": ${e.message}`)
1575
1587
  }
1576
1588
  if (!res.ok) {
1577
1589
  const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
1578
- return { content: [{ type: 'text', text: `Could not author "${name}": ${d.message}` }] }
1590
+ return toolError(`Could not author "${name}": ${d.message}`)
1579
1591
  }
1580
1592
  const out = await res.json()
1581
1593
  const blue = out?.links?.blue ?? 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/cortex-mcp",
3
- "version": "0.9.59",
3
+ "version": "0.9.60",
4
4
  "description": "Connect your AI assistant to Cortex — your org's projects, activity, gaps, and directives, scoped to you.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -52,9 +52,8 @@ No arguments. Read the conversation context.
52
52
  docs are pending, follow the `cortex-author-docs` skill (author each into its page, then
53
53
  `docs-scan --mark`). Specs/plans written to disk this session must not die on disk — a spec IS
54
54
  a page. If no roots are registered or nothing is pending, skip silently.
55
- 7. **Reconcile & verify (prove the sweep don't trust it).** Step 5 relies on your in-the-moment
56
- judgment of "what advanced"; this step closes the loop so nothing is silently missed and no stale
57
- write slips through. Before printing the Output:
55
+ 7. **Reconcile the sweep (don't trust it).** Step 5 relies on your in-the-moment judgment of "what
56
+ advanced"; this step closes the loop so nothing is silently missed. Before printing the Output:
58
57
  a. **Enumerate what you touched** — from the transcript, list the concrete entities this session
59
58
  advanced: the project(s), notable files/artifacts, and the people you coordinated with. Derive
60
59
  this checklist from what actually *happened*, not from what you remember authoring — the whole
@@ -62,18 +61,21 @@ No arguments. Read the conversation context.
62
61
  b. **Assert one outcome per entity** — every item gets exactly `authored [[Page]]` **or**
63
62
  `skipped — <reason>` (e.g. "no material change", "not a node", "already current"). Nothing may be
64
63
  left unaccounted for. If an entity that genuinely advanced has neither, `author` it now (Step 5).
65
- c. **Verify the writes landed and are right** — for each page you claim you authored, `read_page` it
66
- (or check `page_history`) and confirm both: (i) **the change applied** — an `author` that returned
67
- "no change" when you *intended* an update means it did NOT land (stale `base_version`, wrong
68
- brain/namespace, or nothing actually differed) re-check rather than assume; and (ii) **the page
69
- reflects THIS session's first-hand findings**, not a prior you copied forward. This is the
70
- read-after-write half of the read-before-write rule the guard against laundering stale priors
71
- into the wiki. Fix a page you can edit; `set_page_validity` on one you can't. (Cross-brain note:
72
- `read_page` can resolve a page in another brain that `author` won't write if a "verify" read
73
- looks right but your write reported no-op, run `my_brains` / check `authoring_context` before
74
- trusting the read.)
75
- Carry the tally into the Output. If any intended write did not land, say so never report a clean
76
- sweep you didn't confirm.
64
+ Carry the tally into the Output.
65
+
66
+ > **A read-back verification sub-step lived here and was REMOVED 2026-07-31. Do not re-add it
67
+ > without new evidence.** It asked you to `read_page` every page you had just authored, to confirm
68
+ > the write landed. Measured over 397 local transcripts 18 sessions that wrote a page,
69
+ > 2026-07-29→31 **18 `author` calls were rejected and all 18 were retried to success: zero
70
+ > silently lost.** An end-of-session pass would have caught nothing, because the rejection reason
71
+ > arrives *in the tool result at the moment of the call*. Session end is the weakest place to
72
+ > verify a write; the result you already have in hand is the strongest.
73
+ >
74
+ > **What replaces it — at the moment of each write, not at the end:** a rejected `author` comes back
75
+ > as an ORDINARY tool result with **no error flag** — `Could not author "<page>": Cortex API 409:
76
+ > <reason>` — and `No change to "<page>"` is a **200 OK that wrote nothing**. Neither is an error at
77
+ > the protocol level, so nothing will interrupt you. **Read the result text of every write; never
78
+ > skim it.** That inline read is where this step's value actually was.
77
79
 
78
80
  ## Output
79
81
 
@@ -88,7 +90,7 @@ After calling `log_session`, show a short structured summary:
88
90
  **Coordinated with:** people involved
89
91
  **Logged:** ✅ persisted as the session's record (authoritative) (or ⚠ log_session errored — run doctor)
90
92
  **Wiki authored:** [[Node A]], [[Node B]] — pages updated (or "— nothing advanced this session")
91
- **Reconciled:** N touched → M authored, K skipped (reason each); writes verified ✅ (or ⚠ <what didn't land>)
93
+ **Reconciled:** N touched → M authored, K skipped (reason each)
92
94
  ```
93
95
 
94
96
  ## Safety rules