@theronap/cortex-mcp 0.9.21 → 0.9.23

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 +62 -2
  2. package/package.json +1 -1
package/lib/server.mjs CHANGED
@@ -213,13 +213,73 @@ export async function runServer(version) {
213
213
  'project_status',
214
214
  {
215
215
  title: 'Project status',
216
- description: 'Status of a specific project by key (e.g. checkout-v2). Returns only what you can see.',
216
+ description: 'Status of a specific project by key (e.g. checkout-v2). Returns the AUTHORED wiki page (the synthesized understanding) when one exists, falling back to the records-derived line. Returns only what you can see.',
217
217
  inputSchema: { key: z.string().describe('project key, e.g. checkout-v2') },
218
218
  },
219
219
  async ({ key }) => {
220
+ // Prefer the AUTHORED page (synthesized understanding, sovereign over records). Freshest tier leads,
221
+ // each dated, so diverged tiers read as "newest first" instead of an undated stale/fresh blend.
222
+ try {
223
+ const res = await fetchCortex(`${BASE}/api/brain/page?kind=project&key=${encodeURIComponent(key)}`,
224
+ { headers: { Authorization: `Bearer ${TOKEN}` } })
225
+ if (res.ok) {
226
+ const page = await res.json()
227
+ if (page?.authored && Array.isArray(page.tiers) && page.tiers.length) {
228
+ const day = (d) => (d ? String(d).slice(0, 10) : '')
229
+ const blocks = page.tiers.map((t) => {
230
+ const secs = (t.sections ?? []).map((s) => `### ${s.heading}\n${s.body}`).join('\n\n')
231
+ const head = `[${t.tier}${day(t.updated_at) ? ` · authored ${day(t.updated_at)}` : ''}${t.validity && t.validity !== 'current' ? ` · ${t.validity}` : ''}]`
232
+ return [head, t.summary, secs].filter(Boolean).join('\n')
233
+ })
234
+ return { content: [{ type: 'text', text: `# ${page.title ?? key} (authored page)\n\n${blocks.join('\n\n---\n\n')}` }] }
235
+ }
236
+ }
237
+ } catch { /* fall through to the records-derived line */ }
238
+ // Fallback: no authored page yet — surface the records-derived line from the context snapshot.
220
239
  const text = await fetchContext()
221
240
  const line = text.split('\n').find((l) => l.includes(`**${key}**`) || l.includes(key))
222
- return { content: [{ type: 'text', text: line ? `Project ${key}:\n${line.trim()}` : `No visible project "${key}".` }] }
241
+ return { content: [{ type: 'text', text: line ? `Project ${key} (records-derived; not yet authored):\n${line.trim()}` : `No visible project "${key}".` }] }
242
+ },
243
+ )
244
+
245
+ server.registerTool(
246
+ 'read_page',
247
+ {
248
+ title: 'Read a wiki page in full',
249
+ description:
250
+ 'READ the full authored wiki page for one node (project/person/org/you) by its canonical name — every section, every tier you can see. This is how you READ a node; `grep` only LOCATES pages (snippets + their [[links]]), it does not read them. Navigate like a researcher: read the page you need, then FOLLOW its inline [[links]] by calling read_page on each linked name — keep following while the linked pages stay relevant, stop when they do not. You decide how deep to go. Returns only what you are permitted to see.',
251
+ inputSchema: {
252
+ name: z.string().describe('the canonical node name exactly as written (e.g. "Cortex", "Ben", or a [[link]] target)'),
253
+ kind: z.enum(['project', 'person', 'org', 'user']).optional().describe('node kind (default project; pass person/org for people/teams)'),
254
+ },
255
+ },
256
+ async ({ name, kind }) => {
257
+ const k = kind ?? 'project'
258
+ let res
259
+ try {
260
+ res = await fetchCortex(`${BASE}/api/brain/page?kind=${k}&key=${encodeURIComponent(name)}`,
261
+ { headers: { Authorization: `Bearer ${TOKEN}` } })
262
+ } catch (e) {
263
+ return { content: [{ type: 'text', text: `Could not read "${name}": ${e.message}` }] }
264
+ }
265
+ if (res.status === 404) {
266
+ return { content: [{ type: 'text', text: `No ${k} page named "${name}". If it's a person or team, pass kind (person/org). Otherwise \`grep "${name}"\` to locate it — it may be a red-link (a wanted page that isn't authored yet).` }] }
267
+ }
268
+ if (!res.ok) {
269
+ const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
270
+ return { content: [{ type: 'text', text: `Could not read "${name}": ${d.message}` }] }
271
+ }
272
+ const page = await res.json()
273
+ if (!page?.authored || !Array.isArray(page.tiers) || !page.tiers.length) {
274
+ return { content: [{ type: 'text', text: `"${name}" (${k}) exists but has no authored page yet — nothing to read. Try \`grep\` for mentions or \`story\` for a records-derived narrative.` }] }
275
+ }
276
+ const day = (d) => (d ? String(d).slice(0, 10) : '')
277
+ const blocks = page.tiers.map((t) => {
278
+ const secs = (t.sections ?? []).map((s) => `### ${s.heading}\n${s.body}`).join('\n\n')
279
+ const head = `[${t.tier}${day(t.updated_at) ? ` · authored ${day(t.updated_at)}` : ''}${t.validity && t.validity !== 'current' ? ` · ${t.validity}` : ''}]`
280
+ return [head, t.summary, secs].filter(Boolean).join('\n')
281
+ })
282
+ return { content: [{ type: 'text', text: `# ${page.title ?? name} (full authored page)\n\n${blocks.join('\n\n---\n\n')}\n\n— Follow any [[links]] above with read_page to go deeper.` }] }
223
283
  },
224
284
  )
225
285
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/cortex-mcp",
3
- "version": "0.9.21",
3
+ "version": "0.9.23",
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": {