@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.
- package/dist/approval/classify.js +24 -0
- package/dist/approval/policy.js +7 -0
- package/dist/approval/prompt.js +7 -0
- package/dist/approval/types.d.ts +6 -0
- package/dist/brand/voice.d.ts +1 -1
- package/dist/brand/voice.js +1 -1
- package/dist/cli/commands/mcp.d.ts +9 -0
- package/dist/cli/commands/mcp.js +87 -0
- package/dist/cli/commands/run.js +22 -5
- package/dist/cli/program.js +2 -0
- package/dist/cli/session-factory.d.ts +2 -2
- package/dist/cli/session-factory.js +9 -2
- package/dist/config/schema.d.ts +228 -30
- package/dist/config/schema.js +55 -4
- package/dist/constants.d.ts +8 -0
- package/dist/constants.js +8 -0
- package/dist/errors/constructors.d.ts +17 -0
- package/dist/errors/constructors.js +46 -0
- package/dist/errors/types.d.ts +9 -0
- package/dist/errors/types.js +15 -0
- package/dist/lsp/transport.d.ts +6 -15
- package/dist/lsp/transport.js +10 -66
- package/dist/mcp/adapter.d.ts +44 -0
- package/dist/mcp/adapter.js +70 -0
- package/dist/mcp/bounds.d.ts +35 -0
- package/dist/mcp/bounds.js +36 -0
- package/dist/mcp/client.d.ts +19 -0
- package/dist/mcp/client.js +93 -0
- package/dist/mcp/demarcate.d.ts +12 -0
- package/dist/mcp/demarcate.js +71 -0
- package/dist/mcp/index.d.ts +9 -0
- package/dist/mcp/index.js +8 -0
- package/dist/mcp/service.d.ts +54 -0
- package/dist/mcp/service.js +99 -0
- package/dist/mcp/transport.d.ts +30 -0
- package/dist/mcp/transport.js +188 -0
- package/dist/mcp/trust-gate.d.ts +35 -0
- package/dist/mcp/trust-gate.js +40 -0
- package/dist/mcp/trust.d.ts +52 -0
- package/dist/mcp/trust.js +111 -0
- package/dist/mcp/types.d.ts +52 -0
- package/dist/mcp/types.js +7 -0
- package/dist/tools/registry.js +3 -1
- package/dist/tools/types.d.ts +15 -1
- package/dist/utils/child-tree.d.ts +35 -0
- package/dist/utils/child-tree.js +76 -0
- package/package.json +1 -1
|
@@ -22,6 +22,8 @@ export function classify(action, cwd) {
|
|
|
22
22
|
return vcsRequest(action, root);
|
|
23
23
|
case "rollback":
|
|
24
24
|
return rollbackRequest(action, root);
|
|
25
|
+
case "mcp":
|
|
26
|
+
return mcpRequest(action, root);
|
|
25
27
|
default:
|
|
26
28
|
return {
|
|
27
29
|
action,
|
|
@@ -132,6 +134,28 @@ function rollbackRequest(action, root) {
|
|
|
132
134
|
cwd: root,
|
|
133
135
|
};
|
|
134
136
|
}
|
|
137
|
+
// ── mcp (call an external MCP server's tool, C.27) ──────────────────────────────
|
|
138
|
+
/**
|
|
139
|
+
* An MCP tool call. Always `destructive` — the tool is arbitrary code in a
|
|
140
|
+
* trusted-but-external server, running UNSANDBOXED. Crucially, the classifier
|
|
141
|
+
* NEVER consults the server's `readOnlyHint` (or any server-supplied
|
|
142
|
+
* annotation): a server cannot mark its own tool low-risk, so this can only ever
|
|
143
|
+
* be destructive. Grantable at the tightest scope — the exact server+tool pair —
|
|
144
|
+
* so approving one MCP tool for the session never widens to any other tool.
|
|
145
|
+
*/
|
|
146
|
+
function mcpRequest(action, root) {
|
|
147
|
+
const server = action.server ?? "";
|
|
148
|
+
const tool = action.tool ?? "";
|
|
149
|
+
const grantable = server !== "" && tool !== "";
|
|
150
|
+
return {
|
|
151
|
+
action,
|
|
152
|
+
tier: "destructive",
|
|
153
|
+
scope: grantable ? { kind: "mcp-tool", server, tool } : { kind: "none" },
|
|
154
|
+
summary: `call MCP tool ${tool || "(unknown)"} on server ${server || "(unknown)"}`,
|
|
155
|
+
targets: [],
|
|
156
|
+
cwd: root,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
135
159
|
// ── file (write / edit / patch) ────────────────────────────────────────────────
|
|
136
160
|
function fileRequest(action, tier, root) {
|
|
137
161
|
const targets = fileTargets(action, root);
|
package/dist/approval/policy.js
CHANGED
|
@@ -44,6 +44,13 @@ export function scopeCovers(scope, request) {
|
|
|
44
44
|
return (request.action.kind === "test" &&
|
|
45
45
|
(request.action.command ?? "").trim() === scope.command);
|
|
46
46
|
}
|
|
47
|
+
if (scope.kind === "mcp-tool") {
|
|
48
|
+
// MCP grants (C.27): the exact server+tool pair, mcp actions only — a grant
|
|
49
|
+
// for one server's tool can never cover another tool or another server.
|
|
50
|
+
return (request.action.kind === "mcp" &&
|
|
51
|
+
request.action.server === scope.server &&
|
|
52
|
+
request.action.tool === scope.tool);
|
|
53
|
+
}
|
|
47
54
|
// file-subtree
|
|
48
55
|
return (request.targets.length > 0 &&
|
|
49
56
|
request.targets.every((t) => isInside(scope.root, t)));
|
package/dist/approval/prompt.js
CHANGED
|
@@ -69,6 +69,11 @@ function detail(request, t) {
|
|
|
69
69
|
` ${t.muted(`in ${request.cwd}`)}`,
|
|
70
70
|
].join("\n");
|
|
71
71
|
}
|
|
72
|
+
if (request.action.kind === "mcp") {
|
|
73
|
+
// The server runs UNSANDBOXED with the user's privileges — say so at the
|
|
74
|
+
// point of the call, not just at trust time.
|
|
75
|
+
return ` ${t.muted(`external MCP server "${request.action.server ?? ""}" — runs unsandboxed with your privileges`)}`;
|
|
76
|
+
}
|
|
72
77
|
return renderActionPreview(request.action.preview, t);
|
|
73
78
|
}
|
|
74
79
|
/** The choices line, including a short label of what an `a` grant would cover. */
|
|
@@ -87,6 +92,8 @@ function scopeLabel(scope) {
|
|
|
87
92
|
return `re-runs of \`${scope.command}\``;
|
|
88
93
|
if (scope.kind === "file-subtree")
|
|
89
94
|
return `changes under ${path.basename(scope.root)}/`;
|
|
95
|
+
if (scope.kind === "mcp-tool")
|
|
96
|
+
return `re-calls of \`${scope.tool}\` on ${scope.server}`;
|
|
90
97
|
return null;
|
|
91
98
|
}
|
|
92
99
|
// ── default stdin-backed PromptIO ──────────────────────────────────────────────
|
package/dist/approval/types.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ export type RiskTier = "read" | "mutate" | "destructive";
|
|
|
21
21
|
* a grant covers re-runs of precisely that test command, nothing else.
|
|
22
22
|
* - `file-subtree` — an absolute directory (or, under the root-cap, an exact
|
|
23
23
|
* file path); matches targets that resolve inside it.
|
|
24
|
+
* - `mcp-tool` — one exact MCP server+tool pair (C.27): a grant covers re-calls
|
|
25
|
+
* of precisely that tool on that server, and never any other MCP tool.
|
|
24
26
|
* - `none` — nothing safe to grant (e.g. a multi-file patch spanning the root).
|
|
25
27
|
*/
|
|
26
28
|
export type Scope = {
|
|
@@ -32,6 +34,10 @@ export type Scope = {
|
|
|
32
34
|
} | {
|
|
33
35
|
readonly kind: "file-subtree";
|
|
34
36
|
readonly root: string;
|
|
37
|
+
} | {
|
|
38
|
+
readonly kind: "mcp-tool";
|
|
39
|
+
readonly server: string;
|
|
40
|
+
readonly tool: string;
|
|
35
41
|
} | {
|
|
36
42
|
readonly kind: "none";
|
|
37
43
|
};
|
package/dist/brand/voice.d.ts
CHANGED
|
@@ -49,7 +49,7 @@ export interface ForbiddenTerm {
|
|
|
49
49
|
* Deprecated synonyms banned from user-facing copy. Scoped to the curated
|
|
50
50
|
* surfaces the lexicon test scans (command descriptions, error output, brand +
|
|
51
51
|
* onboarding constants) — NOT a raw source grep — so legitimate internal uses
|
|
52
|
-
* (`
|
|
52
|
+
* (`mcp.servers` config key, docker `.Server.Version`, code comments) never
|
|
53
53
|
* false-trip, while every string a user reads is covered.
|
|
54
54
|
*/
|
|
55
55
|
export declare const FORBIDDEN_TERMS: readonly ForbiddenTerm[];
|
package/dist/brand/voice.js
CHANGED
|
@@ -40,7 +40,7 @@ export const CANONICAL_TERMS = {
|
|
|
40
40
|
* Deprecated synonyms banned from user-facing copy. Scoped to the curated
|
|
41
41
|
* surfaces the lexicon test scans (command descriptions, error output, brand +
|
|
42
42
|
* onboarding constants) — NOT a raw source grep — so legitimate internal uses
|
|
43
|
-
* (`
|
|
43
|
+
* (`mcp.servers` config key, docker `.Server.Version`, code comments) never
|
|
44
44
|
* false-trip, while every string a user reads is covered.
|
|
45
45
|
*/
|
|
46
46
|
export const FORBIDDEN_TERMS = [
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
/**
|
|
3
|
+
* `cruxy mcp` — inspect and control MCP server integration (C.27). `list` shows
|
|
4
|
+
* the configured servers and whether this repo's config is trusted; `trust`
|
|
5
|
+
* records the explicit decision to connect them, bound to the current config
|
|
6
|
+
* fingerprint. Trusting a server runs its code UNSANDBOXED with your privileges,
|
|
7
|
+
* so `trust` restates that plainly before recording.
|
|
8
|
+
*/
|
|
9
|
+
export declare function mcpCommand(): Command;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { loadConfig } from "../../config/index.js";
|
|
4
|
+
import { shouldUseColor } from "../../errors/index.js";
|
|
5
|
+
import { themeForColor } from "../../theme/index.js";
|
|
6
|
+
import { fileMcpTrustStore, fingerprintMcpServers, isMcpTrusted, } from "../../mcp/index.js";
|
|
7
|
+
import { logger } from "../../utils/logger.js";
|
|
8
|
+
/**
|
|
9
|
+
* `cruxy mcp` — inspect and control MCP server integration (C.27). `list` shows
|
|
10
|
+
* the configured servers and whether this repo's config is trusted; `trust`
|
|
11
|
+
* records the explicit decision to connect them, bound to the current config
|
|
12
|
+
* fingerprint. Trusting a server runs its code UNSANDBOXED with your privileges,
|
|
13
|
+
* so `trust` restates that plainly before recording.
|
|
14
|
+
*/
|
|
15
|
+
export function mcpCommand() {
|
|
16
|
+
const cmd = new Command("mcp").description("inspect and control MCP integrations");
|
|
17
|
+
cmd
|
|
18
|
+
.command("list", { isDefault: true })
|
|
19
|
+
.description("list configured MCP integrations and their trust status")
|
|
20
|
+
.action(() => {
|
|
21
|
+
const t = themeForColor(shouldUseColor(process.stdout));
|
|
22
|
+
const { config } = loadConfig();
|
|
23
|
+
const servers = config.mcp.servers;
|
|
24
|
+
const names = Object.keys(servers);
|
|
25
|
+
logger.print(`${t.strong("mcp:")} ${config.mcp.enabled ? t.success("enabled") : t.warning("disabled (mcp.enabled = false)")}`);
|
|
26
|
+
if (names.length === 0) {
|
|
27
|
+
logger.print(t.muted("\nno MCP servers configured (mcp.servers)"));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const root = path.resolve(process.cwd());
|
|
31
|
+
const trusted = isMcpTrusted(fileMcpTrustStore(), root, fingerprintMcpServers(servers));
|
|
32
|
+
const trustLabel = trusted
|
|
33
|
+
? t.success("trusted — will connect")
|
|
34
|
+
: t.danger("NOT trusted — will not connect; run `cruxy mcp trust .`");
|
|
35
|
+
logger.print(`\n${t.heading("servers")} ${trustLabel}`);
|
|
36
|
+
for (const [name, cfg] of Object.entries(servers)) {
|
|
37
|
+
logger.print(` ${t.strong(name)} ${t.muted(describeServer(cfg))}`);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
cmd
|
|
41
|
+
.command("trust [path]")
|
|
42
|
+
.description("trust this repo's MCP integrations (they run with your full privileges)")
|
|
43
|
+
.action((target) => {
|
|
44
|
+
const t = themeForColor(shouldUseColor(process.stdout));
|
|
45
|
+
const { config } = loadConfig();
|
|
46
|
+
const servers = config.mcp.servers;
|
|
47
|
+
const root = path.resolve(target ?? process.cwd());
|
|
48
|
+
const names = Object.keys(servers);
|
|
49
|
+
if (names.length === 0) {
|
|
50
|
+
logger.print(t.muted(`no MCP servers configured under ${root}`));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
logger.print(t.danger(t.strong(`trusting ${names.length} MCP server${names.length === 1 ? "" : "s"} — these run their own code UNSANDBOXED with your privileges:`)));
|
|
54
|
+
for (const [name, cfg] of Object.entries(servers)) {
|
|
55
|
+
logger.print(` ${t.strong(name)} ${t.muted(describeServer(cfg))}`);
|
|
56
|
+
}
|
|
57
|
+
fileMcpTrustStore().record({
|
|
58
|
+
root,
|
|
59
|
+
fingerprint: fingerprintMcpServers(servers),
|
|
60
|
+
at: new Date().toISOString(),
|
|
61
|
+
});
|
|
62
|
+
logger.print(`${t.success("trusted")} — cruxy will connect these servers for ${root}. ` +
|
|
63
|
+
t.muted("changing the config will require re-trusting."));
|
|
64
|
+
});
|
|
65
|
+
cmd
|
|
66
|
+
.command("untrust [path]")
|
|
67
|
+
.description("clear MCP trust for this repo so cruxy stops connecting")
|
|
68
|
+
.action((target) => {
|
|
69
|
+
const t = themeForColor(shouldUseColor(process.stdout));
|
|
70
|
+
const root = path.resolve(target ?? process.cwd());
|
|
71
|
+
// Recording an unmatchable fingerprint makes the current config stale
|
|
72
|
+
// (isMcpTrusted requires an exact match), so servers stop connecting.
|
|
73
|
+
fileMcpTrustStore().record({
|
|
74
|
+
root,
|
|
75
|
+
fingerprint: "revoked",
|
|
76
|
+
at: new Date().toISOString(),
|
|
77
|
+
});
|
|
78
|
+
logger.print(`${t.success("untrusted")} — MCP servers for ${root} will no longer connect until re-trusted`);
|
|
79
|
+
});
|
|
80
|
+
return cmd;
|
|
81
|
+
}
|
|
82
|
+
function describeServer(cfg) {
|
|
83
|
+
if (cfg.command) {
|
|
84
|
+
return `(stdio: ${[cfg.command, ...(cfg.args ?? [])].join(" ")})`;
|
|
85
|
+
}
|
|
86
|
+
return cfg.url ? `(url: ${cfg.url})` : "(no transport)";
|
|
87
|
+
}
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -12,6 +12,8 @@ import { runInteractive } from "../repl.js";
|
|
|
12
12
|
import { buildAgentSession } from "../session-factory.js";
|
|
13
13
|
import { apiKeyEnvVar, maybeRunOnboarding } from "../onboard.js";
|
|
14
14
|
import { resetLspServices } from "../../lsp/index.js";
|
|
15
|
+
import { connectMcpTools, resetMcpServices } from "../../mcp/index.js";
|
|
16
|
+
import { defaultPromptIO } from "../../approval/index.js";
|
|
15
17
|
export function runCommand() {
|
|
16
18
|
return new Command("run")
|
|
17
19
|
.description("run a task once, or start an interactive session")
|
|
@@ -91,15 +93,28 @@ export function runCommand() {
|
|
|
91
93
|
interactive: Boolean(process.stdin.isTTY),
|
|
92
94
|
logger,
|
|
93
95
|
});
|
|
94
|
-
|
|
96
|
+
// MCP servers (C.27): connect + trust-gate BEFORE building the session so
|
|
97
|
+
// the tool catalogue is complete when the model first runs. Off by
|
|
98
|
+
// default (no servers connect). A non-interactive run with an untrusted
|
|
99
|
+
// config THROWS CRUXY_E_MCP_UNTRUSTED here — before any server spawns.
|
|
100
|
+
const mcp = await connectMcpTools({
|
|
101
|
+
cwd: process.cwd(),
|
|
102
|
+
config,
|
|
103
|
+
logger,
|
|
104
|
+
interactive: Boolean(process.stdin.isTTY),
|
|
105
|
+
io: defaultPromptIO(shouldUseColor()),
|
|
106
|
+
});
|
|
107
|
+
const session = buildAgentSession(config, apiKey, process.cwd(), Boolean(process.stdin.isTTY), planMode, renderer, checkpoints, sandbox, hooksService.runner, mcp.tools);
|
|
95
108
|
if (interactive) {
|
|
96
109
|
try {
|
|
97
110
|
await runInteractive(session, undefined, renderer, checkpoints, hooksService.commands);
|
|
98
111
|
}
|
|
99
112
|
finally {
|
|
100
|
-
// LSP (C.12): gracefully shut down any
|
|
101
|
-
// during the session (the process-exit
|
|
113
|
+
// LSP (C.12) + MCP (C.27): gracefully shut down any external server
|
|
114
|
+
// processes spawned during the session (the shared process-exit
|
|
115
|
+
// kill-tree is the fail-safe for a hard kill).
|
|
102
116
|
await resetLspServices();
|
|
117
|
+
await resetMcpServices();
|
|
103
118
|
}
|
|
104
119
|
return;
|
|
105
120
|
}
|
|
@@ -118,9 +133,11 @@ export function runCommand() {
|
|
|
118
133
|
}
|
|
119
134
|
finally {
|
|
120
135
|
renderer.close();
|
|
121
|
-
// LSP (C.12): gracefully shut down any
|
|
122
|
-
// the run (the process-exit kill-tree
|
|
136
|
+
// LSP (C.12) + MCP (C.27): gracefully shut down any external server
|
|
137
|
+
// processes spawned during the run (the shared process-exit kill-tree
|
|
138
|
+
// is the fail-safe for a hard kill).
|
|
123
139
|
await resetLspServices();
|
|
140
|
+
await resetMcpServices();
|
|
124
141
|
}
|
|
125
142
|
// End-of-run usage summary (C.22): honest tokens + per-tier breakdown +
|
|
126
143
|
// cost (only when priced). Printed after the live region is torn down.
|
package/dist/cli/program.js
CHANGED
|
@@ -17,6 +17,7 @@ import { testCommand } from "./commands/test.js";
|
|
|
17
17
|
import { hooksCommand } from "./commands/hooks.js";
|
|
18
18
|
import { memoryCommand } from "./commands/memory.js";
|
|
19
19
|
import { usageCommand } from "./commands/usage.js";
|
|
20
|
+
import { mcpCommand } from "./commands/mcp.js";
|
|
20
21
|
import { loadConfig } from "../config/index.js";
|
|
21
22
|
import { maybeRunOnboarding } from "./onboard.js";
|
|
22
23
|
export function buildProgram() {
|
|
@@ -49,6 +50,7 @@ export function buildProgram() {
|
|
|
49
50
|
program.addCommand(hooksCommand());
|
|
50
51
|
program.addCommand(memoryCommand());
|
|
51
52
|
program.addCommand(usageCommand());
|
|
53
|
+
program.addCommand(mcpCommand());
|
|
52
54
|
// Default action: bare `cruxy` -> entrypoint. An unrecognized first operand
|
|
53
55
|
// means an unknown command (Commander runs the default action with it as an
|
|
54
56
|
// operand rather than erroring), so reject it as a usage error.
|
|
@@ -3,7 +3,7 @@ import type { ApprovalDecision } from "../approval/index.js";
|
|
|
3
3
|
import type { CheckpointService } from "../checkpoint/index.js";
|
|
4
4
|
import type { SandboxService } from "../sandbox/index.js";
|
|
5
5
|
import type { StreamRenderer } from "../render/index.js";
|
|
6
|
-
import { type ApproveAction } from "../tools/index.js";
|
|
6
|
+
import { type ApproveAction, type Tool } from "../tools/index.js";
|
|
7
7
|
import { Session, type LifecycleHookRunner } from "../agent/index.js";
|
|
8
8
|
/**
|
|
9
9
|
* Wrap the approval gate with the C.32 auto-checkpoint hook. Ordering is the
|
|
@@ -24,4 +24,4 @@ export declare function withCheckpointGate(requestApproval: (action: ApproveActi
|
|
|
24
24
|
* over the shared U.3 allowlist and a `planRunner` so `session.send` proposes →
|
|
25
25
|
* approves → executes. Plan mode is fully opt-in; the default path is unchanged.
|
|
26
26
|
*/
|
|
27
|
-
export declare function buildAgentSession(config: CruxyConfig, apiKey: string, cwd: string, ttyInteractive: boolean, planMode?: boolean, renderer?: StreamRenderer, checkpoints?: CheckpointService, sandbox?: SandboxService, hooks?: LifecycleHookRunner): Session;
|
|
27
|
+
export declare function buildAgentSession(config: CruxyConfig, apiKey: string, cwd: string, ttyInteractive: boolean, planMode?: boolean, renderer?: StreamRenderer, checkpoints?: CheckpointService, sandbox?: SandboxService, hooks?: LifecycleHookRunner, mcpTools?: Tool[]): Session;
|
|
@@ -4,7 +4,7 @@ import { logger } from "../utils/logger.js";
|
|
|
4
4
|
import { getGitInfo } from "../utils/git.js";
|
|
5
5
|
import { ApprovalService, InteractivePolicy, SessionAllowlist, classify, defaultPromptIO, } from "../approval/index.js";
|
|
6
6
|
import { shouldUseColor } from "../errors/index.js";
|
|
7
|
-
import { buildDefaultRegistry } from "../tools/index.js";
|
|
7
|
+
import { buildDefaultRegistry, } from "../tools/index.js";
|
|
8
8
|
import { Session, } from "../agent/index.js";
|
|
9
9
|
import { PlanExecutionPolicy, runPlanSession } from "../plan/index.js";
|
|
10
10
|
import { routerForConfig } from "../routing/index.js";
|
|
@@ -91,7 +91,7 @@ export function withCheckpointGate(requestApproval, checkpoints, cwd) {
|
|
|
91
91
|
* over the shared U.3 allowlist and a `planRunner` so `session.send` proposes →
|
|
92
92
|
* approves → executes. Plan mode is fully opt-in; the default path is unchanged.
|
|
93
93
|
*/
|
|
94
|
-
export function buildAgentSession(config, apiKey, cwd, ttyInteractive, planMode = false, renderer, checkpoints, sandbox, hooks) {
|
|
94
|
+
export function buildAgentSession(config, apiKey, cwd, ttyInteractive, planMode = false, renderer, checkpoints, sandbox, hooks, mcpTools = []) {
|
|
95
95
|
const provider = createProvider({
|
|
96
96
|
provider: config.model.provider,
|
|
97
97
|
apiKey,
|
|
@@ -148,6 +148,13 @@ export function buildAgentSession(config, apiKey, cwd, ttyInteractive, planMode
|
|
|
148
148
|
execRegistry.register(getDiagnosticsTool);
|
|
149
149
|
execRegistry.register(hoverTool);
|
|
150
150
|
}
|
|
151
|
+
// MCP servers (C.27): the caller connected + trusted the servers and produced
|
|
152
|
+
// these tools through the single adapter seam BEFORE building the session, so
|
|
153
|
+
// registration here is a plain hand-off — every one is destructive-gated,
|
|
154
|
+
// demarcated, and bounded by construction. Empty unless `mcp.enabled` and at
|
|
155
|
+
// least one trusted server produced tools, so the default path is unchanged.
|
|
156
|
+
for (const tool of mcpTools)
|
|
157
|
+
execRegistry.register(tool);
|
|
151
158
|
// One io shared by every prompt in the session (plan approval, the U.3 gate,
|
|
152
159
|
// and any gate inside a subagent), so they all coordinate with the same live
|
|
153
160
|
// region. The full wrapper stack around an ApprovalService is factored here
|