@hasna/mcps 0.0.14 → 0.0.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/bin/mcp.js CHANGED
@@ -16673,6 +16673,7 @@ function getDb() {
16673
16673
  command TEXT NOT NULL,
16674
16674
  args TEXT NOT NULL DEFAULT '[]',
16675
16675
  env TEXT NOT NULL DEFAULT '{}',
16676
+ credential_refs TEXT NOT NULL DEFAULT '{}',
16676
16677
  transport TEXT NOT NULL DEFAULT 'stdio',
16677
16678
  url TEXT,
16678
16679
  source TEXT NOT NULL DEFAULT 'local',
@@ -16699,6 +16700,9 @@ function getDb() {
16699
16700
  try {
16700
16701
  db.exec("ALTER TABLE servers ADD COLUMN last_error TEXT");
16701
16702
  } catch {}
16703
+ try {
16704
+ db.exec("ALTER TABLE servers ADD COLUMN credential_refs TEXT NOT NULL DEFAULT '{}'");
16705
+ } catch {}
16702
16706
  db.exec(`
16703
16707
  CREATE TABLE IF NOT EXISTS sources (
16704
16708
  id TEXT PRIMARY KEY,
@@ -16824,17 +16828,17 @@ __export(exports_sources, {
16824
16828
  clearCache: () => clearCache,
16825
16829
  addSource: () => addSource
16826
16830
  });
16827
- import { mkdirSync as mkdirSync6, existsSync as existsSync7, readFileSync as readFileSync3, writeFileSync as writeFileSync2, readdirSync as readdirSync3, unlinkSync } from "fs";
16828
- import { join as join8 } from "path";
16831
+ import { mkdirSync as mkdirSync6, existsSync as existsSync8, readFileSync as readFileSync4, writeFileSync as writeFileSync2, readdirSync as readdirSync3, unlinkSync } from "fs";
16832
+ import { join as join9 } from "path";
16829
16833
  function getCacheFile(sourceId) {
16830
- return join8(CACHE_DIR, `${sourceId}.json`);
16834
+ return join9(CACHE_DIR, `${sourceId}.json`);
16831
16835
  }
16832
16836
  function readCache(sourceId) {
16833
16837
  try {
16834
16838
  const file = getCacheFile(sourceId);
16835
- if (!existsSync7(file))
16839
+ if (!existsSync8(file))
16836
16840
  return null;
16837
- const data = JSON.parse(readFileSync3(file, "utf-8"));
16841
+ const data = JSON.parse(readFileSync4(file, "utf-8"));
16838
16842
  return data;
16839
16843
  } catch {
16840
16844
  return null;
@@ -16848,7 +16852,7 @@ function writeCache(sourceId, results) {
16848
16852
  }
16849
16853
  function clearCache(sourceId) {
16850
16854
  try {
16851
- if (!existsSync7(CACHE_DIR))
16855
+ if (!existsSync8(CACHE_DIR))
16852
16856
  return;
16853
16857
  const files = readdirSync3(CACHE_DIR);
16854
16858
  for (const file of files) {
@@ -16856,7 +16860,7 @@ function clearCache(sourceId) {
16856
16860
  continue;
16857
16861
  if (!sourceId || file.startsWith(`${sourceId}.`)) {
16858
16862
  try {
16859
- unlinkSync(join8(CACHE_DIR, file));
16863
+ unlinkSync(join9(CACHE_DIR, file));
16860
16864
  } catch {}
16861
16865
  }
16862
16866
  }
@@ -17102,7 +17106,7 @@ var CACHE_DIR, DEFAULT_TTL_MS;
17102
17106
  var init_sources = __esm(() => {
17103
17107
  init_db();
17104
17108
  init_config2();
17105
- CACHE_DIR = join8(MCPS_DIR, "cache");
17109
+ CACHE_DIR = join9(MCPS_DIR, "cache");
17106
17110
  DEFAULT_TTL_MS = 10 * 60 * 1000;
17107
17111
  });
17108
17112
 
@@ -30660,6 +30664,146 @@ function readPackageVersion(moduleUrl, fallback = FALLBACK_VERSION) {
30660
30664
 
30661
30665
  // src/lib/registry.ts
30662
30666
  init_db();
30667
+
30668
+ // src/lib/credentials.ts
30669
+ init_config2();
30670
+ import { existsSync as existsSync7, readFileSync as readFileSync3 } from "fs";
30671
+ import { join as join8 } from "path";
30672
+
30673
+ class CredentialReferenceError extends Error {
30674
+ constructor(message) {
30675
+ super(message);
30676
+ this.name = "CredentialReferenceError";
30677
+ }
30678
+ }
30679
+ var SECRET_KEY_PATTERN = /(?:^|[_-])(api[_-]?key|token|secret|password|passwd|credential|auth|private[_-]?key)(?:$|[_-])/i;
30680
+ var SECRET_VALUE_PATTERN = /^(sk_(?:live|test)_[A-Za-z0-9_]+|ghp_[A-Za-z0-9_]+|github_pat_[A-Za-z0-9_]+|xox[baprs]-[A-Za-z0-9-]+|AIza[A-Za-z0-9_-]{20,}|eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)$/;
30681
+ var REDACTED_CREDENTIAL_VALUE = "<redacted>";
30682
+ function normalizeKey(key) {
30683
+ return key.trim();
30684
+ }
30685
+ function isRecord(value) {
30686
+ return typeof value === "object" && value !== null && !Array.isArray(value);
30687
+ }
30688
+ function isSecretLikeEnvKey(key) {
30689
+ return SECRET_KEY_PATTERN.test(key);
30690
+ }
30691
+ function isSecretLikeValue(value) {
30692
+ return SECRET_VALUE_PATTERN.test(value.trim());
30693
+ }
30694
+ function normalizeCredentialRef(ref) {
30695
+ const source = ref.source;
30696
+ if (source !== "env" && source !== "local-vault" && source !== "hosted") {
30697
+ throw new CredentialReferenceError(`Unsupported credential reference source: ${String(source)}`);
30698
+ }
30699
+ const name = ref.name?.trim();
30700
+ if (!name) {
30701
+ throw new CredentialReferenceError("Credential reference name is required");
30702
+ }
30703
+ return {
30704
+ source,
30705
+ name,
30706
+ required: ref.required !== false,
30707
+ ...ref.description ? { description: ref.description } : {}
30708
+ };
30709
+ }
30710
+ function normalizeCredentialRefs(refs) {
30711
+ const normalized = {};
30712
+ for (const [rawKey, ref] of Object.entries(refs ?? {})) {
30713
+ const key = normalizeKey(rawKey);
30714
+ if (!key)
30715
+ throw new CredentialReferenceError("Credential reference env key is required");
30716
+ normalized[key] = normalizeCredentialRef(ref);
30717
+ }
30718
+ return normalized;
30719
+ }
30720
+ function parseCredentialRefs(value) {
30721
+ if (!isRecord(value))
30722
+ return {};
30723
+ const refs = {};
30724
+ for (const [key, ref] of Object.entries(value)) {
30725
+ if (!isRecord(ref))
30726
+ continue;
30727
+ const source = ref.source;
30728
+ const name = ref.name;
30729
+ if ((source === "env" || source === "local-vault" || source === "hosted") && typeof name === "string" && name.trim()) {
30730
+ refs[key] = normalizeCredentialRef({
30731
+ source,
30732
+ name,
30733
+ required: typeof ref.required === "boolean" ? ref.required : true,
30734
+ description: typeof ref.description === "string" ? ref.description : undefined
30735
+ });
30736
+ }
30737
+ }
30738
+ return refs;
30739
+ }
30740
+ function normalizeLiteralEnv(env) {
30741
+ const normalized = {};
30742
+ for (const [rawKey, rawValue] of Object.entries(env ?? {})) {
30743
+ const key = normalizeKey(rawKey);
30744
+ if (!key)
30745
+ continue;
30746
+ const value = String(rawValue);
30747
+ if (isSecretLikeEnvKey(key) || isSecretLikeValue(value)) {
30748
+ throw new CredentialReferenceError(`Refusing to store raw secret-like env value for "${key}". Use a credential reference instead.`);
30749
+ }
30750
+ normalized[key] = value;
30751
+ }
30752
+ return normalized;
30753
+ }
30754
+ function readLocalVault() {
30755
+ const path = process.env.HASNA_MCPS_CREDENTIAL_VAULT_PATH ?? join8(MCPS_DIR, "credentials.local.json");
30756
+ if (!existsSync7(path))
30757
+ return {};
30758
+ const parsed = JSON.parse(readFileSync3(path, "utf-8"));
30759
+ if (!isRecord(parsed))
30760
+ return {};
30761
+ const values = {};
30762
+ for (const [key, value] of Object.entries(parsed)) {
30763
+ if (typeof value === "string")
30764
+ values[key] = value;
30765
+ }
30766
+ return values;
30767
+ }
30768
+ function resolveCredentialRef(envKey, ref) {
30769
+ if (ref.source === "env") {
30770
+ const value = process.env[ref.name];
30771
+ if (value === undefined && ref.required !== false) {
30772
+ throw new CredentialReferenceError(`Missing required environment credential "${ref.name}" for "${envKey}"`);
30773
+ }
30774
+ return value;
30775
+ }
30776
+ if (ref.source === "local-vault") {
30777
+ const value = readLocalVault()[ref.name];
30778
+ if (value === undefined && ref.required !== false) {
30779
+ throw new CredentialReferenceError(`Missing required local vault credential "${ref.name}" for "${envKey}"`);
30780
+ }
30781
+ return value;
30782
+ }
30783
+ if (ref.required !== false) {
30784
+ throw new CredentialReferenceError(`Hosted credential "${ref.name}" for "${envKey}" cannot be resolved by the local runtime`);
30785
+ }
30786
+ return;
30787
+ }
30788
+ function resolveServerEnv(server) {
30789
+ const resolved = { ...server.env };
30790
+ const refs = normalizeCredentialRefs(server.credentialRefs);
30791
+ for (const [envKey, ref] of Object.entries(refs)) {
30792
+ const value = resolveCredentialRef(envKey, ref);
30793
+ if (value !== undefined)
30794
+ resolved[envKey] = value;
30795
+ }
30796
+ return resolved;
30797
+ }
30798
+ function credentialRefPlaceholders(refs) {
30799
+ const placeholders = {};
30800
+ for (const key of Object.keys(refs ?? {})) {
30801
+ placeholders[key] = REDACTED_CREDENTIAL_VALUE;
30802
+ }
30803
+ return placeholders;
30804
+ }
30805
+
30806
+ // src/lib/registry.ts
30663
30807
  function parseRow(row) {
30664
30808
  return {
30665
30809
  id: row.id,
@@ -30668,6 +30812,7 @@ function parseRow(row) {
30668
30812
  command: row.command,
30669
30813
  args: safeJsonParse(row.args, []),
30670
30814
  env: safeJsonParse(row.env, {}),
30815
+ credentialRefs: parseCredentialRefs(safeJsonParse(row.credential_refs, {})),
30671
30816
  transport: row.transport,
30672
30817
  url: row.url || null,
30673
30818
  source: row.source,
@@ -30735,9 +30880,9 @@ function addServer(opts) {
30735
30880
  if (!id) {
30736
30881
  throw new Error("Unable to generate a valid server ID");
30737
30882
  }
30738
- const row = db2.prepare(`INSERT INTO servers (id, name, description, command, args, env, transport, url, source)
30739
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
30740
- RETURNING *`).get(id, name, opts.description || null, command, JSON.stringify(opts.args || []), JSON.stringify(opts.env || {}), opts.transport || "stdio", opts.url || null, opts.source || "local");
30883
+ const row = db2.prepare(`INSERT INTO servers (id, name, description, command, args, env, credential_refs, transport, url, source)
30884
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
30885
+ RETURNING *`).get(id, name, opts.description || null, command, JSON.stringify(opts.args || []), JSON.stringify(normalizeLiteralEnv(opts.env)), JSON.stringify(normalizeCredentialRefs(opts.credentialRefs)), opts.transport || "stdio", opts.url || null, opts.source || "local");
30741
30886
  return parseRow(row);
30742
30887
  }
30743
30888
  function removeServer(id) {
@@ -30776,7 +30921,11 @@ function updateServer(id, updates) {
30776
30921
  }
30777
30922
  if (updates.env !== undefined) {
30778
30923
  sets.push("env = ?");
30779
- values.push(JSON.stringify(updates.env));
30924
+ values.push(JSON.stringify(normalizeLiteralEnv(updates.env)));
30925
+ }
30926
+ if (updates.credentialRefs !== undefined) {
30927
+ sets.push("credential_refs = ?");
30928
+ values.push(JSON.stringify(normalizeCredentialRefs(updates.credentialRefs)));
30780
30929
  }
30781
30930
  if (updates.transport !== undefined) {
30782
30931
  sets.push("transport = ?");
@@ -30840,6 +30989,185 @@ function getCachedTools(serverId) {
30840
30989
 
30841
30990
  // src/lib/remote.ts
30842
30991
  init_config2();
30992
+
30993
+ // src/lib/local-command-consent.ts
30994
+ class LocalCommandConsentError extends Error {
30995
+ review;
30996
+ constructor(message, review) {
30997
+ super(message);
30998
+ this.name = "LocalCommandConsentError";
30999
+ this.review = review;
31000
+ }
31001
+ }
31002
+ var SHELL_COMMANDS = new Set(["bash", "sh", "zsh", "fish", "cmd", "cmd.exe", "powershell", "powershell.exe", "pwsh"]);
31003
+ var DESTRUCTIVE_COMMANDS = new Set([
31004
+ "rm",
31005
+ "dd",
31006
+ "mkfs",
31007
+ "shutdown",
31008
+ "reboot",
31009
+ "poweroff",
31010
+ "halt",
31011
+ "killall",
31012
+ "pkill"
31013
+ ]);
31014
+ var SHELL_EVAL_FLAGS = new Set(["-c", "/c", "-Command", "-command", "-EncodedCommand", "-encodedcommand"]);
31015
+ var SECRET_FLAG_PATTERN = /(?:^|[-_])(api[-_]?key|token|secret|password|passwd|credential|auth|private[-_]?key)(?:$|[-_])/i;
31016
+ var SECRET_KEY_PATTERN2 = /(?:^|[_-])(api[_-]?key|token|secret|password|passwd|credential|auth|private[_-]?key)(?:$|[_-])/i;
31017
+ var SECRET_VALUE_PATTERN2 = /^(sk_(?:live|test)_[A-Za-z0-9_]+|ghp_[A-Za-z0-9_]+|github_pat_[A-Za-z0-9_]+|xox[baprs]-[A-Za-z0-9-]+|AIza[A-Za-z0-9_-]{20,}|eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)$/;
31018
+ var SHELL_META_PATTERN = /[;&|`<>]|\$\(/;
31019
+ function commandBase(command) {
31020
+ return command.trim().split(/[\\/]/).pop()?.toLowerCase() || command.trim().toLowerCase();
31021
+ }
31022
+ function normalizeArgs(args) {
31023
+ return (args ?? []).map((arg) => String(arg));
31024
+ }
31025
+ function isSecretKey(key) {
31026
+ return SECRET_KEY_PATTERN2.test(key) || SECRET_FLAG_PATTERN.test(key);
31027
+ }
31028
+ function isSecretValue(value) {
31029
+ return SECRET_VALUE_PATTERN2.test(value.trim());
31030
+ }
31031
+ function isSecretAssignment(arg) {
31032
+ const eqIdx = arg.indexOf("=");
31033
+ if (eqIdx <= 0)
31034
+ return false;
31035
+ const key = arg.slice(0, eqIdx);
31036
+ const value = arg.slice(eqIdx + 1);
31037
+ return isSecretKey(key) || isSecretValue(value);
31038
+ }
31039
+ function isSecretArg(args, index) {
31040
+ const arg = args[index] ?? "";
31041
+ const previous = args[index - 1] ?? "";
31042
+ return isSecretAssignment(arg) || isSecretValue(arg) || SECRET_FLAG_PATTERN.test(previous);
31043
+ }
31044
+ function quoteArg(value) {
31045
+ return JSON.stringify(value);
31046
+ }
31047
+ function displayCommand(command, args) {
31048
+ return [quoteArg(command), ...args.map((arg, index) => quoteArg(isSecretArg(args, index) ? "<redacted>" : arg))].join(" ");
31049
+ }
31050
+ function pushRisk(risks, risk) {
31051
+ if (risks.some((existing) => existing.code === risk.code && existing.evidence === risk.evidence))
31052
+ return;
31053
+ risks.push(risk);
31054
+ }
31055
+ function inspectRisks(command, args, env) {
31056
+ const risks = [];
31057
+ const base = commandBase(command);
31058
+ const joined = [command, ...args].join(" ");
31059
+ if (SHELL_COMMANDS.has(base)) {
31060
+ pushRisk(risks, {
31061
+ code: "shell_interpreter",
31062
+ severity: "warning",
31063
+ message: "Command launches a shell interpreter.",
31064
+ evidence: base
31065
+ });
31066
+ if (args.some((arg) => SHELL_EVAL_FLAGS.has(arg))) {
31067
+ pushRisk(risks, {
31068
+ code: "shell_eval",
31069
+ severity: "danger",
31070
+ message: "Shell command evaluates an inline script.",
31071
+ evidence: base
31072
+ });
31073
+ }
31074
+ }
31075
+ if (base === "sudo") {
31076
+ pushRisk(risks, {
31077
+ code: "privilege_escalation",
31078
+ severity: "danger",
31079
+ message: "Command requests elevated privileges.",
31080
+ evidence: base
31081
+ });
31082
+ }
31083
+ if (DESTRUCTIVE_COMMANDS.has(base) || /\brm\s+-[^\s]*[rf][^\s]*\b/.test(joined) || /--no-preserve-root\b/.test(joined)) {
31084
+ pushRisk(risks, {
31085
+ code: "destructive_command",
31086
+ severity: "danger",
31087
+ message: "Command includes a destructive system operation.",
31088
+ evidence: base
31089
+ });
31090
+ }
31091
+ if (/\b(curl|wget)\b[\s\S]*\|[\s\S]*\b(sh|bash|zsh|fish)\b/.test(joined)) {
31092
+ pushRisk(risks, {
31093
+ code: "download_pipe_shell",
31094
+ severity: "danger",
31095
+ message: "Command downloads remote content and pipes it to a shell."
31096
+ });
31097
+ }
31098
+ if ([command, ...args].some((part) => SHELL_META_PATTERN.test(part))) {
31099
+ pushRisk(risks, {
31100
+ code: "shell_metacharacters",
31101
+ severity: "warning",
31102
+ message: "Command or arguments contain shell metacharacters."
31103
+ });
31104
+ }
31105
+ if (args.some((arg, index) => isSecretArg(args, index))) {
31106
+ pushRisk(risks, {
31107
+ code: "inline_secret",
31108
+ severity: "danger",
31109
+ message: "Command arguments appear to contain inline secret material."
31110
+ });
31111
+ }
31112
+ const secretEnvKeys = Object.keys(env).filter(isSecretKey).sort();
31113
+ if (secretEnvKeys.length > 0) {
31114
+ pushRisk(risks, {
31115
+ code: "secret_env",
31116
+ severity: "warning",
31117
+ message: "Environment contains secret-like keys; values are redacted from consent output.",
31118
+ evidence: secretEnvKeys.join(", ")
31119
+ });
31120
+ }
31121
+ return risks;
31122
+ }
31123
+ function inspectLocalCommand(input) {
31124
+ const args = normalizeArgs(input.args);
31125
+ const env = input.env ?? {};
31126
+ const transport = input.transport ?? "stdio";
31127
+ const risks = inspectRisks(input.command, args, env);
31128
+ return {
31129
+ requiresConsent: transport === "stdio",
31130
+ operation: input.operation ?? "launch",
31131
+ command: input.command,
31132
+ args,
31133
+ displayCommand: displayCommand(input.command, args),
31134
+ envKeys: Object.keys(env).sort(),
31135
+ risks,
31136
+ hasDangerousRisk: risks.some((risk) => risk.severity === "danger")
31137
+ };
31138
+ }
31139
+ function formatLocalCommandReview(review) {
31140
+ const lines = [
31141
+ `Command: ${review.displayCommand}`,
31142
+ review.envKeys.length > 0 ? `Env keys: ${review.envKeys.join(", ")}` : "Env keys: <none>"
31143
+ ];
31144
+ if (review.risks.length > 0) {
31145
+ lines.push("Risks:");
31146
+ for (const risk of review.risks) {
31147
+ lines.push(`- ${risk.severity}: ${risk.code} - ${risk.message}${risk.evidence ? ` (${risk.evidence})` : ""}`);
31148
+ }
31149
+ } else {
31150
+ lines.push("Risks: none detected");
31151
+ }
31152
+ return lines.join(`
31153
+ `);
31154
+ }
31155
+ function assertLocalCommandConsent(input, consent = {}) {
31156
+ const review = inspectLocalCommand(input);
31157
+ if (!review.requiresConsent)
31158
+ return review;
31159
+ if (consent.approved !== true) {
31160
+ throw new LocalCommandConsentError(`local stdio command approval is required before ${review.operation}.
31161
+ ${formatLocalCommandReview(review)}`, review);
31162
+ }
31163
+ if (review.hasDangerousRisk && consent.allowRisky !== true) {
31164
+ throw new LocalCommandConsentError(`risky command approval is required before ${review.operation}.
31165
+ ${formatLocalCommandReview(review)}`, review);
31166
+ }
31167
+ return review;
31168
+ }
31169
+
31170
+ // src/lib/remote.ts
30843
31171
  function parseRegistryEntry(entry) {
30844
31172
  const s = entry.server;
30845
31173
  return {
@@ -30870,7 +31198,7 @@ async function getRegistryServer(id) {
30870
31198
  const all = entries.map(parseRegistryEntry);
30871
31199
  return all.find((s) => s.id === id) || null;
30872
31200
  }
30873
- async function installFromRegistry(id) {
31201
+ async function installFromRegistry(id, options = {}) {
30874
31202
  const server = await getRegistryServer(id);
30875
31203
  if (!server) {
30876
31204
  throw new Error(`Server "${id}" not found in registry`);
@@ -30890,6 +31218,13 @@ async function installFromRegistry(id) {
30890
31218
  transport = pkg.transport.type;
30891
31219
  }
30892
31220
  }
31221
+ assertLocalCommandConsent({
31222
+ command,
31223
+ args,
31224
+ transport,
31225
+ env: {},
31226
+ operation: "register"
31227
+ }, options.localCommandConsent);
30893
31228
  return addServer({
30894
31229
  name: server.name,
30895
31230
  description: server.description,
@@ -30915,8 +31250,8 @@ init_sources();
30915
31250
 
30916
31251
  // src/lib/install.ts
30917
31252
  import { execFileSync } from "child_process";
30918
- import { existsSync as existsSync8, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync7 } from "fs";
30919
- import { join as join9 } from "path";
31253
+ import { existsSync as existsSync9, readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync7 } from "fs";
31254
+ import { join as join10 } from "path";
30920
31255
  import { homedir as homedir8 } from "os";
30921
31256
  function installToClaude(entry) {
30922
31257
  try {
@@ -30928,7 +31263,7 @@ function installToClaude(entry) {
30928
31263
  "--scope",
30929
31264
  "user"
30930
31265
  ];
30931
- for (const [k, v] of Object.entries(entry.env)) {
31266
+ for (const [k, v] of Object.entries(assertAgentInstallEnv(entry))) {
30932
31267
  args.push("--env", `${k}=${v}`);
30933
31268
  }
30934
31269
  args.push(entry.id, "--", entry.command, ...entry.args);
@@ -30940,9 +31275,9 @@ function installToClaude(entry) {
30940
31275
  }
30941
31276
  function installToCodex(entry) {
30942
31277
  try {
30943
- const configDir = join9(homedir8(), ".codex");
30944
- const configPath = join9(configDir, "config.toml");
30945
- if (!existsSync8(configDir)) {
31278
+ const configDir = join10(homedir8(), ".codex");
31279
+ const configPath = join10(configDir, "config.toml");
31280
+ if (!existsSync9(configDir)) {
30946
31281
  mkdirSync7(configDir, { recursive: true });
30947
31282
  }
30948
31283
  const block = `
@@ -30950,7 +31285,7 @@ function installToCodex(entry) {
30950
31285
  ` + `command = ${JSON.stringify(entry.command)}
30951
31286
  ` + `args = [${entry.args.map((a) => JSON.stringify(a)).join(", ")}]
30952
31287
  `;
30953
- const existing = existsSync8(configPath) ? readFileSync4(configPath, "utf-8") : "";
31288
+ const existing = existsSync9(configPath) ? readFileSync5(configPath, "utf-8") : "";
30954
31289
  if (existing.includes(`[mcp_servers.${entry.id}]`)) {
30955
31290
  return { agent: "codex", success: true };
30956
31291
  }
@@ -30962,21 +31297,22 @@ function installToCodex(entry) {
30962
31297
  }
30963
31298
  function installToGemini(entry) {
30964
31299
  try {
30965
- const configDir = join9(homedir8(), ".gemini");
30966
- const configPath = join9(configDir, "settings.json");
30967
- if (!existsSync8(configDir)) {
31300
+ const configDir = join10(homedir8(), ".gemini");
31301
+ const configPath = join10(configDir, "settings.json");
31302
+ if (!existsSync9(configDir)) {
30968
31303
  mkdirSync7(configDir, { recursive: true });
30969
31304
  }
30970
31305
  let settings = {};
30971
- if (existsSync8(configPath)) {
30972
- settings = JSON.parse(readFileSync4(configPath, "utf-8"));
31306
+ if (existsSync9(configPath)) {
31307
+ settings = JSON.parse(readFileSync5(configPath, "utf-8"));
30973
31308
  }
30974
31309
  if (!settings.mcpServers)
30975
31310
  settings.mcpServers = {};
31311
+ const env = assertAgentInstallEnv(entry);
30976
31312
  settings.mcpServers[entry.id] = {
30977
31313
  command: entry.command,
30978
31314
  args: entry.args,
30979
- ...Object.keys(entry.env).length > 0 ? { env: entry.env } : {}
31315
+ ...Object.keys(env).length > 0 ? { env } : {}
30980
31316
  };
30981
31317
  writeFileSync3(configPath, JSON.stringify(settings, null, 2), "utf-8");
30982
31318
  return { agent: "gemini", success: true };
@@ -30984,7 +31320,43 @@ function installToGemini(entry) {
30984
31320
  return { agent: "gemini", success: false, error: err.message };
30985
31321
  }
30986
31322
  }
30987
- function installToAgents(entry, targets = ["claude", "codex", "gemini"]) {
31323
+ function assertAgentInstallEnv(entry) {
31324
+ const refs = entry.credentialRefs ?? {};
31325
+ if (Object.keys(refs).length > 0) {
31326
+ throw new CredentialReferenceError(`Server "${entry.id}" uses credential references; refusing to materialize secrets into local agent config files`);
31327
+ }
31328
+ for (const [key, value] of Object.entries(entry.env)) {
31329
+ if (isSecretLikeEnvKey(key) || isSecretLikeValue(value)) {
31330
+ throw new CredentialReferenceError(`Server "${entry.id}" has legacy raw secret-like env "${key}"; move it to a credential reference before installing to agents`);
31331
+ }
31332
+ }
31333
+ return entry.env;
31334
+ }
31335
+ function installToAgents(entry, targets = ["claude", "codex", "gemini"], options = {}) {
31336
+ try {
31337
+ assertLocalCommandConsent({
31338
+ command: entry.command,
31339
+ args: entry.args,
31340
+ env: { ...entry.env, ...credentialRefPlaceholders(entry.credentialRefs) },
31341
+ transport: entry.transport,
31342
+ operation: "install"
31343
+ }, options.localCommandConsent);
31344
+ } catch (err) {
31345
+ return targets.map((target) => ({
31346
+ agent: target,
31347
+ success: false,
31348
+ error: err.message
31349
+ }));
31350
+ }
31351
+ try {
31352
+ assertAgentInstallEnv(entry);
31353
+ } catch (err) {
31354
+ return targets.map((target) => ({
31355
+ agent: target,
31356
+ success: false,
31357
+ error: err.message
31358
+ }));
31359
+ }
30988
31360
  return targets.map((target) => {
30989
31361
  if (target === "claude")
30990
31362
  return installToClaude(entry);
@@ -33401,7 +33773,7 @@ function requireUrl(entry) {
33401
33773
  throw new Error(`Server "${entry.id}" has an invalid URL: ${entry.url}`);
33402
33774
  }
33403
33775
  }
33404
- async function connectToServer(entry) {
33776
+ async function connectToServer(entry, options = {}) {
33405
33777
  if (connections.has(entry.id)) {
33406
33778
  return connections.get(entry.id);
33407
33779
  }
@@ -33417,10 +33789,17 @@ async function connectToServer(entry) {
33417
33789
  if (!entry.command?.trim()) {
33418
33790
  throw new Error(`Server "${entry.id}" is missing a command`);
33419
33791
  }
33792
+ assertLocalCommandConsent({
33793
+ command: entry.command,
33794
+ args: entry.args,
33795
+ env: { ...entry.env, ...credentialRefPlaceholders(entry.credentialRefs) },
33796
+ transport: entry.transport,
33797
+ operation: "launch"
33798
+ }, options.localCommandConsent);
33420
33799
  transport = new StdioClientTransport({
33421
33800
  command: entry.command,
33422
33801
  args: entry.args,
33423
- env: buildEnv(entry.env)
33802
+ env: buildEnv(resolveServerEnv(entry))
33424
33803
  });
33425
33804
  } else if (entry.transport === "sse") {
33426
33805
  transport = new SSEClientTransport(requireUrl(entry));
@@ -33533,7 +33912,7 @@ async function callTool(prefixedName, args) {
33533
33912
  ]
33534
33913
  };
33535
33914
  }
33536
- async function connectAllEnabled() {
33915
+ async function connectAllEnabled(options = {}) {
33537
33916
  const servers = listServers().filter((s) => s.enabled);
33538
33917
  const results = [];
33539
33918
  let index = 0;
@@ -33545,7 +33924,7 @@ async function connectAllEnabled() {
33545
33924
  return;
33546
33925
  const server = servers[current];
33547
33926
  try {
33548
- const conn = await connectToServer(server);
33927
+ const conn = await connectToServer(server, options);
33549
33928
  results.push(conn);
33550
33929
  } catch (err) {
33551
33930
  console.error(`Failed to connect to ${server.name}: ${err.message}`);
@@ -33558,13 +33937,28 @@ async function connectAllEnabled() {
33558
33937
 
33559
33938
  // src/lib/doctor.ts
33560
33939
  import { execFileSync as execFileSync2 } from "child_process";
33561
- async function diagnoseServer(server) {
33940
+ async function diagnoseServer(server, options = {}) {
33562
33941
  const checks4 = [];
33942
+ let hasLocalConsent = true;
33563
33943
  if (server.transport === "stdio") {
33944
+ try {
33945
+ assertLocalCommandConsent({
33946
+ command: server.command,
33947
+ args: server.args,
33948
+ env: { ...server.env, ...credentialRefPlaceholders(server.credentialRefs) },
33949
+ transport: server.transport,
33950
+ operation: "diagnose"
33951
+ }, options.localCommandConsent);
33952
+ } catch (err) {
33953
+ hasLocalConsent = false;
33954
+ checks4.push({ name: "local command consent", pass: false, message: err.message });
33955
+ }
33564
33956
  try {
33565
33957
  const path = execFileSync2("which", [server.command], { stdio: "pipe" }).toString().trim();
33566
33958
  let version2 = "";
33567
33959
  try {
33960
+ if (!hasLocalConsent)
33961
+ throw new Error("local stdio command approval is required before version probing");
33568
33962
  version2 = execFileSync2(server.command, ["--version"], { stdio: "pipe" }).toString().trim().split(`
33569
33963
  `)[0];
33570
33964
  } catch {}
@@ -33580,12 +33974,29 @@ async function diagnoseServer(server) {
33580
33974
  }
33581
33975
  }
33582
33976
  const missingEnv = Object.entries(server.env).filter(([, v]) => !v);
33583
- if (Object.keys(server.env).length === 0) {
33584
- checks4.push({ name: "env vars", pass: true, message: "no env vars required" });
33585
- } else if (missingEnv.length > 0) {
33586
- checks4.push({ name: "env vars", pass: false, message: `missing values for: ${missingEnv.map(([k]) => k).join(", ")}` });
33977
+ const credentialRefCount = Object.keys(server.credentialRefs ?? {}).length;
33978
+ let credentialError = null;
33979
+ try {
33980
+ resolveServerEnv(server);
33981
+ } catch (err) {
33982
+ credentialError = err.message;
33983
+ }
33984
+ if (Object.keys(server.env).length === 0 && credentialRefCount === 0) {
33985
+ checks4.push({ name: "env vars", pass: true, message: "no env vars or credential refs required" });
33986
+ } else if (missingEnv.length > 0 || credentialError) {
33987
+ const parts = [];
33988
+ if (missingEnv.length > 0)
33989
+ parts.push(`missing literal values for: ${missingEnv.map(([k]) => k).join(", ")}`);
33990
+ if (credentialError)
33991
+ parts.push(credentialError);
33992
+ checks4.push({ name: "env vars", pass: false, message: parts.join("; ") });
33587
33993
  } else {
33588
- checks4.push({ name: "env vars", pass: true, message: `${Object.keys(server.env).length} env var(s) set` });
33994
+ const literalCount = Object.keys(server.env).length;
33995
+ checks4.push({
33996
+ name: "env vars",
33997
+ pass: true,
33998
+ message: `${literalCount} literal env var(s), ${credentialRefCount} credential ref(s) available`
33999
+ });
33589
34000
  }
33590
34001
  if (server.transport !== "stdio" && server.url) {
33591
34002
  try {
@@ -33595,10 +34006,10 @@ async function diagnoseServer(server) {
33595
34006
  checks4.push({ name: "URL reachable", pass: false, message: `unreachable: ${err.message}` });
33596
34007
  }
33597
34008
  }
33598
- if (server.enabled) {
34009
+ if (server.enabled && hasLocalConsent) {
33599
34010
  try {
33600
34011
  await Promise.race([
33601
- connectToServer(server),
34012
+ connectToServer(server, { localCommandConsent: options.localCommandConsent }),
33602
34013
  new Promise((_, reject) => setTimeout(() => reject(new Error("timeout after 10s")), 1e4))
33603
34014
  ]);
33604
34015
  await disconnectServer(server.id);
@@ -33606,8 +34017,10 @@ async function diagnoseServer(server) {
33606
34017
  } catch (err) {
33607
34018
  checks4.push({ name: "connect & list tools", pass: false, message: err.message });
33608
34019
  }
33609
- } else {
34020
+ } else if (!server.enabled) {
33610
34021
  checks4.push({ name: "connect & list tools", pass: true, message: "skipped (server disabled)" });
34022
+ } else {
34023
+ checks4.push({ name: "connect & list tools", pass: false, message: "skipped until local stdio command is approved" });
33611
34024
  }
33612
34025
  return {
33613
34026
  server,
@@ -33820,11 +34233,11 @@ function seedDefaultMachines() {
33820
34233
  // src/lib/fleet.ts
33821
34234
  init_config2();
33822
34235
  import { spawn as spawn2 } from "child_process";
33823
- import { existsSync as existsSync9, mkdirSync as mkdirSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
33824
- import { join as join10 } from "path";
34236
+ import { existsSync as existsSync10, mkdirSync as mkdirSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
34237
+ import { join as join11 } from "path";
33825
34238
  var NPM_SEARCH_URL = "https://registry.npmjs.org/-/v1/search";
33826
34239
  var NPM_REGISTRY_URL = "https://registry.npmjs.org";
33827
- var CATALOG_CACHE_PATH = join10(MCPS_DIR, "cache", "hasna-catalog.json");
34240
+ var CATALOG_CACHE_PATH = join11(MCPS_DIR, "cache", "hasna-catalog.json");
33828
34241
  var DEFAULT_CATALOG_CACHE_TTL_MS = 60 * 60 * 1000;
33829
34242
  var DEFAULT_REMOTE_TIMEOUT_MS = 180000;
33830
34243
  var DEFAULT_HANDSHAKE_TIMEOUT_MS = 2500;
@@ -33834,9 +34247,9 @@ function normalizeQueryList(values) {
33834
34247
  }
33835
34248
  function readCatalogCache(maxAgeMs) {
33836
34249
  try {
33837
- if (!existsSync9(CATALOG_CACHE_PATH))
34250
+ if (!existsSync10(CATALOG_CACHE_PATH))
33838
34251
  return null;
33839
- const parsed = JSON.parse(readFileSync5(CATALOG_CACHE_PATH, "utf-8"));
34252
+ const parsed = JSON.parse(readFileSync6(CATALOG_CACHE_PATH, "utf-8"));
33840
34253
  if (!parsed.cachedAt || !Array.isArray(parsed.entries))
33841
34254
  return null;
33842
34255
  if (Date.now() - parsed.cachedAt > maxAgeMs)
@@ -33848,7 +34261,7 @@ function readCatalogCache(maxAgeMs) {
33848
34261
  }
33849
34262
  function writeCatalogCache(entries) {
33850
34263
  try {
33851
- mkdirSync8(join10(MCPS_DIR, "cache"), { recursive: true });
34264
+ mkdirSync8(join11(MCPS_DIR, "cache"), { recursive: true });
33852
34265
  writeFileSync4(CATALOG_CACHE_PATH, JSON.stringify({ cachedAt: Date.now(), entries }, null, 2), "utf-8");
33853
34266
  } catch {}
33854
34267
  }
@@ -34606,11 +35019,19 @@ function installProviderProfile(id, options = {}) {
34606
35019
  if (!command) {
34607
35020
  throw new Error(`Provider profile "${id}" does not define an install fallback command`);
34608
35021
  }
35022
+ assertLocalCommandConsent({
35023
+ command,
35024
+ args,
35025
+ env: fallback?.env ?? {},
35026
+ transport: useFallback ? "stdio" : profile.transport,
35027
+ operation: "register"
35028
+ }, options.localCommandConsent);
34609
35029
  return addServer({
34610
35030
  name: options.name ?? profile.displayName,
34611
35031
  description: profile.description ?? undefined,
34612
35032
  command,
34613
35033
  args,
35034
+ env: fallback?.env,
34614
35035
  transport: useFallback ? "stdio" : profile.transport,
34615
35036
  url: useFallback ? fallback?.url : profile.endpoint ?? undefined,
34616
35037
  source: "provider-profile"
@@ -34632,6 +35053,16 @@ function jsonContent(value) {
34632
35053
  function errorContent(text) {
34633
35054
  return { ...textContent(text), isError: true };
34634
35055
  }
35056
+ function localConsent(input) {
35057
+ return {
35058
+ approved: input.allow_local_stdio === true,
35059
+ allowRisky: input.allow_risky_command === true,
35060
+ source: "mcp"
35061
+ };
35062
+ }
35063
+ function readCredentialRefs(input) {
35064
+ return normalizeCredentialRefs(input.credential_refs ?? input.credentialRefs);
35065
+ }
34635
35066
  function buildMcpTools() {
34636
35067
  const definitions = [
34637
35068
  {
@@ -34656,23 +35087,60 @@ function buildMcpTools() {
34656
35087
  description: exports_external2.string().optional().describe("Description"),
34657
35088
  transport: exports_external2.enum(["stdio", "sse", "streamable-http"]).optional().describe("Transport type"),
34658
35089
  url: exports_external2.string().optional().describe("URL for remote transports"),
34659
- env: exports_external2.record(exports_external2.string()).optional().describe("Environment variables")
35090
+ env: exports_external2.record(exports_external2.string()).optional().describe("Environment variables"),
35091
+ credential_refs: exports_external2.record(exports_external2.object({
35092
+ source: exports_external2.enum(["env", "local-vault", "hosted"]),
35093
+ name: exports_external2.string(),
35094
+ required: exports_external2.boolean().optional(),
35095
+ description: exports_external2.string().optional()
35096
+ })).optional().describe("Credential references by server env key"),
35097
+ allow_local_stdio: exports_external2.boolean().optional().describe("Approve registering this local stdio command"),
35098
+ allow_risky_command: exports_external2.boolean().optional().describe("Approve registering risky local command patterns")
34660
35099
  },
34661
- run: ({ command, args, name, description, transport, url: url2, env }) => jsonContent(addServer({
34662
- command: String(command),
34663
- args: Array.isArray(args) ? args.map(String) : [],
34664
- name: typeof name === "string" ? name : undefined,
34665
- description: typeof description === "string" ? description : undefined,
34666
- transport,
34667
- url: typeof url2 === "string" ? url2 : undefined,
34668
- env: isRecordOfStrings(env) ? env : {}
34669
- }))
35100
+ run: (input) => {
35101
+ const command = String(input.command);
35102
+ const args = Array.isArray(input.args) ? input.args.map(String) : [];
35103
+ const env = isRecordOfStrings(input.env) ? input.env : {};
35104
+ const credentialRefs = readCredentialRefs(input);
35105
+ const transport = input.transport;
35106
+ try {
35107
+ assertLocalCommandConsent({
35108
+ command,
35109
+ args,
35110
+ env: { ...env, ...Object.fromEntries(Object.keys(credentialRefs).map((key) => [key, "<credential-ref>"])) },
35111
+ transport,
35112
+ operation: "register"
35113
+ }, localConsent(input));
35114
+ return jsonContent(addServer({
35115
+ command,
35116
+ args,
35117
+ name: typeof input.name === "string" ? input.name : undefined,
35118
+ description: typeof input.description === "string" ? input.description : undefined,
35119
+ transport,
35120
+ url: typeof input.url === "string" ? input.url : undefined,
35121
+ env,
35122
+ credentialRefs
35123
+ }));
35124
+ } catch (err) {
35125
+ return errorContent(err.message);
35126
+ }
35127
+ }
34670
35128
  },
34671
35129
  {
34672
35130
  name: "install_from_registry",
34673
35131
  description: "Install an MCP server from the official registry",
34674
- paramsSchema: { id: exports_external2.string().describe("Registry server ID") },
34675
- run: async ({ id }) => jsonContent(await installFromRegistry(String(id)))
35132
+ paramsSchema: {
35133
+ id: exports_external2.string().describe("Registry server ID"),
35134
+ allow_local_stdio: exports_external2.boolean().optional().describe("Approve registering registry stdio commands"),
35135
+ allow_risky_command: exports_external2.boolean().optional().describe("Approve registering risky local command patterns")
35136
+ },
35137
+ run: async (input) => {
35138
+ try {
35139
+ return jsonContent(await installFromRegistry(String(input.id), { localCommandConsent: localConsent(input) }));
35140
+ } catch (err) {
35141
+ return errorContent(err.message);
35142
+ }
35143
+ }
34676
35144
  },
34677
35145
  {
34678
35146
  name: "remove_server",
@@ -34718,26 +35186,52 @@ function buildMcpTools() {
34718
35186
  command: exports_external2.string().optional().describe("New command"),
34719
35187
  args: exports_external2.array(exports_external2.string()).optional().describe("New args list"),
34720
35188
  transport: exports_external2.enum(["stdio", "sse", "streamable-http"]).optional().describe("New transport type"),
34721
- url: exports_external2.string().optional().describe("New URL for remote transports")
35189
+ url: exports_external2.string().optional().describe("New URL for remote transports"),
35190
+ credential_refs: exports_external2.record(exports_external2.object({
35191
+ source: exports_external2.enum(["env", "local-vault", "hosted"]),
35192
+ name: exports_external2.string(),
35193
+ required: exports_external2.boolean().optional(),
35194
+ description: exports_external2.string().optional()
35195
+ })).optional().describe("Credential references by server env key"),
35196
+ allow_local_stdio: exports_external2.boolean().optional().describe("Approve updating this local stdio command"),
35197
+ allow_risky_command: exports_external2.boolean().optional().describe("Approve risky local command patterns")
34722
35198
  },
34723
- run: ({ id, name, description, command, args, transport, url: url2 }) => {
34724
- const serverId = String(id);
35199
+ run: (input) => {
35200
+ const serverId = String(input.id);
34725
35201
  const existing = getServer(serverId);
34726
35202
  if (!existing)
34727
35203
  return errorContent(`Server "${serverId}" not found.`);
34728
35204
  const fields = {};
34729
- if (typeof name === "string")
34730
- fields.name = name;
34731
- if (typeof description === "string")
34732
- fields.description = description;
34733
- if (typeof command === "string")
34734
- fields.command = command;
34735
- if (Array.isArray(args))
34736
- fields.args = args.map(String);
34737
- if (transport === "stdio" || transport === "sse" || transport === "streamable-http")
34738
- fields.transport = transport;
34739
- if (typeof url2 === "string")
34740
- fields.url = url2;
35205
+ if (typeof input.name === "string")
35206
+ fields.name = input.name;
35207
+ if (typeof input.description === "string")
35208
+ fields.description = input.description;
35209
+ if (typeof input.command === "string")
35210
+ fields.command = input.command;
35211
+ if (Array.isArray(input.args))
35212
+ fields.args = input.args.map(String);
35213
+ if (input.credential_refs !== undefined || input.credentialRefs !== undefined)
35214
+ fields.credentialRefs = readCredentialRefs(input);
35215
+ if (input.transport === "stdio" || input.transport === "sse" || input.transport === "streamable-http")
35216
+ fields.transport = input.transport;
35217
+ if (typeof input.url === "string")
35218
+ fields.url = input.url;
35219
+ if (fields.command !== undefined || fields.args !== undefined || fields.transport !== undefined) {
35220
+ try {
35221
+ assertLocalCommandConsent({
35222
+ command: fields.command ?? existing.command,
35223
+ args: fields.args ?? existing.args,
35224
+ env: {
35225
+ ...existing.env,
35226
+ ...Object.fromEntries(Object.keys(fields.credentialRefs ?? existing.credentialRefs ?? {}).map((key) => [key, "<credential-ref>"]))
35227
+ },
35228
+ transport: fields.transport ?? existing.transport,
35229
+ operation: "register"
35230
+ }, localConsent(input));
35231
+ } catch (err) {
35232
+ return errorContent(err.message);
35233
+ }
35234
+ }
34741
35235
  return jsonContent(redactServerEnv(updateServer(serverId, fields)));
34742
35236
  }
34743
35237
  },
@@ -34819,13 +35313,16 @@ function buildMcpTools() {
34819
35313
  paramsSchema: {
34820
35314
  id: exports_external2.string().describe("Provider profile ID"),
34821
35315
  name: exports_external2.string().optional().describe("Override registered server name"),
34822
- use_fallback: exports_external2.boolean().optional().describe("Install the stdio fallback command instead of the direct remote transport")
35316
+ use_fallback: exports_external2.boolean().optional().describe("Install the stdio fallback command instead of the direct remote transport"),
35317
+ allow_local_stdio: exports_external2.boolean().optional().describe("Approve registering provider stdio fallback commands"),
35318
+ allow_risky_command: exports_external2.boolean().optional().describe("Approve risky local command patterns")
34823
35319
  },
34824
- run: ({ id, name, use_fallback }) => {
35320
+ run: (input) => {
34825
35321
  try {
34826
- return jsonContent(redactServerEnv(installProviderProfile(String(id), {
34827
- name: typeof name === "string" ? name : undefined,
34828
- useFallback: use_fallback === true
35322
+ return jsonContent(redactServerEnv(installProviderProfile(String(input.id), {
35323
+ name: typeof input.name === "string" ? input.name : undefined,
35324
+ useFallback: input.use_fallback === true,
35325
+ localCommandConsent: localConsent(input)
34829
35326
  })));
34830
35327
  } catch (err) {
34831
35328
  return errorContent(err.message);
@@ -34898,15 +35395,19 @@ function buildMcpTools() {
34898
35395
  description: "Install a registered MCP server into Claude Code, Codex, and/or Gemini",
34899
35396
  paramsSchema: {
34900
35397
  id: exports_external2.string().describe("Server ID to install (from list_servers)"),
34901
- targets: exports_external2.array(exports_external2.enum(["claude", "codex", "gemini"])).optional().describe("Target agents to install into (default: all)")
35398
+ targets: exports_external2.array(exports_external2.enum(["claude", "codex", "gemini"])).optional().describe("Target agents to install into (default: all)"),
35399
+ allow_local_stdio: exports_external2.boolean().optional().describe("Approve installing local stdio commands into local agent configs"),
35400
+ allow_risky_command: exports_external2.boolean().optional().describe("Approve installing risky local command patterns")
34902
35401
  },
34903
- run: ({ id, targets }) => {
34904
- const serverId = String(id);
35402
+ run: (input) => {
35403
+ const serverId = String(input.id);
34905
35404
  const entry = getServer(serverId);
34906
35405
  if (!entry)
34907
35406
  return errorContent(`Server "${serverId}" not found.`);
34908
- const agentTargets = Array.isArray(targets) ? targets : undefined;
34909
- return jsonContent(installToAgents(entry, agentTargets ?? ["claude", "codex", "gemini"]));
35407
+ const agentTargets = Array.isArray(input.targets) ? input.targets : undefined;
35408
+ return jsonContent(installToAgents(entry, agentTargets ?? ["claude", "codex", "gemini"], {
35409
+ localCommandConsent: localConsent(input)
35410
+ }));
34910
35411
  }
34911
35412
  },
34912
35413
  {
@@ -34918,11 +35419,14 @@ function buildMcpTools() {
34918
35419
  {
34919
35420
  name: "connect_and_list_tools",
34920
35421
  description: "Connect to all enabled MCP servers and list their available tools",
34921
- paramsSchema: {},
34922
- run: async () => {
35422
+ paramsSchema: {
35423
+ allow_local_stdio: exports_external2.boolean().optional().describe("Approve launching enabled local stdio commands"),
35424
+ allow_risky_command: exports_external2.boolean().optional().describe("Approve launching risky local command patterns")
35425
+ },
35426
+ run: async (input) => {
34923
35427
  let liveTools = [];
34924
35428
  try {
34925
- await connectAllEnabled();
35429
+ await connectAllEnabled({ localCommandConsent: localConsent(input) });
34926
35430
  liveTools = listAllTools();
34927
35431
  } finally {
34928
35432
  await disconnectAll().catch(() => {
@@ -34937,11 +35441,13 @@ function buildMcpTools() {
34937
35441
  description: `Call a tool on a connected upstream MCP server. Tool name format: server_id${TOOL_PREFIX_SEPARATOR}tool_name`,
34938
35442
  paramsSchema: {
34939
35443
  tool_name: exports_external2.string().describe(`Prefixed tool name (server_id${TOOL_PREFIX_SEPARATOR}tool_name)`),
34940
- arguments: exports_external2.record(exports_external2.unknown()).optional().describe("Tool arguments as key-value pairs")
35444
+ arguments: exports_external2.record(exports_external2.unknown()).optional().describe("Tool arguments as key-value pairs"),
35445
+ allow_local_stdio: exports_external2.boolean().optional().describe("Approve launching this local stdio command"),
35446
+ allow_risky_command: exports_external2.boolean().optional().describe("Approve launching risky local command patterns")
34941
35447
  },
34942
- run: async ({ tool_name, arguments: args }) => {
35448
+ run: async (input) => {
34943
35449
  try {
34944
- const toolName = String(tool_name);
35450
+ const toolName = String(input.tool_name);
34945
35451
  const sepIdx = toolName.indexOf(TOOL_PREFIX_SEPARATOR);
34946
35452
  if (sepIdx === -1)
34947
35453
  return errorContent(`Error: Invalid tool name "${toolName}"`);
@@ -34951,8 +35457,8 @@ function buildMcpTools() {
34951
35457
  return errorContent(`Error: Server "${serverId}" not found.`);
34952
35458
  if (!entry.enabled)
34953
35459
  return errorContent(`Error: Server "${serverId}" is disabled.`);
34954
- await connectToServer(entry);
34955
- const result = await callTool(toolName, readRecord(args));
35460
+ await connectToServer(entry, { localCommandConsent: localConsent(input) });
35461
+ const result = await callTool(toolName, readRecord(input.arguments));
34956
35462
  return { content: result.content };
34957
35463
  } catch (error2) {
34958
35464
  return errorContent(`Error: ${error2.message}`);
@@ -34962,13 +35468,17 @@ function buildMcpTools() {
34962
35468
  {
34963
35469
  name: "diagnose_server",
34964
35470
  description: "Run health checks on a registered MCP server",
34965
- paramsSchema: { id: exports_external2.string().describe("Server ID") },
34966
- run: async ({ id }) => {
34967
- const serverId = String(id);
35471
+ paramsSchema: {
35472
+ id: exports_external2.string().describe("Server ID"),
35473
+ allow_local_stdio: exports_external2.boolean().optional().describe("Approve launching local stdio diagnostics"),
35474
+ allow_risky_command: exports_external2.boolean().optional().describe("Approve diagnosing risky local command patterns")
35475
+ },
35476
+ run: async (input) => {
35477
+ const serverId = String(input.id);
34968
35478
  const entry = getServer(serverId);
34969
35479
  if (!entry)
34970
35480
  return errorContent(`Server "${serverId}" not found.`);
34971
- return jsonContent(await diagnoseServer(entry));
35481
+ return jsonContent(await diagnoseServer(entry, { localCommandConsent: localConsent(input) }));
34972
35482
  }
34973
35483
  },
34974
35484
  {