@theronap/cortex-mcp 0.9.132 → 0.9.133
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 +53 -7
- package/package.json +1 -1
package/lib/server.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
3
3
|
import { z } from 'zod'
|
|
4
|
-
import { writeFileSync, mkdirSync } from 'fs'
|
|
4
|
+
import { writeFileSync, mkdirSync, readFileSync, statSync } from 'fs'
|
|
5
5
|
import { homedir } from 'os'
|
|
6
6
|
import { join } from 'path'
|
|
7
7
|
import { createHash, randomUUID } from 'crypto'
|
|
@@ -927,8 +927,13 @@ function renderNudge(payload) {
|
|
|
927
927
|
// attached: [...] filed under N pages
|
|
928
928
|
//
|
|
929
929
|
// Reporting only a count would make the first indistinguishable from the second, which is how
|
|
930
|
-
// a broken write hides inside a normal-looking result.
|
|
931
|
-
//
|
|
930
|
+
// a broken write hides inside a normal-looking result.
|
|
931
|
+
//
|
|
932
|
+
// ⚠ AND `undefined` IS A FOURTH STATE: NOT ATTEMPTED. Session-page attachment only runs for
|
|
933
|
+
// claude-code sources — a handoff, an email, a calendar event has no session to join on. The
|
|
934
|
+
// server used to send null in that case, having never tried, and this reported a FAILURE for
|
|
935
|
+
// the ordinary case (observed 2026-09-03 on the first handoff record materialised). Silence is
|
|
936
|
+
// correct here, and it also covers a server too old to send the field at all.
|
|
932
937
|
const lines = []
|
|
933
938
|
const sa = body?.sessionAttachments
|
|
934
939
|
if (sa === null) {
|
|
@@ -943,6 +948,19 @@ function renderNudge(payload) {
|
|
|
943
948
|
lines.push(`${sa.droppedCrossBrain} page(s) this session wrote live in another brain and were skipped — a record belongs to one brain.`)
|
|
944
949
|
}
|
|
945
950
|
}
|
|
951
|
+
// Containment carried from capture. Three states, and they are not interchangeable:
|
|
952
|
+
// true placed in the container the capture named
|
|
953
|
+
// false the edge already existed — a re-materialise, not a failure
|
|
954
|
+
// null/undefined no container was ever requested (say nothing)
|
|
955
|
+
// containerSkipped is the only one that needs a warning: it means a container WAS asked for and
|
|
956
|
+
// did not happen, which is exactly the silence this whole path exists to remove.
|
|
957
|
+
if (body?.containerSkipped) {
|
|
958
|
+
lines.push(`⚠ CONTAINER NOT APPLIED — ${body.containerSkipped}. The record exists but is not inside the event you named; place it with contain_record or leave it out.`)
|
|
959
|
+
} else if (body?.contained === true) {
|
|
960
|
+
lines.push('Placed inside the container named at capture.')
|
|
961
|
+
} else if (body?.contained === false) {
|
|
962
|
+
lines.push('Already inside the container named at capture (no change).')
|
|
963
|
+
}
|
|
946
964
|
const text = lines.length
|
|
947
965
|
? `${lines.join('\n')}\n\n${JSON.stringify(body, null, 2)}`
|
|
948
966
|
: JSON.stringify(body, null, 2)
|
|
@@ -2276,10 +2294,11 @@ function renderNudge(payload) {
|
|
|
2276
2294
|
'capture_record',
|
|
2277
2295
|
{
|
|
2278
2296
|
title: 'Hand something over and get it into the brain',
|
|
2279
|
-
description: "Put a document, transcript, notes or any pasted content into the brain as a real record, routed to where it belongs. USE THIS WHEN THERE IS NO CONNECTOR — a lecture recording, an export from a tool nobody has wired up, something a person just handed you. ⚠ `identifiers` IS HOW IT REACHES A PAGE: a record routes to whatever page CLAIMS an identifier it carries (series:… for a class or recurring meeting, repo:owner/name, project:slug, email:someone@example.com). WITHOUT ONE IT REACHES NO PAGE — that is not an error, but it means nobody will find it, so say so rather than reporting success. `occurred_at` is WHEN THE THING HAPPENED, not now: a transcript handed over today may belong to yesterday's class, and filing it under today puts it in the wrong interval invisibly. `container_record_id` additionally places it INSIDE a specific event (a class session, a meeting) — that is an explicit human decision and is recorded as one; overlap alone never places anything.",
|
|
2297
|
+
description: "Put a document, transcript, notes or any pasted content into the brain as a real record, routed to where it belongs. USE THIS WHEN THERE IS NO CONNECTOR — a lecture recording, an export from a tool nobody has wired up, something a person just handed you. ⚠ `identifiers` IS HOW IT REACHES A PAGE: a record routes to whatever page CLAIMS an identifier it carries (series:… for a class or recurring meeting, repo:owner/name, project:slug, email:someone@example.com). WITHOUT ONE IT REACHES NO PAGE — that is not an error, but it means nobody will find it, so say so rather than reporting success. `occurred_at` is WHEN THE THING HAPPENED, not now: a transcript handed over today may belong to yesterday's class, and filing it under today puts it in the wrong interval invisibly. `container_record_id` additionally places it INSIDE a specific event (a class session, a meeting) — that is an explicit human decision and is recorded as one; overlap alone never places anything. \u26a0 PASS `content_path` FOR ANYTHING LONG: a file is read and sent verbatim, whereas retyping a transcript into `content` routes every character through a model that is not guaranteed to reproduce it exactly. Use `content` only for something short you are composing yourself.",
|
|
2280
2298
|
inputSchema: {
|
|
2281
2299
|
title: z.string().describe('a short name for this record'),
|
|
2282
|
-
content: z.string().describe('the actual content
|
|
2300
|
+
content: z.string().optional().describe('the actual content, inline. For anything long prefer content_path — see below.'),
|
|
2301
|
+
content_path: z.string().optional().describe('ABSOLUTE PATH to a file whose contents become the record, read and sent verbatim. USE THIS FOR TRANSCRIPTS AND DOCUMENTS: it is the only way the stored text is guaranteed byte-identical to the source. Exactly one of content / content_path.'),
|
|
2283
2302
|
occurred_at: z.string().describe('when the thing HAPPENED, ISO 8601 — not when you are uploading it'),
|
|
2284
2303
|
ends_at: z.string().optional().describe('when it ended, ISO 8601. Omit if unknown; NEVER guess one.'),
|
|
2285
2304
|
identifiers: z.array(z.string()).optional().describe('routing identifiers — how it reaches a page. e.g. ["series:geol-100-001-f2026"]'),
|
|
@@ -2289,14 +2308,34 @@ function renderNudge(payload) {
|
|
|
2289
2308
|
brain: z.string().optional().describe('which brain, by name or org id. REQUIRED if you belong to more than one — placing a record IS a disclosure decision.'),
|
|
2290
2309
|
},
|
|
2291
2310
|
},
|
|
2292
|
-
async ({ title, content, occurred_at, ends_at, identifiers, container_record_id, summary, source, brain }) => {
|
|
2311
|
+
async ({ title, content, content_path, occurred_at, ends_at, identifiers, container_record_id, summary, source, brain }) => {
|
|
2312
|
+
// 🔴 A FILE IS READ HERE RATHER THAN RETYPED UPSTREAM, AND THAT IS A FIDELITY GUARANTEE,
|
|
2313
|
+
// NOT A CONVENIENCE. Passing a 39,000-character lecture transcript through `content` means the
|
|
2314
|
+
// caller has to emit every character of it, and a model reproducing a long document verbatim is
|
|
2315
|
+
// likely-correct, never certain-correct. Measured 2026-09-03: the first real transcript captured
|
|
2316
|
+
// this way had to carry a note in its own body saying it was re-entered and unverified. Reading
|
|
2317
|
+
// the file makes byte-identity structural instead of a matter of care.
|
|
2318
|
+
if ((content == null || content === '') === (content_path == null || content_path === '')) {
|
|
2319
|
+
return toolError('Pass exactly one of content or content_path.')
|
|
2320
|
+
}
|
|
2321
|
+
let text = content
|
|
2322
|
+
if (content_path) {
|
|
2323
|
+
try {
|
|
2324
|
+
const st = statSync(content_path)
|
|
2325
|
+
if (!st.isFile()) return toolError(`Not a file: ${content_path}`)
|
|
2326
|
+
text = readFileSync(content_path, 'utf8')
|
|
2327
|
+
} catch (e) {
|
|
2328
|
+
return toolError(`Could not read ${content_path}: ${e.message}`)
|
|
2329
|
+
}
|
|
2330
|
+
if (!text.trim()) return toolError(`${content_path} is empty — nothing to capture.`)
|
|
2331
|
+
}
|
|
2293
2332
|
let res
|
|
2294
2333
|
try {
|
|
2295
2334
|
res = await fetchCortex(`${BASE}/api/records/capture`, {
|
|
2296
2335
|
method: 'POST',
|
|
2297
2336
|
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
2298
2337
|
body: JSON.stringify({
|
|
2299
|
-
title, content, occurredAt: occurred_at, endsAt: ends_at, identifiers,
|
|
2338
|
+
title, content: text, occurredAt: occurred_at, endsAt: ends_at, identifiers,
|
|
2300
2339
|
containerRecordId: container_record_id, summary, source, brain,
|
|
2301
2340
|
}),
|
|
2302
2341
|
})
|
|
@@ -2317,6 +2356,13 @@ function renderNudge(payload) {
|
|
|
2317
2356
|
if (body.duplicate) l.push('This matched an existing unit rather than creating a new one.')
|
|
2318
2357
|
if (body.routed_by?.length) l.push(`Carries: ${body.routed_by.join(', ')} — it will route on those once materialised.`)
|
|
2319
2358
|
else l.push('⚠ NO IDENTIFIERS — once materialised it will reach no page.')
|
|
2359
|
+
// ⚠ A CONTAINER THE CALLER ASKED FOR MUST NOT VANISH FROM THE REPORT. It cannot be applied
|
|
2360
|
+
// yet — there is no record — but it IS carried in the sealed payload and applied at
|
|
2361
|
+
// materialisation. Saying nothing here is what made a caller re-derive a placement they had
|
|
2362
|
+
// already specified (observed 2026-09-03).
|
|
2363
|
+
if (body.containerDeferred) {
|
|
2364
|
+
l.push(`Container ${body.containerRecordId} is carried with it and applied when you materialise — you do not need to place it again.`)
|
|
2365
|
+
}
|
|
2320
2366
|
return { content: [{ type: 'text', text: l.join('\n') }] }
|
|
2321
2367
|
}
|
|
2322
2368
|
const lines = [`Captured "${title}" as record ${body.id}.`]
|
package/package.json
CHANGED