@cruxy/cli 0.20.0 → 0.21.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.
Files changed (47) hide show
  1. package/dist/approval/classify.js +24 -0
  2. package/dist/approval/policy.js +7 -0
  3. package/dist/approval/prompt.js +7 -0
  4. package/dist/approval/types.d.ts +6 -0
  5. package/dist/brand/voice.d.ts +1 -1
  6. package/dist/brand/voice.js +1 -1
  7. package/dist/cli/commands/mcp.d.ts +9 -0
  8. package/dist/cli/commands/mcp.js +87 -0
  9. package/dist/cli/commands/run.js +22 -5
  10. package/dist/cli/program.js +2 -0
  11. package/dist/cli/session-factory.d.ts +2 -2
  12. package/dist/cli/session-factory.js +9 -2
  13. package/dist/config/schema.d.ts +228 -30
  14. package/dist/config/schema.js +55 -4
  15. package/dist/constants.d.ts +8 -0
  16. package/dist/constants.js +8 -0
  17. package/dist/errors/constructors.d.ts +17 -0
  18. package/dist/errors/constructors.js +46 -0
  19. package/dist/errors/types.d.ts +9 -0
  20. package/dist/errors/types.js +15 -0
  21. package/dist/lsp/transport.d.ts +6 -15
  22. package/dist/lsp/transport.js +10 -66
  23. package/dist/mcp/adapter.d.ts +44 -0
  24. package/dist/mcp/adapter.js +70 -0
  25. package/dist/mcp/bounds.d.ts +35 -0
  26. package/dist/mcp/bounds.js +36 -0
  27. package/dist/mcp/client.d.ts +19 -0
  28. package/dist/mcp/client.js +93 -0
  29. package/dist/mcp/demarcate.d.ts +12 -0
  30. package/dist/mcp/demarcate.js +71 -0
  31. package/dist/mcp/index.d.ts +9 -0
  32. package/dist/mcp/index.js +8 -0
  33. package/dist/mcp/service.d.ts +54 -0
  34. package/dist/mcp/service.js +99 -0
  35. package/dist/mcp/transport.d.ts +30 -0
  36. package/dist/mcp/transport.js +188 -0
  37. package/dist/mcp/trust-gate.d.ts +35 -0
  38. package/dist/mcp/trust-gate.js +40 -0
  39. package/dist/mcp/trust.d.ts +52 -0
  40. package/dist/mcp/trust.js +111 -0
  41. package/dist/mcp/types.d.ts +52 -0
  42. package/dist/mcp/types.js +7 -0
  43. package/dist/tools/registry.js +3 -1
  44. package/dist/tools/types.d.ts +15 -1
  45. package/dist/utils/child-tree.d.ts +35 -0
  46. package/dist/utils/child-tree.js +76 -0
  47. package/package.json +1 -1
@@ -446,16 +446,16 @@ export declare const LspConfigSchema: z.ZodObject<{
446
446
  }, "strict", z.ZodTypeAny, {
447
447
  startupTimeout: number;
448
448
  requestTimeout: number;
449
- enabled: boolean;
450
449
  servers: Record<string, string>;
450
+ enabled: boolean;
451
451
  maxServers: number;
452
452
  idleTimeout: number;
453
453
  maxResults: number;
454
454
  }, {
455
455
  startupTimeout?: number | undefined;
456
456
  requestTimeout?: number | undefined;
457
- enabled?: boolean | undefined;
458
457
  servers?: Record<string, string> | undefined;
458
+ enabled?: boolean | undefined;
459
459
  maxServers?: number | undefined;
460
460
  idleTimeout?: number | undefined;
461
461
  maxResults?: number | undefined;
@@ -593,20 +593,135 @@ export declare const UsageConfigSchema: z.ZodObject<{
593
593
  } | undefined;
594
594
  }>;
595
595
  export type UsageConfig = z.infer<typeof UsageConfigSchema>;
596
- /** MCP server entry — stdio or URL transport (wired up in a later phase). */
597
- export declare const McpServerSchema: z.ZodObject<{
596
+ /**
597
+ * One MCP server entry (C.27). A `command` (+ optional `args`/`env`) is a stdio
598
+ * server cruxy spawns as a child process; a `url` names a remote server. Exactly
599
+ * one transport must be given. Trusting a stdio server runs its code UNSANDBOXED
600
+ * with your privileges, which is why connection is gated by an explicit,
601
+ * fingerprinted trust decision (see `mcp/trust.ts`).
602
+ */
603
+ export declare const McpServerSchema: z.ZodEffects<z.ZodObject<{
604
+ /** stdio transport: the server program to spawn. */
598
605
  command: z.ZodOptional<z.ZodString>;
599
- args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
606
+ /** Arguments for `command`. */
607
+ args: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
608
+ /** Extra environment variables for the spawned server (stdio only). */
609
+ env: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
610
+ /** Remote transport: the server URL (mutually exclusive with `command`). */
600
611
  url: z.ZodOptional<z.ZodString>;
601
612
  }, "strict", z.ZodTypeAny, {
613
+ args: string[];
614
+ env: Record<string, string>;
615
+ command?: string | undefined;
616
+ url?: string | undefined;
617
+ }, {
602
618
  command?: string | undefined;
603
619
  url?: string | undefined;
604
620
  args?: string[] | undefined;
621
+ env?: Record<string, string> | undefined;
622
+ }>, {
623
+ args: string[];
624
+ env: Record<string, string>;
625
+ command?: string | undefined;
626
+ url?: string | undefined;
605
627
  }, {
606
628
  command?: string | undefined;
607
629
  url?: string | undefined;
608
630
  args?: string[] | undefined;
631
+ env?: Record<string, string> | undefined;
632
+ }>;
633
+ export type McpServerConfig = z.infer<typeof McpServerSchema>;
634
+ /**
635
+ * MCP client integration (C.27): connect to trusted MCP servers and expose their
636
+ * tools to the agent. OFF by default — like the sandbox, LSP, and hooks, it runs
637
+ * EXTERNAL code (a stdio server executes UNSANDBOXED with your privileges), a
638
+ * real execution surface you opt into explicitly. When off, nothing connects or
639
+ * spawns and no MCP tool is ever registered. Every server-advertised tool is
640
+ * gated (destructive tier — a server can never self-declare a tool "safe"), its
641
+ * description and results are demarcated as untrusted external data with upstream
642
+ * model names scrubbed, and results are NEVER persisted. The tool list a server
643
+ * advertises is bounded (count + per-tool description/schema size) so a hostile
644
+ * server can't blow the context budget.
645
+ */
646
+ export declare const McpConfigSchema: z.ZodObject<{
647
+ /** Master switch. When false, no server is connected/spawned and no MCP tool
648
+ * is registered (the feature stays fully inert). */
649
+ enabled: z.ZodDefault<z.ZodBoolean>;
650
+ /** Named MCP servers, keyed by a short server id used as the tool prefix
651
+ * (`mcp__<server>__<tool>`) and in the trust prompt. */
652
+ servers: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEffects<z.ZodObject<{
653
+ /** stdio transport: the server program to spawn. */
654
+ command: z.ZodOptional<z.ZodString>;
655
+ /** Arguments for `command`. */
656
+ args: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
657
+ /** Extra environment variables for the spawned server (stdio only). */
658
+ env: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
659
+ /** Remote transport: the server URL (mutually exclusive with `command`). */
660
+ url: z.ZodOptional<z.ZodString>;
661
+ }, "strict", z.ZodTypeAny, {
662
+ args: string[];
663
+ env: Record<string, string>;
664
+ command?: string | undefined;
665
+ url?: string | undefined;
666
+ }, {
667
+ command?: string | undefined;
668
+ url?: string | undefined;
669
+ args?: string[] | undefined;
670
+ env?: Record<string, string> | undefined;
671
+ }>, {
672
+ args: string[];
673
+ env: Record<string, string>;
674
+ command?: string | undefined;
675
+ url?: string | undefined;
676
+ }, {
677
+ command?: string | undefined;
678
+ url?: string | undefined;
679
+ args?: string[] | undefined;
680
+ env?: Record<string, string> | undefined;
681
+ }>>>;
682
+ /** Fail a server's `initialize` handshake (its tools are skipped) if it does
683
+ * not complete within this many ms. */
684
+ startupTimeout: z.ZodDefault<z.ZodNumber>;
685
+ /** Fail a single `tools/call` if the server does not respond within this many
686
+ * ms — the connection is kept, only the one call errors. */
687
+ requestTimeout: z.ZodDefault<z.ZodNumber>;
688
+ /** Max tools accepted from ONE server; extras are dropped with a visible note
689
+ * (a hostile server can't advertise thousands of tools to flood context). */
690
+ maxToolsPerServer: z.ZodDefault<z.ZodNumber>;
691
+ /** Max characters kept from a single tool's description; the rest is truncated
692
+ * with a visible marker. */
693
+ maxDescriptionChars: z.ZodDefault<z.ZodNumber>;
694
+ /** Max bytes kept from a single tool's advertised JSON input schema; an
695
+ * over-cap schema is replaced with a permissive one and a visible note. */
696
+ maxSchemaBytes: z.ZodDefault<z.ZodNumber>;
697
+ }, "strict", z.ZodTypeAny, {
698
+ startupTimeout: number;
699
+ requestTimeout: number;
700
+ servers: Record<string, {
701
+ args: string[];
702
+ env: Record<string, string>;
703
+ command?: string | undefined;
704
+ url?: string | undefined;
705
+ }>;
706
+ enabled: boolean;
707
+ maxToolsPerServer: number;
708
+ maxDescriptionChars: number;
709
+ maxSchemaBytes: number;
710
+ }, {
711
+ startupTimeout?: number | undefined;
712
+ requestTimeout?: number | undefined;
713
+ servers?: Record<string, {
714
+ command?: string | undefined;
715
+ url?: string | undefined;
716
+ args?: string[] | undefined;
717
+ env?: Record<string, string> | undefined;
718
+ }> | undefined;
719
+ enabled?: boolean | undefined;
720
+ maxToolsPerServer?: number | undefined;
721
+ maxDescriptionChars?: number | undefined;
722
+ maxSchemaBytes?: number | undefined;
609
723
  }>;
724
+ export type McpConfig = z.infer<typeof McpConfigSchema>;
610
725
  export declare const CruxyConfigSchema: z.ZodObject<{
611
726
  model: z.ZodDefault<z.ZodObject<{
612
727
  provider: z.ZodDefault<z.ZodEnum<["cruxy", "anthropic", "openai", "custom"]>>;
@@ -823,16 +938,16 @@ export declare const CruxyConfigSchema: z.ZodObject<{
823
938
  }, "strict", z.ZodTypeAny, {
824
939
  startupTimeout: number;
825
940
  requestTimeout: number;
826
- enabled: boolean;
827
941
  servers: Record<string, string>;
942
+ enabled: boolean;
828
943
  maxServers: number;
829
944
  idleTimeout: number;
830
945
  maxResults: number;
831
946
  }, {
832
947
  startupTimeout?: number | undefined;
833
948
  requestTimeout?: number | undefined;
834
- enabled?: boolean | undefined;
835
949
  servers?: Record<string, string> | undefined;
950
+ enabled?: boolean | undefined;
836
951
  maxServers?: number | undefined;
837
952
  idleTimeout?: number | undefined;
838
953
  maxResults?: number | undefined;
@@ -1104,19 +1219,84 @@ export declare const CruxyConfigSchema: z.ZodObject<{
1104
1219
  } | undefined;
1105
1220
  } | undefined;
1106
1221
  }>>;
1107
- mcpServers: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
1108
- command: z.ZodOptional<z.ZodString>;
1109
- args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
1110
- url: z.ZodOptional<z.ZodString>;
1222
+ mcp: z.ZodDefault<z.ZodObject<{
1223
+ /** Master switch. When false, no server is connected/spawned and no MCP tool
1224
+ * is registered (the feature stays fully inert). */
1225
+ enabled: z.ZodDefault<z.ZodBoolean>;
1226
+ /** Named MCP servers, keyed by a short server id used as the tool prefix
1227
+ * (`mcp__<server>__<tool>`) and in the trust prompt. */
1228
+ servers: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEffects<z.ZodObject<{
1229
+ /** stdio transport: the server program to spawn. */
1230
+ command: z.ZodOptional<z.ZodString>;
1231
+ /** Arguments for `command`. */
1232
+ args: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
1233
+ /** Extra environment variables for the spawned server (stdio only). */
1234
+ env: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
1235
+ /** Remote transport: the server URL (mutually exclusive with `command`). */
1236
+ url: z.ZodOptional<z.ZodString>;
1237
+ }, "strict", z.ZodTypeAny, {
1238
+ args: string[];
1239
+ env: Record<string, string>;
1240
+ command?: string | undefined;
1241
+ url?: string | undefined;
1242
+ }, {
1243
+ command?: string | undefined;
1244
+ url?: string | undefined;
1245
+ args?: string[] | undefined;
1246
+ env?: Record<string, string> | undefined;
1247
+ }>, {
1248
+ args: string[];
1249
+ env: Record<string, string>;
1250
+ command?: string | undefined;
1251
+ url?: string | undefined;
1252
+ }, {
1253
+ command?: string | undefined;
1254
+ url?: string | undefined;
1255
+ args?: string[] | undefined;
1256
+ env?: Record<string, string> | undefined;
1257
+ }>>>;
1258
+ /** Fail a server's `initialize` handshake (its tools are skipped) if it does
1259
+ * not complete within this many ms. */
1260
+ startupTimeout: z.ZodDefault<z.ZodNumber>;
1261
+ /** Fail a single `tools/call` if the server does not respond within this many
1262
+ * ms — the connection is kept, only the one call errors. */
1263
+ requestTimeout: z.ZodDefault<z.ZodNumber>;
1264
+ /** Max tools accepted from ONE server; extras are dropped with a visible note
1265
+ * (a hostile server can't advertise thousands of tools to flood context). */
1266
+ maxToolsPerServer: z.ZodDefault<z.ZodNumber>;
1267
+ /** Max characters kept from a single tool's description; the rest is truncated
1268
+ * with a visible marker. */
1269
+ maxDescriptionChars: z.ZodDefault<z.ZodNumber>;
1270
+ /** Max bytes kept from a single tool's advertised JSON input schema; an
1271
+ * over-cap schema is replaced with a permissive one and a visible note. */
1272
+ maxSchemaBytes: z.ZodDefault<z.ZodNumber>;
1111
1273
  }, "strict", z.ZodTypeAny, {
1112
- command?: string | undefined;
1113
- url?: string | undefined;
1114
- args?: string[] | undefined;
1274
+ startupTimeout: number;
1275
+ requestTimeout: number;
1276
+ servers: Record<string, {
1277
+ args: string[];
1278
+ env: Record<string, string>;
1279
+ command?: string | undefined;
1280
+ url?: string | undefined;
1281
+ }>;
1282
+ enabled: boolean;
1283
+ maxToolsPerServer: number;
1284
+ maxDescriptionChars: number;
1285
+ maxSchemaBytes: number;
1115
1286
  }, {
1116
- command?: string | undefined;
1117
- url?: string | undefined;
1118
- args?: string[] | undefined;
1119
- }>>>;
1287
+ startupTimeout?: number | undefined;
1288
+ requestTimeout?: number | undefined;
1289
+ servers?: Record<string, {
1290
+ command?: string | undefined;
1291
+ url?: string | undefined;
1292
+ args?: string[] | undefined;
1293
+ env?: Record<string, string> | undefined;
1294
+ }> | undefined;
1295
+ enabled?: boolean | undefined;
1296
+ maxToolsPerServer?: number | undefined;
1297
+ maxDescriptionChars?: number | undefined;
1298
+ maxSchemaBytes?: number | undefined;
1299
+ }>>;
1120
1300
  logLevel: z.ZodDefault<z.ZodEnum<["debug", "info", "warn", "error", "silent"]>>;
1121
1301
  }, "strict", z.ZodTypeAny, {
1122
1302
  cruxy: {
@@ -1218,8 +1398,8 @@ export declare const CruxyConfigSchema: z.ZodObject<{
1218
1398
  lsp: {
1219
1399
  startupTimeout: number;
1220
1400
  requestTimeout: number;
1221
- enabled: boolean;
1222
1401
  servers: Record<string, string>;
1402
+ enabled: boolean;
1223
1403
  maxServers: number;
1224
1404
  idleTimeout: number;
1225
1405
  maxResults: number;
@@ -1237,11 +1417,20 @@ export declare const CruxyConfigSchema: z.ZodObject<{
1237
1417
  map: Partial<Record<"main-turn" | "subagent" | "plan" | "commit-msg" | "classify" | "summarize", "kavi" | "vaani" | "mira">>;
1238
1418
  default?: "kavi" | "vaani" | "mira" | undefined;
1239
1419
  };
1240
- mcpServers: Record<string, {
1241
- command?: string | undefined;
1242
- url?: string | undefined;
1243
- args?: string[] | undefined;
1244
- }>;
1420
+ mcp: {
1421
+ startupTimeout: number;
1422
+ requestTimeout: number;
1423
+ servers: Record<string, {
1424
+ args: string[];
1425
+ env: Record<string, string>;
1426
+ command?: string | undefined;
1427
+ url?: string | undefined;
1428
+ }>;
1429
+ enabled: boolean;
1430
+ maxToolsPerServer: number;
1431
+ maxDescriptionChars: number;
1432
+ maxSchemaBytes: number;
1433
+ };
1245
1434
  logLevel: "debug" | "info" | "warn" | "error" | "silent";
1246
1435
  }, {
1247
1436
  cruxy?: {
@@ -1343,8 +1532,8 @@ export declare const CruxyConfigSchema: z.ZodObject<{
1343
1532
  lsp?: {
1344
1533
  startupTimeout?: number | undefined;
1345
1534
  requestTimeout?: number | undefined;
1346
- enabled?: boolean | undefined;
1347
1535
  servers?: Record<string, string> | undefined;
1536
+ enabled?: boolean | undefined;
1348
1537
  maxServers?: number | undefined;
1349
1538
  idleTimeout?: number | undefined;
1350
1539
  maxResults?: number | undefined;
@@ -1362,11 +1551,20 @@ export declare const CruxyConfigSchema: z.ZodObject<{
1362
1551
  map?: Partial<Record<"main-turn" | "subagent" | "plan" | "commit-msg" | "classify" | "summarize", "kavi" | "vaani" | "mira">> | undefined;
1363
1552
  default?: "kavi" | "vaani" | "mira" | undefined;
1364
1553
  } | undefined;
1365
- mcpServers?: Record<string, {
1366
- command?: string | undefined;
1367
- url?: string | undefined;
1368
- args?: string[] | undefined;
1369
- }> | undefined;
1554
+ mcp?: {
1555
+ startupTimeout?: number | undefined;
1556
+ requestTimeout?: number | undefined;
1557
+ servers?: Record<string, {
1558
+ command?: string | undefined;
1559
+ url?: string | undefined;
1560
+ args?: string[] | undefined;
1561
+ env?: Record<string, string> | undefined;
1562
+ }> | undefined;
1563
+ enabled?: boolean | undefined;
1564
+ maxToolsPerServer?: number | undefined;
1565
+ maxDescriptionChars?: number | undefined;
1566
+ maxSchemaBytes?: number | undefined;
1567
+ } | undefined;
1370
1568
  logLevel?: "debug" | "info" | "warn" | "error" | "silent" | undefined;
1371
1569
  }>;
1372
1570
  export type CruxyConfig = z.infer<typeof CruxyConfigSchema>;
@@ -365,12 +365,63 @@ export const UsageConfigSchema = z
365
365
  .default({}),
366
366
  })
367
367
  .strict();
368
- /** MCP server entry — stdio or URL transport (wired up in a later phase). */
368
+ /**
369
+ * One MCP server entry (C.27). A `command` (+ optional `args`/`env`) is a stdio
370
+ * server cruxy spawns as a child process; a `url` names a remote server. Exactly
371
+ * one transport must be given. Trusting a stdio server runs its code UNSANDBOXED
372
+ * with your privileges, which is why connection is gated by an explicit,
373
+ * fingerprinted trust decision (see `mcp/trust.ts`).
374
+ */
369
375
  export const McpServerSchema = z
370
376
  .object({
371
- command: z.string().optional(),
372
- args: z.array(z.string()).optional(),
377
+ /** stdio transport: the server program to spawn. */
378
+ command: z.string().min(1).optional(),
379
+ /** Arguments for `command`. */
380
+ args: z.array(z.string()).default([]),
381
+ /** Extra environment variables for the spawned server (stdio only). */
382
+ env: z.record(z.string(), z.string()).default({}),
383
+ /** Remote transport: the server URL (mutually exclusive with `command`). */
373
384
  url: z.string().url().optional(),
385
+ })
386
+ .strict()
387
+ .refine((s) => Boolean(s.command) !== Boolean(s.url), {
388
+ message: "an MCP server needs exactly one of `command` (stdio) or `url`",
389
+ });
390
+ /**
391
+ * MCP client integration (C.27): connect to trusted MCP servers and expose their
392
+ * tools to the agent. OFF by default — like the sandbox, LSP, and hooks, it runs
393
+ * EXTERNAL code (a stdio server executes UNSANDBOXED with your privileges), a
394
+ * real execution surface you opt into explicitly. When off, nothing connects or
395
+ * spawns and no MCP tool is ever registered. Every server-advertised tool is
396
+ * gated (destructive tier — a server can never self-declare a tool "safe"), its
397
+ * description and results are demarcated as untrusted external data with upstream
398
+ * model names scrubbed, and results are NEVER persisted. The tool list a server
399
+ * advertises is bounded (count + per-tool description/schema size) so a hostile
400
+ * server can't blow the context budget.
401
+ */
402
+ export const McpConfigSchema = z
403
+ .object({
404
+ /** Master switch. When false, no server is connected/spawned and no MCP tool
405
+ * is registered (the feature stays fully inert). */
406
+ enabled: z.boolean().default(false),
407
+ /** Named MCP servers, keyed by a short server id used as the tool prefix
408
+ * (`mcp__<server>__<tool>`) and in the trust prompt. */
409
+ servers: z.record(z.string(), McpServerSchema).default({}),
410
+ /** Fail a server's `initialize` handshake (its tools are skipped) if it does
411
+ * not complete within this many ms. */
412
+ startupTimeout: z.number().int().positive().default(15000),
413
+ /** Fail a single `tools/call` if the server does not respond within this many
414
+ * ms — the connection is kept, only the one call errors. */
415
+ requestTimeout: z.number().int().positive().default(30000),
416
+ /** Max tools accepted from ONE server; extras are dropped with a visible note
417
+ * (a hostile server can't advertise thousands of tools to flood context). */
418
+ maxToolsPerServer: z.number().int().positive().default(32),
419
+ /** Max characters kept from a single tool's description; the rest is truncated
420
+ * with a visible marker. */
421
+ maxDescriptionChars: z.number().int().positive().default(1024),
422
+ /** Max bytes kept from a single tool's advertised JSON input schema; an
423
+ * over-cap schema is replaced with a permissive one and a visible note. */
424
+ maxSchemaBytes: z.number().int().positive().default(8192),
374
425
  })
375
426
  .strict();
376
427
  export const CruxyConfigSchema = z
@@ -393,7 +444,7 @@ export const CruxyConfigSchema = z
393
444
  routing: RoutingConfigSchema.default({}),
394
445
  memory: MemoryConfigSchema.default({}),
395
446
  usage: UsageConfigSchema.default({}),
396
- mcpServers: z.record(z.string(), McpServerSchema).default({}),
447
+ mcp: McpConfigSchema.default({}),
397
448
  logLevel: z.enum(LOG_LEVELS).default("info"),
398
449
  })
399
450
  .strict();
@@ -41,6 +41,14 @@ export declare const MEMORY_FILE_NAME = "entries.json";
41
41
  * (`~/.cruxy/memory-trust.json`) — its own file, independent of hook trust, so
42
42
  * cloning a repo carries zero memory trust (C.29 supply-chain safety). */
43
43
  export declare const MEMORY_TRUST_FILE_NAME = "memory-trust.json";
44
+ /**
45
+ * MCP server integration (C.27). Per-repo trust for configured MCP servers, in
46
+ * the GLOBAL dir only (`~/.cruxy/mcp-trust.json`) — its own file, independent of
47
+ * hook and memory trust, so cloning a repo carries zero MCP trust. Trusting a
48
+ * server runs its code UNSANDBOXED with your privileges, so this decision is
49
+ * fingerprinted and re-checked on every run (C.27 supply-chain safety).
50
+ */
51
+ export declare const MCP_TRUST_FILE_NAME = "mcp-trust.json";
44
52
  /**
45
53
  * Usage telemetry + cost tracking (C.22). Per-run/per-session usage records live
46
54
  * in the GLOBAL dir only (`~/.cruxy/usage/runs.json`), `0600` — LOCAL accounting
package/dist/constants.js CHANGED
@@ -61,6 +61,14 @@ export const MEMORY_FILE_NAME = "entries.json";
61
61
  * (`~/.cruxy/memory-trust.json`) — its own file, independent of hook trust, so
62
62
  * cloning a repo carries zero memory trust (C.29 supply-chain safety). */
63
63
  export const MEMORY_TRUST_FILE_NAME = "memory-trust.json";
64
+ /**
65
+ * MCP server integration (C.27). Per-repo trust for configured MCP servers, in
66
+ * the GLOBAL dir only (`~/.cruxy/mcp-trust.json`) — its own file, independent of
67
+ * hook and memory trust, so cloning a repo carries zero MCP trust. Trusting a
68
+ * server runs its code UNSANDBOXED with your privileges, so this decision is
69
+ * fingerprinted and re-checked on every run (C.27 supply-chain safety).
70
+ */
71
+ export const MCP_TRUST_FILE_NAME = "mcp-trust.json";
64
72
  /**
65
73
  * Usage telemetry + cost tracking (C.22). Per-run/per-session usage records live
66
74
  * in the GLOBAL dir only (`~/.cruxy/usage/runs.json`), `0600` — LOCAL accounting
@@ -180,6 +180,23 @@ export declare function lspTimeout(language: string, phase: "startup" | "request
180
180
  * that answered with zero results (that is an ordinary, non-error outcome).
181
181
  */
182
182
  export declare function lspCrashed(language: string, detail?: string): CruxyError;
183
+ /**
184
+ * A project configures MCP servers that this repo has not trusted, and cruxy is
185
+ * running non-interactively so it cannot ask. Fail closed BEFORE any server is
186
+ * spawned: trusting a server runs its code UNSANDBOXED with the user's full
187
+ * privileges (a stdio server's own side effects can't be contained by the shell
188
+ * sandbox), so an untrusted config must never connect silently. The wording says
189
+ * this plainly — it is a real escalation, not a "gated shell in a box".
190
+ */
191
+ export declare function mcpUntrusted(root: string, servers: string[]): CruxyError;
192
+ /**
193
+ * A trusted MCP server could not be reached — spawn failed, the `initialize`
194
+ * handshake errored/timed out, or `tools/list` failed. That server simply
195
+ * contributes no tools (the run continues); the coded reason is surfaced so a
196
+ * misconfigured server is visible, not a silent absence. External server text is
197
+ * gag-scrubbed (U.8) before it reaches the user-facing cause.
198
+ */
199
+ export declare function mcpConnect(server: string, underlying?: unknown): CruxyError;
183
200
  export declare function internal(underlying?: unknown): CruxyError;
184
201
  /**
185
202
  * Map a known provider/transport error (from `@cruxy/sdk`) to a typed
@@ -733,6 +733,52 @@ export function lspCrashed(language, detail) {
733
733
  meta: { language },
734
734
  });
735
735
  }
736
+ // ── MCP client (exit 16) — C.27 ───────────────────────────────────────────────
737
+ /**
738
+ * A project configures MCP servers that this repo has not trusted, and cruxy is
739
+ * running non-interactively so it cannot ask. Fail closed BEFORE any server is
740
+ * spawned: trusting a server runs its code UNSANDBOXED with the user's full
741
+ * privileges (a stdio server's own side effects can't be contained by the shell
742
+ * sandbox), so an untrusted config must never connect silently. The wording says
743
+ * this plainly — it is a real escalation, not a "gated shell in a box".
744
+ */
745
+ export function mcpUntrusted(root, servers) {
746
+ const list = servers.join(", ");
747
+ return new CruxyError({
748
+ code: ErrorCode.McpUntrusted,
749
+ title: "this project's MCP servers have not been trusted",
750
+ cause: `${servers.length} configured server${servers.length === 1 ? "" : "s"} (${list}) ` +
751
+ "would run UNSANDBOXED with your full privileges — cruxy will not connect to them " +
752
+ "non-interactively without an explicit, recorded trust decision",
753
+ nextSteps: [
754
+ "review the servers with `cruxy mcp list`",
755
+ "then trust them with `cruxy mcp trust .` (re-trust is required if the config changes)",
756
+ "or set `mcp.enabled = false` to disable MCP for this project",
757
+ ],
758
+ meta: { root, servers },
759
+ });
760
+ }
761
+ /**
762
+ * A trusted MCP server could not be reached — spawn failed, the `initialize`
763
+ * handshake errored/timed out, or `tools/list` failed. That server simply
764
+ * contributes no tools (the run continues); the coded reason is surfaced so a
765
+ * misconfigured server is visible, not a silent absence. External server text is
766
+ * gag-scrubbed (U.8) before it reaches the user-facing cause.
767
+ */
768
+ export function mcpConnect(server, underlying) {
769
+ return new CruxyError({
770
+ code: ErrorCode.McpConnect,
771
+ title: `could not connect to MCP server "${server}"`,
772
+ cause: scrubbedMessageOf(underlying) ??
773
+ "the server failed to start, handshake, or list its tools",
774
+ nextSteps: [
775
+ "re-run with --verbose to see the server's error",
776
+ `verify the command in \`mcp.servers.${server}\` runs standalone`,
777
+ ],
778
+ meta: { server },
779
+ underlying,
780
+ });
781
+ }
736
782
  // ── internal (exit 1) ─────────────────────────────────────────────────────────
737
783
  export function internal(underlying) {
738
784
  return new CruxyError({
@@ -93,6 +93,15 @@ export declare const ErrorCode: {
93
93
  /** A language server crashed (and, where applicable, a single restart also
94
94
  * failed). Distinct from "server returned no results". */
95
95
  readonly LspCrashed: "CRUXY_E_LSP_CRASHED";
96
+ /** A project configures MCP servers that have not been trusted for this repo.
97
+ * Trusting a server runs its code UNSANDBOXED with your privileges, so an
98
+ * untrusted config is never connected silently — and in non-interactive mode
99
+ * it fails closed BEFORE any server is spawned (supply-chain safety). */
100
+ readonly McpUntrusted: "CRUXY_E_MCP_UNTRUSTED";
101
+ /** A trusted MCP server failed to spawn, complete the `initialize` handshake,
102
+ * or list its tools. Surfaced (that server contributes no tools) rather than
103
+ * silently swallowed; never fatal to the run. */
104
+ readonly McpConnect: "CRUXY_E_MCP_CONNECT";
96
105
  };
97
106
  export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
98
107
  /** The process exit code for an error code (defaults to 1 for safety). */
@@ -110,6 +110,16 @@ export const ErrorCode = {
110
110
  /** A language server crashed (and, where applicable, a single restart also
111
111
  * failed). Distinct from "server returned no results". */
112
112
  LspCrashed: "CRUXY_E_LSP_CRASHED",
113
+ // MCP client (exit 16) — C.27
114
+ /** A project configures MCP servers that have not been trusted for this repo.
115
+ * Trusting a server runs its code UNSANDBOXED with your privileges, so an
116
+ * untrusted config is never connected silently — and in non-interactive mode
117
+ * it fails closed BEFORE any server is spawned (supply-chain safety). */
118
+ McpUntrusted: "CRUXY_E_MCP_UNTRUSTED",
119
+ /** A trusted MCP server failed to spawn, complete the `initialize` handshake,
120
+ * or list its tools. Surfaced (that server contributes no tools) rather than
121
+ * silently swallowed; never fatal to the run. */
122
+ McpConnect: "CRUXY_E_MCP_CONNECT",
113
123
  };
114
124
  /**
115
125
  * Category exit codes. Distinct per category so a caller (CI, a script) can
@@ -187,6 +197,11 @@ const EXIT_CODES = {
187
197
  [ErrorCode.LspServerNotFound]: 15,
188
198
  [ErrorCode.LspTimeout]: 15,
189
199
  [ErrorCode.LspCrashed]: 15,
200
+ // MCP client (C.27). An untrusted MCP config is an execution-safety stop that
201
+ // fails closed before any spawn; a connect failure surfaces per-server and is
202
+ // never fatal on its own. Grouped for a greppable exit code.
203
+ [ErrorCode.McpUntrusted]: 16,
204
+ [ErrorCode.McpConnect]: 16,
190
205
  };
191
206
  /** The process exit code for an error code (defaults to 1 for safety). */
192
207
  export function exitCodeFor(code) {
@@ -1,4 +1,10 @@
1
+ import { killTree, killTrackedTrees, trackedTreeCount } from "../utils/child-tree.js";
1
2
  import type { LspTransport, ServerSpec } from "./types.js";
3
+ export { killTree };
4
+ /** @deprecated Use the shared backstop; kept for LSP tests. */
5
+ export declare const killTrackedServers: typeof killTrackedTrees;
6
+ /** @deprecated Use the shared backstop; kept for LSP tests. */
7
+ export declare const trackedServerCount: typeof trackedTreeCount;
2
8
  export declare class StdioTransport implements LspTransport {
3
9
  private readonly child;
4
10
  private nextId;
@@ -31,18 +37,3 @@ export declare class TransportTimeoutError extends Error {
31
37
  readonly timeoutMs: number;
32
38
  constructor(method: string, timeoutMs: number);
33
39
  }
34
- /**
35
- * Kill the process's entire group (POSIX negative-PID `SIGKILL`), same helper
36
- * shape as run_command's `killTree`. Swallows errors — the process may be gone.
37
- */
38
- export declare function killTree(pid: number | undefined): void;
39
- /**
40
- * Force-kill the process group of every tracked-but-not-yet-shut-down server,
41
- * then forget them. This is exactly what the `exit`/`SIGINT`/`SIGTERM`/`SIGHUP`
42
- * handlers run — the last line against orphaned language servers on a hard exit.
43
- * Exported so it is directly testable (like `resetIndexServices`) without having
44
- * to raise real process signals. Idempotent: a second call is a no-op.
45
- */
46
- export declare function killTrackedServers(): void;
47
- /** Number of servers currently tracked by the exit backstop (for tests). */
48
- export declare function trackedServerCount(): number;