@yitom/agy-acp-map 0.1.13 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/agy-headless.exe +0 -0
- package/dist/bin.js +230 -19
- package/dist/core/session-core.d.ts +28 -0
- package/dist/core/types.d.ts +4 -0
- package/dist/index.js +232 -20
- package/dist/lib/mcp-servers.d.ts +84 -0
- package/dist/lib/session-history.d.ts +19 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -64,6 +64,21 @@ bun run smoke:all # units + full live matrix
|
|
|
64
64
|
|
|
65
65
|
**Windows:** install [Bun](https://bun.sh), put `agy` on `PATH`, then the same commands.
|
|
66
66
|
|
|
67
|
+
## Run via bunx / npx (no install, codex-style)
|
|
68
|
+
|
|
69
|
+
The npm tarball is thin (~5MB: `dist/bin.js` + `dist/agy-headless.exe`, no runtime
|
|
70
|
+
needed beyond node/bun). First run downloads `@latest` into the runner cache,
|
|
71
|
+
later runs reuse it:
|
|
72
|
+
|
|
73
|
+
```powershell
|
|
74
|
+
bunx @yitom/agy-acp-map@latest # bun users (preferred: no prompt, no shim flash)
|
|
75
|
+
npx -y @yitom/agy-acp-map@latest # node users
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Requirements are the same as source runs: a logged-in `agy` on `PATH`. The
|
|
79
|
+
optional 95MB single-file `agy-acp-win-x64.exe` is **not** in the npm package —
|
|
80
|
+
grab it from GitHub Releases if you want zero-runtime distribution.
|
|
81
|
+
|
|
67
82
|
|
|
68
83
|
## Launch flags / 启动参数 (v0.1.2)
|
|
69
84
|
|
package/dist/agy-headless.exe
CHANGED
|
Binary file
|
package/dist/bin.js
CHANGED
|
@@ -12463,7 +12463,7 @@ class SessionHistoryStore {
|
|
|
12463
12463
|
filePath(sessionId) {
|
|
12464
12464
|
return path8.join(this.directory, historyFileName(sessionId));
|
|
12465
12465
|
}
|
|
12466
|
-
appendTurn(sessionId, userText, assistantText) {
|
|
12466
|
+
appendTurn(sessionId, userText, assistantText, opts) {
|
|
12467
12467
|
const records = [];
|
|
12468
12468
|
const now = new Date().toISOString();
|
|
12469
12469
|
if (userText) {
|
|
@@ -12483,7 +12483,8 @@ class SessionHistoryStore {
|
|
|
12483
12483
|
messageId: `history_agent_${randomUUID2()}`,
|
|
12484
12484
|
role: "assistant",
|
|
12485
12485
|
text: assistantText,
|
|
12486
|
-
createdAt: new Date().toISOString()
|
|
12486
|
+
createdAt: new Date().toISOString(),
|
|
12487
|
+
...opts?.partial ? { partial: true } : {}
|
|
12487
12488
|
});
|
|
12488
12489
|
}
|
|
12489
12490
|
if (!records.length)
|
|
@@ -12539,11 +12540,168 @@ class SessionHistoryStore {
|
|
|
12539
12540
|
}
|
|
12540
12541
|
}
|
|
12541
12542
|
|
|
12543
|
+
// src/lib/mcp-servers.ts
|
|
12544
|
+
import { execFile } from "node:child_process";
|
|
12545
|
+
function fail(msg) {
|
|
12546
|
+
throw new RequestError(-32602, msg);
|
|
12547
|
+
}
|
|
12548
|
+
function nonEmptyString(v) {
|
|
12549
|
+
return typeof v === "string" && v.length > 0;
|
|
12550
|
+
}
|
|
12551
|
+
function strArray(v, what) {
|
|
12552
|
+
if (v === undefined)
|
|
12553
|
+
return [];
|
|
12554
|
+
if (!Array.isArray(v) || v.some((x) => typeof x !== "string")) {
|
|
12555
|
+
fail(`mcpServers[].${what} must be an array of strings`);
|
|
12556
|
+
}
|
|
12557
|
+
return v;
|
|
12558
|
+
}
|
|
12559
|
+
function nameValueList(v, what) {
|
|
12560
|
+
if (v === undefined)
|
|
12561
|
+
return [];
|
|
12562
|
+
if (!Array.isArray(v))
|
|
12563
|
+
fail(`mcpServers[].${what} must be an array`);
|
|
12564
|
+
return v.map((e, i) => {
|
|
12565
|
+
if (!e || typeof e !== "object")
|
|
12566
|
+
fail(`mcpServers[].${what}[${i}] must be {name, value}`);
|
|
12567
|
+
const rec = e;
|
|
12568
|
+
if (!nonEmptyString(rec.name) || typeof rec.value !== "string") {
|
|
12569
|
+
fail(`mcpServers[].${what}[${i}] must be {name: string, value: string}`);
|
|
12570
|
+
}
|
|
12571
|
+
return { name: rec.name, value: rec.value };
|
|
12572
|
+
});
|
|
12573
|
+
}
|
|
12574
|
+
function normalizeMcpServer(input) {
|
|
12575
|
+
if (!input || typeof input !== "object")
|
|
12576
|
+
fail("mcpServers[] must be an object");
|
|
12577
|
+
const s = input;
|
|
12578
|
+
if (!nonEmptyString(s.name))
|
|
12579
|
+
fail("mcpServers[].name must be a non-empty string");
|
|
12580
|
+
const name = s.name.trim();
|
|
12581
|
+
if (name.length > 64 || /[\s\x00-\x1f]/.test(name)) {
|
|
12582
|
+
fail(`mcpServers[].name must be ≤64 chars with no whitespace: ${JSON.stringify(name)}`);
|
|
12583
|
+
}
|
|
12584
|
+
const t = typeof s.type === "string" ? s.type.trim().toLowerCase() : "";
|
|
12585
|
+
if (t === "sse" || t === "acp") {
|
|
12586
|
+
fail(`mcpServers[] '${name}': type '${t}' has no 'agy mcp add' equivalent ` + `(agy supports stdio|http only). Resend as stdio or http.`);
|
|
12587
|
+
}
|
|
12588
|
+
if (t !== "" && t !== "stdio" && t !== "http") {
|
|
12589
|
+
fail(`mcpServers[] '${name}': unknown type ${JSON.stringify(s.type)} (want stdio|http)`);
|
|
12590
|
+
}
|
|
12591
|
+
const hasCommand = nonEmptyString(s.command);
|
|
12592
|
+
const hasUrl = nonEmptyString(s.url);
|
|
12593
|
+
if (hasCommand && hasUrl) {
|
|
12594
|
+
fail(`mcpServers[] '${name}': ambiguous (both command and url set); send one`);
|
|
12595
|
+
}
|
|
12596
|
+
if (t === "http" || !hasCommand && hasUrl) {
|
|
12597
|
+
if (!hasUrl)
|
|
12598
|
+
fail(`mcpServers[] '${name}': http server needs a url`);
|
|
12599
|
+
const url = s.url;
|
|
12600
|
+
if (!/^https?:\/\//i.test(url))
|
|
12601
|
+
fail(`mcpServers[] '${name}': url must start with http(s)://`);
|
|
12602
|
+
return { name, kind: "http", args: [], env: [], url, headers: nameValueList(s.headers, "headers") };
|
|
12603
|
+
}
|
|
12604
|
+
if (!hasCommand) {
|
|
12605
|
+
fail(`mcpServers[] '${name}': stdio server needs a command (or send type:"http" with a url)`);
|
|
12606
|
+
}
|
|
12607
|
+
return {
|
|
12608
|
+
name,
|
|
12609
|
+
kind: "stdio",
|
|
12610
|
+
command: s.command,
|
|
12611
|
+
args: strArray(s.args, "args"),
|
|
12612
|
+
env: nameValueList(s.env, "env"),
|
|
12613
|
+
headers: []
|
|
12614
|
+
};
|
|
12615
|
+
}
|
|
12616
|
+
function validateMcpServers(input) {
|
|
12617
|
+
if (input === undefined)
|
|
12618
|
+
return [];
|
|
12619
|
+
if (!Array.isArray(input))
|
|
12620
|
+
fail("mcpServers must be an array");
|
|
12621
|
+
const seen = new Set;
|
|
12622
|
+
return input.map((e) => {
|
|
12623
|
+
const n = normalizeMcpServer(e);
|
|
12624
|
+
if (seen.has(n.name))
|
|
12625
|
+
fail(`mcpServers[] duplicate name: '${n.name}'`);
|
|
12626
|
+
seen.add(n.name);
|
|
12627
|
+
return n;
|
|
12628
|
+
});
|
|
12629
|
+
}
|
|
12630
|
+
function mcpServerToAgyAddArgs(s) {
|
|
12631
|
+
const argv = ["mcp", "add"];
|
|
12632
|
+
if (s.kind === "stdio") {
|
|
12633
|
+
for (const e of s.env)
|
|
12634
|
+
argv.push("--env", `${e.name}=${e.value}`);
|
|
12635
|
+
argv.push(s.name, s.command, ...s.args);
|
|
12636
|
+
return argv;
|
|
12637
|
+
}
|
|
12638
|
+
for (const h of s.headers)
|
|
12639
|
+
argv.push("--header", `${h.name}: ${h.value}`);
|
|
12640
|
+
argv.push("--type", "http", s.name, s.url);
|
|
12641
|
+
return argv;
|
|
12642
|
+
}
|
|
12643
|
+
function execFileAsync(bin, args, timeoutMs) {
|
|
12644
|
+
return new Promise((resolve) => {
|
|
12645
|
+
execFile(bin, args, { timeout: timeoutMs, maxBuffer: 512 * 1024, windowsHide: true }, (err, stdout, stderr) => {
|
|
12646
|
+
const e = err;
|
|
12647
|
+
resolve({
|
|
12648
|
+
status: typeof e?.code === "number" ? e.code : e ? 1 : 0,
|
|
12649
|
+
stdout: String(stdout ?? ""),
|
|
12650
|
+
stderr: e?.message ? `${e.message}
|
|
12651
|
+
${String(stderr ?? "")}` : String(stderr ?? "")
|
|
12652
|
+
});
|
|
12653
|
+
});
|
|
12654
|
+
});
|
|
12655
|
+
}
|
|
12656
|
+
var defaultMcpRunFn = (bin, args, opts) => execFileAsync(bin, args, opts.timeoutMs);
|
|
12657
|
+
function tail(text, n = 600) {
|
|
12658
|
+
const t = String(text || "").trim();
|
|
12659
|
+
return t.length > n ? "…" + t.slice(-n) : t;
|
|
12660
|
+
}
|
|
12661
|
+
async function syncMcpServers(bin, servers, runFn = defaultMcpRunFn, timeoutMs = 30000) {
|
|
12662
|
+
const added = [];
|
|
12663
|
+
for (const s of servers) {
|
|
12664
|
+
let r;
|
|
12665
|
+
try {
|
|
12666
|
+
r = await runFn(bin, mcpServerToAgyAddArgs(s), { timeoutMs });
|
|
12667
|
+
} catch (err) {
|
|
12668
|
+
throw new RequestError(-32603, `failed to register MCP server '${s.name}': ${err?.message || err}`);
|
|
12669
|
+
}
|
|
12670
|
+
if (r.status !== 0) {
|
|
12671
|
+
throw new RequestError(-32603, `failed to register MCP server '${s.name}' (exit ${r.status}): ${tail(`${r.stdout}
|
|
12672
|
+
${r.stderr}`)}`);
|
|
12673
|
+
}
|
|
12674
|
+
added.push(s.name);
|
|
12675
|
+
}
|
|
12676
|
+
return { added };
|
|
12677
|
+
}
|
|
12678
|
+
async function removeMcpServers(bin, names, runFn = defaultMcpRunFn, timeoutMs = 30000) {
|
|
12679
|
+
const removed = [];
|
|
12680
|
+
const warnings = [];
|
|
12681
|
+
for (const name of names) {
|
|
12682
|
+
try {
|
|
12683
|
+
const r = await runFn(bin, ["mcp", "remove", name], { timeoutMs });
|
|
12684
|
+
if (r.status !== 0) {
|
|
12685
|
+
warnings.push(`mcp remove '${name}' exit ${r.status}: ${tail(`${r.stdout}
|
|
12686
|
+
${r.stderr}`, 200)}`);
|
|
12687
|
+
} else {
|
|
12688
|
+
removed.push(name);
|
|
12689
|
+
}
|
|
12690
|
+
} catch (err) {
|
|
12691
|
+
warnings.push(`mcp remove '${name}' threw: ${err?.message || err}`);
|
|
12692
|
+
}
|
|
12693
|
+
}
|
|
12694
|
+
if (warnings.length) {
|
|
12695
|
+
console.warn(`[ACP-MCP] cleanup warnings: ${warnings.join(" | ")}`);
|
|
12696
|
+
}
|
|
12697
|
+
return { removed, warnings };
|
|
12698
|
+
}
|
|
12699
|
+
|
|
12542
12700
|
// src/core/types.ts
|
|
12543
12701
|
var AGENT_INFO = {
|
|
12544
12702
|
name: "agy-acp",
|
|
12545
12703
|
title: "agy ACP (stream-json)",
|
|
12546
|
-
version: "0.1.
|
|
12704
|
+
version: "0.1.16"
|
|
12547
12705
|
};
|
|
12548
12706
|
var BRIDGE_CAPABILITIES = {
|
|
12549
12707
|
prompt: true,
|
|
@@ -12651,6 +12809,17 @@ function debugLog(msg) {
|
|
|
12651
12809
|
|
|
12652
12810
|
// src/core/session-core.ts
|
|
12653
12811
|
var EMPTY_SESSION_MAX_AGE_MS = 60 * 60 * 1000;
|
|
12812
|
+
function resolveAgyBin() {
|
|
12813
|
+
const rawBin = process.env.AGY_BIN;
|
|
12814
|
+
let bin = rawBin && rawBin !== "undefined" && rawBin !== "null" ? rawBin : "agy";
|
|
12815
|
+
if (bin === "agy" || bin === "agy.exe") {
|
|
12816
|
+
const geminiBin = path10.join(process.env.USERPROFILE || process.env.HOME || "", ".gemini", "bin", process.platform === "win32" ? "agy.exe" : "agy");
|
|
12817
|
+
if (fs9.existsSync(geminiBin)) {
|
|
12818
|
+
bin = geminiBin;
|
|
12819
|
+
}
|
|
12820
|
+
}
|
|
12821
|
+
return bin;
|
|
12822
|
+
}
|
|
12654
12823
|
function deriveTitle(text, maxLen = 60) {
|
|
12655
12824
|
const line = String(text || "").split(/\r?\n/).map((s) => s.trim()).find(Boolean) || "";
|
|
12656
12825
|
const flat = line.replace(/\s+/g, " ");
|
|
@@ -12662,7 +12831,10 @@ class AgySessionCore {
|
|
|
12662
12831
|
sessionStore;
|
|
12663
12832
|
historyStore;
|
|
12664
12833
|
catalogPromise = null;
|
|
12834
|
+
mcpRunner;
|
|
12835
|
+
mcpRefs = new Map;
|
|
12665
12836
|
constructor(options) {
|
|
12837
|
+
this.mcpRunner = options?.mcpRunner ?? defaultMcpRunFn;
|
|
12666
12838
|
if (options?.sessionStore instanceof SessionStore) {
|
|
12667
12839
|
this.sessionStore = options.sessionStore;
|
|
12668
12840
|
} else if (typeof options?.sessionStore === "string") {
|
|
@@ -12711,14 +12883,8 @@ class AgySessionCore {
|
|
|
12711
12883
|
disableSlashCommands: disableSlash,
|
|
12712
12884
|
printTimeout
|
|
12713
12885
|
});
|
|
12714
|
-
const rawBin =
|
|
12715
|
-
|
|
12716
|
-
if (bin === "agy" || bin === "agy.exe") {
|
|
12717
|
-
const geminiBin = path10.join(process.env.USERPROFILE || process.env.HOME || "", ".gemini", "bin", process.platform === "win32" ? "agy.exe" : "agy");
|
|
12718
|
-
if (fs9.existsSync(geminiBin)) {
|
|
12719
|
-
bin = geminiBin;
|
|
12720
|
-
}
|
|
12721
|
-
}
|
|
12886
|
+
const rawBin = resolveAgyBin();
|
|
12887
|
+
const bin = rawBin;
|
|
12722
12888
|
let execBin = bin;
|
|
12723
12889
|
let execArgs = args;
|
|
12724
12890
|
if (/\.(js|cjs|mjs|ts)$/i.test(bin)) {
|
|
@@ -12792,7 +12958,8 @@ class AgySessionCore {
|
|
|
12792
12958
|
}
|
|
12793
12959
|
persistTurnHistory(sessionId, userText, assistantText, stopReason, eligible = true) {
|
|
12794
12960
|
try {
|
|
12795
|
-
|
|
12961
|
+
const partial = stopReason === "cancelled" || !eligible;
|
|
12962
|
+
this.historyStore.appendTurn(sessionId, userText, assistantText, partial ? { partial: true } : undefined);
|
|
12796
12963
|
} catch (err) {
|
|
12797
12964
|
console.warn(`[ACP-HISTORY] Warning: failed to persist turn ${sessionId}:`, err);
|
|
12798
12965
|
}
|
|
@@ -12848,6 +13015,43 @@ class AgySessionCore {
|
|
|
12848
13015
|
if (!session.agent && agents.length)
|
|
12849
13016
|
session.agent = agents[0].value;
|
|
12850
13017
|
}
|
|
13018
|
+
async applySessionMcpServers(sessionId, input) {
|
|
13019
|
+
const servers = validateMcpServers(input ?? []);
|
|
13020
|
+
if (!servers.length)
|
|
13021
|
+
return [];
|
|
13022
|
+
const live = this.sessions.get(sessionId);
|
|
13023
|
+
const hadLiveProc = live ? live.proc.isWritable() : false;
|
|
13024
|
+
await syncMcpServers(resolveAgyBin(), servers, this.mcpRunner);
|
|
13025
|
+
const names = servers.map((s) => ({ name: s.name }));
|
|
13026
|
+
if (live)
|
|
13027
|
+
live.mcpServers = names;
|
|
13028
|
+
this.trackMcpServers(sessionId, names.map((n) => n.name));
|
|
13029
|
+
if (hadLiveProc) {
|
|
13030
|
+
console.warn(`[ACP-MCP] session ${sessionId}: MCP servers [${names.map((n) => n.name).join(", ")}] ` + `registered while the session process is live; they apply to fresh spawns (reconnect to use them).`);
|
|
13031
|
+
}
|
|
13032
|
+
return names;
|
|
13033
|
+
}
|
|
13034
|
+
trackMcpServers(sessionId, names) {
|
|
13035
|
+
for (const name of names) {
|
|
13036
|
+
let set = this.mcpRefs.get(name);
|
|
13037
|
+
if (!set) {
|
|
13038
|
+
set = new Set;
|
|
13039
|
+
this.mcpRefs.set(name, set);
|
|
13040
|
+
}
|
|
13041
|
+
set.add(sessionId);
|
|
13042
|
+
}
|
|
13043
|
+
}
|
|
13044
|
+
untrackMcpServers(sessionId) {
|
|
13045
|
+
const freed = [];
|
|
13046
|
+
for (const [name, set] of this.mcpRefs) {
|
|
13047
|
+
set.delete(sessionId);
|
|
13048
|
+
if (set.size === 0) {
|
|
13049
|
+
this.mcpRefs.delete(name);
|
|
13050
|
+
freed.push(name);
|
|
13051
|
+
}
|
|
13052
|
+
}
|
|
13053
|
+
return freed;
|
|
13054
|
+
}
|
|
12851
13055
|
async createSession(params, protocolVersion = 1) {
|
|
12852
13056
|
const cwd = params?.cwd;
|
|
12853
13057
|
debugLog(`createSession v${protocolVersion} cwd=${cwd}`);
|
|
@@ -12870,6 +13074,7 @@ class AgySessionCore {
|
|
|
12870
13074
|
if (params?.mcpServers !== undefined && !Array.isArray(params.mcpServers)) {
|
|
12871
13075
|
throw new RequestError(-32602, "mcpServers must be an array");
|
|
12872
13076
|
}
|
|
13077
|
+
validateMcpServers(params?.mcpServers);
|
|
12873
13078
|
const launch = extractLaunchConfig(params);
|
|
12874
13079
|
const discovery = await this.getDiscovery();
|
|
12875
13080
|
const sessionId = randomUUID3();
|
|
@@ -12912,6 +13117,7 @@ class AgySessionCore {
|
|
|
12912
13117
|
};
|
|
12913
13118
|
this.applyCatalogDefaults(session, discovery);
|
|
12914
13119
|
this.sessions.set(sessionId, session);
|
|
13120
|
+
await this.applySessionMcpServers(sessionId, params?.mcpServers);
|
|
12915
13121
|
this.persistSession(session);
|
|
12916
13122
|
this.warmupSession(sessionId);
|
|
12917
13123
|
return {
|
|
@@ -12940,14 +13146,10 @@ class AgySessionCore {
|
|
|
12940
13146
|
throw new RequestError(-32602, protocolVersion === 2 ? 'only replayFrom.type="start" is supported by this agent' : "session/resume with replayFrom is only supported by ACP v2");
|
|
12941
13147
|
}
|
|
12942
13148
|
}
|
|
12943
|
-
if (params?.mcpServers !== undefined) {
|
|
12944
|
-
|
|
12945
|
-
throw new RequestError(-32602, "mcpServers must be an array");
|
|
12946
|
-
}
|
|
12947
|
-
if (params.mcpServers.length > 0) {
|
|
12948
|
-
throw new RequestError(-32602, "mcpServers are not supported by this agent");
|
|
12949
|
-
}
|
|
13149
|
+
if (params?.mcpServers !== undefined && !Array.isArray(params.mcpServers)) {
|
|
13150
|
+
throw new RequestError(-32602, "mcpServers must be an array");
|
|
12950
13151
|
}
|
|
13152
|
+
validateMcpServers(params?.mcpServers);
|
|
12951
13153
|
let additionalDirectories = [];
|
|
12952
13154
|
if (params?.additionalDirectories !== undefined) {
|
|
12953
13155
|
if (!Array.isArray(params.additionalDirectories)) {
|
|
@@ -13022,6 +13224,7 @@ class AgySessionCore {
|
|
|
13022
13224
|
}
|
|
13023
13225
|
const discovery = await this.getDiscovery();
|
|
13024
13226
|
this.applyCatalogDefaults(session, discovery);
|
|
13227
|
+
await this.applySessionMcpServers(sessionId, params?.mcpServers);
|
|
13025
13228
|
if (!session.title) {
|
|
13026
13229
|
try {
|
|
13027
13230
|
const first = this.historyStore.firstUserText(sessionId);
|
|
@@ -13198,6 +13401,14 @@ class AgySessionCore {
|
|
|
13198
13401
|
}
|
|
13199
13402
|
this.sessionStore.delete(sessionId);
|
|
13200
13403
|
this.historyStore.delete(sessionId);
|
|
13404
|
+
try {
|
|
13405
|
+
const freed = this.untrackMcpServers(sessionId);
|
|
13406
|
+
if (freed.length) {
|
|
13407
|
+
await removeMcpServers(resolveAgyBin(), freed, this.mcpRunner);
|
|
13408
|
+
}
|
|
13409
|
+
} catch (err) {
|
|
13410
|
+
console.warn(`[ACP-MCP] cleanup after delete ${sessionId} failed: ${err?.message || err}`);
|
|
13411
|
+
}
|
|
13201
13412
|
return {};
|
|
13202
13413
|
}
|
|
13203
13414
|
async closeSession(params) {
|
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import { type DiscoveryResult } from '../lib/agy-discovery.ts';
|
|
2
2
|
import { SessionStore } from '../lib/session-store.ts';
|
|
3
3
|
import { SessionHistoryStore } from '../lib/session-history.ts';
|
|
4
|
+
import { type McpRunFn } from '../lib/mcp-servers.ts';
|
|
4
5
|
import { type SdkSession, type ProtocolVersion } from './types.ts';
|
|
5
6
|
export interface SessionCoreOptions {
|
|
6
7
|
sessionStore?: SessionStore | string;
|
|
7
8
|
historyStore?: SessionHistoryStore | string;
|
|
9
|
+
/**
|
|
10
|
+
* `agy mcp ...` runner override (tests inject a fake; prod spawns agy).
|
|
11
|
+
* Keeps MCP sync unit-testable without touching the real global config.
|
|
12
|
+
*/
|
|
13
|
+
mcpRunner?: McpRunFn;
|
|
8
14
|
}
|
|
9
15
|
/** Rows without any completed turn older than this are hidden from
|
|
10
16
|
* session/list (still resumable/deletable by id — the store keeps them). */
|
|
11
17
|
export declare const EMPTY_SESSION_MAX_AGE_MS: number;
|
|
18
|
+
/** Resolve the agy binary (AGY_BIN, else ~/.gemini/bin/agy). Shared by spawn and `agy mcp ...` sync. */
|
|
19
|
+
export declare function resolveAgyBin(): string;
|
|
12
20
|
/** First user prompt collapsed to one line, capped for list display. */
|
|
13
21
|
export declare function deriveTitle(text: string, maxLen?: number): string;
|
|
14
22
|
export declare class AgySessionCore {
|
|
@@ -16,6 +24,13 @@ export declare class AgySessionCore {
|
|
|
16
24
|
readonly sessionStore: SessionStore;
|
|
17
25
|
readonly historyStore: SessionHistoryStore;
|
|
18
26
|
private catalogPromise;
|
|
27
|
+
private readonly mcpRunner;
|
|
28
|
+
/**
|
|
29
|
+
* MCP server name → sessionIds that registered it (this process only).
|
|
30
|
+
* Drives delete-time cleanup: a server is `agy mcp remove`d only when its
|
|
31
|
+
* last referencing session goes away.
|
|
32
|
+
*/
|
|
33
|
+
private readonly mcpRefs;
|
|
19
34
|
constructor(options?: SessionCoreOptions);
|
|
20
35
|
getDiscovery(): Promise<DiscoveryResult>;
|
|
21
36
|
/** Warm-up toggle: AGY_ACP_WARMUP=0/false/no disables connect-time pre-spawn. */
|
|
@@ -41,6 +56,19 @@ export declare class AgySessionCore {
|
|
|
41
56
|
replayHistory(sessionId: string, protocolVersion: ProtocolVersion, notifyClient: (update: any) => Promise<void> | void): Promise<void>;
|
|
42
57
|
sessionMeta(session: SdkSession): Record<string, unknown> | undefined;
|
|
43
58
|
applyCatalogDefaults(session: SdkSession, discovery: DiscoveryResult): void;
|
|
59
|
+
/**
|
|
60
|
+
* Sync ACP mcpServers into agy's MCP config BEFORE the session process
|
|
61
|
+
* spawns, so tools are listed from the first turn. Throws loud on any
|
|
62
|
+
* failure (a client that asked for MCP must never get a silent
|
|
63
|
+
* tools-less session). Tracks names per session for delete-time cleanup.
|
|
64
|
+
*/
|
|
65
|
+
private applySessionMcpServers;
|
|
66
|
+
private trackMcpServers;
|
|
67
|
+
/**
|
|
68
|
+
* Drop one session's references; returns names no other live session
|
|
69
|
+
* references anymore (safe to `agy mcp remove`).
|
|
70
|
+
*/
|
|
71
|
+
private untrackMcpServers;
|
|
44
72
|
createSession(params: any, protocolVersion?: ProtocolVersion): Promise<{
|
|
45
73
|
session: SdkSession;
|
|
46
74
|
discovery: DiscoveryResult;
|
package/dist/core/types.d.ts
CHANGED
|
@@ -58,6 +58,10 @@ export interface SdkSession {
|
|
|
58
58
|
disableSlashCommands?: boolean;
|
|
59
59
|
printTimeout?: string;
|
|
60
60
|
deleted?: boolean;
|
|
61
|
+
/** MCP servers this session registered (tracked for delete-time cleanup). */
|
|
62
|
+
mcpServers?: Array<{
|
|
63
|
+
name: string;
|
|
64
|
+
}>;
|
|
61
65
|
}
|
|
62
66
|
/**
|
|
63
67
|
* Sequential FIFO execution queue to eliminate notification ordering races.
|
package/dist/index.js
CHANGED
|
@@ -12250,7 +12250,7 @@ class SessionHistoryStore {
|
|
|
12250
12250
|
filePath(sessionId) {
|
|
12251
12251
|
return path8.join(this.directory, historyFileName(sessionId));
|
|
12252
12252
|
}
|
|
12253
|
-
appendTurn(sessionId, userText, assistantText) {
|
|
12253
|
+
appendTurn(sessionId, userText, assistantText, opts) {
|
|
12254
12254
|
const records = [];
|
|
12255
12255
|
const now = new Date().toISOString();
|
|
12256
12256
|
if (userText) {
|
|
@@ -12270,7 +12270,8 @@ class SessionHistoryStore {
|
|
|
12270
12270
|
messageId: `history_agent_${randomUUID2()}`,
|
|
12271
12271
|
role: "assistant",
|
|
12272
12272
|
text: assistantText,
|
|
12273
|
-
createdAt: new Date().toISOString()
|
|
12273
|
+
createdAt: new Date().toISOString(),
|
|
12274
|
+
...opts?.partial ? { partial: true } : {}
|
|
12274
12275
|
});
|
|
12275
12276
|
}
|
|
12276
12277
|
if (!records.length)
|
|
@@ -12326,11 +12327,168 @@ class SessionHistoryStore {
|
|
|
12326
12327
|
}
|
|
12327
12328
|
}
|
|
12328
12329
|
|
|
12330
|
+
// src/lib/mcp-servers.ts
|
|
12331
|
+
import { execFile } from "node:child_process";
|
|
12332
|
+
function fail(msg) {
|
|
12333
|
+
throw new RequestError(-32602, msg);
|
|
12334
|
+
}
|
|
12335
|
+
function nonEmptyString(v) {
|
|
12336
|
+
return typeof v === "string" && v.length > 0;
|
|
12337
|
+
}
|
|
12338
|
+
function strArray(v, what) {
|
|
12339
|
+
if (v === undefined)
|
|
12340
|
+
return [];
|
|
12341
|
+
if (!Array.isArray(v) || v.some((x) => typeof x !== "string")) {
|
|
12342
|
+
fail(`mcpServers[].${what} must be an array of strings`);
|
|
12343
|
+
}
|
|
12344
|
+
return v;
|
|
12345
|
+
}
|
|
12346
|
+
function nameValueList(v, what) {
|
|
12347
|
+
if (v === undefined)
|
|
12348
|
+
return [];
|
|
12349
|
+
if (!Array.isArray(v))
|
|
12350
|
+
fail(`mcpServers[].${what} must be an array`);
|
|
12351
|
+
return v.map((e, i) => {
|
|
12352
|
+
if (!e || typeof e !== "object")
|
|
12353
|
+
fail(`mcpServers[].${what}[${i}] must be {name, value}`);
|
|
12354
|
+
const rec = e;
|
|
12355
|
+
if (!nonEmptyString(rec.name) || typeof rec.value !== "string") {
|
|
12356
|
+
fail(`mcpServers[].${what}[${i}] must be {name: string, value: string}`);
|
|
12357
|
+
}
|
|
12358
|
+
return { name: rec.name, value: rec.value };
|
|
12359
|
+
});
|
|
12360
|
+
}
|
|
12361
|
+
function normalizeMcpServer(input) {
|
|
12362
|
+
if (!input || typeof input !== "object")
|
|
12363
|
+
fail("mcpServers[] must be an object");
|
|
12364
|
+
const s = input;
|
|
12365
|
+
if (!nonEmptyString(s.name))
|
|
12366
|
+
fail("mcpServers[].name must be a non-empty string");
|
|
12367
|
+
const name = s.name.trim();
|
|
12368
|
+
if (name.length > 64 || /[\s\x00-\x1f]/.test(name)) {
|
|
12369
|
+
fail(`mcpServers[].name must be ≤64 chars with no whitespace: ${JSON.stringify(name)}`);
|
|
12370
|
+
}
|
|
12371
|
+
const t = typeof s.type === "string" ? s.type.trim().toLowerCase() : "";
|
|
12372
|
+
if (t === "sse" || t === "acp") {
|
|
12373
|
+
fail(`mcpServers[] '${name}': type '${t}' has no 'agy mcp add' equivalent ` + `(agy supports stdio|http only). Resend as stdio or http.`);
|
|
12374
|
+
}
|
|
12375
|
+
if (t !== "" && t !== "stdio" && t !== "http") {
|
|
12376
|
+
fail(`mcpServers[] '${name}': unknown type ${JSON.stringify(s.type)} (want stdio|http)`);
|
|
12377
|
+
}
|
|
12378
|
+
const hasCommand = nonEmptyString(s.command);
|
|
12379
|
+
const hasUrl = nonEmptyString(s.url);
|
|
12380
|
+
if (hasCommand && hasUrl) {
|
|
12381
|
+
fail(`mcpServers[] '${name}': ambiguous (both command and url set); send one`);
|
|
12382
|
+
}
|
|
12383
|
+
if (t === "http" || !hasCommand && hasUrl) {
|
|
12384
|
+
if (!hasUrl)
|
|
12385
|
+
fail(`mcpServers[] '${name}': http server needs a url`);
|
|
12386
|
+
const url = s.url;
|
|
12387
|
+
if (!/^https?:\/\//i.test(url))
|
|
12388
|
+
fail(`mcpServers[] '${name}': url must start with http(s)://`);
|
|
12389
|
+
return { name, kind: "http", args: [], env: [], url, headers: nameValueList(s.headers, "headers") };
|
|
12390
|
+
}
|
|
12391
|
+
if (!hasCommand) {
|
|
12392
|
+
fail(`mcpServers[] '${name}': stdio server needs a command (or send type:"http" with a url)`);
|
|
12393
|
+
}
|
|
12394
|
+
return {
|
|
12395
|
+
name,
|
|
12396
|
+
kind: "stdio",
|
|
12397
|
+
command: s.command,
|
|
12398
|
+
args: strArray(s.args, "args"),
|
|
12399
|
+
env: nameValueList(s.env, "env"),
|
|
12400
|
+
headers: []
|
|
12401
|
+
};
|
|
12402
|
+
}
|
|
12403
|
+
function validateMcpServers(input) {
|
|
12404
|
+
if (input === undefined)
|
|
12405
|
+
return [];
|
|
12406
|
+
if (!Array.isArray(input))
|
|
12407
|
+
fail("mcpServers must be an array");
|
|
12408
|
+
const seen = new Set;
|
|
12409
|
+
return input.map((e) => {
|
|
12410
|
+
const n = normalizeMcpServer(e);
|
|
12411
|
+
if (seen.has(n.name))
|
|
12412
|
+
fail(`mcpServers[] duplicate name: '${n.name}'`);
|
|
12413
|
+
seen.add(n.name);
|
|
12414
|
+
return n;
|
|
12415
|
+
});
|
|
12416
|
+
}
|
|
12417
|
+
function mcpServerToAgyAddArgs(s) {
|
|
12418
|
+
const argv = ["mcp", "add"];
|
|
12419
|
+
if (s.kind === "stdio") {
|
|
12420
|
+
for (const e of s.env)
|
|
12421
|
+
argv.push("--env", `${e.name}=${e.value}`);
|
|
12422
|
+
argv.push(s.name, s.command, ...s.args);
|
|
12423
|
+
return argv;
|
|
12424
|
+
}
|
|
12425
|
+
for (const h of s.headers)
|
|
12426
|
+
argv.push("--header", `${h.name}: ${h.value}`);
|
|
12427
|
+
argv.push("--type", "http", s.name, s.url);
|
|
12428
|
+
return argv;
|
|
12429
|
+
}
|
|
12430
|
+
function execFileAsync(bin, args, timeoutMs) {
|
|
12431
|
+
return new Promise((resolve) => {
|
|
12432
|
+
execFile(bin, args, { timeout: timeoutMs, maxBuffer: 512 * 1024, windowsHide: true }, (err, stdout, stderr) => {
|
|
12433
|
+
const e = err;
|
|
12434
|
+
resolve({
|
|
12435
|
+
status: typeof e?.code === "number" ? e.code : e ? 1 : 0,
|
|
12436
|
+
stdout: String(stdout ?? ""),
|
|
12437
|
+
stderr: e?.message ? `${e.message}
|
|
12438
|
+
${String(stderr ?? "")}` : String(stderr ?? "")
|
|
12439
|
+
});
|
|
12440
|
+
});
|
|
12441
|
+
});
|
|
12442
|
+
}
|
|
12443
|
+
var defaultMcpRunFn = (bin, args, opts) => execFileAsync(bin, args, opts.timeoutMs);
|
|
12444
|
+
function tail(text, n = 600) {
|
|
12445
|
+
const t = String(text || "").trim();
|
|
12446
|
+
return t.length > n ? "…" + t.slice(-n) : t;
|
|
12447
|
+
}
|
|
12448
|
+
async function syncMcpServers(bin, servers, runFn = defaultMcpRunFn, timeoutMs = 30000) {
|
|
12449
|
+
const added = [];
|
|
12450
|
+
for (const s of servers) {
|
|
12451
|
+
let r;
|
|
12452
|
+
try {
|
|
12453
|
+
r = await runFn(bin, mcpServerToAgyAddArgs(s), { timeoutMs });
|
|
12454
|
+
} catch (err) {
|
|
12455
|
+
throw new RequestError(-32603, `failed to register MCP server '${s.name}': ${err?.message || err}`);
|
|
12456
|
+
}
|
|
12457
|
+
if (r.status !== 0) {
|
|
12458
|
+
throw new RequestError(-32603, `failed to register MCP server '${s.name}' (exit ${r.status}): ${tail(`${r.stdout}
|
|
12459
|
+
${r.stderr}`)}`);
|
|
12460
|
+
}
|
|
12461
|
+
added.push(s.name);
|
|
12462
|
+
}
|
|
12463
|
+
return { added };
|
|
12464
|
+
}
|
|
12465
|
+
async function removeMcpServers(bin, names, runFn = defaultMcpRunFn, timeoutMs = 30000) {
|
|
12466
|
+
const removed = [];
|
|
12467
|
+
const warnings = [];
|
|
12468
|
+
for (const name of names) {
|
|
12469
|
+
try {
|
|
12470
|
+
const r = await runFn(bin, ["mcp", "remove", name], { timeoutMs });
|
|
12471
|
+
if (r.status !== 0) {
|
|
12472
|
+
warnings.push(`mcp remove '${name}' exit ${r.status}: ${tail(`${r.stdout}
|
|
12473
|
+
${r.stderr}`, 200)}`);
|
|
12474
|
+
} else {
|
|
12475
|
+
removed.push(name);
|
|
12476
|
+
}
|
|
12477
|
+
} catch (err) {
|
|
12478
|
+
warnings.push(`mcp remove '${name}' threw: ${err?.message || err}`);
|
|
12479
|
+
}
|
|
12480
|
+
}
|
|
12481
|
+
if (warnings.length) {
|
|
12482
|
+
console.warn(`[ACP-MCP] cleanup warnings: ${warnings.join(" | ")}`);
|
|
12483
|
+
}
|
|
12484
|
+
return { removed, warnings };
|
|
12485
|
+
}
|
|
12486
|
+
|
|
12329
12487
|
// src/core/types.ts
|
|
12330
12488
|
var AGENT_INFO = {
|
|
12331
12489
|
name: "agy-acp",
|
|
12332
12490
|
title: "agy ACP (stream-json)",
|
|
12333
|
-
version: "0.1.
|
|
12491
|
+
version: "0.1.16"
|
|
12334
12492
|
};
|
|
12335
12493
|
var BRIDGE_CAPABILITIES = {
|
|
12336
12494
|
prompt: true,
|
|
@@ -12451,6 +12609,17 @@ function installFileLogging() {
|
|
|
12451
12609
|
|
|
12452
12610
|
// src/core/session-core.ts
|
|
12453
12611
|
var EMPTY_SESSION_MAX_AGE_MS = 60 * 60 * 1000;
|
|
12612
|
+
function resolveAgyBin() {
|
|
12613
|
+
const rawBin = process.env.AGY_BIN;
|
|
12614
|
+
let bin = rawBin && rawBin !== "undefined" && rawBin !== "null" ? rawBin : "agy";
|
|
12615
|
+
if (bin === "agy" || bin === "agy.exe") {
|
|
12616
|
+
const geminiBin = path10.join(process.env.USERPROFILE || process.env.HOME || "", ".gemini", "bin", process.platform === "win32" ? "agy.exe" : "agy");
|
|
12617
|
+
if (fs9.existsSync(geminiBin)) {
|
|
12618
|
+
bin = geminiBin;
|
|
12619
|
+
}
|
|
12620
|
+
}
|
|
12621
|
+
return bin;
|
|
12622
|
+
}
|
|
12454
12623
|
function deriveTitle(text, maxLen = 60) {
|
|
12455
12624
|
const line = String(text || "").split(/\r?\n/).map((s) => s.trim()).find(Boolean) || "";
|
|
12456
12625
|
const flat = line.replace(/\s+/g, " ");
|
|
@@ -12462,7 +12631,10 @@ class AgySessionCore {
|
|
|
12462
12631
|
sessionStore;
|
|
12463
12632
|
historyStore;
|
|
12464
12633
|
catalogPromise = null;
|
|
12634
|
+
mcpRunner;
|
|
12635
|
+
mcpRefs = new Map;
|
|
12465
12636
|
constructor(options) {
|
|
12637
|
+
this.mcpRunner = options?.mcpRunner ?? defaultMcpRunFn;
|
|
12466
12638
|
if (options?.sessionStore instanceof SessionStore) {
|
|
12467
12639
|
this.sessionStore = options.sessionStore;
|
|
12468
12640
|
} else if (typeof options?.sessionStore === "string") {
|
|
@@ -12511,14 +12683,8 @@ class AgySessionCore {
|
|
|
12511
12683
|
disableSlashCommands: disableSlash,
|
|
12512
12684
|
printTimeout
|
|
12513
12685
|
});
|
|
12514
|
-
const rawBin =
|
|
12515
|
-
|
|
12516
|
-
if (bin === "agy" || bin === "agy.exe") {
|
|
12517
|
-
const geminiBin = path10.join(process.env.USERPROFILE || process.env.HOME || "", ".gemini", "bin", process.platform === "win32" ? "agy.exe" : "agy");
|
|
12518
|
-
if (fs9.existsSync(geminiBin)) {
|
|
12519
|
-
bin = geminiBin;
|
|
12520
|
-
}
|
|
12521
|
-
}
|
|
12686
|
+
const rawBin = resolveAgyBin();
|
|
12687
|
+
const bin = rawBin;
|
|
12522
12688
|
let execBin = bin;
|
|
12523
12689
|
let execArgs = args;
|
|
12524
12690
|
if (/\.(js|cjs|mjs|ts)$/i.test(bin)) {
|
|
@@ -12592,7 +12758,8 @@ class AgySessionCore {
|
|
|
12592
12758
|
}
|
|
12593
12759
|
persistTurnHistory(sessionId, userText, assistantText, stopReason, eligible = true) {
|
|
12594
12760
|
try {
|
|
12595
|
-
|
|
12761
|
+
const partial = stopReason === "cancelled" || !eligible;
|
|
12762
|
+
this.historyStore.appendTurn(sessionId, userText, assistantText, partial ? { partial: true } : undefined);
|
|
12596
12763
|
} catch (err) {
|
|
12597
12764
|
console.warn(`[ACP-HISTORY] Warning: failed to persist turn ${sessionId}:`, err);
|
|
12598
12765
|
}
|
|
@@ -12648,6 +12815,43 @@ class AgySessionCore {
|
|
|
12648
12815
|
if (!session.agent && agents.length)
|
|
12649
12816
|
session.agent = agents[0].value;
|
|
12650
12817
|
}
|
|
12818
|
+
async applySessionMcpServers(sessionId, input) {
|
|
12819
|
+
const servers = validateMcpServers(input ?? []);
|
|
12820
|
+
if (!servers.length)
|
|
12821
|
+
return [];
|
|
12822
|
+
const live = this.sessions.get(sessionId);
|
|
12823
|
+
const hadLiveProc = live ? live.proc.isWritable() : false;
|
|
12824
|
+
await syncMcpServers(resolveAgyBin(), servers, this.mcpRunner);
|
|
12825
|
+
const names = servers.map((s) => ({ name: s.name }));
|
|
12826
|
+
if (live)
|
|
12827
|
+
live.mcpServers = names;
|
|
12828
|
+
this.trackMcpServers(sessionId, names.map((n) => n.name));
|
|
12829
|
+
if (hadLiveProc) {
|
|
12830
|
+
console.warn(`[ACP-MCP] session ${sessionId}: MCP servers [${names.map((n) => n.name).join(", ")}] ` + `registered while the session process is live; they apply to fresh spawns (reconnect to use them).`);
|
|
12831
|
+
}
|
|
12832
|
+
return names;
|
|
12833
|
+
}
|
|
12834
|
+
trackMcpServers(sessionId, names) {
|
|
12835
|
+
for (const name of names) {
|
|
12836
|
+
let set = this.mcpRefs.get(name);
|
|
12837
|
+
if (!set) {
|
|
12838
|
+
set = new Set;
|
|
12839
|
+
this.mcpRefs.set(name, set);
|
|
12840
|
+
}
|
|
12841
|
+
set.add(sessionId);
|
|
12842
|
+
}
|
|
12843
|
+
}
|
|
12844
|
+
untrackMcpServers(sessionId) {
|
|
12845
|
+
const freed = [];
|
|
12846
|
+
for (const [name, set] of this.mcpRefs) {
|
|
12847
|
+
set.delete(sessionId);
|
|
12848
|
+
if (set.size === 0) {
|
|
12849
|
+
this.mcpRefs.delete(name);
|
|
12850
|
+
freed.push(name);
|
|
12851
|
+
}
|
|
12852
|
+
}
|
|
12853
|
+
return freed;
|
|
12854
|
+
}
|
|
12651
12855
|
async createSession(params, protocolVersion = 1) {
|
|
12652
12856
|
const cwd = params?.cwd;
|
|
12653
12857
|
debugLog(`createSession v${protocolVersion} cwd=${cwd}`);
|
|
@@ -12670,6 +12874,7 @@ class AgySessionCore {
|
|
|
12670
12874
|
if (params?.mcpServers !== undefined && !Array.isArray(params.mcpServers)) {
|
|
12671
12875
|
throw new RequestError(-32602, "mcpServers must be an array");
|
|
12672
12876
|
}
|
|
12877
|
+
validateMcpServers(params?.mcpServers);
|
|
12673
12878
|
const launch = extractLaunchConfig(params);
|
|
12674
12879
|
const discovery = await this.getDiscovery();
|
|
12675
12880
|
const sessionId = randomUUID3();
|
|
@@ -12712,6 +12917,7 @@ class AgySessionCore {
|
|
|
12712
12917
|
};
|
|
12713
12918
|
this.applyCatalogDefaults(session, discovery);
|
|
12714
12919
|
this.sessions.set(sessionId, session);
|
|
12920
|
+
await this.applySessionMcpServers(sessionId, params?.mcpServers);
|
|
12715
12921
|
this.persistSession(session);
|
|
12716
12922
|
this.warmupSession(sessionId);
|
|
12717
12923
|
return {
|
|
@@ -12740,14 +12946,10 @@ class AgySessionCore {
|
|
|
12740
12946
|
throw new RequestError(-32602, protocolVersion === 2 ? 'only replayFrom.type="start" is supported by this agent' : "session/resume with replayFrom is only supported by ACP v2");
|
|
12741
12947
|
}
|
|
12742
12948
|
}
|
|
12743
|
-
if (params?.mcpServers !== undefined) {
|
|
12744
|
-
|
|
12745
|
-
throw new RequestError(-32602, "mcpServers must be an array");
|
|
12746
|
-
}
|
|
12747
|
-
if (params.mcpServers.length > 0) {
|
|
12748
|
-
throw new RequestError(-32602, "mcpServers are not supported by this agent");
|
|
12749
|
-
}
|
|
12949
|
+
if (params?.mcpServers !== undefined && !Array.isArray(params.mcpServers)) {
|
|
12950
|
+
throw new RequestError(-32602, "mcpServers must be an array");
|
|
12750
12951
|
}
|
|
12952
|
+
validateMcpServers(params?.mcpServers);
|
|
12751
12953
|
let additionalDirectories = [];
|
|
12752
12954
|
if (params?.additionalDirectories !== undefined) {
|
|
12753
12955
|
if (!Array.isArray(params.additionalDirectories)) {
|
|
@@ -12822,6 +13024,7 @@ class AgySessionCore {
|
|
|
12822
13024
|
}
|
|
12823
13025
|
const discovery = await this.getDiscovery();
|
|
12824
13026
|
this.applyCatalogDefaults(session, discovery);
|
|
13027
|
+
await this.applySessionMcpServers(sessionId, params?.mcpServers);
|
|
12825
13028
|
if (!session.title) {
|
|
12826
13029
|
try {
|
|
12827
13030
|
const first = this.historyStore.firstUserText(sessionId);
|
|
@@ -12998,6 +13201,14 @@ class AgySessionCore {
|
|
|
12998
13201
|
}
|
|
12999
13202
|
this.sessionStore.delete(sessionId);
|
|
13000
13203
|
this.historyStore.delete(sessionId);
|
|
13204
|
+
try {
|
|
13205
|
+
const freed = this.untrackMcpServers(sessionId);
|
|
13206
|
+
if (freed.length) {
|
|
13207
|
+
await removeMcpServers(resolveAgyBin(), freed, this.mcpRunner);
|
|
13208
|
+
}
|
|
13209
|
+
} catch (err) {
|
|
13210
|
+
console.warn(`[ACP-MCP] cleanup after delete ${sessionId} failed: ${err?.message || err}`);
|
|
13211
|
+
}
|
|
13001
13212
|
return {};
|
|
13002
13213
|
}
|
|
13003
13214
|
async closeSession(params) {
|
|
@@ -17667,7 +17878,8 @@ __export(exports_core3, {
|
|
|
17667
17878
|
BRIDGE_CAPABILITIES: () => BRIDGE_CAPABILITIES,
|
|
17668
17879
|
EMPTY_SESSION_MAX_AGE_MS: () => EMPTY_SESSION_MAX_AGE_MS,
|
|
17669
17880
|
catalogChoices: () => catalogChoices,
|
|
17670
|
-
deriveTitle: () => deriveTitle
|
|
17881
|
+
deriveTitle: () => deriveTitle,
|
|
17882
|
+
resolveAgyBin: () => resolveAgyBin
|
|
17671
17883
|
});
|
|
17672
17884
|
// src/v1/index.ts
|
|
17673
17885
|
var exports_v1 = {};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACP `mcpServers` → `agy mcp add/remove` bridging.
|
|
3
|
+
*
|
|
4
|
+
* The bridge owns MCP registration end-to-end:
|
|
5
|
+
* - `session/new|resume` accept the standard ACP `mcpServers` list and sync
|
|
6
|
+
* it into agy's MCP config (`~/.gemini/config/mcp_config.json`) via
|
|
7
|
+
* `agy mcp add` BEFORE the session process spawns, so tools are listed
|
|
8
|
+
* from the first turn.
|
|
9
|
+
* - `session/delete` removes the servers this session registered (refcounted
|
|
10
|
+
* per bridge process; best-effort, never breaks close/delete).
|
|
11
|
+
* - Permissions stay under the bridge safety policy: MCP tool calls run with
|
|
12
|
+
* the session's `--dangerously-skip-permissions`/`--sandbox` flags, and
|
|
13
|
+
* headless soft-denies still surface `permissions.allow` guidance.
|
|
14
|
+
*
|
|
15
|
+
* Supported ACP shapes: stdio `{name, command, args, env[]}` and http
|
|
16
|
+
* `{name, url, headers[], type:"http"}`. `sse`/`acp` transports have no
|
|
17
|
+
* `agy mcp add` equivalent and fail fast (no silent downgrade).
|
|
18
|
+
*/
|
|
19
|
+
export interface AcpMcpServerInput {
|
|
20
|
+
name?: unknown;
|
|
21
|
+
type?: unknown;
|
|
22
|
+
command?: unknown;
|
|
23
|
+
args?: unknown;
|
|
24
|
+
env?: unknown;
|
|
25
|
+
url?: unknown;
|
|
26
|
+
headers?: unknown;
|
|
27
|
+
[k: string]: unknown;
|
|
28
|
+
}
|
|
29
|
+
export interface NormalizedMcpServer {
|
|
30
|
+
name: string;
|
|
31
|
+
kind: 'stdio' | 'http';
|
|
32
|
+
command?: string;
|
|
33
|
+
args: string[];
|
|
34
|
+
env: Array<{
|
|
35
|
+
name: string;
|
|
36
|
+
value: string;
|
|
37
|
+
}>;
|
|
38
|
+
url?: string;
|
|
39
|
+
headers: Array<{
|
|
40
|
+
name: string;
|
|
41
|
+
value: string;
|
|
42
|
+
}>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Validate one ACP McpServer entry. Throws RequestError(-32602) — loud,
|
|
46
|
+
* so a client that sent an unusable server never gets a silent no-tools
|
|
47
|
+
* session.
|
|
48
|
+
*/
|
|
49
|
+
export declare function normalizeMcpServer(input: unknown): NormalizedMcpServer;
|
|
50
|
+
/** Validate a full session/new|resume mcpServers list (non-array → loud). */
|
|
51
|
+
export declare function validateMcpServers(input: unknown): NormalizedMcpServer[];
|
|
52
|
+
/**
|
|
53
|
+
* Build `agy mcp add` argv. agy rejects flags placed after <name>, so all
|
|
54
|
+
* flags come first: mcp add [--env K=V] [--header K:V] [--type t] <name>
|
|
55
|
+
* <commandOrUrl> [args...].
|
|
56
|
+
*/
|
|
57
|
+
export declare function mcpServerToAgyAddArgs(s: NormalizedMcpServer): string[];
|
|
58
|
+
export interface McpRunResult {
|
|
59
|
+
status: number | null;
|
|
60
|
+
stdout: string;
|
|
61
|
+
stderr: string;
|
|
62
|
+
}
|
|
63
|
+
/** Injectable `agy mcp ...` runner (tests stub it; prod spawns agy). */
|
|
64
|
+
export type McpRunFn = (bin: string, args: string[], opts: {
|
|
65
|
+
timeoutMs: number;
|
|
66
|
+
}) => Promise<McpRunResult>;
|
|
67
|
+
/** Production runner: real `agy mcp ...` subprocess. */
|
|
68
|
+
export declare const defaultMcpRunFn: McpRunFn;
|
|
69
|
+
/**
|
|
70
|
+
* Register every server via `agy mcp add` (idempotent: add == upsert).
|
|
71
|
+
* Any failure throws RequestError — a client that asked for MCP must never
|
|
72
|
+
* get a silent tools-less session.
|
|
73
|
+
*/
|
|
74
|
+
export declare function syncMcpServers(bin: string, servers: NormalizedMcpServer[], runFn?: McpRunFn, timeoutMs?: number): Promise<{
|
|
75
|
+
added: string[];
|
|
76
|
+
}>;
|
|
77
|
+
/**
|
|
78
|
+
* Remove servers by name. Best-effort by design: returns per-name warnings
|
|
79
|
+
* instead of throwing, so session close/delete can never break on cleanup.
|
|
80
|
+
*/
|
|
81
|
+
export declare function removeMcpServers(bin: string, names: string[], runFn?: McpRunFn, timeoutMs?: number): Promise<{
|
|
82
|
+
removed: string[];
|
|
83
|
+
warnings: string[];
|
|
84
|
+
}>;
|
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
* only stores the text that an ACP client can display: user prompts and final
|
|
6
6
|
* assistant text. Tool calls, tool output, thoughts, and internal events are
|
|
7
7
|
* deliberately excluded.
|
|
8
|
+
*
|
|
9
|
+
* Display/persist parity: whatever text the client actually saw must be
|
|
10
|
+
* reloadable. Interrupted turns (cancelled / failed) still persist their
|
|
11
|
+
* partial text, marked with `partial: true`, so `session/load` replay shows
|
|
12
|
+
* exactly what was on screen instead of dropping the turn entirely.
|
|
8
13
|
*/
|
|
9
14
|
export type SessionHistoryRole = 'user' | 'assistant';
|
|
10
15
|
export type SessionHistoryRecord = {
|
|
@@ -14,6 +19,12 @@ export type SessionHistoryRecord = {
|
|
|
14
19
|
role: SessionHistoryRole;
|
|
15
20
|
text: string;
|
|
16
21
|
createdAt: string;
|
|
22
|
+
/**
|
|
23
|
+
* True when the turn did not complete (cancelled / failed) and `text` is
|
|
24
|
+
* only the partial output the client saw. Replay ignores the flag and
|
|
25
|
+
* returns the text; readers must tolerate its absence (old journals).
|
|
26
|
+
*/
|
|
27
|
+
partial?: boolean;
|
|
17
28
|
};
|
|
18
29
|
export declare function resolveHistoryDir(sessionStorePath?: string, env?: NodeJS.ProcessEnv, homedir?: () => string): string;
|
|
19
30
|
export declare class SessionHistoryStore {
|
|
@@ -21,12 +32,16 @@ export declare class SessionHistoryStore {
|
|
|
21
32
|
constructor(directory?: string);
|
|
22
33
|
filePath(sessionId: string): string;
|
|
23
34
|
/**
|
|
24
|
-
* Append one completed turn as
|
|
35
|
+
* Append one completed (or interrupted) turn as JSONL records.
|
|
25
36
|
*
|
|
26
|
-
* The assistant record is
|
|
27
|
-
*
|
|
37
|
+
* The assistant record is written whenever the turn produced visible text —
|
|
38
|
+
* including cancelled/failed turns (`partial: true`). Only a truly empty
|
|
39
|
+
* assistant answer is omitted, so cancelled/error-only turns never become
|
|
40
|
+
* fake answers, while partial output stays reloadable.
|
|
28
41
|
*/
|
|
29
|
-
appendTurn(sessionId: string, userText: string, assistantText?: string
|
|
42
|
+
appendTurn(sessionId: string, userText: string, assistantText?: string, opts?: {
|
|
43
|
+
partial?: boolean;
|
|
44
|
+
}): void;
|
|
30
45
|
/** Read valid records and ignore incomplete/corrupt trailing lines. */
|
|
31
46
|
read(sessionId: string): SessionHistoryRecord[];
|
|
32
47
|
/**
|