@yagni-app/code-staging 1.0.5-staging.1233.1 → 1.0.5-staging.1238.1
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/claudePlugins.d.ts +3 -1
- package/dist/claudePlugins.js +3 -1
- package/dist/cli.js +7 -0
- package/dist/doctor.d.ts +20 -0
- package/dist/doctor.js +83 -0
- package/dist/extension/index.d.ts +5 -5
- package/dist/extension/index.js +69 -20
- package/dist/extension/mcp/approval.d.ts +45 -0
- package/dist/extension/mcp/approval.js +164 -0
- package/dist/extension/mcp/auth.d.ts +123 -0
- package/dist/extension/mcp/auth.js +547 -0
- package/dist/extension/mcp/authStore.d.ts +61 -0
- package/dist/extension/mcp/authStore.js +105 -0
- package/dist/extension/mcp/cliConfig.d.ts +12 -0
- package/dist/extension/mcp/cliConfig.js +12 -0
- package/dist/extension/mcp/config.d.ts +130 -0
- package/dist/extension/mcp/config.js +265 -0
- package/dist/extension/mcp/log.d.ts +28 -0
- package/dist/extension/mcp/log.js +82 -0
- package/dist/extension/mcp/manager.d.ts +94 -0
- package/dist/extension/mcp/manager.js +233 -0
- package/dist/extension/mcp/names.d.ts +25 -0
- package/dist/extension/mcp/names.js +40 -0
- package/dist/extension/mcp/panel.d.ts +34 -0
- package/dist/extension/mcp/panel.js +258 -0
- package/dist/extension/mcp/prompts.d.ts +23 -0
- package/dist/extension/mcp/prompts.js +93 -0
- package/dist/extension/mcp/startup.d.ts +55 -0
- package/dist/extension/mcp/startup.js +149 -0
- package/dist/extension/mcp/tools.d.ts +31 -0
- package/dist/extension/mcp/tools.js +117 -0
- package/dist/extension/mcp/transports.d.ts +17 -0
- package/dist/extension/mcp/transports.js +44 -0
- package/dist/extension/permission/gate.d.ts +7 -0
- package/dist/extension/permission/gate.js +4 -2
- package/dist/mcpCommand.d.ts +105 -0
- package/dist/mcpCommand.js +730 -0
- package/dist/upgrade.d.ts +11 -2
- package/dist/upgrade.js +48 -8
- package/package.json +3 -2
- package/dist/extension/mcpTools.d.ts +0 -57
- package/dist/extension/mcpTools.js +0 -132
package/dist/claudePlugins.d.ts
CHANGED
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
* disabled via `enabledPlugins`.
|
|
19
19
|
*
|
|
20
20
|
* Deliberately NOT here: network installation of any kind, plugin hooks, MCP
|
|
21
|
-
* servers
|
|
21
|
+
* servers (standard MCP support lives in `src/mcpCommand.ts` + the extension's
|
|
22
|
+
* `mcp/` module — see `yagni mcp --help`), LSP servers, themes, output styles.
|
|
23
|
+
* Discovery is read-only.
|
|
22
24
|
*
|
|
23
25
|
* Everything is fail-soft: malformed JSON, missing dirs, or hostile path
|
|
24
26
|
* entries degrade to "that plugin absent" — never a failed launch. Path
|
package/dist/claudePlugins.js
CHANGED
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
* disabled via `enabledPlugins`.
|
|
19
19
|
*
|
|
20
20
|
* Deliberately NOT here: network installation of any kind, plugin hooks, MCP
|
|
21
|
-
* servers
|
|
21
|
+
* servers (standard MCP support lives in `src/mcpCommand.ts` + the extension's
|
|
22
|
+
* `mcp/` module — see `yagni mcp --help`), LSP servers, themes, output styles.
|
|
23
|
+
* Discovery is read-only.
|
|
22
24
|
*
|
|
23
25
|
* Everything is fail-soft: malformed JSON, missing dirs, or hostile path
|
|
24
26
|
* entries degrade to "that plugin absent" — never a failed launch. Path
|
package/dist/cli.js
CHANGED
|
@@ -23,6 +23,7 @@ import { agentDir, credentialsDir, piPackageDir } from "./credentials.js";
|
|
|
23
23
|
import { DISTRIBUTION } from "./distribution.js";
|
|
24
24
|
import { connectCommand } from "./connectClaudeCode.js";
|
|
25
25
|
import { goCommand } from "./goHeadless.js";
|
|
26
|
+
import { mcpCommand } from "./mcpCommand.js";
|
|
26
27
|
import { login } from "./login.js";
|
|
27
28
|
import { logout } from "./logout.js";
|
|
28
29
|
import { tokenCommand } from "./token.js";
|
|
@@ -653,6 +654,12 @@ export async function main(argv) {
|
|
|
653
654
|
if (command === "connect") {
|
|
654
655
|
return connectCommand(rest);
|
|
655
656
|
}
|
|
657
|
+
// MCP server configuration: same three-scope model and file homes the
|
|
658
|
+
// in-session /mcp panel uses, shared code via the bundled extension's
|
|
659
|
+
// config entry. `list` runs real health-check connections.
|
|
660
|
+
if (command === "mcp") {
|
|
661
|
+
return mcpCommand(rest);
|
|
662
|
+
}
|
|
656
663
|
// The headless pipeline entry. `go` is a real subcommand, not passthrough:
|
|
657
664
|
// the interactive run stays `/go` inside a session, and a `yagni go` without
|
|
658
665
|
// --headless is refused with usage rather than launched as an agent prompt.
|
package/dist/doctor.d.ts
CHANGED
|
@@ -67,6 +67,24 @@ export declare function checkGh(onPath: boolean): CheckResult;
|
|
|
67
67
|
* most machines have no collector, and that is the healthy default.
|
|
68
68
|
*/
|
|
69
69
|
export declare function checkOtelExport(config: OtelLaunchConfig | undefined): CheckResult;
|
|
70
|
+
/** Config-only MCP snapshot (no connections — health checks live in `mcp list`/`get`). */
|
|
71
|
+
export interface McpProbe {
|
|
72
|
+
/** YAGNI_CODE_MCP_DISABLED kill-switch is active. */
|
|
73
|
+
disabled: boolean;
|
|
74
|
+
/** Count of configured servers (post-merge, before the approval gate). */
|
|
75
|
+
serverCount: number;
|
|
76
|
+
/** Project-scope servers that are neither enabled nor disabled. */
|
|
77
|
+
undecidedProjectServers: string[];
|
|
78
|
+
/** Config parse/validation errors (already human-readable). */
|
|
79
|
+
errors: string[];
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Advisory (never required) MCP line: reports config health without connecting
|
|
83
|
+
* to anything — `yagni mcp list`/`get` already run the real health checks, and
|
|
84
|
+
* doctor must never spawn an unapproved server process. The kill-switch and
|
|
85
|
+
* pending-approval cases read as `warn`, config errors as `fail` (non-required).
|
|
86
|
+
*/
|
|
87
|
+
export declare function checkMcpConfig(probe: McpProbe): CheckResult;
|
|
70
88
|
/** What the Windows bash probe found (pi needs a bash — Git Bash — on win32). */
|
|
71
89
|
export interface BashProbe {
|
|
72
90
|
found: boolean;
|
|
@@ -95,6 +113,8 @@ export interface DoctorDeps {
|
|
|
95
113
|
probeBash?: () => BashProbe;
|
|
96
114
|
currentVersion?: string;
|
|
97
115
|
probeLatestVersion?: () => Promise<string | null>;
|
|
116
|
+
/** MCP config snapshot (config-only, no connections). */
|
|
117
|
+
probeMcp?: () => Promise<McpProbe>;
|
|
98
118
|
log?: (msg: string) => void;
|
|
99
119
|
}
|
|
100
120
|
/** Whether a `gh` executable is resolvable on PATH (no subprocess spawn). */
|
package/dist/doctor.js
CHANGED
|
@@ -20,6 +20,7 @@ import { classifyTokenExpiry } from "./launch.js";
|
|
|
20
20
|
import { resolveOtelLaunchWithWorkspace } from "./otel.js";
|
|
21
21
|
import { resolveExtensionPath, resolvePiCliPath, resolvePiPackageDir } from "./paths.js";
|
|
22
22
|
import { readActiveProfile } from "./profiles.js";
|
|
23
|
+
import { resolveMcpConfigPath } from "./mcpCommand.js";
|
|
23
24
|
// ── Pure check builders ─────────────────────────────────────────────────────
|
|
24
25
|
export function checkPiEngine(probe) {
|
|
25
26
|
if (!probe.binPath || !probe.binExists) {
|
|
@@ -225,6 +226,55 @@ export function checkOtelExport(config) {
|
|
|
225
226
|
required: false,
|
|
226
227
|
};
|
|
227
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* Advisory (never required) MCP line: reports config health without connecting
|
|
231
|
+
* to anything — `yagni mcp list`/`get` already run the real health checks, and
|
|
232
|
+
* doctor must never spawn an unapproved server process. The kill-switch and
|
|
233
|
+
* pending-approval cases read as `warn`, config errors as `fail` (non-required).
|
|
234
|
+
*/
|
|
235
|
+
export function checkMcpConfig(probe) {
|
|
236
|
+
if (probe.disabled) {
|
|
237
|
+
return {
|
|
238
|
+
name: "mcp servers",
|
|
239
|
+
status: "warn",
|
|
240
|
+
detail: "disabled (YAGNI_CODE_MCP_DISABLED=1)",
|
|
241
|
+
hint: "unset YAGNI_CODE_MCP_DISABLED to re-enable MCP servers",
|
|
242
|
+
required: false,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
if (probe.errors.length > 0) {
|
|
246
|
+
return {
|
|
247
|
+
name: "mcp servers",
|
|
248
|
+
status: "fail",
|
|
249
|
+
detail: `${probe.serverCount} configured, ${probe.errors.length} config error${probe.errors.length === 1 ? "" : "s"}`,
|
|
250
|
+
hint: `run \`yagni mcp list\` (first error: ${probe.errors[0]})`,
|
|
251
|
+
required: false,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
if (probe.serverCount === 0) {
|
|
255
|
+
return {
|
|
256
|
+
name: "mcp servers",
|
|
257
|
+
status: "ok",
|
|
258
|
+
detail: "none configured",
|
|
259
|
+
required: false,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
if (probe.undecidedProjectServers.length > 0) {
|
|
263
|
+
return {
|
|
264
|
+
name: "mcp servers",
|
|
265
|
+
status: "warn",
|
|
266
|
+
detail: `${probe.serverCount} configured, ${probe.undecidedProjectServers.length} awaiting approval`,
|
|
267
|
+
hint: "run /mcp to approve the project servers",
|
|
268
|
+
required: false,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
name: "mcp servers",
|
|
273
|
+
status: "ok",
|
|
274
|
+
detail: `${probe.serverCount} configured`,
|
|
275
|
+
required: false,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
228
278
|
export function checkBash(probe) {
|
|
229
279
|
if (!probe.found) {
|
|
230
280
|
return {
|
|
@@ -312,6 +362,37 @@ function defaultProbeStateDir() {
|
|
|
312
362
|
return { path, exists: false, mode: null };
|
|
313
363
|
}
|
|
314
364
|
}
|
|
365
|
+
async function defaultProbeMcp(env = process.env) {
|
|
366
|
+
const disabled = env.YAGNI_CODE_MCP_DISABLED === "1";
|
|
367
|
+
if (disabled)
|
|
368
|
+
return { disabled: true, serverCount: 0, undecidedProjectServers: [], errors: [] };
|
|
369
|
+
try {
|
|
370
|
+
const mod = (await import(resolveMcpConfigPath()));
|
|
371
|
+
if (typeof mod?.loadMcpServers !== "function") {
|
|
372
|
+
return { disabled: false, serverCount: 0, undecidedProjectServers: [], errors: ["extension MCP config entry missing"] };
|
|
373
|
+
}
|
|
374
|
+
const loaded = mod.loadMcpServers(process.cwd(), env);
|
|
375
|
+
const repoRoot = mod.resolveProjectRoot(process.cwd());
|
|
376
|
+
const { state } = mod.readProjectApproval(repoRoot);
|
|
377
|
+
const undecided = loaded.servers
|
|
378
|
+
.filter((s) => s.scope === "project" && mod.decisionFor(state, s.name) === "undecided")
|
|
379
|
+
.map((s) => s.name);
|
|
380
|
+
return {
|
|
381
|
+
disabled: false,
|
|
382
|
+
serverCount: loaded.servers.length,
|
|
383
|
+
undecidedProjectServers: undecided,
|
|
384
|
+
errors: loaded.errors.map((e) => (e.serverName ? `${e.serverName}: ${e.message}` : e.message)),
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
catch (err) {
|
|
388
|
+
return {
|
|
389
|
+
disabled: false,
|
|
390
|
+
serverCount: 0,
|
|
391
|
+
undecidedProjectServers: [],
|
|
392
|
+
errors: [err instanceof Error ? err.message : String(err)],
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
}
|
|
315
396
|
async function defaultProbeBackend(baseUrl, token) {
|
|
316
397
|
if (!token)
|
|
317
398
|
return { kind: "skipped" };
|
|
@@ -378,6 +459,7 @@ export async function gatherChecks(deps = {}) {
|
|
|
378
459
|
const readProfile = deps.readActiveProfile ?? readActiveProfile;
|
|
379
460
|
const probeBackend = deps.probeBackend ?? defaultProbeBackend;
|
|
380
461
|
const probeStateDir = deps.probeStateDir ?? defaultProbeStateDir;
|
|
462
|
+
const probeMcp = deps.probeMcp ?? (() => defaultProbeMcp());
|
|
381
463
|
const ghOnPath = deps.ghOnPath ?? (() => ghOnPathDefault());
|
|
382
464
|
const platform = deps.platform ?? process.platform;
|
|
383
465
|
const probeBash = deps.probeBash ?? (() => bashOnWindowsDefault());
|
|
@@ -406,6 +488,7 @@ export async function gatherChecks(deps = {}) {
|
|
|
406
488
|
checks.push(checkBackend(backend));
|
|
407
489
|
checks.push(checkStateDir(probeStateDir()));
|
|
408
490
|
checks.push(checkGh(ghOnPath()));
|
|
491
|
+
checks.push(checkMcpConfig(await probeMcp()));
|
|
409
492
|
checks.push(checkOtelExport(await resolveOtelLaunchWithWorkspace({
|
|
410
493
|
env: process.env,
|
|
411
494
|
cwd: process.cwd(),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { type SpendResponse } from "./costHud.js";
|
|
3
3
|
import { runInitPass as defaultRunInitPass } from "./initPass.js";
|
|
4
|
-
import {
|
|
4
|
+
import { startMcp as defaultStartMcp } from "./mcp/startup.js";
|
|
5
5
|
import { type FlushOutcome, type SpoolClientOpts } from "./spool.js";
|
|
6
6
|
import { type TokenProvider } from "./tokenProvider.js";
|
|
7
7
|
import { type CatalogResult, type ContextBrief } from "./config.js";
|
|
@@ -53,11 +53,11 @@ export interface RegisterYagniDeps {
|
|
|
53
53
|
/** The beat's once-per-repo marker store. Injectable (no disk in tests). */
|
|
54
54
|
mineBeatMarkers?: MineBeatMarkers;
|
|
55
55
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
56
|
+
* Local-MCP startup seam: injectable so tests assert the wiring (approval
|
|
57
|
+
* skip, kill-switch, mutating-tool gate) without touching real config
|
|
58
|
+
* homes or spawning servers. Default runs the real startup.
|
|
59
59
|
*/
|
|
60
|
-
|
|
60
|
+
startMcp?: typeof defaultStartMcp;
|
|
61
61
|
/**
|
|
62
62
|
* The CLI init pass (Onramp Door B). Injectable so tests assert it fires on a
|
|
63
63
|
* fresh-workspace first-run and is skipped otherwise, without a network or disk.
|
package/dist/extension/index.js
CHANGED
|
@@ -29,7 +29,8 @@ import { createYagniFooterFactory, cyclePermissionMode, formatCwd, GIT_MUTATING_
|
|
|
29
29
|
import { RerouteNotifier } from "./rerouteNotice.js";
|
|
30
30
|
import { isFreshWorkspace, registerTeamSetupCommand, runInitPass as defaultRunInitPass } from "./initPass.js";
|
|
31
31
|
import { isInitDone as defaultIsInitDone, markInitDone as defaultMarkInitDone } from "./initDone.js";
|
|
32
|
-
import {
|
|
32
|
+
import { looksMutating } from "./mcp/tools.js";
|
|
33
|
+
import { startMcp as defaultStartMcp, wireShutdown, deriveStartupConnectivityNotices } from "./mcp/startup.js";
|
|
33
34
|
import { registerGoCommand } from "./pipeline/goCommand.js";
|
|
34
35
|
import { registerGoCompareCommand } from "./pipeline/goCompareCommand.js";
|
|
35
36
|
import { DEFAULT_PERMISSION_POLICY, createModeHolder, registerPermissionGate } from "./permission/gate.js";
|
|
@@ -293,16 +294,65 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
293
294
|
const decisionCapture = evalMode ? undefined : makeDecisionCapture(decisionClientOpts);
|
|
294
295
|
if (!evalMode)
|
|
295
296
|
registerDecisionCommands(pi, decisionClientOpts);
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
297
|
+
// Local MCP servers (standard client, replaces the backend-proxy /mcp of
|
|
298
|
+
// the YAG-446 era): load user/project/local config, approval-gate project
|
|
299
|
+
// servers, connect, register one tool per server tool plus /mcp panel.
|
|
300
|
+
// Fail-soft everywhere; skipped entirely in eval mode (external side
|
|
301
|
+
// effects) and under the YAGNI_CODE_MCP_DISABLED kill-switch.
|
|
302
|
+
const startMcpImpl = deps.startMcp ?? defaultStartMcp;
|
|
303
|
+
// One-shot notices queued by startMcp at session start (the `.mcp.json`
|
|
304
|
+
// approval prompt); flushed once on the first provider response. Connectivity
|
|
305
|
+
// notices are NOT here — they are recomputed live at flush (see below).
|
|
306
|
+
const mcpApprovalNotices = [];
|
|
307
|
+
const mcpOutcome = evalMode
|
|
308
|
+
? { manager: undefined, mutatingToolNames: [] }
|
|
309
|
+
: await startMcpImpl(pi, {
|
|
310
|
+
cwd: process.cwd(),
|
|
311
|
+
env,
|
|
312
|
+
hasUI: true,
|
|
313
|
+
notify: (msg) => mcpApprovalNotices.push(msg),
|
|
314
|
+
});
|
|
315
|
+
const mcpMutatingSet = new Set(mcpOutcome.mutatingToolNames);
|
|
316
|
+
// Wire names carry the original tool name after the second `__`: parse it
|
|
317
|
+
// out and re-run the heuristic for tools registered after startup (a
|
|
318
|
+
// reconnect can re-register tools the original classification never saw).
|
|
319
|
+
const mcpStartupIsMutating = (wireName) => {
|
|
320
|
+
const parts = wireName.split("__");
|
|
321
|
+
const original = parts.slice(2).join("__");
|
|
322
|
+
if (!original)
|
|
323
|
+
return false;
|
|
324
|
+
return looksMutating(original);
|
|
325
|
+
};
|
|
326
|
+
if (!evalMode && mcpOutcome.manager) {
|
|
327
|
+
wireShutdown(pi, mcpOutcome.manager);
|
|
328
|
+
// Startup notify (documented CC deviation): one quiet line per notice on
|
|
329
|
+
// the first provider response — never raw stdout, never blocking.
|
|
330
|
+
//
|
|
331
|
+
// Connectivity notices are recomputed against LIVE manager state at flush
|
|
332
|
+
// time (not the frozen startMcp snapshot): if the user healed a server via
|
|
333
|
+
// /mcp before the first turn, it must no longer read as broken/unauth.
|
|
334
|
+
// The static queue only carries non-connectivity notices (e.g. the
|
|
335
|
+
// `.mcp.json` approval prompt), which stay one-shot.
|
|
336
|
+
let connectivityNoticesFlushed = false;
|
|
337
|
+
pi.on("after_provider_response", (_event, ctx) => {
|
|
338
|
+
try {
|
|
339
|
+
if (!ctx.hasUI)
|
|
340
|
+
return;
|
|
341
|
+
const connectivity = connectivityNoticesFlushed
|
|
342
|
+
? []
|
|
343
|
+
: deriveStartupConnectivityNotices(mcpOutcome.manager);
|
|
344
|
+
connectivityNoticesFlushed = true;
|
|
345
|
+
for (const notice of connectivity)
|
|
346
|
+
ctx.ui.notify(notice, "info");
|
|
347
|
+
while (mcpApprovalNotices.length > 0) {
|
|
348
|
+
ctx.ui.notify(mcpApprovalNotices.shift(), "info");
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
// A notice must never break a turn.
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
}
|
|
306
356
|
// P3 + W4: interactive permission tiers + plan mode via /mode, now with a
|
|
307
357
|
// session bless store (three-way review-mode select) and a capture hook that
|
|
308
358
|
// drafts a decision on "don't ask again". Default auto, so still additive.
|
|
@@ -461,18 +511,17 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
461
511
|
}
|
|
462
512
|
})();
|
|
463
513
|
},
|
|
464
|
-
...(
|
|
514
|
+
...(mcpMutatingSet.size > 0 || mcpOutcome.manager
|
|
465
515
|
? {
|
|
466
516
|
policy: {
|
|
467
|
-
planBlockTools: [
|
|
468
|
-
|
|
469
|
-
...mcpMutatingTools,
|
|
470
|
-
],
|
|
471
|
-
reviewConfirmTools: [
|
|
472
|
-
...DEFAULT_PERMISSION_POLICY.reviewConfirmTools,
|
|
473
|
-
...mcpMutatingTools,
|
|
474
|
-
],
|
|
517
|
+
planBlockTools: [...DEFAULT_PERMISSION_POLICY.planBlockTools],
|
|
518
|
+
reviewConfirmTools: [...DEFAULT_PERMISSION_POLICY.reviewConfirmTools],
|
|
475
519
|
alwaysConfirmTools: DEFAULT_PERMISSION_POLICY.alwaysConfirmTools,
|
|
520
|
+
// Predicate (not a static spread): matches any registered
|
|
521
|
+
// mcp__* tool that startup classified as mutating, including
|
|
522
|
+
// tools re-registered after a /mcp reconnect.
|
|
523
|
+
isMutating: (toolName) => mcpMutatingSet.has(toolName) ||
|
|
524
|
+
(toolName.startsWith("mcp__") && mcpStartupIsMutating(toolName)),
|
|
476
525
|
},
|
|
477
526
|
}
|
|
478
527
|
: {}),
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Approval gate for project-scope `.mcp.json` servers, mirroring Claude Code's
|
|
3
|
+
* decision shape: a `.mcp.json` in the repo (shared, VCS-controlled) cannot
|
|
4
|
+
* spawn processes until the user has approved it. Choices live in the
|
|
5
|
+
* per-project entry of `~/.yagni-code/mcp.json` under Claude Code's exact key
|
|
6
|
+
* names (`enabledMcpjsonServers` / `disabledMcpjsonServers` /
|
|
7
|
+
* `enableAllProjectMcpServers`), so a decision made in one tool is respected
|
|
8
|
+
* by the other.
|
|
9
|
+
*/
|
|
10
|
+
import { McpConfigError, UserMcpProjectEntry } from "./config.js";
|
|
11
|
+
export type ProjectApprovalDecision = "enabled" | "disabled" | "undecided";
|
|
12
|
+
export interface ProjectApprovalState {
|
|
13
|
+
enabledMcpjsonServers: string[];
|
|
14
|
+
disabledMcpjsonServers: string[];
|
|
15
|
+
enableAllProjectMcpServers: boolean;
|
|
16
|
+
}
|
|
17
|
+
/** Read the approval state for one project (abs path). Missing file/entry → fresh state. */
|
|
18
|
+
export declare function readProjectApproval(repoRoot: string): {
|
|
19
|
+
state: ProjectApprovalState;
|
|
20
|
+
errors: McpConfigError[];
|
|
21
|
+
};
|
|
22
|
+
/** Effective decision for one project-scope server. Undecided by default. */
|
|
23
|
+
export declare function decisionFor(state: ProjectApprovalState, serverName: string): ProjectApprovalDecision;
|
|
24
|
+
/** Which configured project servers await a decision (drives the session-start prompt). */
|
|
25
|
+
export declare function undecidedProjectServers(state: ProjectApprovalState, projectServerNames: string[]): string[];
|
|
26
|
+
/**
|
|
27
|
+
* Record a decision. `enableAll: true` mirrors Claude Code's "Yes, approve all
|
|
28
|
+
* project servers in this repo" (sets the blanket flag); a single enable/disable
|
|
29
|
+
* appends to the corresponding list. Idempotent.
|
|
30
|
+
*/
|
|
31
|
+
export declare function recordProjectDecision(repoRoot: string, serverName: string | "all", decision: "enabled" | "disabled"): {
|
|
32
|
+
errors: McpConfigError[];
|
|
33
|
+
};
|
|
34
|
+
/** Clear this project's `.mcp.json` decisions (`yagni mcp reset-project-choices`). */
|
|
35
|
+
export declare function resetProjectChoices(repoRoot: string): {
|
|
36
|
+
errors: McpConfigError[];
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Mutate `~/.yagni-code/mcp.json`'s per-project entry under a read-modify-write
|
|
40
|
+
* helper shared with CLI commands (add/remove at local scope).
|
|
41
|
+
*/
|
|
42
|
+
export declare function updateProjectEntry(repoRoot: string, mutate: (entry: UserMcpProjectEntry) => void): {
|
|
43
|
+
errors: McpConfigError[];
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=approval.d.ts.map
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Approval gate for project-scope `.mcp.json` servers, mirroring Claude Code's
|
|
3
|
+
* decision shape: a `.mcp.json` in the repo (shared, VCS-controlled) cannot
|
|
4
|
+
* spawn processes until the user has approved it. Choices live in the
|
|
5
|
+
* per-project entry of `~/.yagni-code/mcp.json` under Claude Code's exact key
|
|
6
|
+
* names (`enabledMcpjsonServers` / `disabledMcpjsonServers` /
|
|
7
|
+
* `enableAllProjectMcpServers`), so a decision made in one tool is respected
|
|
8
|
+
* by the other.
|
|
9
|
+
*/
|
|
10
|
+
import { mcpConfigPath, readProjectMcpConfig, readUserMcpConfig, writeUserMcpConfig, } from "./config.js";
|
|
11
|
+
/** Read the approval state for one project (abs path). Missing file/entry → fresh state. */
|
|
12
|
+
export function readProjectApproval(repoRoot) {
|
|
13
|
+
const { file, errors } = readUserMcpConfig();
|
|
14
|
+
const entry = (file.projects ?? {})[repoRoot] ?? {};
|
|
15
|
+
return {
|
|
16
|
+
state: {
|
|
17
|
+
enabledMcpjsonServers: entry.enabledMcpjsonServers ?? [],
|
|
18
|
+
disabledMcpjsonServers: entry.disabledMcpjsonServers ?? [],
|
|
19
|
+
enableAllProjectMcpServers: entry.enableAllProjectMcpServers ?? false,
|
|
20
|
+
},
|
|
21
|
+
errors,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** Effective decision for one project-scope server. Undecided by default. */
|
|
25
|
+
export function decisionFor(state, serverName) {
|
|
26
|
+
if (state.enableAllProjectMcpServers)
|
|
27
|
+
return "enabled";
|
|
28
|
+
if (state.enabledMcpjsonServers.includes(serverName))
|
|
29
|
+
return "enabled";
|
|
30
|
+
if (state.disabledMcpjsonServers.includes(serverName))
|
|
31
|
+
return "disabled";
|
|
32
|
+
return "undecided";
|
|
33
|
+
}
|
|
34
|
+
/** Which configured project servers await a decision (drives the session-start prompt). */
|
|
35
|
+
export function undecidedProjectServers(state, projectServerNames) {
|
|
36
|
+
return projectServerNames.filter((name) => decisionFor(state, name) === "undecided");
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Record a decision. `enableAll: true` mirrors Claude Code's "Yes, approve all
|
|
40
|
+
* project servers in this repo" (sets the blanket flag); a single enable/disable
|
|
41
|
+
* appends to the corresponding list. Idempotent.
|
|
42
|
+
*/
|
|
43
|
+
export function recordProjectDecision(repoRoot, serverName, decision) {
|
|
44
|
+
const { file, errors } = readUserMcpConfig();
|
|
45
|
+
if (errors.length > 0)
|
|
46
|
+
return { errors };
|
|
47
|
+
const projects = file.projects ?? {};
|
|
48
|
+
const entry = projects[repoRoot] ?? {};
|
|
49
|
+
if (serverName === "all") {
|
|
50
|
+
if (decision === "enabled") {
|
|
51
|
+
projects[repoRoot] = { ...entry, enableAllProjectMcpServers: true };
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
// "Disable all" clears the blanket flag and marks every current server
|
|
55
|
+
// disabled by name (a new server added to .mcp.json later re-prompts).
|
|
56
|
+
const projectServerNames = currentProjectServerNames(repoRoot);
|
|
57
|
+
projects[repoRoot] = {
|
|
58
|
+
...entry,
|
|
59
|
+
enableAllProjectMcpServers: false,
|
|
60
|
+
enabledMcpjsonServers: [],
|
|
61
|
+
disabledMcpjsonServers: projectServerNames,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
const enabled = new Set(entry.enabledMcpjsonServers ?? []);
|
|
67
|
+
const disabled = new Set(entry.disabledMcpjsonServers ?? []);
|
|
68
|
+
if (decision === "enabled") {
|
|
69
|
+
enabled.add(serverName);
|
|
70
|
+
disabled.delete(serverName);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
disabled.add(serverName);
|
|
74
|
+
enabled.delete(serverName);
|
|
75
|
+
}
|
|
76
|
+
projects[repoRoot] = {
|
|
77
|
+
...entry,
|
|
78
|
+
enabledMcpjsonServers: [...enabled],
|
|
79
|
+
disabledMcpjsonServers: [...disabled],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
file.projects = projects;
|
|
83
|
+
try {
|
|
84
|
+
writeUserMcpConfig(file);
|
|
85
|
+
return { errors: [] };
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
return {
|
|
89
|
+
errors: [
|
|
90
|
+
{
|
|
91
|
+
sourcePath: mcpConfigPath(),
|
|
92
|
+
message: `could not persist approval decision (${err instanceof Error ? err.message : String(err)})`,
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function currentProjectServerNames(repoRoot) {
|
|
99
|
+
// Read fresh so a just-edited .mcp.json is reflected in a "disable all" write.
|
|
100
|
+
return Object.keys(readProjectMcpConfig(repoRoot).servers);
|
|
101
|
+
}
|
|
102
|
+
/** Clear this project's `.mcp.json` decisions (`yagni mcp reset-project-choices`). */
|
|
103
|
+
export function resetProjectChoices(repoRoot) {
|
|
104
|
+
const { file, errors } = readUserMcpConfig();
|
|
105
|
+
if (errors.length > 0)
|
|
106
|
+
return { errors };
|
|
107
|
+
const projects = file.projects ?? {};
|
|
108
|
+
const entry = projects[repoRoot] ?? {};
|
|
109
|
+
delete entry.enabledMcpjsonServers;
|
|
110
|
+
delete entry.disabledMcpjsonServers;
|
|
111
|
+
delete entry.enableAllProjectMcpServers;
|
|
112
|
+
// Keep the entry only if it still carries local servers; otherwise drop it.
|
|
113
|
+
if (Object.keys(entry).length > 0)
|
|
114
|
+
projects[repoRoot] = entry;
|
|
115
|
+
else
|
|
116
|
+
delete projects[repoRoot];
|
|
117
|
+
file.projects = projects;
|
|
118
|
+
try {
|
|
119
|
+
writeUserMcpConfig(file);
|
|
120
|
+
return { errors: [] };
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
return {
|
|
124
|
+
errors: [
|
|
125
|
+
{
|
|
126
|
+
sourcePath: mcpConfigPath(),
|
|
127
|
+
message: `could not reset project choices (${err instanceof Error ? err.message : String(err)})`,
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Mutate `~/.yagni-code/mcp.json`'s per-project entry under a read-modify-write
|
|
135
|
+
* helper shared with CLI commands (add/remove at local scope).
|
|
136
|
+
*/
|
|
137
|
+
export function updateProjectEntry(repoRoot, mutate) {
|
|
138
|
+
const { file, errors } = readUserMcpConfig();
|
|
139
|
+
if (errors.length > 0)
|
|
140
|
+
return { errors };
|
|
141
|
+
const projects = file.projects ?? {};
|
|
142
|
+
const entry = projects[repoRoot] ?? {};
|
|
143
|
+
mutate(entry);
|
|
144
|
+
if (Object.keys(entry).length > 0)
|
|
145
|
+
projects[repoRoot] = entry;
|
|
146
|
+
else
|
|
147
|
+
delete projects[repoRoot];
|
|
148
|
+
file.projects = projects;
|
|
149
|
+
try {
|
|
150
|
+
writeUserMcpConfig(file);
|
|
151
|
+
return { errors: [] };
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
return {
|
|
155
|
+
errors: [
|
|
156
|
+
{
|
|
157
|
+
sourcePath: mcpConfigPath(),
|
|
158
|
+
message: `could not persist project entry (${err instanceof Error ? err.message : String(err)})`,
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=approval.js.map
|