@theronap/cortex-mcp 0.1.0 → 0.2.0

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.
@@ -2,103 +2,56 @@
2
2
  /**
3
3
  * cortex-mcp — connect your AI assistant to Cortex.
4
4
  *
5
- * Zero-install usage. Add to your Claude Code config (~/.claude.json mcpServers):
5
+ * Subcommands:
6
+ * (none) run the MCP server (stdio) — used by your Claude config
7
+ * setup <TOKEN> wire BOTH the MCP server + capture hook into your Claude config
8
+ * capture the Stop-hook capturer (invoked by Claude Code, not by hand)
9
+ * --version | -v
10
+ * --help | -h
6
11
  *
7
- * "cortex": {
8
- * "command": "npx",
9
- * "args": ["-y", "cortex-mcp"],
10
- * "env": { "CORTEX_TOKEN": "<your-personal-token>" }
11
- * }
12
+ * Zero-install onboarding:
13
+ * npx -y @theronap/cortex-mcp setup <your-token>
12
14
  *
13
- * Get your CORTEX_TOKEN from the Cortex console → Connect your AI.
14
- *
15
- * Env:
16
- * CORTEX_TOKEN (required) your personal token — identifies you + your org
17
- * CORTEX_URL (optional) defaults to https://cortex-console.vercel.app
15
+ * Get your token from the Cortex console → Connect your AI.
18
16
  */
19
17
 
20
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
21
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
22
- import { z } from 'zod'
18
+ const VERSION = '0.2.0'
19
+ const cmd = process.argv[2]
20
+ const rest = process.argv.slice(3)
21
+
22
+ if (cmd === '--version' || cmd === '-v') {
23
+ process.stdout.write(`cortex-mcp ${VERSION}\n`)
24
+ process.exit(0)
25
+ }
23
26
 
24
- const VERSION = '0.1.0'
25
- const arg = process.argv[2]
26
- if (arg === '--version' || arg === '-v') { process.stdout.write(`cortex-mcp ${VERSION}\n`); process.exit(0) }
27
- if (arg === '--help' || arg === '-h') {
27
+ if (cmd === '--help' || cmd === '-h' || cmd === 'help') {
28
28
  process.stdout.write(
29
29
  `cortex-mcp ${VERSION} — connect your AI assistant to Cortex\n\n` +
30
- `Set CORTEX_TOKEN (from the Cortex console → Connect your AI) and run via your\n` +
31
- `MCP client. Optional CORTEX_URL overrides the API base.\n\n` +
32
- `Claude Code config:\n` +
33
- ` "cortex": { "command": "npx", "args": ["-y", "cortex-mcp"],\n` +
34
- ` "env": { "CORTEX_TOKEN": "..." } }\n`
30
+ `Onboard (one command):\n` +
31
+ ` npx -y @theronap/cortex-mcp setup <YOUR_TOKEN>\n\n` +
32
+ `This wires your Claude config so your AI sees your Cortex context and your\n` +
33
+ `sessions flow into the org automatically. Restart Claude Code after.\n\n` +
34
+ `Subcommands:\n` +
35
+ ` setup <token> wire MCP server + capture hook into ~/.claude config\n` +
36
+ ` capture Stop-hook capturer (invoked by Claude Code)\n` +
37
+ ` (no args) run the MCP server (used by your Claude config)\n\n` +
38
+ `Get your token from the Cortex console → Connect your AI.\n`,
35
39
  )
36
40
  process.exit(0)
37
41
  }
38
42
 
39
- const TOKEN = process.env.CORTEX_TOKEN
40
- const BASE = (process.env.CORTEX_URL ?? 'https://cortex-console.vercel.app').replace(/\/$/, '')
41
-
42
- if (!TOKEN) {
43
- process.stderr.write('cortex-mcp: CORTEX_TOKEN is required. Get yours from the Cortex console → Connect your AI.\n')
44
- process.exit(1)
43
+ if (cmd === 'setup') {
44
+ const { runSetup } = await import('../lib/setup.mjs')
45
+ runSetup(rest)
46
+ process.exit(0)
45
47
  }
46
48
 
47
- // Cache context for 5 minutes so repeated tool calls don't re-fetch.
48
- let cache = null
49
- async function fetchContext() {
50
- const now = Date.now()
51
- if (cache && now - cache.ts < 5 * 60 * 1000) return cache.text
52
- const res = await fetch(`${BASE}/api/mcp-context`, { headers: { Authorization: `Bearer ${TOKEN}` } })
53
- if (!res.ok) {
54
- let detail = 'unknown'
55
- try { detail = (await res.json()).error ?? detail } catch { /* ignore */ }
56
- throw new Error(`Cortex API ${res.status}: ${detail}`)
57
- }
58
- const { context } = await res.json()
59
- cache = { text: context, ts: now }
60
- return context
49
+ if (cmd === 'capture') {
50
+ const { runCapture } = await import('../lib/capture.mjs')
51
+ await runCapture()
52
+ process.exit(0)
61
53
  }
62
54
 
63
- const server = new McpServer({ name: 'cortex', version: VERSION })
64
-
65
- server.registerTool(
66
- 'my_context',
67
- {
68
- title: 'My Cortex context',
69
- description: 'Your current work context from the org — your projects, recent activity, gaps, and any directives from leadership. Scoped to what you are permitted to see.',
70
- inputSchema: {},
71
- },
72
- async () => ({ content: [{ type: 'text', text: await fetchContext() }] }),
73
- )
74
-
75
- server.registerTool(
76
- 'search_org',
77
- {
78
- title: 'Search the org',
79
- description: 'Search your visible work activity and projects by keyword.',
80
- inputSchema: { query: z.string().describe('keyword to search for') },
81
- },
82
- async ({ query }) => {
83
- const text = await fetchContext()
84
- const q = query.toLowerCase()
85
- const lines = text.split('\n').filter((l) => l.toLowerCase().includes(q))
86
- return { content: [{ type: 'text', text: lines.length ? `Matches for "${query}":\n${lines.join('\n')}` : `No visible results for "${query}".` }] }
87
- },
88
- )
89
-
90
- server.registerTool(
91
- 'project_status',
92
- {
93
- title: 'Project status',
94
- description: 'Status of a specific project by key (e.g. checkout-v2). Returns only what you can see.',
95
- inputSchema: { key: z.string().describe('project key, e.g. checkout-v2') },
96
- },
97
- async ({ key }) => {
98
- const text = await fetchContext()
99
- const line = text.split('\n').find((l) => l.includes(`**${key}**`) || l.includes(key))
100
- return { content: [{ type: 'text', text: line ? `Project ${key}:\n${line.trim()}` : `No visible project "${key}".` }] }
101
- },
102
- )
103
-
104
- await server.connect(new StdioServerTransport())
55
+ // Default: run the MCP server.
56
+ const { runServer } = await import('../lib/server.mjs')
57
+ await runServer(VERSION)
@@ -0,0 +1,62 @@
1
+ import { readFileSync } from 'fs'
2
+
3
+ // Claude Code Stop hook → POSTs a session digest to Cortex cloud, which
4
+ // summarizes server-side and upserts ONE record per session. Node-native
5
+ // (no bun, no repo clone). Always exits 0 — capture must never break a session.
6
+
7
+ function readStdin() {
8
+ try { return readFileSync(0, 'utf8') } catch { return '' }
9
+ }
10
+
11
+ // Claude transcripts are JSONL; pull human-readable text from the tail so the
12
+ // server has real content to summarize (not raw tool JSON).
13
+ function transcriptTail(path) {
14
+ let raw = ''
15
+ try { raw = readFileSync(path, 'utf8') } catch { return '' }
16
+ const lines = raw.split('\n').filter(Boolean).slice(-60)
17
+ const texts = []
18
+ for (const line of lines) {
19
+ try {
20
+ const obj = JSON.parse(line)
21
+ const content = obj?.message?.content ?? obj?.content
22
+ if (typeof content === 'string') texts.push(content)
23
+ else if (Array.isArray(content)) {
24
+ for (const c of content) if (c?.type === 'text' && c.text) texts.push(c.text)
25
+ }
26
+ } catch { /* skip non-JSON */ }
27
+ }
28
+ return texts.join('\n').slice(-6000)
29
+ }
30
+
31
+ export async function runCapture() {
32
+ const token = process.env.CORTEX_TOKEN
33
+ if (!token) { process.stderr.write('cortex: CORTEX_TOKEN not set, skipping\n'); return }
34
+ const base = (process.env.CORTEX_URL ?? 'https://cortex-console.vercel.app').replace(/\/$/, '')
35
+
36
+ let hook = {}
37
+ try { hook = JSON.parse(readStdin()) } catch { /* no/invalid stdin */ }
38
+ const repo = hook.cwd?.split('/').filter(Boolean).pop() ?? 'general'
39
+ const transcript = hook.transcript_path ? transcriptTail(hook.transcript_path) : ''
40
+
41
+ if (!transcript && !hook.session_id) { process.stderr.write('cortex: empty session, skipping\n'); return }
42
+
43
+ const res = await fetch(`${base}/api/ingest`, {
44
+ method: 'POST',
45
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
46
+ body: JSON.stringify({
47
+ source: 'claude-code',
48
+ project: repo,
49
+ sessionId: hook.session_id,
50
+ transcript,
51
+ title: `Worked in ${repo}`,
52
+ payload: { session_id: hook.session_id, cwd: hook.cwd },
53
+ }),
54
+ }).catch((e) => { process.stderr.write(`cortex: ingest failed: ${e}\n`); return null })
55
+
56
+ if (res && res.ok) {
57
+ const j = await res.json().catch(() => ({}))
58
+ process.stderr.write(`cortex: ${j.inserted ? 'captured' : 'updated'} "${j.title ?? repo}" → ${repo}\n`)
59
+ } else if (res) {
60
+ process.stderr.write(`cortex: ingest ${res.status}\n`)
61
+ }
62
+ }
package/lib/server.mjs ADDED
@@ -0,0 +1,75 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
3
+ import { z } from 'zod'
4
+
5
+ // The Cortex MCP server (stdio). Serves the signed-in employee's scoped org
6
+ // context to their AI assistant. CORTEX_TOKEN identifies the user + org.
7
+
8
+ export async function runServer(version) {
9
+ const TOKEN = process.env.CORTEX_TOKEN
10
+ const BASE = (process.env.CORTEX_URL ?? 'https://cortex-console.vercel.app').replace(/\/$/, '')
11
+
12
+ if (!TOKEN) {
13
+ process.stderr.write('cortex-mcp: CORTEX_TOKEN is required. Get yours from the Cortex console → Connect your AI.\n')
14
+ process.exit(1)
15
+ }
16
+
17
+ // Cache context for 5 minutes so repeated tool calls don't re-fetch.
18
+ let cache = null
19
+ async function fetchContext() {
20
+ const now = Date.now()
21
+ if (cache && now - cache.ts < 5 * 60 * 1000) return cache.text
22
+ const res = await fetch(`${BASE}/api/mcp-context`, { headers: { Authorization: `Bearer ${TOKEN}` } })
23
+ if (!res.ok) {
24
+ let detail = 'unknown'
25
+ try { detail = (await res.json()).error ?? detail } catch { /* ignore */ }
26
+ throw new Error(`Cortex API ${res.status}: ${detail}`)
27
+ }
28
+ const { context } = await res.json()
29
+ cache = { text: context, ts: now }
30
+ return context
31
+ }
32
+
33
+ const server = new McpServer({ name: 'cortex', version })
34
+
35
+ server.registerTool(
36
+ 'my_context',
37
+ {
38
+ title: 'My Cortex context',
39
+ description: 'Your current work context from the org — your projects, recent activity, gaps, and any directives from leadership. Scoped to what you are permitted to see.',
40
+ inputSchema: {},
41
+ },
42
+ async () => ({ content: [{ type: 'text', text: await fetchContext() }] }),
43
+ )
44
+
45
+ server.registerTool(
46
+ 'search_org',
47
+ {
48
+ title: 'Search the org',
49
+ description: 'Search your visible work activity and projects by keyword.',
50
+ inputSchema: { query: z.string().describe('keyword to search for') },
51
+ },
52
+ async ({ query }) => {
53
+ const text = await fetchContext()
54
+ const q = query.toLowerCase()
55
+ const lines = text.split('\n').filter((l) => l.toLowerCase().includes(q))
56
+ return { content: [{ type: 'text', text: lines.length ? `Matches for "${query}":\n${lines.join('\n')}` : `No visible results for "${query}".` }] }
57
+ },
58
+ )
59
+
60
+ server.registerTool(
61
+ 'project_status',
62
+ {
63
+ title: 'Project status',
64
+ description: 'Status of a specific project by key (e.g. checkout-v2). Returns only what you can see.',
65
+ inputSchema: { key: z.string().describe('project key, e.g. checkout-v2') },
66
+ },
67
+ async ({ key }) => {
68
+ const text = await fetchContext()
69
+ const line = text.split('\n').find((l) => l.includes(`**${key}**`) || l.includes(key))
70
+ return { content: [{ type: 'text', text: line ? `Project ${key}:\n${line.trim()}` : `No visible project "${key}".` }] }
71
+ },
72
+ )
73
+
74
+ await server.connect(new StdioServerTransport())
75
+ }
package/lib/setup.mjs ADDED
@@ -0,0 +1,112 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync } from 'fs'
2
+ import { homedir } from 'os'
3
+ import { join, dirname } from 'path'
4
+
5
+ // One-command employee onboarding. Wires both:
6
+ // 1. ~/.claude.json → the cortex MCP server (context-serving)
7
+ // 2. ~/.claude/settings.json → the capture Stop hook (activity ingest)
8
+ //
9
+ // Safe by construction: backs up each file before touching it, validates JSON,
10
+ // merges into existing structures (never clobbers other MCP servers / hooks),
11
+ // and is idempotent (re-running just updates the cortex entries in place).
12
+
13
+ const PKG = '@theronap/cortex-mcp'
14
+
15
+ function readJson(path) {
16
+ if (!existsSync(path)) return {}
17
+ const raw = readFileSync(path, 'utf8').trim()
18
+ if (!raw) return {}
19
+ return JSON.parse(raw) // throws on malformed — caller handles
20
+ }
21
+
22
+ function backup(path) {
23
+ if (!existsSync(path)) return null
24
+ const bak = `${path}.cortex-bak`
25
+ copyFileSync(path, bak)
26
+ return bak
27
+ }
28
+
29
+ function ensureDir(path) {
30
+ const dir = dirname(path)
31
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
32
+ }
33
+
34
+ export function runSetup(argv) {
35
+ const token = argv[0]
36
+ if (!token || token.startsWith('-')) {
37
+ process.stderr.write(
38
+ 'Usage: npx @theronap/cortex-mcp setup <CORTEX_TOKEN>\n\n' +
39
+ 'Get your token from the Cortex console → Connect your AI.\n'
40
+ )
41
+ process.exit(1)
42
+ }
43
+ const base = process.env.CORTEX_URL ?? 'https://cortex-console.vercel.app'
44
+ const home = homedir()
45
+ const claudeJson = join(home, '.claude.json')
46
+ const settingsJson = join(home, '.claude', 'settings.json')
47
+
48
+ const log = (m) => process.stdout.write(m + '\n')
49
+ log('')
50
+ log('Cortex setup — wiring your AI assistant…')
51
+
52
+ // ── 1. MCP server in ~/.claude.json ──────────────────────────────────────
53
+ try {
54
+ let cfg
55
+ try { cfg = readJson(claudeJson) } catch (e) {
56
+ process.stderr.write(`\n✗ ${claudeJson} is not valid JSON — fix or remove it, then re-run.\n`)
57
+ process.exit(1)
58
+ }
59
+ const bak = backup(claudeJson)
60
+ cfg.mcpServers = cfg.mcpServers ?? {}
61
+ cfg.mcpServers.cortex = {
62
+ type: 'stdio',
63
+ command: 'npx',
64
+ args: ['-y', PKG],
65
+ env: { CORTEX_TOKEN: token },
66
+ }
67
+ ensureDir(claudeJson)
68
+ writeFileSync(claudeJson, JSON.stringify(cfg, null, 2))
69
+ log(` ✓ MCP server → ${claudeJson}${bak ? ' (backup saved)' : ''}`)
70
+ } catch (e) {
71
+ process.stderr.write(` ✗ failed to update ${claudeJson}: ${e.message}\n`)
72
+ process.exit(1)
73
+ }
74
+
75
+ // ── 2. Capture Stop hook in ~/.claude/settings.json ──────────────────────
76
+ try {
77
+ let s
78
+ try { s = readJson(settingsJson) } catch (e) {
79
+ process.stderr.write(`\n✗ ${settingsJson} is not valid JSON — fix or remove it, then re-run.\n`)
80
+ process.exit(1)
81
+ }
82
+ const bak = backup(settingsJson)
83
+ s.hooks = s.hooks ?? {}
84
+ s.hooks.Stop = Array.isArray(s.hooks.Stop) ? s.hooks.Stop : []
85
+
86
+ const captureCmd = `CORTEX_TOKEN=${token} npx -y ${PKG} capture`
87
+ // Remove any prior cortex capture hook (idempotent: drop old token / old path forms).
88
+ for (const grp of s.hooks.Stop) {
89
+ if (Array.isArray(grp.hooks)) {
90
+ grp.hooks = grp.hooks.filter((h) => !/cortex-mcp.*capture|capture-session-cloud/.test(h.command ?? ''))
91
+ }
92
+ }
93
+ // Find or create a matcher:"" group and append.
94
+ let grp = s.hooks.Stop.find((g) => (g.matcher ?? '') === '')
95
+ if (!grp) { grp = { matcher: '', hooks: [] }; s.hooks.Stop.push(grp) }
96
+ grp.hooks = grp.hooks ?? []
97
+ grp.hooks.push({ type: 'command', command: captureCmd })
98
+
99
+ ensureDir(settingsJson)
100
+ writeFileSync(settingsJson, JSON.stringify(s, null, 2))
101
+ log(` ✓ Capture hook → ${settingsJson}${bak ? ' (backup saved)' : ''}`)
102
+ } catch (e) {
103
+ process.stderr.write(` ✗ failed to update ${settingsJson}: ${e.message}\n`)
104
+ process.exit(1)
105
+ }
106
+
107
+ log('')
108
+ log('Done. Restart Claude Code, then your AI will see your Cortex context')
109
+ log('and your sessions will flow into the org automatically.')
110
+ log(`Console: ${base}`)
111
+ log('')
112
+ }
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@theronap/cortex-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
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": {
7
7
  "cortex-mcp": "bin/cortex-mcp.mjs"
8
8
  },
9
9
  "files": [
10
- "bin"
10
+ "bin",
11
+ "lib"
11
12
  ],
12
13
  "engines": {
13
14
  "node": ">=18"