@theronap/cortex-mcp 0.9.66 → 0.9.67

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.
Files changed (2) hide show
  1. package/lib/server.mjs +26 -44
  2. package/package.json +1 -1
package/lib/server.mjs CHANGED
@@ -59,7 +59,7 @@ export async function runServer(version) {
59
59
  // PGL-21 fix: "one MCP process = one AI session" is an assumption, not a guarantee — the HOST
60
60
  // (Claude Code) can restart this stdio process mid-conversation (reconnects, tool-loading events),
61
61
  // and a fresh randomUUID() on every restart silently orphaned the PRIOR process's session-scoped
62
- // write-pointer row (session_write_pointers): an explicit set_active_brain(scope:'session') would
62
+ // ADR-0022 deleted the write pointer, so a session no longer carries a brain of its own.
63
63
  // stop taking effect one restart later, falling back to the account pointer with no visible cause
64
64
  // — reproduced live 2026-07-27. Claude Code sets CLAUDE_CODE_SESSION_ID for the lifetime of one
65
65
  // logical conversation across any number of subprocess restarts, so prefer it as the session key;
@@ -1126,50 +1126,11 @@ export async function runServer(version) {
1126
1126
  },
1127
1127
  )
1128
1128
 
1129
- server.registerTool(
1130
- 'set_active_brain',
1131
- {
1132
- title: 'Set the default brain for NEW pages',
1133
- description: "RARELY NEEDED — do not reach for this reflexively. Editing an EXISTING page routes itself: author/set_page_validity/rollback resolve the brain from the page (via base_version, ref, or name), so switching first is unnecessary and switching WRONG is now impossible to cause. This only sets the default for creating a page that exists in NO brain yet, and for log_session/capture. If you find yourself about to call this so an edit lands correctly, don't — just author; it will find the page. Pass org_id = null to clear. SCOPE: 'session' (default) changes ONLY this session; 'account' changes the person-wide pointer and redirects every other open session that has not set its own.",
1134
- inputSchema: {
1135
- org_id: z.string().nullable().describe('the org id of the brain to write to (from my_brains), or null to clear the pointer'),
1136
- scope: z.enum(['session', 'account']).optional()
1137
- .describe("'session' (default) = this window only; 'account' = person-wide, affects your other open sessions too"),
1138
- },
1139
- },
1140
- async ({ org_id, scope }) => {
1141
- let res
1142
- try {
1143
- res = await fetchCortex(`${BASE}/api/brains`, {
1144
- method: 'PUT',
1145
- headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
1146
- body: JSON.stringify({ orgId: org_id ?? null, scope: scope ?? 'session' }),
1147
- })
1148
- } catch (e) {
1149
- return toolError(`Could not set active brain: ${e.message}`)
1150
- }
1151
- if (!res.ok) {
1152
- const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
1153
- return toolError(`Could not set active brain: ${d.message}`)
1154
- }
1155
- const r = await res.json()
1156
- // Always say WHICH scope changed. The default is session-only, so a caller expecting the old
1157
- // person-wide stickiness must be able to see that it did NOT change their other windows.
1158
- const where = r.scope === 'session' ? 'this session only' : 'ALL your sessions (person-wide)'
1159
- if (!r.activeOrgId) {
1160
- return { content: [{ type: 'text', text: r.scope === 'session'
1161
- ? 'Cleared this session\'s brain override — writes fall back to your account pointer.'
1162
- : 'Cleared your account-wide active-brain pointer (writes fall back to your default brain).' }] }
1163
- }
1164
- return { content: [{ type: 'text', text: `Writes now land in brain ${r.activeOrgId} — ${where}.` }] }
1165
- },
1166
- )
1167
-
1168
1129
  server.registerTool(
1169
1130
  'create_brain',
1170
1131
  {
1171
1132
  title: 'Create a new brain under your existing account',
1172
- description: 'Create a brand-new brain (org/workspace) — a fully independent knowledge graph — under your EXISTING account. No new login, no new email/password: this adds a second membership to the account you are already using. Reads never cross brains; new pages default to your active brain, so use set_active_brain if you want NEW pages to land in this one; edits to existing pages always route to whichever brain holds them.',
1133
+ description: 'Create a brand-new brain (org/workspace) — a fully independent knowledge graph — under your EXISTING account. No new login, no new email/password: this adds a second membership to the account you are already using. Reads never cross brains; new pages default to your active brain, so use pass `brain` when an operation needs one named',
1173
1134
  inputSchema: { name: z.string().describe('display name for the new brain, e.g. "Cortex Codebase"') },
1174
1135
  },
1175
1136
  async ({ name }) => {
@@ -1188,7 +1149,7 @@ export async function runServer(version) {
1188
1149
  return toolError(`Could not create brain: ${d.message}`)
1189
1150
  }
1190
1151
  const r = await res.json()
1191
- 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.` }] }
1152
+ return { content: [{ type: 'text', text: `Created brain "${name}" [${r.orgId}]. Reads already span it. Edits to pages in it route themselves from the page, so nothing needs pointing at it; pass brain="${name}" on an operation that creates something new here.` }] }
1192
1153
  },
1193
1154
  )
1194
1155
 
@@ -1755,7 +1716,7 @@ export async function runServer(version) {
1755
1716
  {
1756
1717
  title: 'Author a wiki node (live, while it is hot)',
1757
1718
  description:
1758
- 'Write your CURRENT understanding of a project/person/org/you into the org wiki as a maintained page. Call `authoring_context` FIRST. Author from your own synthesis of the session — the compiled mental model, not a transcript dump: what it IS, where it stands, dated decisions, open threads, key people. Weave inline [[links]] to other nodes (canonical names from the namespace; red-links for wanted-but-absent nodes). The server re-authorizes the tier and resolves links. CREATES the node if it does not exist yet (project/person/org) — the conversation IS the evidence, so a brand-new entity that surfaced only in this session is authorable on the spot; you do NOT need prior records. Because such a node has nothing external to corroborate it, author it DELIBERATELY: only when you genuinely understand it is a real, distinct entity, and use its exact canonical name so it does not duplicate one already in the namespace (`user` nodes are never created). Use this continuously whenever your understanding of a node meaningfully advanced, and at session end (/log). Authoring is PRE-AUTHORIZED — never ask the user "should I update the page?" before calling this (every edit is versioned + reversible via page_history/rollback_page); update, then briefly report what you updated. ⚠ WHICH BRAIN A NEW PAGE GOES IN IS A CONTENT DECISION, SO MAKE IT FROM THE CONTENT. Only a page that exists in NO brain needs this — an update resolves its brain from the page itself. If you hold more than one brain, call `my_brains` (it returns each brain\'s name, page count and sample titles, which is enough to tell what each one is FOR) and pass `brain` explicitly rather than letting the write pointer decide; the pointer is stale out-of-band state that knows nothing about what you are writing. State which brain you picked and why in one short line, then proceed — do NOT ask when the answer is obvious from the content. DO ask when it is genuinely ambiguous: brains are a confidentiality boundary, so a page born in the wrong one can expose private work to a teammate, and that is not a filing error you can quietly fix later.',
1719
+ 'Write your CURRENT understanding of a project/person/org/you into the org wiki as a maintained page. Call `authoring_context` FIRST. Author from your own synthesis of the session — the compiled mental model, not a transcript dump: what it IS, where it stands, dated decisions, open threads, key people. Weave inline [[links]] to other nodes (canonical names from the namespace; red-links for wanted-but-absent nodes). The server re-authorizes the tier and resolves links. CREATES the node if it does not exist yet (project/person/org) — the conversation IS the evidence, so a brand-new entity that surfaced only in this session is authorable on the spot; you do NOT need prior records. Because such a node has nothing external to corroborate it, author it DELIBERATELY: only when you genuinely understand it is a real, distinct entity, and use its exact canonical name so it does not duplicate one already in the namespace (`user` nodes are never created). Use this continuously whenever your understanding of a node meaningfully advanced, and at session end (/log). Authoring is PRE-AUTHORIZED — never ask the user "should I update the page?" before calling this (every edit is versioned + reversible via page_history/rollback_page); update, then briefly report what you updated. ⚠ WHICH BRAIN A NEW PAGE GOES IN IS A CONTENT DECISION, SO MAKE IT FROM THE CONTENT. Only a page that exists in NO brain needs this — an update resolves its brain from the page itself. If you hold more than one brain, the server REFUSES a create it cannot attribute (409 `create_needs_brain`) rather than letting the write pointer decide — the pointer is stale out-of-band state that knows nothing about what you are writing. So call `my_brains` (it returns each brain\'s name, page count and sample titles, which is enough to tell what each one is FOR) and pass `brain` up front; that turns a refused round trip into a single call. State which brain you picked and why in one short line, then proceed — do NOT ask when the answer is obvious from the content. DO ask when it is genuinely ambiguous: brains are a confidentiality boundary, so a page born in the wrong one can expose private work to a teammate, and that is not a filing error you can quietly fix later.',
1759
1720
  inputSchema: {
1760
1721
  kind: z.enum(['project', 'person', 'org', 'user']).describe('the node type'),
1761
1722
  name: z.string().describe('the canonical node name — an EXACT existing name from the namespace to update it, or a new name to create the node (project/person/org). e.g. "Cortex" or "Theron Peterson"'),
@@ -1789,7 +1750,28 @@ export async function runServer(version) {
1789
1750
  return toolError(`Could not author "${name}": ${e.message}`)
1790
1751
  }
1791
1752
  if (!res.ok) {
1792
- const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
1753
+ // Read the body ONCE — classify() consumes it as text, so parsing after it is a spent stream.
1754
+ const raw = await res.text()
1755
+ let err = null
1756
+ try { err = JSON.parse(raw) } catch { /* not JSON; classify handles it below */ }
1757
+ // BRAIN-RESOLUTION REFUSALS carry the only thing that makes them actionable: which brains, and
1758
+ // what is in each. classify() flattens a response to a generic status message, so routing these
1759
+ // through it would drop the payload and leave the agent to guess a brain — the exact guess the
1760
+ // 409 exists to prevent. Page counts and sample titles matter more than the names: per
1761
+ // brain_identity.ts a brain's NAME is actively misleading about its contents.
1762
+ if (err?.error === 'create_needs_brain' || err?.error === 'ambiguous_brain' || err?.error === 'unknown_brain') {
1763
+ const list = (err.brains ?? []).map((b) => {
1764
+ const count = b.pageCount != null ? ` — ${b.pageCount} page${b.pageCount === 1 ? '' : 's'}` : ''
1765
+ const sample = b.sampleTitles?.length ? `: ${b.sampleTitles.slice(0, 4).join(', ')}` : ''
1766
+ return ` • ${b.name ?? b.brain} (${b.orgId})${count}${sample}`
1767
+ }).join('\n')
1768
+ return toolError(
1769
+ `Could not author "${name}": ${err.message ?? err.error}` +
1770
+ (list ? `\n\nYour brains:\n${list}` : '') +
1771
+ `\n\nRe-run author with brain:"<name>" — choose by what each brain HOLDS, not by its name.`,
1772
+ )
1773
+ }
1774
+ const d = classify(res.status, res.headers.get('content-type'), raw, res.headers.get('x-vercel-id'))
1793
1775
  return toolError(`Could not author "${name}": ${d.message}`)
1794
1776
  }
1795
1777
  const out = await res.json()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/cortex-mcp",
3
- "version": "0.9.66",
3
+ "version": "0.9.67",
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": {