@theronap/agnoclast-mcp 0.9.232 โ†’ 0.9.233

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.
@@ -0,0 +1,102 @@
1
+ // AGNOCLAST LITE โ€” LOCKED TOOLS on the stdio server (Theron, 2026-10-13 3:19 PM MDT: "What if we stopped the
2
+ // agent from using the tools but we did allow them to see that the tools are there as something that would be
3
+ // open to them if they either used the trial or paid for the plan?").
4
+ //
5
+ // The list, the ๐Ÿ”’ line and every answer live on the server (web/lib/engine/locked_tools.ts) so the claude.ai
6
+ // connector and this server can never disagree. This module only:
7
+ // ยท asks once at start-up whether this account is locked (GET /api/lite/locked), and marks those tools'
8
+ // descriptions with the server's ๐Ÿ”’ line before the first tools/list goes out โ€” the rest of each
9
+ // description is left exactly as written;
10
+ // ยท for a call to a locked tool, asks the server for the answer (POST /api/lite/locked) and returns it
11
+ // INSTEAD of running the tool. If the server now says "not locked" (the trial started mid-session), the
12
+ // tool runs as usual.
13
+ // ยท FAILS CLOSED when it cannot know (Codex review, 2026-10-13): if start-up could not learn the account's
14
+ // state, every tool in REGULAR_ONLY_TOOLS asks per call, and a call whose check cannot be made runs
15
+ // nothing and says so. A definite "not locked" (Regular) at start-up costs nothing per call.
16
+
17
+ /**
18
+ * The fallback list, used only when start-up could not ask the server. The same names as the server's
19
+ * LOCKED_TOOLS (web/lib/engine/locked_tools.ts) that exist here โ€” web/lib/engine/trial.test.ts pins the two.
20
+ */
21
+ export const REGULAR_ONLY_TOOLS = [
22
+ 'list_records', 'read_record', 'timeline_pull', 'record_status', 'set_record_notes', 'set_record_privacy',
23
+ 'relabel_record_people', 'supersede_record', 'move_record', 'contain_record', 'pending_records', 'claim_record',
24
+ 'route_record', 'unroute_record', 'request_file', 'get_file', 'create_custom_source', 'search_spam_email',
25
+ 'gmail_modify', 'draft_email_reply', 'request_send_email', 'send_imessage', 'create_brain', 'add_to_brain',
26
+ ]
27
+
28
+ /** What a locked tool answers when the check itself could not be made. Nothing was run. */
29
+ export const LOCK_CHECK_FAILED =
30
+ 'Not run: this tool is part of the Agnoclast Regular plan, and Agnoclast did not answer whether this account can use it right now, ' +
31
+ 'so nothing was done. Try again in a moment.'
32
+
33
+ /** Start-up budget: the MCP handshake must not wait on this for long. */
34
+ export const LOCK_FETCH_TIMEOUT_MS = 2500
35
+
36
+ /**
37
+ * GET /api/lite/locked โ†’ { known: true, locked: false } (Regular, or a server that predates locked tools: a
38
+ * 404) | { known: true, locked: true, tools, lockLine } | { known: false, locked: false } (no answer). Never throws.
39
+ */
40
+ export async function fetchLockState(base, token, fetchImpl) {
41
+ try {
42
+ const res = await fetchImpl(`${base}/api/lite/locked`, { headers: { Authorization: `Bearer ${token}` } }, { skipAutofile: true })
43
+ if (res.status === 404) return { known: true, locked: false }
44
+ if (!res.ok) return { known: false, locked: false }
45
+ const j = await res.json()
46
+ if (j?.locked === false) return { known: true, locked: false }
47
+ if (j?.locked !== true || !Array.isArray(j.tools) || typeof j.lockLine !== 'string') return { known: false, locked: false }
48
+ return { known: true, locked: true, tools: j.tools.filter((t) => typeof t === 'string'), lockLine: j.lockLine }
49
+ } catch {
50
+ return { known: false, locked: false }
51
+ }
52
+ }
53
+
54
+ /** The lock state, or "unknown" if it did not arrive within the budget. */
55
+ export async function lockStateWithin(promise, ms = LOCK_FETCH_TIMEOUT_MS) {
56
+ let timer
57
+ const timeout = new Promise((resolve) => { timer = setTimeout(() => resolve({ known: false, locked: false }), ms) })
58
+ try { return await Promise.race([promise, timeout]) } finally { clearTimeout(timer) }
59
+ }
60
+
61
+ /**
62
+ * Which registered tools must ask before they run, marking the locked ones' descriptions (lockLine + the
63
+ * original). Locked โ†’ the server's list. Unknown โ†’ REGULAR_ONLY_TOOLS, unmarked (we do not know they are
64
+ * locked; each call asks). Regular โ†’ none. `handles` maps a name to { handle, description }.
65
+ */
66
+ export function applyLock(handles, lock) {
67
+ const checked = new Set()
68
+ if (lock?.known === false) {
69
+ for (const name of REGULAR_ONLY_TOOLS) if (handles.has(name)) checked.add(name)
70
+ return checked
71
+ }
72
+ if (!lock?.locked) return checked
73
+ for (const name of lock.tools) {
74
+ const h = handles.get(name)
75
+ if (!h) continue
76
+ h.handle.update({ description: `${lock.lockLine}${h.description ?? ''}` })
77
+ checked.add(name)
78
+ }
79
+ return checked
80
+ }
81
+
82
+ /**
83
+ * POST /api/lite/locked { tool, args } โ†’ { run: true } to run the tool as usual, or { run: false, text } to
84
+ * answer instead. A check that cannot be made answers LOCK_CHECK_FAILED: nothing runs. Never throws.
85
+ */
86
+ export async function lockedAnswerFor(base, token, tool, args, fetchImpl) {
87
+ try {
88
+ const res = await fetchImpl(`${base}/api/lite/locked`, {
89
+ method: 'POST',
90
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
91
+ body: JSON.stringify({ tool, args: args && typeof args === 'object' && !Array.isArray(args) ? args : {} }),
92
+ }, { skipAutofile: true })
93
+ if (res.status === 404) return { run: true } // a server that predates locked tools locks nothing
94
+ if (!res.ok) return { run: false, text: LOCK_CHECK_FAILED }
95
+ const j = await res.json()
96
+ if (j?.locked === false) return { run: true }
97
+ if (j?.locked === true && typeof j.text === 'string') return { run: false, text: j.text }
98
+ return { run: false, text: LOCK_CHECK_FAILED }
99
+ } catch {
100
+ return { run: false, text: LOCK_CHECK_FAILED }
101
+ }
102
+ }
package/lib/server.mjs CHANGED
@@ -30,6 +30,7 @@ import { renderRecord, READ_RECORD_DEFAULT_CHARS, READ_RECORD_PARTS } from './re
30
30
  import { formatCreated, formatDryRun, formatSources, homeFromArgs, pickSource, renderHomeProblem } from './custom_sources.mjs'
31
31
  import { formatSeriesRefusal, formatSeriesUpdated } from './series_render.mjs'
32
32
  import { formatSends } from './sends_render.mjs'
33
+ import { applyLock, fetchLockState, lockedAnswerFor, lockStateWithin } from './locked_tools.mjs'
33
34
 
34
35
  // PAGE tiers (ADR-0071). Six for a PAGE; a RECORD keeps three (`privacy` on log_session and
35
36
  // set_record_privacy below) โ€” an owner-relative tier is applied to a page's records when they are read
@@ -387,11 +388,30 @@ export async function runServer(version) {
387
388
  }
388
389
 
389
390
  const server = new McpServer({ name: 'cortex', version })
391
+ // AGNOCLAST LITE โ€” locked tools (lib/locked_tools.mjs; Theron, 2026-10-13 3:19 PM). Asked now, in parallel
392
+ // with registering ~120 tools, and awaited (bounded) just before the handshake, so the first tools/list a
393
+ // Lite account's AI sees already marks the Regular-only tools ๐Ÿ”’. Regular accounts: nothing changes.
394
+ const lockP = fetchLockState(BASE, TOKEN, fetchCortex)
395
+ const toolHandles = new Map()
396
+ let lockedTools = new Set()
390
397
  // ADR-0070: every tool runs with its NAME in async context, so an outage fetchCortex auto-files is
391
398
  // attributed to the tool that hit it rather than guessed from a URL shared by several tools.
392
399
  const registerTool = server.registerTool.bind(server)
393
- server.registerTool = (name, config, handler) =>
394
- registerTool(name, config, (...args) => runWithTool(name, () => handler(...args)))
400
+ server.registerTool = (name, config, handler) => {
401
+ const takesArgs = !!config?.inputSchema
402
+ const handle = registerTool(name, config, (...args) => runWithTool(name, async () => {
403
+ // A LOCKED tool runs nothing: the server's answer says what it would have done and how Regular is
404
+ // reached. "Not locked any more" (the trial started mid-session) โ†’ the tool runs as usual; a check that
405
+ // cannot be made โ†’ nothing runs (fails closed).
406
+ if (lockedTools.has(name)) {
407
+ const check = await lockedAnswerFor(BASE, TOKEN, name, takesArgs ? args[0] : {}, fetchCortex)
408
+ if (!check.run) return { content: [{ type: 'text', text: check.text }] }
409
+ }
410
+ return handler(...args)
411
+ }))
412
+ toolHandles.set(name, { handle, description: config?.description })
413
+ return handle
414
+ }
395
415
 
396
416
 
397
417
  // โ”€โ”€ THE PRE-CLAIM NUDGE, rendered โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@@ -6710,5 +6730,6 @@ function renderNudge(payload) {
6710
6730
  },
6711
6731
  )
6712
6732
 
6733
+ lockedTools = applyLock(toolHandles, await lockStateWithin(lockP))
6713
6734
  await server.connect(new StdioServerTransport())
6714
6735
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/agnoclast-mcp",
3
- "version": "0.9.232",
3
+ "version": "0.9.233",
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": {