@theronap/cortex-mcp 0.9.90 → 0.9.91
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 +90 -16
- package/package.json +1 -1
package/lib/server.mjs
CHANGED
|
@@ -40,6 +40,38 @@ async function redLinkTriage(BASE, TOKEN, name) {
|
|
|
40
40
|
// file-request tools, and a module-level `fail` would be silently shadowed by it from that point on.
|
|
41
41
|
const toolError = (text) => ({ content: [{ type: 'text', text }], isError: true })
|
|
42
42
|
|
|
43
|
+
// SECTION CURRENCY (gate 3) — ONE renderer, used by BOTH read_page and project_status.
|
|
44
|
+
//
|
|
45
|
+
// Extracted as a PURE function on the pickBrainMatch precedent: that fix pulled a shared rule out of two
|
|
46
|
+
// resolvers specifically so they could not drift, after they drifted. This is the same situation found
|
|
47
|
+
// 2026-08-15. THREE surfaces render authored sections from the same server-computed fields:
|
|
48
|
+
// • renderAuthoredNodeBody (web/lib/engine/authored_page_tiers.ts) — console/web
|
|
49
|
+
// • read_page here — printed a date and "⚠ Undated section" as two adjacent lines
|
|
50
|
+
// • project_status here — printed NO currency at all: no as-of, no warning, nothing
|
|
51
|
+
// PR #558 fixed only the first. project_status is the tool the routing docs reach for FIRST, and gate 3
|
|
52
|
+
// reads "a reader can date any claim without a second query" — a reader there could date nothing.
|
|
53
|
+
//
|
|
54
|
+
// The two dates are DIFFERENT facts and both true: `asOf` is when the section's text last CHANGED; an
|
|
55
|
+
// explicit date in the prose is when the CLAIM was true. Stated as one sentence they inform; stacked as
|
|
56
|
+
// two lines they read as the page contradicting itself.
|
|
57
|
+
//
|
|
58
|
+
// ⚠ Do NOT "simplify" this by keying on `asOf` — it is set on every section always, so the detector
|
|
59
|
+
// would go silent everywhere. The branch must key on `hasExplicitDate`, and on `=== false` rather than
|
|
60
|
+
// falsy: `undefined` is an older server mid-rolling-deploy that has computed no verdict, and inventing
|
|
61
|
+
// one there is the 0093 don't-impute violation.
|
|
62
|
+
export const sectionCurrencyStamp = (s, day) => {
|
|
63
|
+
const on = s.asOf ? day(s.asOf) : ''
|
|
64
|
+
return s.hasExplicitDate === false
|
|
65
|
+
? `${on ? ` · text last written ${on} —` : ' —'} ⚠ the claim itself carries no date; verify before relying on it`
|
|
66
|
+
: (on ? ` · as of ${on}` : '')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ⚠ The newline is a FIX, not cosmetics. read_page's old form was `${asOf}${currency}${s.body}`, where
|
|
70
|
+
// only the UNDATED branch contributed a trailing \n — so every DATED section ran its body straight onto
|
|
71
|
+
// the header line ("· as of 2026-08-14Identifiers are candidates, never publication."). The defect was
|
|
72
|
+
// invisible on exactly the sections that were healthy.
|
|
73
|
+
export const renderSection = (s, day) => `### ${s.heading}${sectionCurrencyStamp(s, day)}\n${s.body}`
|
|
74
|
+
|
|
43
75
|
// The Agnoclast MCP server (stdio). Serves the signed-in employee's scoped org
|
|
44
76
|
// context to their AI assistant. CORTEX_TOKEN identifies the user + org.
|
|
45
77
|
|
|
@@ -539,7 +571,7 @@ export async function runServer(version) {
|
|
|
539
571
|
{
|
|
540
572
|
title: 'Check private intake changes',
|
|
541
573
|
description:
|
|
542
|
-
'Session heartbeat for Gate 4 private intake. Returns an operational delta since your cursor (no ciphertext) plus cleanupDueCount. Call at session start, before each user turn, after long actions, and at least every two minutes while actively working.
|
|
574
|
+
'Session heartbeat for Gate 4 private intake. Returns an operational delta since your cursor (no ciphertext) plus cleanupDueCount. Call at session start, before each user turn, after long actions, and at least every two minutes while actively working. cleanupDueCount is advisory: prefer to service it with claimKind=cleanup when you reach a natural break, but never park the work you were actually asked to do in order to drain the queue first. NOTE afterSeq is this feed\'s own sequence — the changeSeq returned by ingest is a different counter, and passing it here seeks past the end and looks like a dead feed.',
|
|
543
575
|
inputSchema: {
|
|
544
576
|
afterSeq: z.number().optional().describe('cursor from the previous call (default 0)'),
|
|
545
577
|
limit: z.number().optional().describe('max change rows (default 100)'),
|
|
@@ -565,25 +597,66 @@ export async function runServer(version) {
|
|
|
565
597
|
{
|
|
566
598
|
title: 'Claim private intake items',
|
|
567
599
|
description:
|
|
568
|
-
'Claim a lease on private intake items (relevance or cleanup). Returns decrypted payloads for the lease holder only.
|
|
600
|
+
'Claim a lease on private intake items (relevance or cleanup). Returns decrypted payloads for the lease holder only. Requires x-cortex-session-key (set automatically by this MCP server). Aged work never blocks a claim: the response reports cleanupDueCount and how far behind the oldest unit is, and servicing it is expected but always your call. Claiming is a commitment to process — hand back anything you will not finish with intake_release, or intake_defer if it is the owner\'s decision to make.',
|
|
569
601
|
inputSchema: {
|
|
570
602
|
claimKind: z.enum(['relevance', 'cleanup']).optional().describe('default relevance'),
|
|
571
|
-
limit: z.number().optional().describe('max items (default 10)'),
|
|
603
|
+
limit: z.number().optional().describe('max items (default 10). Ignored when intakeItemIds is given.'),
|
|
604
|
+
intakeItemIds: z.array(z.string()).optional().describe(
|
|
605
|
+
'claim these specific units instead of the head of the queue (max 50). Without it you get FIFO, ' +
|
|
606
|
+
'so reaching one known unit means claiming everything ahead of it. Ids come from intake_changes ' +
|
|
607
|
+
'or from an ingest response. The reply adds an `outcomes` entry per requested id — claimed, ' +
|
|
608
|
+
'held_by_you (you already hold a live lease on it — proceed, do not re-claim), ' +
|
|
609
|
+
'held (someone else has a live lease; the holder is a stable hash, never their session key), ' +
|
|
610
|
+
'ineligible (already resolved or deferred to the owner), not_found, or unavailable (a momentary ' +
|
|
611
|
+
'lock — retrying is reasonable). Outcomes are best-effort, not a snapshot you can rely on.',
|
|
612
|
+
),
|
|
572
613
|
},
|
|
573
614
|
},
|
|
574
|
-
async ({ claimKind, limit }) => {
|
|
615
|
+
async ({ claimKind, limit, intakeItemIds }) => {
|
|
575
616
|
const res = await fetchCortex(`${BASE}/api/intake/claim`, {
|
|
576
617
|
method: 'POST',
|
|
577
618
|
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
578
|
-
body: JSON.stringify({
|
|
619
|
+
body: JSON.stringify({
|
|
620
|
+
claimKind: claimKind ?? 'relevance',
|
|
621
|
+
limit,
|
|
622
|
+
includePayload: true,
|
|
623
|
+
// Forwarded only when present. An empty array is a real request to claim nothing and must
|
|
624
|
+
// survive as one; sending `[]` where the caller sent nothing would silently switch a FIFO
|
|
625
|
+
// claim into a no-op.
|
|
626
|
+
...(intakeItemIds ? { intakeItemIds } : {}),
|
|
627
|
+
}),
|
|
579
628
|
})
|
|
580
629
|
if (!res.ok) {
|
|
581
630
|
const body = await res.text()
|
|
582
631
|
if (res.status === 403) return toolError('Private intake is not enabled for this account.')
|
|
632
|
+
// Kept deliberately after the server stopped sending it. A published MCP build outlives any
|
|
633
|
+
// one deployment, so this client will meet servers that still refuse relevance claims while
|
|
634
|
+
// cleanup is due. Surfacing that as its own message beats a generic HTTP failure.
|
|
583
635
|
if (res.status === 409) return toolError(body)
|
|
584
636
|
throw new Error(classify(res.status, res.headers.get('content-type'), body, res.headers.get('x-vercel-id')).message)
|
|
585
637
|
}
|
|
586
|
-
|
|
638
|
+
|
|
639
|
+
const j = await res.json()
|
|
640
|
+
|
|
641
|
+
// The nudge that replaced the 409. It has to be legible without doing arithmetic on a
|
|
642
|
+
// timestamp, so say how far behind rather than printing an ISO string and hoping — "6 days"
|
|
643
|
+
// is a reason to act and "2026-08-09T…" is a field to skim past.
|
|
644
|
+
let nudge = ''
|
|
645
|
+
if ((j?.cleanupDueCount ?? 0) > 0) {
|
|
646
|
+
const n = j.cleanupDueCount
|
|
647
|
+
let behind = ''
|
|
648
|
+
const oldest = j?.oldestCleanupDueAt ? Date.parse(j.oldestCleanupDueAt) : NaN
|
|
649
|
+
if (Number.isFinite(oldest)) {
|
|
650
|
+
const mins = Math.max(0, Math.floor((Date.now() - oldest) / 60000))
|
|
651
|
+
behind = mins >= 1440 ? `, oldest ${Math.floor(mins / 1440)}d overdue`
|
|
652
|
+
: mins >= 60 ? `, oldest ${Math.floor(mins / 60)}h overdue`
|
|
653
|
+
: `, oldest ${mins}m overdue`
|
|
654
|
+
}
|
|
655
|
+
nudge = `\n\n⏳ ${n} intake unit${n === 1 ? '' : 's'} past the cleanup deadline${behind}. `
|
|
656
|
+
+ `Nothing is blocked — run intake_claim with claimKind:"cleanup" when you reach a natural break.`
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
return { content: [{ type: 'text', text: JSON.stringify(j, null, 2) + nudge }] }
|
|
587
660
|
},
|
|
588
661
|
)
|
|
589
662
|
|
|
@@ -679,15 +752,16 @@ export async function runServer(version) {
|
|
|
679
752
|
inputSchema: {
|
|
680
753
|
intakeItemId: z.string().describe('intake item uuid from intake_claim'),
|
|
681
754
|
question: z.string().describe('what you need the owner to decide, in their words not yours — this is the entire message they get, so "which brain should this iMessage thread go to, if any?" beats "needs triage"'),
|
|
755
|
+
options: z.array(z.string()).optional().describe('the concrete choices, when the decision is a pick rather than an open question — e.g. ["Personal","TTO","Discard — nothing to record"]. Stored on the question and rendered by intake_cleanup_status, so the owner can answer with a choice instead of prose.'),
|
|
682
756
|
},
|
|
683
757
|
},
|
|
684
|
-
async ({ intakeItemId, question }) => {
|
|
758
|
+
async ({ intakeItemId, question, options }) => {
|
|
685
759
|
let res
|
|
686
760
|
try {
|
|
687
761
|
res = await fetchCortex(`${BASE}/api/intake/defer`, {
|
|
688
762
|
method: 'POST',
|
|
689
763
|
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
|
|
690
|
-
body: JSON.stringify({ intakeItemId, question }),
|
|
764
|
+
body: JSON.stringify({ intakeItemId, question, ...(options?.length ? { options } : {}) }),
|
|
691
765
|
})
|
|
692
766
|
} catch (e) {
|
|
693
767
|
return toolError(`Could not defer: ${e.message}`)
|
|
@@ -874,7 +948,11 @@ export async function runServer(version) {
|
|
|
874
948
|
const day = (d) => (d ? String(d).slice(0, 10) : '')
|
|
875
949
|
const renderMatch = (m) => {
|
|
876
950
|
const blocks = m.tiers.map((t) => {
|
|
877
|
-
|
|
951
|
+
// Was heading-then-body with NO currency at all — no as-of, no warning — while
|
|
952
|
+
// read_page showed it. project_status is what the routing docs reach for first, so a
|
|
953
|
+
// reader here could date nothing. Same renderer as read_page now, so the two cannot
|
|
954
|
+
// drift again.
|
|
955
|
+
const secs = (t.sections ?? []).map((s) => renderSection(s, day)).join('\n\n')
|
|
878
956
|
const head = `[${t.tier}${day(t.updated_at) ? ` · authored ${day(t.updated_at)}` : ''}${t.validity && t.validity !== 'current' ? ` · ${t.validity}` : ''}]`
|
|
879
957
|
return [head, t.summary, secs].filter(Boolean).join('\n')
|
|
880
958
|
})
|
|
@@ -1011,13 +1089,9 @@ export async function runServer(version) {
|
|
|
1011
1089
|
const renderMatch = (m, tagBrain) => {
|
|
1012
1090
|
const blocks = m.tiers.map((t) => {
|
|
1013
1091
|
const secs = (t.sections ?? []).map((s) => {
|
|
1014
|
-
//
|
|
1015
|
-
// not
|
|
1016
|
-
|
|
1017
|
-
? '\n⚠ **Undated section — verify before relying on its claims.**\n'
|
|
1018
|
-
: ''
|
|
1019
|
-
const asOf = s.asOf ? ` · as of ${day(s.asOf)}` : ''
|
|
1020
|
-
return `### ${s.heading}${asOf}${currency}${s.body}`
|
|
1092
|
+
// Shared with project_status via renderSection — see its definition for why this is one
|
|
1093
|
+
// function and not two (they drifted; #558 fixed one of three surfaces).
|
|
1094
|
+
return renderSection(s, day)
|
|
1021
1095
|
}).join('\n\n')
|
|
1022
1096
|
// ADR-0018: a null version isn't "nothing to show" — it means this variant predates content-
|
|
1023
1097
|
// hash tracking (a 2026-06-29 import scar) and CANNOT be re-authored via base_version until an
|