@danielblomma/cortex-mcp 2.0.2 → 2.0.3

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/README.md CHANGED
@@ -255,30 +255,9 @@ Input:
255
255
  - `depth` (int, 1-3, default `1`)
256
256
  - `include_edges` (bool, default `true`)
257
257
 
258
- ### `context.find_callers`
258
+ ### `context.impact`
259
259
 
260
- Return chunk callers for a chunk or file entity using the indexed call graph.
261
-
262
- Input:
263
-
264
- - `entity_id` (string, required)
265
- - `depth` (int, 1-4, default `1`)
266
- - `include_edges` (bool, default `true`)
267
-
268
- ### `context.trace_calls`
269
-
270
- Trace call graph neighbors from a chunk or file entity in the requested direction.
271
-
272
- Input:
273
-
274
- - `entity_id` (string, required)
275
- - `depth` (int, 1-4, default `2`)
276
- - `direction` (`"outgoing"` | `"incoming"` | `"both"`, default `"outgoing"`)
277
- - `include_edges` (bool, default `true`)
278
-
279
- ### `context.impact_analysis`
280
-
281
- Analyze likely impacted call-graph entities starting from an entity id or search query.
260
+ Traverse likely impact paths across config, code and SQL starting from an entity id or query.
282
261
 
283
262
  Input:
284
263
 
@@ -286,8 +265,9 @@ Input:
286
265
  - `query` (string, optional)
287
266
  - `depth` (int, 1-4, default `2`)
288
267
  - `top_k` (int, 1-20, default `8`)
289
- - `direction` (`"incoming"` | `"outgoing"` | `"both"`, default `"incoming"`)
290
268
  - `include_edges` (bool, default `true`)
269
+ - `profile` (`"all"` | `"config_only"` | `"config_to_sql"` | `"code_only"` | `"sql_only"`, default `"all"`)
270
+ - `sort_by` (`"impact_score"` | `"shortest_path"` | `"semantic_score"` | `"graph_score"` | `"trust_score"`, default `"impact_score"`)
291
271
 
292
272
  ### `context.get_rules`
293
273
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@danielblomma/cortex-mcp",
3
3
  "mcpName": "io.github.DanielBlomma/cortex",
4
- "version": "2.0.2",
4
+ "version": "2.0.3",
5
5
  "description": "Local, repo-scoped context platform for coding assistants. Semantic search, graph relationships, and architectural rule context.",
6
6
  "type": "module",
7
7
  "author": "Daniel Blomma",
@@ -95,6 +95,43 @@ export type TelemetryEvent = {
95
95
  duration_ms?: number;
96
96
  };
97
97
 
98
+ function subtractCounter(current: number, pushed: number): number {
99
+ return Math.max(0, current - pushed);
100
+ }
101
+
102
+ function hasUsage(metrics: TelemetryMetrics): boolean {
103
+ if (
104
+ metrics.total_tool_calls > 0 ||
105
+ metrics.successful_tool_calls > 0 ||
106
+ metrics.failed_tool_calls > 0 ||
107
+ metrics.total_duration_ms > 0 ||
108
+ metrics.session_starts > 0 ||
109
+ metrics.session_ends > 0 ||
110
+ metrics.session_duration_ms_total > 0 ||
111
+ metrics.searches > 0 ||
112
+ metrics.related_lookups > 0 ||
113
+ metrics.caller_lookups > 0 ||
114
+ metrics.trace_lookups > 0 ||
115
+ metrics.impact_analyses > 0 ||
116
+ metrics.rule_lookups > 0 ||
117
+ metrics.reloads > 0 ||
118
+ metrics.total_results_returned > 0 ||
119
+ metrics.estimated_tokens_saved > 0 ||
120
+ metrics.estimated_tokens_total > 0
121
+ ) {
122
+ return true;
123
+ }
124
+
125
+ return Object.values(metrics.tool_metrics).some(
126
+ (bucket) =>
127
+ bucket.calls > 0 ||
128
+ bucket.failures > 0 ||
129
+ bucket.total_duration_ms > 0 ||
130
+ bucket.total_results_returned > 0 ||
131
+ bucket.estimated_tokens_saved > 0,
132
+ );
133
+ }
134
+
98
135
  export class TelemetryCollector {
99
136
  private metrics: TelemetryMetrics;
100
137
  private readonly metricsPath: string;
@@ -167,6 +204,7 @@ export class TelemetryCollector {
167
204
  case "context.trace_calls":
168
205
  this.metrics.trace_lookups++;
169
206
  break;
207
+ case "context.impact":
170
208
  case "context.impact_analysis":
171
209
  this.metrics.impact_analyses++;
172
210
  break;
@@ -211,7 +249,143 @@ export class TelemetryCollector {
211
249
  }
212
250
 
213
251
  getMetrics(): TelemetryMetrics {
214
- return { ...this.metrics };
252
+ return {
253
+ ...this.metrics,
254
+ tool_metrics: Object.fromEntries(
255
+ Object.entries(this.metrics.tool_metrics).map(([toolName, bucket]) => [
256
+ toolName,
257
+ { ...bucket },
258
+ ]),
259
+ ),
260
+ };
261
+ }
262
+
263
+ acknowledgePush(pushed: TelemetryMetrics): void {
264
+ const nextToolMetrics: TelemetryMetrics["tool_metrics"] = {};
265
+ const toolNames = new Set([
266
+ ...Object.keys(this.metrics.tool_metrics),
267
+ ...Object.keys(pushed.tool_metrics ?? {}),
268
+ ]);
269
+
270
+ for (const toolName of toolNames) {
271
+ const currentBucket = this.metrics.tool_metrics[toolName] ?? {
272
+ calls: 0,
273
+ failures: 0,
274
+ total_duration_ms: 0,
275
+ total_results_returned: 0,
276
+ estimated_tokens_saved: 0,
277
+ };
278
+ const pushedBucket = pushed.tool_metrics?.[toolName] ?? {
279
+ calls: 0,
280
+ failures: 0,
281
+ total_duration_ms: 0,
282
+ total_results_returned: 0,
283
+ estimated_tokens_saved: 0,
284
+ };
285
+
286
+ const nextBucket = {
287
+ calls: subtractCounter(currentBucket.calls, pushedBucket.calls),
288
+ failures: subtractCounter(currentBucket.failures, pushedBucket.failures),
289
+ total_duration_ms: subtractCounter(
290
+ currentBucket.total_duration_ms,
291
+ pushedBucket.total_duration_ms,
292
+ ),
293
+ total_results_returned: subtractCounter(
294
+ currentBucket.total_results_returned,
295
+ pushedBucket.total_results_returned,
296
+ ),
297
+ estimated_tokens_saved: subtractCounter(
298
+ currentBucket.estimated_tokens_saved,
299
+ pushedBucket.estimated_tokens_saved,
300
+ ),
301
+ };
302
+
303
+ if (
304
+ nextBucket.calls > 0 ||
305
+ nextBucket.failures > 0 ||
306
+ nextBucket.total_duration_ms > 0 ||
307
+ nextBucket.total_results_returned > 0 ||
308
+ nextBucket.estimated_tokens_saved > 0
309
+ ) {
310
+ nextToolMetrics[toolName] = nextBucket;
311
+ }
312
+ }
313
+
314
+ const nextMetrics: TelemetryMetrics = {
315
+ ...this.metrics,
316
+ period_start: pushed.period_end,
317
+ total_tool_calls: subtractCounter(
318
+ this.metrics.total_tool_calls,
319
+ pushed.total_tool_calls,
320
+ ),
321
+ successful_tool_calls: subtractCounter(
322
+ this.metrics.successful_tool_calls,
323
+ pushed.successful_tool_calls,
324
+ ),
325
+ failed_tool_calls: subtractCounter(
326
+ this.metrics.failed_tool_calls,
327
+ pushed.failed_tool_calls,
328
+ ),
329
+ total_duration_ms: subtractCounter(
330
+ this.metrics.total_duration_ms,
331
+ pushed.total_duration_ms,
332
+ ),
333
+ session_starts: subtractCounter(
334
+ this.metrics.session_starts,
335
+ pushed.session_starts,
336
+ ),
337
+ session_ends: subtractCounter(this.metrics.session_ends, pushed.session_ends),
338
+ session_duration_ms_total: subtractCounter(
339
+ this.metrics.session_duration_ms_total,
340
+ pushed.session_duration_ms_total,
341
+ ),
342
+ searches: subtractCounter(this.metrics.searches, pushed.searches),
343
+ related_lookups: subtractCounter(
344
+ this.metrics.related_lookups,
345
+ pushed.related_lookups,
346
+ ),
347
+ caller_lookups: subtractCounter(
348
+ this.metrics.caller_lookups,
349
+ pushed.caller_lookups,
350
+ ),
351
+ trace_lookups: subtractCounter(
352
+ this.metrics.trace_lookups,
353
+ pushed.trace_lookups,
354
+ ),
355
+ impact_analyses: subtractCounter(
356
+ this.metrics.impact_analyses,
357
+ pushed.impact_analyses,
358
+ ),
359
+ rule_lookups: subtractCounter(
360
+ this.metrics.rule_lookups,
361
+ pushed.rule_lookups,
362
+ ),
363
+ reloads: subtractCounter(this.metrics.reloads, pushed.reloads),
364
+ total_results_returned: subtractCounter(
365
+ this.metrics.total_results_returned,
366
+ pushed.total_results_returned,
367
+ ),
368
+ estimated_tokens_saved: subtractCounter(
369
+ this.metrics.estimated_tokens_saved,
370
+ pushed.estimated_tokens_saved,
371
+ ),
372
+ estimated_tokens_total: subtractCounter(
373
+ this.metrics.estimated_tokens_total,
374
+ pushed.estimated_tokens_total,
375
+ ),
376
+ client_version: this.clientVersion,
377
+ instance_id: this.instanceId,
378
+ tool_metrics: nextToolMetrics,
379
+ };
380
+
381
+ if (hasUsage(nextMetrics)) {
382
+ this.metrics = nextMetrics;
383
+ this.metrics.period_end = new Date().toISOString();
384
+ } else {
385
+ this.metrics = emptyMetrics(this.clientVersion, this.instanceId);
386
+ }
387
+
388
+ this.dirty = true;
215
389
  }
216
390
 
217
391
  flush(): void {
@@ -1,5 +1,6 @@
1
- import { readFileSync, existsSync } from "node:fs";
1
+ import { readFileSync, existsSync, writeFileSync, mkdirSync, rmSync } from "node:fs";
2
2
  import { basename, join } from "node:path";
3
+ import { randomUUID } from "node:crypto";
3
4
  import { CortexDaemon } from "./server.js";
4
5
  import type {
5
6
  PolicyCheckPayload,
@@ -11,7 +12,7 @@ import type {
11
12
  } from "./protocol.js";
12
13
  import { loadEnterpriseConfig, resolveEnterpriseActivation } from "../core/config.js";
13
14
  import { pushMetrics } from "../enterprise/telemetry/sync.js";
14
- import type { TelemetryMetrics } from "../core/telemetry/collector.js";
15
+ import { TelemetryCollector, type TelemetryMetrics } from "../core/telemetry/collector.js";
15
16
  import { AuditWriter, type AuditEntry } from "../core/audit/writer.js";
16
17
  import { PolicyStore } from "../core/policy/store.js";
17
18
  import {
@@ -89,6 +90,76 @@ function readMetrics(contextDir: string): TelemetryMetrics | null {
89
90
  }
90
91
  }
91
92
 
93
+ // Pending-push state: snapshot + push_id are written to disk before the
94
+ // network call. If the daemon crashes mid-push, the next tick replays the
95
+ // same push_id so the server can deduplicate.
96
+ type PendingPush = {
97
+ snapshot: TelemetryMetrics;
98
+ push_id: string;
99
+ written_at: string;
100
+ };
101
+
102
+ function pendingPushPath(contextDir: string): string {
103
+ return join(contextDir, "telemetry", "pending-push.json");
104
+ }
105
+
106
+ function readPendingPush(contextDir: string): PendingPush | null {
107
+ const path = pendingPushPath(contextDir);
108
+ if (!existsSync(path)) return null;
109
+ try {
110
+ return JSON.parse(readFileSync(path, "utf8")) as PendingPush;
111
+ } catch {
112
+ return null;
113
+ }
114
+ }
115
+
116
+ function writePendingPush(contextDir: string, pending: PendingPush): void {
117
+ const path = pendingPushPath(contextDir);
118
+ mkdirSync(join(contextDir, "telemetry"), { recursive: true });
119
+ writeFileSync(path, JSON.stringify(pending, null, 2), "utf8");
120
+ }
121
+
122
+ function deletePendingPush(contextDir: string): void {
123
+ const path = pendingPushPath(contextDir);
124
+ try {
125
+ rmSync(path, { force: true });
126
+ } catch {
127
+ // best effort
128
+ }
129
+ }
130
+
131
+ function ackOnDisk(contextDir: string, pushed: TelemetryMetrics): void {
132
+ const collector = new TelemetryCollector(contextDir, pushed.client_version || "unknown");
133
+ collector.acknowledgePush(pushed);
134
+ collector.flush();
135
+ }
136
+
137
+ // Per-cwd exponential backoff so a flapping endpoint doesn't get hammered.
138
+ // 1m, 2m, 4m, 8m, 16m, cap 30m. Reset on success.
139
+ type TelemetryBackoffState = { nextPushAt: number; consecutiveFailures: number };
140
+ const telemetryBackoff = new Map<string, TelemetryBackoffState>();
141
+ const TELEMETRY_BACKOFF_BASE_MS = 60_000;
142
+ const TELEMETRY_BACKOFF_CAP_MS = 30 * 60_000;
143
+
144
+ function shouldSkipTelemetryPush(cwd: string, now = Date.now()): boolean {
145
+ const state = telemetryBackoff.get(cwd);
146
+ return state ? now < state.nextPushAt : false;
147
+ }
148
+
149
+ function recordTelemetryPushOutcome(cwd: string, success: boolean, now = Date.now()): void {
150
+ if (success) {
151
+ telemetryBackoff.delete(cwd);
152
+ return;
153
+ }
154
+ const prev = telemetryBackoff.get(cwd) ?? { nextPushAt: 0, consecutiveFailures: 0 };
155
+ const failures = prev.consecutiveFailures + 1;
156
+ const delay = Math.min(TELEMETRY_BACKOFF_BASE_MS * 2 ** (failures - 1), TELEMETRY_BACKOFF_CAP_MS);
157
+ telemetryBackoff.set(cwd, {
158
+ consecutiveFailures: failures,
159
+ nextPushAt: now + delay,
160
+ });
161
+ }
162
+
92
163
  async function telemetryFlush(
93
164
  payload: TelemetryFlushPayload,
94
165
  ): Promise<TelemetryFlushResult> {
@@ -111,31 +182,68 @@ async function telemetryFlush(
111
182
  return { flushed: false, events_pushed: 0 };
112
183
  }
113
184
 
185
+ if (shouldSkipTelemetryPush(cwd)) {
186
+ return { flushed: false, events_pushed: 0 };
187
+ }
188
+
189
+ const repo = basename(cwd);
190
+ const endpoint = config.telemetry.endpoint;
191
+ const apiKey = config.telemetry.api_key;
192
+
193
+ // Recovery: if a pending push exists, retry it first with the same
194
+ // push_id so the server can deduplicate against an earlier in-flight
195
+ // attempt that may have crashed before delete.
196
+ const pending = readPendingPush(contextDir);
197
+ if (pending) {
198
+ const result = await pushMetrics(pending.snapshot, endpoint, apiKey, {
199
+ repo,
200
+ session_id: payload.session_id,
201
+ push_id: pending.push_id,
202
+ });
203
+ recordTelemetryPushOutcome(cwd, result.success);
204
+ if (!result.success) {
205
+ process.stderr.write(
206
+ `[cortex-daemon] pending telemetry push retry failed: ${result.error ?? "unknown"}\n`,
207
+ );
208
+ return { flushed: false, events_pushed: 0 };
209
+ }
210
+ ackOnDisk(contextDir, pending.snapshot);
211
+ deletePendingPush(contextDir);
212
+ return { flushed: true, events_pushed: pending.snapshot.total_tool_calls };
213
+ }
214
+
114
215
  const metrics = readMetrics(contextDir);
115
216
  if (!metrics) {
116
- // No metrics on disk yet — MCP probably hasn't flushed once. Nothing
117
- // to push from disk. (MCP's interval flush + session-end push handle
118
- // the in-memory case.)
217
+ // No metrics on disk yet — MCP hasn't flushed. Nothing to push.
119
218
  return { flushed: false, events_pushed: 0 };
120
219
  }
121
220
 
122
- const result = await pushMetrics(
123
- metrics,
124
- config.telemetry.endpoint,
125
- config.telemetry.api_key,
126
- {
127
- repo: basename(cwd),
128
- session_id: payload.session_id,
129
- },
130
- );
221
+ const push_id = randomUUID();
222
+ writePendingPush(contextDir, {
223
+ snapshot: metrics,
224
+ push_id,
225
+ written_at: new Date().toISOString(),
226
+ });
227
+
228
+ const result = await pushMetrics(metrics, endpoint, apiKey, {
229
+ repo,
230
+ session_id: payload.session_id,
231
+ push_id,
232
+ });
233
+
234
+ recordTelemetryPushOutcome(cwd, result.success);
131
235
 
132
236
  if (!result.success) {
133
237
  process.stderr.write(
134
238
  `[cortex-daemon] telemetry push failed: ${result.error ?? "unknown"}\n`,
135
239
  );
240
+ // Pending stays on disk; next tick (after backoff) will retry.
136
241
  return { flushed: false, events_pushed: 0 };
137
242
  }
138
243
 
244
+ ackOnDisk(contextDir, metrics);
245
+ deletePendingPush(contextDir);
246
+
139
247
  return {
140
248
  flushed: true,
141
249
  events_pushed: metrics.total_tool_calls,
@@ -267,6 +375,33 @@ async function main(): Promise<void> {
267
375
  });
268
376
  }
269
377
 
378
+ // Periodic telemetry push. Daemon owns the network call so MCP doesn't
379
+ // race with itself or with this loop. Walks active sessions, dedupes
380
+ // cwds, and runs the existing per-cwd flush handler.
381
+ const telemetryPushRaw = parseInt(process.env.CORTEX_TELEMETRY_PUSH_MS ?? "", 10);
382
+ const telemetryPushMs =
383
+ Number.isFinite(telemetryPushRaw) && telemetryPushRaw > 0
384
+ ? telemetryPushRaw
385
+ : 5 * 60 * 1000;
386
+ if (process.env.CORTEX_DISABLE_TELEMETRY_PUSH !== "1") {
387
+ const telemetryTimer = setInterval(async () => {
388
+ const cwds = new Set<string>();
389
+ for (const [, state] of tracker.getActiveSessions()) {
390
+ if (state.cwd) cwds.add(state.cwd);
391
+ }
392
+ for (const cwd of cwds) {
393
+ try {
394
+ await telemetryFlush({ reason: "interval", cwd });
395
+ } catch (err) {
396
+ process.stderr.write(
397
+ `[cortex-daemon] telemetry push failed for ${cwd}: ${err instanceof Error ? err.message : String(err)}\n`,
398
+ );
399
+ }
400
+ }
401
+ }, telemetryPushMs);
402
+ if (typeof telemetryTimer.unref === "function") telemetryTimer.unref();
403
+ }
404
+
270
405
  if (process.env.CORTEX_DISABLE_TAMPER_CHECK !== "1") {
271
406
  const checkTimer = setInterval(() => {
272
407
  const detected = tracker.detectTamper({
@@ -9,7 +9,6 @@ import {
9
9
  } from "../core/config.js";
10
10
  import { deployBundledModel } from "./model/deploy.js";
11
11
  import { TelemetryCollector } from "../core/telemetry/collector.js";
12
- import { pushMetrics } from "./telemetry/sync.js";
13
12
  import { AuditWriter, type AuditEntry } from "../core/audit/writer.js";
14
13
  import { pushAuditEvents, queueAuditEvent, setAuditPushContext } from "./audit/push.js";
15
14
  import { PolicyStore } from "../core/policy/store.js";
@@ -203,24 +202,10 @@ export function recordToolActivity(activity: ToolActivity): void {
203
202
  export async function onSessionEnd(): Promise<void> {
204
203
  if (!activeConfig) return;
205
204
  const config = activeConfig;
206
- if (config.telemetry.enabled && config.telemetry.endpoint && activeCollector) {
205
+ // Telemetry push is owned by the daemon. MCP only persists in-memory
206
+ // metrics to disk so the daemon can pick them up on its next push tick.
207
+ if (config.telemetry.enabled && activeCollector) {
207
208
  activeCollector.flush();
208
- try {
209
- const result = await pushMetrics(
210
- activeCollector.getMetrics(),
211
- config.telemetry.endpoint,
212
- config.telemetry.api_key,
213
- {
214
- repo: activeRepo ?? undefined,
215
- session_id: activeSessionId ?? undefined,
216
- },
217
- );
218
- if (!result.success) {
219
- process.stderr.write(`[cortex-enterprise] Shutdown telemetry push failed: ${result.error}\n`);
220
- }
221
- } catch (err) {
222
- process.stderr.write(`[cortex-enterprise] Shutdown telemetry push error: ${err}\n`);
223
- }
224
209
  }
225
210
 
226
211
  await flushComplianceQueues(config, "shutdown");
@@ -351,28 +336,14 @@ export async function register(server: McpServer): Promise<void> {
351
336
  process.stderr.write(`[cortex-enterprise] Active: ${features.join(", ")}\n`);
352
337
  }
353
338
 
354
- // Schedule telemetry flush + push
339
+ // Telemetry push is owned by the daemon (single network writer).
340
+ // MCP only persists in-memory metrics to disk on a tick so the daemon
341
+ // can read and push them.
355
342
  if (config.telemetry.enabled) {
356
- // Push any accumulated metrics from previous sessions on startup
357
- if (config.telemetry.endpoint) {
358
- pushMetrics(collector.getMetrics(), config.telemetry.endpoint, config.telemetry.api_key, {
359
- repo: activeRepo ?? undefined,
360
- session_id: activeSessionId ?? undefined,
361
- })
362
- .then((r) => { if (!r.success) process.stderr.write(`[cortex-enterprise] Startup telemetry push failed: ${r.error}\n`); })
363
- .catch((err) => { process.stderr.write(`[cortex-enterprise] Startup telemetry push error: ${err}\n`); });
364
- }
365
-
366
343
  const intervalMs = config.telemetry.interval_minutes * 60000;
367
- const timer = setInterval(async () => {
344
+ const timer = setInterval(() => {
368
345
  try {
369
346
  collector.flush();
370
- if (config.telemetry.endpoint) {
371
- await pushMetrics(collector.getMetrics(), config.telemetry.endpoint, config.telemetry.api_key, {
372
- repo: activeRepo ?? undefined,
373
- session_id: activeSessionId ?? undefined,
374
- });
375
- }
376
347
  } catch (err) {
377
348
  process.stderr.write(`[cortex-enterprise] Telemetry flush error: ${err}\n`);
378
349
  }
@@ -67,6 +67,7 @@ export const OUTBOUND_DATA_BOUNDARY = {
67
67
  type TelemetryPushContext = {
68
68
  repo?: string;
69
69
  session_id?: string;
70
+ push_id?: string;
70
71
  };
71
72
 
72
73
  const MAX_OBJECT_KEYS = 12;
@@ -207,6 +208,7 @@ export function buildTelemetryPushPayload(
207
208
  repo: context.repo,
208
209
  instance_id: metrics.instance_id,
209
210
  session_id: context.session_id,
211
+ push_id: context.push_id,
210
212
  tool_metrics: metrics.tool_metrics,
211
213
  };
212
214
  }
@@ -11,6 +11,7 @@ export type PushResult = {
11
11
  export type PushContext = {
12
12
  repo?: string;
13
13
  session_id?: string;
14
+ push_id?: string;
14
15
  };
15
16
 
16
17
  let lastPush: PushResult | null = null;
@@ -443,6 +443,8 @@ async function main(): Promise<void> {
443
443
 
444
444
  if (reset) {
445
445
  fs.rmSync(DB_PATH, { recursive: true, force: true });
446
+ fs.rmSync(`${DB_PATH}.wal`, { force: true });
447
+ fs.rmSync(`${DB_PATH}.shm`, { force: true });
446
448
  }
447
449
  fs.mkdirSync(path.dirname(DB_PATH), { recursive: true });
448
450
 
@@ -0,0 +1,30 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import path from "node:path";
6
+
7
+ import { TelemetryCollector } from "../dist/core/telemetry/collector.js";
8
+
9
+ function createContextDir(prefix) {
10
+ return mkdtempSync(path.join(tmpdir(), prefix));
11
+ }
12
+
13
+ test("TelemetryCollector counts context.impact as an impact analysis", () => {
14
+ const contextDir = createContextDir("cortex-telemetry-");
15
+ const collector = new TelemetryCollector(contextDir, "test-version");
16
+
17
+ collector.recordEvent({
18
+ tool: "context.impact",
19
+ phase: "success",
20
+ result_count: 2,
21
+ estimated_tokens_saved: 800,
22
+ duration_ms: 15,
23
+ });
24
+
25
+ const metrics = collector.getMetrics();
26
+ assert.equal(metrics.total_tool_calls, 1);
27
+ assert.equal(metrics.successful_tool_calls, 1);
28
+ assert.equal(metrics.impact_analyses, 1);
29
+ assert.equal(metrics.tool_metrics["context.impact"].calls, 1);
30
+ });
@@ -1,160 +0,0 @@
1
- # MCP Marketplace Submission
2
-
3
- ## Package Information
4
-
5
- **Name:** `@danielblomma/cortex-mcp`
6
- **Description:** Local, repo-scoped context platform for coding assistants. Semantic search, graph relationships, and architectural rule context.
7
- **Author:** Daniel Blomma
8
- **License:** MIT
9
- **Repository:** https://github.com/DanielBlomma/cortex
10
-
11
- ## MCP Server Details
12
-
13
- ### Tools Provided
14
-
15
- 1. **context.search**
16
- - Semantic search across indexed entities (files, rules, ADRs)
17
- - Hybrid ranking (semantic + graph + trust + recency)
18
- - Optional content return for high-signal snippets
19
-
20
- 2. **context.get_related**
21
- - Graph-based entity relationships
22
- - Finds connected rules/files/ADRs with optional edge details
23
-
24
- 3. **context.get_rules**
25
- - Active rules and architectural decisions
26
- - Scope-based filtering
27
-
28
- 4. **context.reload**
29
- - Hot-reload graph after code changes
30
-
31
- ### Advanced Features (Experimental)
32
-
33
- Cortex can extract function-level chunks and build call graphs in experimental builds:
34
-
35
- - `context.find_callers` - What calls this function?
36
- - `context.trace_calls` - What does this function call?
37
- - `context.impact_analysis` - What is impacted if this function changes?
38
- - Requires JavaScript/TypeScript codebase and semantic chunking/call graph indexing enabled.
39
-
40
- Note: these APIs are experimental and are not part of the stable tool contract in this submission.
41
-
42
- ### Installation
43
-
44
- #### For MCP Marketplace Users
45
-
46
- ```bash
47
- # Install CLI globally
48
- npm i -g @danielblomma/cortex-mcp
49
-
50
- # Navigate to your project
51
- cd ~/my-project
52
-
53
- # Initialize Cortex in your project
54
- cortex init --bootstrap
55
- ```
56
-
57
- This will:
58
- - Create `.context/` directory with graph schema
59
- - Set up MCP server for Claude Desktop/Code
60
- - Start background sync for automatic updates
61
- - Build a local context graph for indexed files/rules/ADRs
62
-
63
- #### Manual MCP Configuration
64
-
65
- If `cortex init` doesn't auto-register, add to Claude's MCP config:
66
-
67
- **Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json`):
68
- ```json
69
- {
70
- "mcpServers": {
71
- "cortex": {
72
- "command": "cortex",
73
- "args": ["mcp"],
74
- "env": {
75
- "CORTEX_PROJECT_ROOT": "/absolute/path/to/your-project"
76
- }
77
- }
78
- }
79
- }
80
- ```
81
-
82
- **Codex** (`~/.config/codex/mcp-config.json`):
83
- ```json
84
- {
85
- "mcpServers": {
86
- "cortex-myproject": {
87
- "command": "cortex",
88
- "args": ["mcp"],
89
- "cwd": "/absolute/path/to/your-project"
90
- }
91
- }
92
- }
93
- ```
94
-
95
- ### Usage
96
-
97
- Once installed and initialized, Cortex tools are available in Claude:
98
-
99
- ```
100
- "Find files that handle authentication"
101
- "Show related files for this ADR"
102
- "What are the active architectural rules for this API?"
103
- ```
104
-
105
- ### Key Features
106
-
107
- - **Semantic search**: ranked retrieval across source files, rules and ADRs
108
- - **Graph relationships**: quickly discover related entities and constraints
109
- - **Experimental call graph APIs**: function caller/callee and impact traversal in semantic chunking builds
110
- - **Local & private**: All data stays on your machine
111
- - **Incremental updates**: Background sync keeps context fresh
112
- - **Flexible ingestion**: configurable source paths and ranking signals
113
-
114
- ### Requirements
115
-
116
- - Node.js 18+
117
- - Git repository (for change tracking)
118
- - ~50MB disk space per project
119
-
120
- ### Unique Value Proposition
121
-
122
- Unlike other MCP servers that provide external data (GitHub, web search), Cortex provides **deep, structured knowledge of YOUR codebase**:
123
-
124
- - Search with semantic ranking across files, rules, and ADRs
125
- - Understand rule and ADR dependencies in your repo
126
- - Enforce architectural rules and ADRs
127
- - Context that evolves with your code
128
-
129
- Perfect for:
130
- - Large codebases where plain keyword search is not enough
131
- - Refactoring guided by rule and ADR context
132
- - Onboarding (architectural rules, design decisions)
133
- - Code review (what constraints and related entities apply?)
134
-
135
- ### Limitations
136
-
137
- - **Setup required**: Not instant plug-and-play (needs `cortex init`)
138
- - **Per-project**: Each repo needs its own Cortex instance
139
- - **Local only**: No cloud sync (by design - your code stays private)
140
-
141
- ### Support
142
-
143
- - Issues: https://github.com/DanielBlomma/cortex/issues
144
- - Docs: https://github.com/DanielBlomma/cortex/blob/main/README.md
145
-
146
- ## Submission Checklist
147
-
148
- - [x] MCP SDK integration (JSON-RPC over stdio)
149
- - [x] Tools documented with schemas
150
- - [ ] npm package published (@danielblomma/cortex-mcp)
151
- - [x] Marketplace-ready README
152
- - [ ] Example usage screenshots/GIFs
153
- - [ ] Submit PR to modelcontextprotocol/servers
154
-
155
- ## Next Steps
156
-
157
- 1. Publish to npm as `@danielblomma/cortex-mcp`
158
- 2. Test installation from marketplace perspective
159
- 3. Submit to https://github.com/modelcontextprotocol/servers
160
- 4. Add to Anthropic's community registry