@agentproto/runtime 5.2.1 → 5.3.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.
@@ -91,12 +91,63 @@ interface DiscoveredMcp {
91
91
 
92
92
  declare const IMPORTED_MCPS_PATH: () => string;
93
93
  declare const IMPORTED_MCPS_VERSION: 1;
94
+ /** Where an import came from. `plugin` (written by plugin-local-browser) and
95
+ * `manual` have no discovery source; `kind` otherwise mirrors
96
+ * `DiscoveredMcp["source"]`. */
97
+ interface ImportedMcpOrigin {
98
+ kind: DiscoveredMcp["source"] | "plugin" | "manual";
99
+ scope: string;
100
+ name: string;
101
+ }
102
+ /** `live` = re-read the source harness config at connect time (the snapshot is
103
+ * only a fallback); `snapshot` = the stored copy is authoritative. */
104
+ type ImportedMcpResolve = "live" | "snapshot";
105
+ /** Opaque secret refs (resolved via `resolveMcpSecret`), keyed by the header
106
+ * / env key they fill. Values are refs, NEVER secret material. */
107
+ interface ImportedMcpSecretRefs {
108
+ headers?: Record<string, string>;
109
+ env?: Record<string, string>;
110
+ }
94
111
  interface ImportedMcpEntry {
95
112
  id: string;
96
113
  alias: string;
97
114
  addedAt: string;
98
115
  snapshot: DiscoveredMcp;
116
+ /** Additive (no file-version bump): link back to the source config. */
117
+ origin?: ImportedMcpOrigin;
118
+ resolve?: ImportedMcpResolve;
119
+ secretRefs?: ImportedMcpSecretRefs;
120
+ }
121
+ /** Value written into `snapshot.headers[K]` / `snapshot.env[K]` in place of a
122
+ * literal secret that now lives behind `secretRefs`. */
123
+ declare const SECRET_REF_MARKER = "<secretRef>";
124
+ /** `live` only when discovery can actually re-find the entry: a discovery
125
+ * source AND a scope discovery scans (`global` / `project:*` / `workspace:*`).
126
+ * claude-code `local`-scope and plugin/manual entries stay `snapshot`. */
127
+ declare function deriveResolve(snapshot: DiscoveredMcp): ImportedMcpResolve;
128
+ /** Keychain path + account for one secret slot of an import. Ref format
129
+ * `agentproto/mcp-import/<importId>#<header|env>:<KEY>` (split on LAST `#`). */
130
+ declare function mcpSecretRef(importId: string, kind: "header" | "env", key: string): string;
131
+ /** A value worth moving to the keychain: non-empty, not already the marker,
132
+ * and not a `${VAR}` placeholder (those expand at connect, no secret at rest). */
133
+ declare function isLiteralSecretValue(v: unknown): v is string;
134
+ interface MoveSecretsResult {
135
+ entry: ImportedMcpEntry;
136
+ /** Header/env KEY names moved this call (never values). */
137
+ moved: string[];
138
+ /** Human warnings (key names only). */
139
+ warnings: string[];
99
140
  }
141
+ /**
142
+ * Move every literal header/env value of `entry.snapshot` behind a keychain
143
+ * ref. Keychain write happens FIRST per key; only on success is the snapshot
144
+ * value replaced by the marker (a failed write keeps the literal + warns).
145
+ * Read-back verification when `resolve` is supplied.
146
+ */
147
+ declare function moveEntrySecrets(entry: ImportedMcpEntry, hooks: {
148
+ storeMcpSecret?: (ref: string, value: string) => Promise<void>;
149
+ resolveMcpSecret?: (ref: string) => Promise<string | undefined>;
150
+ }): Promise<MoveSecretsResult>;
100
151
  interface ImportedMcpsConfig {
101
152
  version: typeof IMPORTED_MCPS_VERSION;
102
153
  imports: ImportedMcpEntry[];
@@ -112,7 +163,28 @@ declare function addImport(config: ImportedMcpsConfig, input: {
112
163
  snapshot: DiscoveredMcp;
113
164
  alias?: string;
114
165
  }): ImportedMcpsConfig;
166
+ /**
167
+ * `addImport` + move literal header/env values behind keychain refs (via the
168
+ * `storeMcpSecret` hook). Hook absent → literals kept and a warning returned.
169
+ * Callers persist `config` and surface `warnings` in their reply.
170
+ */
171
+ declare function addImportWithSecrets(config: ImportedMcpsConfig, input: {
172
+ snapshot: DiscoveredMcp;
173
+ alias?: string;
174
+ }, hooks: {
175
+ storeMcpSecret?: (ref: string, value: string) => Promise<void>;
176
+ resolveMcpSecret?: (ref: string) => Promise<string | undefined>;
177
+ }): Promise<{
178
+ config: ImportedMcpsConfig;
179
+ entry: ImportedMcpEntry;
180
+ warnings: string[];
181
+ }>;
115
182
  declare function removeImport(config: ImportedMcpsConfig, id: string): ImportedMcpsConfig;
116
183
  declare function findImport(config: ImportedMcpsConfig, id: string): ImportedMcpEntry | undefined;
184
+ /** KEY names only (never refs' targets or values) — for status surfaces. */
185
+ declare function secretRefKeys(entry: Pick<ImportedMcpEntry, "secretRefs">): {
186
+ headers?: string[];
187
+ env?: string[];
188
+ } | undefined;
117
189
 
118
- export { IMPORTED_MCPS_PATH, IMPORTED_MCPS_VERSION, type ImportedMcpEntry, type ImportedMcpsConfig, addImport, findImport, loadImportedMcps, removeImport, saveImportedMcps };
190
+ export { IMPORTED_MCPS_PATH, IMPORTED_MCPS_VERSION, type ImportedMcpEntry, type ImportedMcpOrigin, type ImportedMcpResolve, type ImportedMcpSecretRefs, type ImportedMcpsConfig, type MoveSecretsResult, SECRET_REF_MARKER, addImport, addImportWithSecrets, deriveResolve, findImport, isLiteralSecretValue, loadImportedMcps, mcpSecretRef, moveEntrySecrets, removeImport, saveImportedMcps, secretRefKeys };
@@ -9,6 +9,86 @@ import { resolve, dirname } from 'path';
9
9
 
10
10
  var IMPORTED_MCPS_PATH = () => resolve(homedir(), ".agentproto", "imported-mcps.json");
11
11
  var IMPORTED_MCPS_VERSION = 1;
12
+ var SECRET_REF_MARKER = "<secretRef>";
13
+ var LIVE_SOURCES = /* @__PURE__ */ new Set([
14
+ "claude-code",
15
+ "cursor",
16
+ "codex",
17
+ "workspace"
18
+ ]);
19
+ function deriveResolve(snapshot) {
20
+ if (!LIVE_SOURCES.has(snapshot.source)) return "snapshot";
21
+ const s = snapshot.scope;
22
+ return s === "global" || s.startsWith("project:") || s.startsWith("workspace:") ? "live" : "snapshot";
23
+ }
24
+ function mcpSecretRef(importId, kind, key) {
25
+ return `agentproto/mcp-import/${importId}#${kind}:${key}`;
26
+ }
27
+ var PLACEHOLDER_RE = /\$\{[A-Z_][A-Z0-9_]*(?::-[^}]*)?\}/i;
28
+ function isLiteralSecretValue(v) {
29
+ return typeof v === "string" && v.length > 0 && v !== SECRET_REF_MARKER && !PLACEHOLDER_RE.test(v);
30
+ }
31
+ async function moveEntrySecrets(entry, hooks) {
32
+ if (!hooks.storeMcpSecret) {
33
+ const has = Object.values(entry.snapshot.headers ?? {}).some(isLiteralSecretValue) || Object.values(entry.snapshot.env ?? {}).some(isLiteralSecretValue);
34
+ return {
35
+ entry,
36
+ moved: [],
37
+ warnings: has ? [
38
+ "no secret store available: header/env values were kept as literals in imported-mcps.json (mode 0600)"
39
+ ] : []
40
+ };
41
+ }
42
+ const moved = [];
43
+ const warnings = [];
44
+ const secretRefs = {
45
+ ...entry.secretRefs?.headers ? { headers: { ...entry.secretRefs.headers } } : {},
46
+ ...entry.secretRefs?.env ? { env: { ...entry.secretRefs.env } } : {}
47
+ };
48
+ const headers = entry.snapshot.headers ? { ...entry.snapshot.headers } : void 0;
49
+ const env = entry.snapshot.env ? { ...entry.snapshot.env } : void 0;
50
+ const passes = [
51
+ ["header", headers],
52
+ ["env", env]
53
+ ];
54
+ for (const [kind, map] of passes) {
55
+ if (!map) continue;
56
+ for (const [key, value] of Object.entries(map)) {
57
+ if (!isLiteralSecretValue(value)) continue;
58
+ const ref = mcpSecretRef(entry.id, kind, key);
59
+ try {
60
+ await hooks.storeMcpSecret(ref, value);
61
+ if (hooks.resolveMcpSecret) {
62
+ const back = await hooks.resolveMcpSecret(ref);
63
+ if (back !== value) throw new Error("read-back mismatch");
64
+ }
65
+ } catch (err) {
66
+ warnings.push(
67
+ `could not store ${kind} "${key}" in the secret store (${err instanceof Error ? err.message : "error"}); kept as a literal`
68
+ );
69
+ continue;
70
+ }
71
+ map[key] = SECRET_REF_MARKER;
72
+ const bucket = kind === "header" ? "headers" : "env";
73
+ secretRefs[bucket] = { ...secretRefs[bucket] ?? {}, [key]: ref };
74
+ moved.push(`${kind}:${key}`);
75
+ }
76
+ }
77
+ if (moved.length === 0) return { entry, moved, warnings };
78
+ return {
79
+ entry: {
80
+ ...entry,
81
+ snapshot: {
82
+ ...entry.snapshot,
83
+ ...headers ? { headers } : {},
84
+ ...env ? { env } : {}
85
+ },
86
+ secretRefs
87
+ },
88
+ moved,
89
+ warnings
90
+ };
91
+ }
12
92
  var EMPTY = {
13
93
  version: IMPORTED_MCPS_VERSION,
14
94
  imports: []
@@ -34,7 +114,10 @@ async function loadImportedMcps(path = IMPORTED_MCPS_PATH()) {
34
114
  async function saveImportedMcps(config, path = IMPORTED_MCPS_PATH()) {
35
115
  await promises.mkdir(dirname(path), { recursive: true });
36
116
  const tmp = `${path}.tmp.${process.pid}`;
37
- await promises.writeFile(tmp, JSON.stringify(config, null, 2) + "\n", "utf8");
117
+ await promises.writeFile(tmp, JSON.stringify(config, null, 2) + "\n", {
118
+ encoding: "utf8",
119
+ mode: 384
120
+ });
38
121
  await promises.rename(tmp, path);
39
122
  }
40
123
  function addImport(config, input) {
@@ -47,11 +130,30 @@ function addImport(config, input) {
47
130
  id: input.snapshot.id,
48
131
  alias,
49
132
  addedAt,
50
- snapshot: input.snapshot
133
+ snapshot: input.snapshot,
134
+ origin: {
135
+ kind: input.snapshot.source,
136
+ scope: input.snapshot.scope,
137
+ name: input.snapshot.name
138
+ },
139
+ resolve: deriveResolve(input.snapshot)
51
140
  };
52
141
  const others = config.imports.filter((e) => e.id !== input.snapshot.id);
53
142
  return { ...config, imports: [...others, next] };
54
143
  }
144
+ async function addImportWithSecrets(config, input, hooks) {
145
+ const added = addImport(config, input);
146
+ const entry = added.imports.find((e) => e.id === input.snapshot.id);
147
+ const moved = await moveEntrySecrets(entry, hooks);
148
+ return {
149
+ config: {
150
+ ...added,
151
+ imports: added.imports.map((e) => e.id === entry.id ? moved.entry : e)
152
+ },
153
+ entry: moved.entry,
154
+ warnings: moved.warnings
155
+ };
156
+ }
55
157
  function removeImport(config, id) {
56
158
  return { ...config, imports: config.imports.filter((e) => e.id !== id) };
57
159
  }
@@ -71,12 +173,52 @@ function normalize(parsed) {
71
173
  const addedAt = typeof e.addedAt === "string" ? e.addedAt : (/* @__PURE__ */ new Date()).toISOString();
72
174
  const snapshot = e.snapshot;
73
175
  if (!id || !snapshot) continue;
74
- imports.push({ id, alias, addedAt, snapshot });
176
+ const extra = {};
177
+ const origin = parseOrigin(e.origin);
178
+ if (origin) extra.origin = origin;
179
+ if (e.resolve === "live" || e.resolve === "snapshot") extra.resolve = e.resolve;
180
+ const secretRefs = parseSecretRefs(e.secretRefs);
181
+ if (secretRefs) extra.secretRefs = secretRefs;
182
+ imports.push({ id, alias, addedAt, snapshot, ...extra });
75
183
  }
76
184
  }
77
185
  return { version: IMPORTED_MCPS_VERSION, imports };
78
186
  }
187
+ function parseOrigin(v) {
188
+ if (!v || typeof v !== "object") return void 0;
189
+ const o = v;
190
+ if (typeof o.kind !== "string" || typeof o.scope !== "string" || typeof o.name !== "string")
191
+ return void 0;
192
+ return { kind: o.kind, scope: o.scope, name: o.name };
193
+ }
194
+ function parseStringMap(v) {
195
+ if (!v || typeof v !== "object" || Array.isArray(v)) return void 0;
196
+ const out = {};
197
+ for (const [k, val] of Object.entries(v)) {
198
+ if (typeof val === "string") out[k] = val;
199
+ }
200
+ return Object.keys(out).length > 0 ? out : void 0;
201
+ }
202
+ function parseSecretRefs(v) {
203
+ if (!v || typeof v !== "object") return void 0;
204
+ const o = v;
205
+ const headers = parseStringMap(o.headers);
206
+ const env = parseStringMap(o.env);
207
+ if (!headers && !env) return void 0;
208
+ return { ...headers ? { headers } : {}, ...env ? { env } : {} };
209
+ }
210
+ function secretRefKeys(entry) {
211
+ const r = entry.secretRefs;
212
+ if (!r) return void 0;
213
+ const headers = r.headers ? Object.keys(r.headers) : [];
214
+ const env = r.env ? Object.keys(r.env) : [];
215
+ if (headers.length === 0 && env.length === 0) return void 0;
216
+ return {
217
+ ...headers.length ? { headers } : {},
218
+ ...env.length ? { env } : {}
219
+ };
220
+ }
79
221
 
80
- export { IMPORTED_MCPS_PATH, IMPORTED_MCPS_VERSION, addImport, findImport, loadImportedMcps, removeImport, saveImportedMcps };
222
+ export { IMPORTED_MCPS_PATH, IMPORTED_MCPS_VERSION, SECRET_REF_MARKER, addImport, addImportWithSecrets, deriveResolve, findImport, isLiteralSecretValue, loadImportedMcps, mcpSecretRef, moveEntrySecrets, removeImport, saveImportedMcps, secretRefKeys };
81
223
  //# sourceMappingURL=mcp-imports.mjs.map
82
224
  //# sourceMappingURL=mcp-imports.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/mcp-imports.ts"],"names":["resolvePath","fs"],"mappings":"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","file":"mcp-imports.mjs","sourcesContent":["/**\n * `~/.agentproto/imported-mcps.json` — the user's curated set of\n * discovered MCPs they want the daemon to know about. v1 is just\n * persistence + read-side; the actual MCP-proxy implementation\n * (where the daemon's /mcp endpoint aggregates the imported\n * servers' tools under namespace prefixes) is v2 work.\n *\n * Purpose: today, \"I see you have chrome-devtools in claude\" is\n * read-only. Once a user imports it, the operator agent can refer\n * to it (\"the user said it's enabled — call it\") and a future\n * proxy layer can actually expose it. We persist the choice now so\n * the data is in place when the proxy lands.\n *\n * File shape:\n * {\n * \"version\": 1,\n * \"imports\": [\n * {\n * \"id\": \"claude-code:project:/path:chrome-devtools\",\n * \"alias\": \"chrome-devtools\",\n * \"addedAt\": \"2026-05-10T...\",\n * \"snapshot\": { ...the DiscoveredMcp at import time }\n * }\n * ]\n * }\n *\n * Snapshotting at import time keeps the daemon resilient to the\n * source config getting deleted (user runs `claude mcp remove ...`).\n * The imported entry stays usable; only the next discovery pass\n * stops listing it as \"available to import.\"\n */\n\nimport { promises as fs } from \"node:fs\"\nimport { homedir } from \"node:os\"\nimport { dirname, resolve as resolvePath } from \"node:path\"\nimport type { DiscoveredMcp } from \"./mcp-discovery.js\"\n\nexport const IMPORTED_MCPS_PATH = (): string =>\n resolvePath(homedir(), \".agentproto\", \"imported-mcps.json\")\n\nexport const IMPORTED_MCPS_VERSION = 1 as const\n\nexport interface ImportedMcpEntry {\n id: string\n alias: string\n addedAt: string\n snapshot: DiscoveredMcp\n}\n\nexport interface ImportedMcpsConfig {\n version: typeof IMPORTED_MCPS_VERSION\n imports: ImportedMcpEntry[]\n}\n\nconst EMPTY: ImportedMcpsConfig = {\n version: IMPORTED_MCPS_VERSION,\n imports: [],\n}\n\nexport async function loadImportedMcps(\n path: string = IMPORTED_MCPS_PATH()\n): Promise<ImportedMcpsConfig> {\n let raw: string\n try {\n raw = await fs.readFile(path, \"utf8\")\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return EMPTY\n throw err\n }\n let parsed: unknown\n try {\n parsed = JSON.parse(raw)\n } catch (err) {\n throw new Error(\n `agentproto: ${path} is not valid JSON (${\n err instanceof Error ? err.message : String(err)\n }). Delete or fix manually.`\n )\n }\n return normalize(parsed)\n}\n\nexport async function saveImportedMcps(\n config: ImportedMcpsConfig,\n path: string = IMPORTED_MCPS_PATH()\n): Promise<void> {\n await fs.mkdir(dirname(path), { recursive: true })\n const tmp = `${path}.tmp.${process.pid}`\n await fs.writeFile(tmp, JSON.stringify(config, null, 2) + \"\\n\", \"utf8\")\n await fs.rename(tmp, path)\n}\n\n/**\n * Add (or replace) an import. Snapshots the discovered MCP at\n * import time so the entry stays usable when the source config\n * disappears.\n */\nexport function addImport(\n config: ImportedMcpsConfig,\n input: { snapshot: DiscoveredMcp; alias?: string }\n): ImportedMcpsConfig {\n const alias = (input.alias ?? input.snapshot.name).trim()\n if (!alias) {\n throw new Error(\"addImport: alias resolves to empty string\")\n }\n const addedAt = new Date().toISOString()\n const next: ImportedMcpEntry = {\n id: input.snapshot.id,\n alias,\n addedAt,\n snapshot: input.snapshot,\n }\n const others = config.imports.filter(e => e.id !== input.snapshot.id)\n return { ...config, imports: [...others, next] }\n}\n\nexport function removeImport(\n config: ImportedMcpsConfig,\n id: string\n): ImportedMcpsConfig {\n return { ...config, imports: config.imports.filter(e => e.id !== id) }\n}\n\nexport function findImport(\n config: ImportedMcpsConfig,\n id: string\n): ImportedMcpEntry | undefined {\n return config.imports.find(e => e.id === id)\n}\n\nfunction normalize(parsed: unknown): ImportedMcpsConfig {\n if (!parsed || typeof parsed !== \"object\") return EMPTY\n const obj = parsed as Record<string, unknown>\n const imports: ImportedMcpEntry[] = []\n if (Array.isArray(obj.imports)) {\n for (const entry of obj.imports) {\n if (!entry || typeof entry !== \"object\") continue\n const e = entry as Record<string, unknown>\n const id = typeof e.id === \"string\" ? e.id : \"\"\n const alias = typeof e.alias === \"string\" ? e.alias : id\n const addedAt =\n typeof e.addedAt === \"string\" ? e.addedAt : new Date().toISOString()\n const snapshot = e.snapshot as DiscoveredMcp | undefined\n if (!id || !snapshot) continue\n imports.push({ id, alias, addedAt, snapshot })\n }\n }\n return { version: IMPORTED_MCPS_VERSION, imports }\n}\n"]}
1
+ {"version":3,"sources":["../src/mcp-imports.ts"],"names":["resolvePath","fs"],"mappings":"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","file":"mcp-imports.mjs","sourcesContent":["/**\n * `~/.agentproto/imported-mcps.json` — the user's curated set of\n * discovered MCPs they want the daemon to know about. v1 is just\n * persistence + read-side; the actual MCP-proxy implementation\n * (where the daemon's /mcp endpoint aggregates the imported\n * servers' tools under namespace prefixes) is v2 work.\n *\n * Purpose: today, \"I see you have chrome-devtools in claude\" is\n * read-only. Once a user imports it, the operator agent can refer\n * to it (\"the user said it's enabled — call it\") and a future\n * proxy layer can actually expose it. We persist the choice now so\n * the data is in place when the proxy lands.\n *\n * File shape:\n * {\n * \"version\": 1,\n * \"imports\": [\n * {\n * \"id\": \"claude-code:project:/path:chrome-devtools\",\n * \"alias\": \"chrome-devtools\",\n * \"addedAt\": \"2026-05-10T...\",\n * \"snapshot\": { ...the DiscoveredMcp at import time }\n * }\n * ]\n * }\n *\n * Snapshotting at import time keeps the daemon resilient to the\n * source config getting deleted (user runs `claude mcp remove ...`).\n * The imported entry stays usable; only the next discovery pass\n * stops listing it as \"available to import.\"\n */\n\nimport { promises as fs } from \"node:fs\"\nimport { homedir } from \"node:os\"\nimport { dirname, resolve as resolvePath } from \"node:path\"\nimport type { DiscoveredMcp } from \"./mcp-discovery.js\"\n\nexport const IMPORTED_MCPS_PATH = (): string =>\n resolvePath(homedir(), \".agentproto\", \"imported-mcps.json\")\n\nexport const IMPORTED_MCPS_VERSION = 1 as const\n\n/** Where an import came from. `plugin` (written by plugin-local-browser) and\n * `manual` have no discovery source; `kind` otherwise mirrors\n * `DiscoveredMcp[\"source\"]`. */\nexport interface ImportedMcpOrigin {\n kind: DiscoveredMcp[\"source\"] | \"plugin\" | \"manual\"\n scope: string\n name: string\n}\n\n/** `live` = re-read the source harness config at connect time (the snapshot is\n * only a fallback); `snapshot` = the stored copy is authoritative. */\nexport type ImportedMcpResolve = \"live\" | \"snapshot\"\n\n/** Opaque secret refs (resolved via `resolveMcpSecret`), keyed by the header\n * / env key they fill. Values are refs, NEVER secret material. */\nexport interface ImportedMcpSecretRefs {\n headers?: Record<string, string>\n env?: Record<string, string>\n}\n\nexport interface ImportedMcpEntry {\n id: string\n alias: string\n addedAt: string\n snapshot: DiscoveredMcp\n /** Additive (no file-version bump): link back to the source config. */\n origin?: ImportedMcpOrigin\n resolve?: ImportedMcpResolve\n secretRefs?: ImportedMcpSecretRefs\n}\n\n/** Value written into `snapshot.headers[K]` / `snapshot.env[K]` in place of a\n * literal secret that now lives behind `secretRefs`. */\nexport const SECRET_REF_MARKER = \"<secretRef>\"\n\n/** Discovery sources whose config can be re-read live. */\nconst LIVE_SOURCES: ReadonlySet<string> = new Set([\n \"claude-code\",\n \"cursor\",\n \"codex\",\n \"workspace\",\n])\n\n/** `live` only when discovery can actually re-find the entry: a discovery\n * source AND a scope discovery scans (`global` / `project:*` / `workspace:*`).\n * claude-code `local`-scope and plugin/manual entries stay `snapshot`. */\nexport function deriveResolve(snapshot: DiscoveredMcp): ImportedMcpResolve {\n if (!LIVE_SOURCES.has(snapshot.source)) return \"snapshot\"\n const s = snapshot.scope\n return s === \"global\" || s.startsWith(\"project:\") || s.startsWith(\"workspace:\")\n ? \"live\"\n : \"snapshot\"\n}\n\n/** Keychain path + account for one secret slot of an import. Ref format\n * `agentproto/mcp-import/<importId>#<header|env>:<KEY>` (split on LAST `#`). */\nexport function mcpSecretRef(\n importId: string,\n kind: \"header\" | \"env\",\n key: string\n): string {\n return `agentproto/mcp-import/${importId}#${kind}:${key}`\n}\n\nconst PLACEHOLDER_RE = /\\$\\{[A-Z_][A-Z0-9_]*(?::-[^}]*)?\\}/i\n\n/** A value worth moving to the keychain: non-empty, not already the marker,\n * and not a `${VAR}` placeholder (those expand at connect, no secret at rest). */\nexport function isLiteralSecretValue(v: unknown): v is string {\n return (\n typeof v === \"string\" &&\n v.length > 0 &&\n v !== SECRET_REF_MARKER &&\n !PLACEHOLDER_RE.test(v)\n )\n}\n\nexport interface MoveSecretsResult {\n entry: ImportedMcpEntry\n /** Header/env KEY names moved this call (never values). */\n moved: string[]\n /** Human warnings (key names only). */\n warnings: string[]\n}\n\n/**\n * Move every literal header/env value of `entry.snapshot` behind a keychain\n * ref. Keychain write happens FIRST per key; only on success is the snapshot\n * value replaced by the marker (a failed write keeps the literal + warns).\n * Read-back verification when `resolve` is supplied.\n */\nexport async function moveEntrySecrets(\n entry: ImportedMcpEntry,\n hooks: {\n storeMcpSecret?: (ref: string, value: string) => Promise<void>\n resolveMcpSecret?: (ref: string) => Promise<string | undefined>\n }\n): Promise<MoveSecretsResult> {\n if (!hooks.storeMcpSecret) {\n const has =\n Object.values(entry.snapshot.headers ?? {}).some(isLiteralSecretValue) ||\n Object.values(entry.snapshot.env ?? {}).some(isLiteralSecretValue)\n return {\n entry,\n moved: [],\n warnings: has\n ? [\n \"no secret store available: header/env values were kept as literals in imported-mcps.json (mode 0600)\",\n ]\n : [],\n }\n }\n const moved: string[] = []\n const warnings: string[] = []\n const secretRefs: ImportedMcpSecretRefs = {\n ...(entry.secretRefs?.headers ? { headers: { ...entry.secretRefs.headers } } : {}),\n ...(entry.secretRefs?.env ? { env: { ...entry.secretRefs.env } } : {}),\n }\n const headers = entry.snapshot.headers ? { ...entry.snapshot.headers } : undefined\n const env = entry.snapshot.env ? { ...entry.snapshot.env } : undefined\n const passes: Array<[\"header\" | \"env\", Record<string, string> | undefined]> = [\n [\"header\", headers],\n [\"env\", env],\n ]\n for (const [kind, map] of passes) {\n if (!map) continue\n for (const [key, value] of Object.entries(map)) {\n if (!isLiteralSecretValue(value)) continue\n const ref = mcpSecretRef(entry.id, kind, key)\n try {\n await hooks.storeMcpSecret(ref, value)\n if (hooks.resolveMcpSecret) {\n const back = await hooks.resolveMcpSecret(ref)\n if (back !== value) throw new Error(\"read-back mismatch\")\n }\n } catch (err) {\n warnings.push(\n `could not store ${kind} \"${key}\" in the secret store (${\n err instanceof Error ? err.message : \"error\"\n }); kept as a literal`\n )\n continue\n }\n map[key] = SECRET_REF_MARKER\n const bucket = kind === \"header\" ? \"headers\" : \"env\"\n secretRefs[bucket] = { ...(secretRefs[bucket] ?? {}), [key]: ref }\n moved.push(`${kind}:${key}`)\n }\n }\n if (moved.length === 0) return { entry, moved, warnings }\n return {\n entry: {\n ...entry,\n snapshot: {\n ...entry.snapshot,\n ...(headers ? { headers } : {}),\n ...(env ? { env } : {}),\n },\n secretRefs,\n },\n moved,\n warnings,\n }\n}\n\nexport interface ImportedMcpsConfig {\n version: typeof IMPORTED_MCPS_VERSION\n imports: ImportedMcpEntry[]\n}\n\nconst EMPTY: ImportedMcpsConfig = {\n version: IMPORTED_MCPS_VERSION,\n imports: [],\n}\n\nexport async function loadImportedMcps(\n path: string = IMPORTED_MCPS_PATH()\n): Promise<ImportedMcpsConfig> {\n let raw: string\n try {\n raw = await fs.readFile(path, \"utf8\")\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return EMPTY\n throw err\n }\n let parsed: unknown\n try {\n parsed = JSON.parse(raw)\n } catch (err) {\n throw new Error(\n `agentproto: ${path} is not valid JSON (${\n err instanceof Error ? err.message : String(err)\n }). Delete or fix manually.`\n )\n }\n return normalize(parsed)\n}\n\nexport async function saveImportedMcps(\n config: ImportedMcpsConfig,\n path: string = IMPORTED_MCPS_PATH()\n): Promise<void> {\n await fs.mkdir(dirname(path), { recursive: true })\n const tmp = `${path}.tmp.${process.pid}`\n // 0600: entries may hold literal upstream credentials (parity with bundles.ts).\n await fs.writeFile(tmp, JSON.stringify(config, null, 2) + \"\\n\", {\n encoding: \"utf8\",\n mode: 0o600,\n })\n await fs.rename(tmp, path)\n}\n\n/**\n * Add (or replace) an import. Snapshots the discovered MCP at\n * import time so the entry stays usable when the source config\n * disappears.\n */\nexport function addImport(\n config: ImportedMcpsConfig,\n input: { snapshot: DiscoveredMcp; alias?: string }\n): ImportedMcpsConfig {\n const alias = (input.alias ?? input.snapshot.name).trim()\n if (!alias) {\n throw new Error(\"addImport: alias resolves to empty string\")\n }\n const addedAt = new Date().toISOString()\n const next: ImportedMcpEntry = {\n id: input.snapshot.id,\n alias,\n addedAt,\n snapshot: input.snapshot,\n origin: {\n kind: input.snapshot.source,\n scope: input.snapshot.scope,\n name: input.snapshot.name,\n },\n resolve: deriveResolve(input.snapshot),\n }\n const others = config.imports.filter(e => e.id !== input.snapshot.id)\n return { ...config, imports: [...others, next] }\n}\n\n/**\n * `addImport` + move literal header/env values behind keychain refs (via the\n * `storeMcpSecret` hook). Hook absent → literals kept and a warning returned.\n * Callers persist `config` and surface `warnings` in their reply.\n */\nexport async function addImportWithSecrets(\n config: ImportedMcpsConfig,\n input: { snapshot: DiscoveredMcp; alias?: string },\n hooks: {\n storeMcpSecret?: (ref: string, value: string) => Promise<void>\n resolveMcpSecret?: (ref: string) => Promise<string | undefined>\n }\n): Promise<{ config: ImportedMcpsConfig; entry: ImportedMcpEntry; warnings: string[] }> {\n const added = addImport(config, input)\n const entry = added.imports.find(e => e.id === input.snapshot.id)!\n const moved = await moveEntrySecrets(entry, hooks)\n return {\n config: {\n ...added,\n imports: added.imports.map(e => (e.id === entry.id ? moved.entry : e)),\n },\n entry: moved.entry,\n warnings: moved.warnings,\n }\n}\n\nexport function removeImport(\n config: ImportedMcpsConfig,\n id: string\n): ImportedMcpsConfig {\n return { ...config, imports: config.imports.filter(e => e.id !== id) }\n}\n\nexport function findImport(\n config: ImportedMcpsConfig,\n id: string\n): ImportedMcpEntry | undefined {\n return config.imports.find(e => e.id === id)\n}\n\nfunction normalize(parsed: unknown): ImportedMcpsConfig {\n if (!parsed || typeof parsed !== \"object\") return EMPTY\n const obj = parsed as Record<string, unknown>\n const imports: ImportedMcpEntry[] = []\n if (Array.isArray(obj.imports)) {\n for (const entry of obj.imports) {\n if (!entry || typeof entry !== \"object\") continue\n const e = entry as Record<string, unknown>\n const id = typeof e.id === \"string\" ? e.id : \"\"\n const alias = typeof e.alias === \"string\" ? e.alias : id\n const addedAt =\n typeof e.addedAt === \"string\" ? e.addedAt : new Date().toISOString()\n const snapshot = e.snapshot as DiscoveredMcp | undefined\n if (!id || !snapshot) continue\n const extra: Partial<ImportedMcpEntry> = {}\n const origin = parseOrigin(e.origin)\n if (origin) extra.origin = origin\n if (e.resolve === \"live\" || e.resolve === \"snapshot\") extra.resolve = e.resolve\n const secretRefs = parseSecretRefs(e.secretRefs)\n if (secretRefs) extra.secretRefs = secretRefs\n imports.push({ id, alias, addedAt, snapshot, ...extra })\n }\n }\n return { version: IMPORTED_MCPS_VERSION, imports }\n}\n\nfunction parseOrigin(v: unknown): ImportedMcpOrigin | undefined {\n if (!v || typeof v !== \"object\") return undefined\n const o = v as Record<string, unknown>\n if (\n typeof o.kind !== \"string\" ||\n typeof o.scope !== \"string\" ||\n typeof o.name !== \"string\"\n )\n return undefined\n return { kind: o.kind as ImportedMcpOrigin[\"kind\"], scope: o.scope, name: o.name }\n}\n\nfunction parseStringMap(v: unknown): Record<string, string> | undefined {\n if (!v || typeof v !== \"object\" || Array.isArray(v)) return undefined\n const out: Record<string, string> = {}\n for (const [k, val] of Object.entries(v as Record<string, unknown>)) {\n if (typeof val === \"string\") out[k] = val\n }\n return Object.keys(out).length > 0 ? out : undefined\n}\n\nfunction parseSecretRefs(v: unknown): ImportedMcpSecretRefs | undefined {\n if (!v || typeof v !== \"object\") return undefined\n const o = v as Record<string, unknown>\n const headers = parseStringMap(o.headers)\n const env = parseStringMap(o.env)\n if (!headers && !env) return undefined\n return { ...(headers ? { headers } : {}), ...(env ? { env } : {}) }\n}\n\n/** KEY names only (never refs' targets or values) — for status surfaces. */\nexport function secretRefKeys(\n entry: Pick<ImportedMcpEntry, \"secretRefs\">\n): { headers?: string[]; env?: string[] } | undefined {\n const r = entry.secretRefs\n if (!r) return undefined\n const headers = r.headers ? Object.keys(r.headers) : []\n const env = r.env ? Object.keys(r.env) : []\n if (headers.length === 0 && env.length === 0) return undefined\n return {\n ...(headers.length ? { headers } : {}),\n ...(env.length ? { env } : {}),\n }\n}\n"]}