@theronap/cortex-mcp 0.4.3 → 0.4.4

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/capture.mjs CHANGED
@@ -65,7 +65,7 @@ export async function runCapture() {
65
65
  const j = await res.json().catch(() => ({}))
66
66
  process.stderr.write(`cortex: ${j.inserted ? 'captured' : 'updated'} "${j.title ?? repo}" → ${repo}\n`)
67
67
  } else {
68
- const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
68
+ const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'), res.headers.get('x-deny-reason'))
69
69
  process.stderr.write(`cortex: ingest failed — ${d.message}\n`)
70
70
  }
71
71
  }
package/lib/diagnose.mjs CHANGED
@@ -6,8 +6,10 @@
6
6
  // to tell the truth about WHAT failed.
7
7
  //
8
8
  // KEY SIGNAL: the Cortex app ALWAYS returns JSON ({ error: ... }). So a NON-JSON body on a
9
- // 4xx/5xx means infrastructure handled the request, not Cortex auth — re-running setup or
10
- // regenerating the token will not help; it is usually transient and worth a retry.
9
+ // 4xx/5xx means something else handled the request, not Cortex auth — re-running setup or
10
+ // regenerating the token will not help. Two cases: (1) an egress allowlist block
11
+ // (x-deny-reason: host_not_allowed) — NOT retriable, the host must be added to the allowlist;
12
+ // (2) a transient infrastructure hiccup — retriable.
11
13
 
12
14
  export const isUuid = (s) =>
13
15
  /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s ?? '')
@@ -39,10 +41,11 @@ export function resolveBase(rawUrl) {
39
41
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
40
42
 
41
43
  // A non-OK response → an actionable diagnosis: { kind, retriable, message }.
42
- // kind: 'auth' → token bad/revoked/wrong-deployment (NOT retriable)
43
- // 'infra' → blocked by infrastructure, non-JSON body (retriable, usually transient)
44
- // 'app' a real Cortex API error with a JSON message (retriable only if 5xx)
45
- export function classify(status, contentType, bodyText, requestId) {
44
+ // kind: 'auth' → token bad/revoked/wrong-deployment (NOT retriable)
45
+ // 'egress' → blocked by network egress allowlist (NOT retriable, not a Cortex/Vercel issue)
46
+ // 'infra' blocked by infrastructure, non-JSON body (retriable, usually transient)
47
+ // 'app' → a real Cortex API error with a JSON message (retriable only if 5xx)
48
+ export function classify(status, contentType, bodyText, requestId, denyReason) {
46
49
  const isJson = (contentType ?? '').includes('application/json')
47
50
  let appError = null
48
51
  if (isJson) { try { appError = JSON.parse(bodyText)?.error ?? null } catch { /* not json after all */ } }
@@ -56,6 +59,15 @@ export function classify(status, contentType, bodyText, requestId) {
56
59
  `the Cortex console → Connect your AI, then re-run setup.${rid}`,
57
60
  }
58
61
  }
62
+ if (denyReason === 'host_not_allowed' || /host (not in allowlist|not allowed)/i.test(bodyText ?? '')) {
63
+ return {
64
+ kind: 'egress', retriable: false,
65
+ message: `Blocked by your network's egress allowlist (HTTP ${status}: ${denyReason ?? 'host_not_allowed'}). ` +
66
+ `The request never left this environment — this is NOT a Cortex, token, or Vercel issue. ` +
67
+ `Add ${CANONICAL_BASE} to your environment's outbound allowlist, or run from a machine with open egress. ` +
68
+ `Re-running setup or regenerating the token will not help.${rid}`,
69
+ }
70
+ }
59
71
  if (!isJson) {
60
72
  return {
61
73
  kind: 'infra', retriable: true,
@@ -79,10 +91,11 @@ export async function fetchCortex(url, opts = {}, { retries = 2, baseDelayMs = 4
79
91
  try {
80
92
  const res = await fetch(url, opts)
81
93
  const ct = res.headers.get('content-type') ?? ''
94
+ const denyReason = res.headers.get('x-deny-reason') ?? ''
82
95
  const transient =
83
96
  res.status === 429 ||
84
97
  res.status >= 500 ||
85
- (res.status === 403 && !ct.includes('application/json')) // infra block, not app authz
98
+ (res.status === 403 && !ct.includes('application/json') && !denyReason) // egress block is not transient
86
99
  if (transient && attempt < retries) {
87
100
  await sleep(baseDelayMs * 2 ** attempt)
88
101
  continue
@@ -122,7 +135,7 @@ export async function checkToken(token, base) {
122
135
  const contentType = res.headers.get('content-type')
123
136
  const body = await res.text()
124
137
  if (!res.ok) {
125
- return { ok: false, status: res.status, requestId, diagnosis: classify(res.status, contentType, body, requestId) }
138
+ return { ok: false, status: res.status, requestId, diagnosis: classify(res.status, contentType, body, requestId, res.headers.get('x-deny-reason')) }
126
139
  }
127
140
  let projectCount
128
141
  try {
package/lib/server.mjs CHANGED
@@ -25,7 +25,7 @@ export async function runServer(version) {
25
25
  const res = await fetchCortex(`${BASE}/api/mcp-context`, { headers: { Authorization: `Bearer ${TOKEN}` } })
26
26
  if (!res.ok) {
27
27
  const body = await res.text()
28
- throw new Error(classify(res.status, res.headers.get('content-type'), body, res.headers.get('x-vercel-id')).message)
28
+ throw new Error(classify(res.status, res.headers.get('content-type'), body, res.headers.get('x-vercel-id'), res.headers.get('x-deny-reason')).message)
29
29
  }
30
30
  const { context } = await res.json()
31
31
  cache = { text: context, ts: now }
@@ -92,7 +92,7 @@ export async function runServer(version) {
92
92
  return { content: [{ type: 'text', text: `Could not assemble story: ${e.message}` }] }
93
93
  }
94
94
  if (!res.ok) {
95
- const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
95
+ const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'), res.headers.get('x-deny-reason'))
96
96
  return { content: [{ type: 'text', text: `Could not assemble story: ${d.message}` }] }
97
97
  }
98
98
  const { answer, sourceCount, sources } = await res.json()
@@ -124,7 +124,7 @@ export async function runServer(version) {
124
124
  return { content: [{ type: 'text', text: `Could not set privacy: ${e.message}` }] }
125
125
  }
126
126
  if (!res.ok) {
127
- const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'))
127
+ const d = classify(res.status, res.headers.get('content-type'), await res.text(), res.headers.get('x-vercel-id'), res.headers.get('x-deny-reason'))
128
128
  return { content: [{ type: 'text', text: `Could not set privacy: ${d.message}` }] }
129
129
  }
130
130
  const out = await res.json()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theronap/cortex-mcp",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
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": {