@danielblomma/cortex-mcp 1.7.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.
Files changed (79) hide show
  1. package/README.md +4 -24
  2. package/bin/cortex.mjs +679 -32
  3. package/bin/style.mjs +349 -0
  4. package/package.json +4 -3
  5. package/scaffold/mcp/src/cli/enterprise-setup.ts +124 -0
  6. package/scaffold/mcp/src/cli/govern.ts +987 -0
  7. package/scaffold/mcp/src/cli/run.ts +306 -0
  8. package/scaffold/mcp/src/cli/telemetry-test.ts +158 -0
  9. package/scaffold/mcp/src/cli/ungoverned-detector.ts +168 -0
  10. package/scaffold/mcp/src/core/audit/query.ts +81 -0
  11. package/scaffold/mcp/src/core/audit/writer.ts +68 -0
  12. package/scaffold/mcp/src/core/config.ts +329 -0
  13. package/scaffold/mcp/src/core/index.ts +34 -0
  14. package/scaffold/mcp/src/core/license.ts +202 -0
  15. package/scaffold/mcp/src/core/policy/enforce.ts +98 -0
  16. package/scaffold/mcp/src/core/policy/injection.ts +229 -0
  17. package/scaffold/mcp/src/core/policy/store.ts +197 -0
  18. package/scaffold/mcp/src/core/rbac/check.ts +40 -0
  19. package/scaffold/mcp/src/core/telemetry/collector.ts +408 -0
  20. package/scaffold/mcp/src/core/validators/builtins.ts +711 -0
  21. package/scaffold/mcp/src/core/validators/config.ts +47 -0
  22. package/scaffold/mcp/src/core/validators/engine.ts +199 -0
  23. package/scaffold/mcp/src/core/validators/evaluators/code_comments.ts +294 -0
  24. package/scaffold/mcp/src/core/validators/evaluators/regex.ts +144 -0
  25. package/scaffold/mcp/src/daemon/client.ts +155 -0
  26. package/scaffold/mcp/src/daemon/egress-proxy.ts +331 -0
  27. package/scaffold/mcp/src/daemon/heartbeat-pusher.ts +147 -0
  28. package/scaffold/mcp/src/daemon/heartbeat-tracker.ts +223 -0
  29. package/scaffold/mcp/src/daemon/host-events-pusher.ts +285 -0
  30. package/scaffold/mcp/src/daemon/main.ts +435 -0
  31. package/scaffold/mcp/src/daemon/paths.ts +41 -0
  32. package/scaffold/mcp/src/daemon/protocol.ts +101 -0
  33. package/scaffold/mcp/src/daemon/server.ts +227 -0
  34. package/scaffold/mcp/src/daemon/sync-checker.ts +213 -0
  35. package/scaffold/mcp/src/daemon/ungoverned-scanner.ts +149 -0
  36. package/scaffold/mcp/src/enterprise/audit/push.ts +84 -0
  37. package/scaffold/mcp/src/enterprise/index.ts +386 -0
  38. package/scaffold/mcp/src/enterprise/model/deploy.ts +33 -0
  39. package/scaffold/mcp/src/enterprise/policy/sync.ts +146 -0
  40. package/scaffold/mcp/src/enterprise/privacy/boundary.ts +214 -0
  41. package/scaffold/mcp/src/enterprise/reviews/push.ts +79 -0
  42. package/scaffold/mcp/src/enterprise/telemetry/sync.ts +73 -0
  43. package/scaffold/mcp/src/enterprise/tools/enterprise.ts +1031 -0
  44. package/scaffold/mcp/src/enterprise/tools/walk.ts +79 -0
  45. package/scaffold/mcp/src/enterprise/violations/push.ts +102 -0
  46. package/scaffold/mcp/src/enterprise/workflow/push.ts +60 -0
  47. package/scaffold/mcp/src/enterprise/workflow/state.ts +535 -0
  48. package/scaffold/mcp/src/hooks/pre-compact.ts +54 -0
  49. package/scaffold/mcp/src/hooks/pre-tool-use.ts +96 -0
  50. package/scaffold/mcp/src/hooks/session-end.ts +73 -0
  51. package/scaffold/mcp/src/hooks/session-start.ts +78 -0
  52. package/scaffold/mcp/src/hooks/shared.ts +134 -0
  53. package/scaffold/mcp/src/hooks/stop.ts +60 -0
  54. package/scaffold/mcp/src/hooks/user-prompt-submit.ts +64 -0
  55. package/scaffold/mcp/src/loadGraph.ts +2 -0
  56. package/scaffold/mcp/src/plugin.ts +150 -0
  57. package/scaffold/mcp/src/server.ts +218 -7
  58. package/scaffold/mcp/tests/copilot-shim.test.mjs +146 -0
  59. package/scaffold/mcp/tests/daemon-client.test.mjs +32 -0
  60. package/scaffold/mcp/tests/egress-proxy.test.mjs +239 -0
  61. package/scaffold/mcp/tests/enterprise-config.test.mjs +154 -0
  62. package/scaffold/mcp/tests/govern-install.test.mjs +320 -0
  63. package/scaffold/mcp/tests/govern-repair.test.mjs +157 -0
  64. package/scaffold/mcp/tests/govern-status.test.mjs +538 -0
  65. package/scaffold/mcp/tests/govern.test.mjs +74 -0
  66. package/scaffold/mcp/tests/heartbeat-pusher.test.mjs +154 -0
  67. package/scaffold/mcp/tests/heartbeat-tracker.test.mjs +237 -0
  68. package/scaffold/mcp/tests/host-events-pusher.test.mjs +347 -0
  69. package/scaffold/mcp/tests/policy-check.test.mjs +220 -0
  70. package/scaffold/mcp/tests/repo-name.test.mjs +134 -0
  71. package/scaffold/mcp/tests/run.test.mjs +109 -0
  72. package/scaffold/mcp/tests/sync-checker.test.mjs +188 -0
  73. package/scaffold/mcp/tests/telemetry-collector.test.mjs +30 -0
  74. package/scaffold/mcp/tests/ungoverned-detector.test.mjs +191 -0
  75. package/scaffold/mcp/tests/ungoverned-scanner.test.mjs +198 -0
  76. package/scaffold/scripts/bootstrap.sh +0 -11
  77. package/scaffold/scripts/doctor.sh +24 -4
  78. package/types.js +5 -0
  79. package/docs/MCP_MARKETPLACE.md +0 -160
@@ -0,0 +1,408 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
2
+ import { createHash } from "node:crypto";
3
+ import { hostname, platform, arch } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ export type TelemetryMetrics = {
7
+ period_start: string;
8
+ period_end: string;
9
+ total_tool_calls: number;
10
+ successful_tool_calls: number;
11
+ failed_tool_calls: number;
12
+ total_duration_ms: number;
13
+ session_starts: number;
14
+ session_ends: number;
15
+ session_duration_ms_total: number;
16
+ searches: number;
17
+ related_lookups: number;
18
+ caller_lookups: number;
19
+ trace_lookups: number;
20
+ impact_analyses: number;
21
+ rule_lookups: number;
22
+ reloads: number;
23
+ total_results_returned: number;
24
+ estimated_tokens_saved: number;
25
+ estimated_tokens_total: number;
26
+ client_version: string;
27
+ instance_id: string;
28
+ tool_metrics: Record<string, {
29
+ calls: number;
30
+ failures: number;
31
+ total_duration_ms: number;
32
+ total_results_returned: number;
33
+ estimated_tokens_saved: number;
34
+ }>;
35
+ };
36
+
37
+ const AVG_TOKENS_PER_RESULT = 400;
38
+
39
+ function generateInstanceId(contextDir: string): string {
40
+ const idPath = join(contextDir, "telemetry", "machine_id");
41
+ if (existsSync(idPath)) {
42
+ try {
43
+ const existing = readFileSync(idPath, "utf8").trim();
44
+ if (existing.length > 0) return existing;
45
+ } catch (err) {
46
+ process.stderr.write(`[cortex-enterprise] machine_id exists but is unreadable: ${err instanceof Error ? err.message : String(err)}\n`);
47
+ }
48
+ }
49
+ // Note: hostname|platform|arch may collide on machines with identical defaults.
50
+ // Consider adding a random salt if fleet-wide uniqueness is critical.
51
+ const fingerprint = `${hostname()}|${platform()}|${arch()}`;
52
+ const id = createHash("sha256").update(fingerprint).digest("hex").slice(0, 16);
53
+ try {
54
+ mkdirSync(join(contextDir, "telemetry"), { recursive: true });
55
+ writeFileSync(idPath, id, "utf8");
56
+ } catch (err) {
57
+ process.stderr.write(`[cortex-enterprise] Could not persist instance id: ${err instanceof Error ? err.message : String(err)}\n`);
58
+ }
59
+ return id;
60
+ }
61
+
62
+ function emptyMetrics(clientVersion: string, instanceId: string): TelemetryMetrics {
63
+ const now = new Date().toISOString();
64
+ return {
65
+ period_start: now,
66
+ period_end: now,
67
+ total_tool_calls: 0,
68
+ successful_tool_calls: 0,
69
+ failed_tool_calls: 0,
70
+ total_duration_ms: 0,
71
+ session_starts: 0,
72
+ session_ends: 0,
73
+ session_duration_ms_total: 0,
74
+ searches: 0,
75
+ related_lookups: 0,
76
+ caller_lookups: 0,
77
+ trace_lookups: 0,
78
+ impact_analyses: 0,
79
+ rule_lookups: 0,
80
+ reloads: 0,
81
+ total_results_returned: 0,
82
+ estimated_tokens_saved: 0,
83
+ estimated_tokens_total: 0,
84
+ client_version: clientVersion,
85
+ instance_id: instanceId,
86
+ tool_metrics: {},
87
+ };
88
+ }
89
+
90
+ export type TelemetryEvent = {
91
+ tool: string;
92
+ phase: "success" | "error";
93
+ result_count?: number;
94
+ estimated_tokens_saved?: number;
95
+ duration_ms?: number;
96
+ };
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
+
135
+ export class TelemetryCollector {
136
+ private metrics: TelemetryMetrics;
137
+ private readonly metricsPath: string;
138
+ private readonly clientVersion: string;
139
+ private readonly instanceId: string;
140
+ private dirty = false;
141
+
142
+ constructor(contextDir: string, clientVersion = "unknown") {
143
+ this.clientVersion = clientVersion;
144
+ this.instanceId = generateInstanceId(contextDir);
145
+ const telemetryDir = join(contextDir, "telemetry");
146
+ this.metricsPath = join(telemetryDir, "metrics.json");
147
+
148
+ // Load existing metrics or start fresh
149
+ try {
150
+ const raw = readFileSync(this.metricsPath, "utf8");
151
+ this.metrics = JSON.parse(raw);
152
+ this.metrics.client_version = clientVersion;
153
+ this.metrics.instance_id = this.instanceId;
154
+ } catch {
155
+ this.metrics = emptyMetrics(clientVersion, this.instanceId);
156
+ }
157
+ }
158
+
159
+ private bucket(toolName: string) {
160
+ if (!this.metrics.tool_metrics[toolName]) {
161
+ this.metrics.tool_metrics[toolName] = {
162
+ calls: 0,
163
+ failures: 0,
164
+ total_duration_ms: 0,
165
+ total_results_returned: 0,
166
+ estimated_tokens_saved: 0,
167
+ };
168
+ }
169
+ return this.metrics.tool_metrics[toolName];
170
+ }
171
+
172
+ recordEvent(event: TelemetryEvent): void {
173
+ const resultCount = event.result_count ?? 0;
174
+ const tokensSaved = event.estimated_tokens_saved ?? 0;
175
+ const durationMs = event.duration_ms ?? 0;
176
+ const toolBucket = this.bucket(event.tool);
177
+
178
+ this.metrics.total_tool_calls++;
179
+ this.metrics.total_duration_ms += durationMs;
180
+ this.metrics.period_end = new Date().toISOString();
181
+
182
+ toolBucket.calls++;
183
+ toolBucket.total_duration_ms += durationMs;
184
+
185
+ if (event.phase === "error") {
186
+ this.metrics.failed_tool_calls++;
187
+ toolBucket.failures++;
188
+ this.dirty = true;
189
+ return;
190
+ }
191
+
192
+ this.metrics.successful_tool_calls++;
193
+
194
+ switch (event.tool) {
195
+ case "context.search":
196
+ this.metrics.searches++;
197
+ break;
198
+ case "context.get_related":
199
+ this.metrics.related_lookups++;
200
+ break;
201
+ case "context.find_callers":
202
+ this.metrics.caller_lookups++;
203
+ break;
204
+ case "context.trace_calls":
205
+ this.metrics.trace_lookups++;
206
+ break;
207
+ case "context.impact":
208
+ case "context.impact_analysis":
209
+ this.metrics.impact_analyses++;
210
+ break;
211
+ case "context.get_rules":
212
+ this.metrics.rule_lookups++;
213
+ break;
214
+ case "context.reload":
215
+ this.metrics.reloads++;
216
+ break;
217
+ }
218
+
219
+ this.metrics.total_results_returned += resultCount;
220
+ this.metrics.estimated_tokens_saved += tokensSaved;
221
+ this.metrics.estimated_tokens_total += tokensSaved + resultCount * AVG_TOKENS_PER_RESULT;
222
+
223
+ toolBucket.total_results_returned += resultCount;
224
+ toolBucket.estimated_tokens_saved += tokensSaved;
225
+ this.dirty = true;
226
+ }
227
+
228
+ record(toolName: string, resultCount: number, tokensSaved: number): void {
229
+ this.recordEvent({
230
+ tool: toolName,
231
+ phase: "success",
232
+ result_count: resultCount,
233
+ estimated_tokens_saved: tokensSaved,
234
+ duration_ms: 0,
235
+ });
236
+ }
237
+
238
+ recordSessionStart(): void {
239
+ this.metrics.session_starts++;
240
+ this.metrics.period_end = new Date().toISOString();
241
+ this.dirty = true;
242
+ }
243
+
244
+ recordSessionEnd(durationMs: number): void {
245
+ this.metrics.session_ends++;
246
+ this.metrics.session_duration_ms_total += Math.max(0, durationMs);
247
+ this.metrics.period_end = new Date().toISOString();
248
+ this.dirty = true;
249
+ }
250
+
251
+ getMetrics(): TelemetryMetrics {
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;
389
+ }
390
+
391
+ flush(): void {
392
+ if (!this.dirty) return;
393
+
394
+ try {
395
+ const dir = join(this.metricsPath, "..");
396
+ mkdirSync(dir, { recursive: true });
397
+ writeFileSync(this.metricsPath, JSON.stringify(this.metrics, null, 2));
398
+ this.dirty = false;
399
+ } catch {
400
+ process.stderr.write("[cortex-enterprise] Failed to flush telemetry metrics\n");
401
+ }
402
+ }
403
+
404
+ reset(): void {
405
+ this.metrics = emptyMetrics(this.clientVersion, this.instanceId);
406
+ this.dirty = true;
407
+ }
408
+ }