@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/index.js CHANGED
@@ -12040,6 +12040,7 @@ function getDb() {
12040
12040
  command TEXT NOT NULL,
12041
12041
  args TEXT NOT NULL DEFAULT '[]',
12042
12042
  env TEXT NOT NULL DEFAULT '{}',
12043
+ credential_refs TEXT NOT NULL DEFAULT '{}',
12043
12044
  transport TEXT NOT NULL DEFAULT 'stdio',
12044
12045
  url TEXT,
12045
12046
  source TEXT NOT NULL DEFAULT 'local',
@@ -12066,6 +12067,9 @@ function getDb() {
12066
12067
  try {
12067
12068
  db.exec("ALTER TABLE servers ADD COLUMN last_error TEXT");
12068
12069
  } catch {}
12070
+ try {
12071
+ db.exec("ALTER TABLE servers ADD COLUMN credential_refs TEXT NOT NULL DEFAULT '{}'");
12072
+ } catch {}
12069
12073
  db.exec(`
12070
12074
  CREATE TABLE IF NOT EXISTS sources (
12071
12075
  id TEXT PRIMARY KEY,
@@ -19126,17 +19130,17 @@ __export(exports_sources, {
19126
19130
  clearCache: () => clearCache,
19127
19131
  addSource: () => addSource
19128
19132
  });
19129
- import { mkdirSync as mkdirSync6, existsSync as existsSync6, readFileSync as readFileSync2, writeFileSync as writeFileSync2, readdirSync as readdirSync3, unlinkSync } from "fs";
19130
- import { join as join7 } from "path";
19133
+ import { mkdirSync as mkdirSync6, existsSync as existsSync7, readFileSync as readFileSync3, writeFileSync as writeFileSync2, readdirSync as readdirSync3, unlinkSync } from "fs";
19134
+ import { join as join8 } from "path";
19131
19135
  function getCacheFile(sourceId) {
19132
- return join7(CACHE_DIR, `${sourceId}.json`);
19136
+ return join8(CACHE_DIR, `${sourceId}.json`);
19133
19137
  }
19134
19138
  function readCache(sourceId) {
19135
19139
  try {
19136
19140
  const file = getCacheFile(sourceId);
19137
- if (!existsSync6(file))
19141
+ if (!existsSync7(file))
19138
19142
  return null;
19139
- const data = JSON.parse(readFileSync2(file, "utf-8"));
19143
+ const data = JSON.parse(readFileSync3(file, "utf-8"));
19140
19144
  return data;
19141
19145
  } catch {
19142
19146
  return null;
@@ -19150,7 +19154,7 @@ function writeCache(sourceId, results) {
19150
19154
  }
19151
19155
  function clearCache(sourceId) {
19152
19156
  try {
19153
- if (!existsSync6(CACHE_DIR))
19157
+ if (!existsSync7(CACHE_DIR))
19154
19158
  return;
19155
19159
  const files = readdirSync3(CACHE_DIR);
19156
19160
  for (const file of files) {
@@ -19158,7 +19162,7 @@ function clearCache(sourceId) {
19158
19162
  continue;
19159
19163
  if (!sourceId || file.startsWith(`${sourceId}.`)) {
19160
19164
  try {
19161
- unlinkSync(join7(CACHE_DIR, file));
19165
+ unlinkSync(join8(CACHE_DIR, file));
19162
19166
  } catch {}
19163
19167
  }
19164
19168
  }
@@ -19404,7 +19408,7 @@ var CACHE_DIR, DEFAULT_TTL_MS;
19404
19408
  var init_sources = __esm(() => {
19405
19409
  init_db();
19406
19410
  init_config2();
19407
- CACHE_DIR = join7(MCPS_DIR, "cache");
19411
+ CACHE_DIR = join8(MCPS_DIR, "cache");
19408
19412
  DEFAULT_TTL_MS = 10 * 60 * 1000;
19409
19413
  });
19410
19414
 
@@ -19412,7 +19416,7 @@ var init_sources = __esm(() => {
19412
19416
  var require_package = __commonJS((exports, module) => {
19413
19417
  module.exports = {
19414
19418
  name: "@hasna/mcps",
19415
- version: "0.0.14",
19419
+ version: "0.0.16",
19416
19420
  description: "Meta-MCP registry & CLI \u2014 discover, manage, and proxy MCP servers",
19417
19421
  type: "module",
19418
19422
  repository: {
@@ -21142,10 +21146,164 @@ var {
21142
21146
  import React10 from "react";
21143
21147
  import { render } from "ink";
21144
21148
  import chalk2 from "chalk";
21145
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
21149
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
21146
21150
 
21147
21151
  // src/lib/registry.ts
21148
21152
  init_db();
21153
+
21154
+ // src/lib/credentials.ts
21155
+ init_config2();
21156
+ import { existsSync as existsSync6, readFileSync as readFileSync2 } from "fs";
21157
+ import { join as join7 } from "path";
21158
+
21159
+ class CredentialReferenceError extends Error {
21160
+ constructor(message) {
21161
+ super(message);
21162
+ this.name = "CredentialReferenceError";
21163
+ }
21164
+ }
21165
+ var SECRET_KEY_PATTERN = /(?:^|[_-])(api[_-]?key|token|secret|password|passwd|credential|auth|private[_-]?key)(?:$|[_-])/i;
21166
+ 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_-]+)$/;
21167
+ var REDACTED_CREDENTIAL_VALUE = "<redacted>";
21168
+ function normalizeKey(key) {
21169
+ return key.trim();
21170
+ }
21171
+ function isRecord(value) {
21172
+ return typeof value === "object" && value !== null && !Array.isArray(value);
21173
+ }
21174
+ function isSecretLikeEnvKey(key) {
21175
+ return SECRET_KEY_PATTERN.test(key);
21176
+ }
21177
+ function isSecretLikeValue(value) {
21178
+ return SECRET_VALUE_PATTERN.test(value.trim());
21179
+ }
21180
+ function normalizeCredentialRef(ref) {
21181
+ const source = ref.source;
21182
+ if (source !== "env" && source !== "local-vault" && source !== "hosted") {
21183
+ throw new CredentialReferenceError(`Unsupported credential reference source: ${String(source)}`);
21184
+ }
21185
+ const name = ref.name?.trim();
21186
+ if (!name) {
21187
+ throw new CredentialReferenceError("Credential reference name is required");
21188
+ }
21189
+ return {
21190
+ source,
21191
+ name,
21192
+ required: ref.required !== false,
21193
+ ...ref.description ? { description: ref.description } : {}
21194
+ };
21195
+ }
21196
+ function normalizeCredentialRefs(refs) {
21197
+ const normalized = {};
21198
+ for (const [rawKey, ref] of Object.entries(refs ?? {})) {
21199
+ const key = normalizeKey(rawKey);
21200
+ if (!key)
21201
+ throw new CredentialReferenceError("Credential reference env key is required");
21202
+ normalized[key] = normalizeCredentialRef(ref);
21203
+ }
21204
+ return normalized;
21205
+ }
21206
+ function parseCredentialRefs(value) {
21207
+ if (!isRecord(value))
21208
+ return {};
21209
+ const refs = {};
21210
+ for (const [key, ref] of Object.entries(value)) {
21211
+ if (!isRecord(ref))
21212
+ continue;
21213
+ const source = ref.source;
21214
+ const name = ref.name;
21215
+ if ((source === "env" || source === "local-vault" || source === "hosted") && typeof name === "string" && name.trim()) {
21216
+ refs[key] = normalizeCredentialRef({
21217
+ source,
21218
+ name,
21219
+ required: typeof ref.required === "boolean" ? ref.required : true,
21220
+ description: typeof ref.description === "string" ? ref.description : undefined
21221
+ });
21222
+ }
21223
+ }
21224
+ return refs;
21225
+ }
21226
+ function normalizeLiteralEnv(env) {
21227
+ const normalized = {};
21228
+ for (const [rawKey, rawValue] of Object.entries(env ?? {})) {
21229
+ const key = normalizeKey(rawKey);
21230
+ if (!key)
21231
+ continue;
21232
+ const value = String(rawValue);
21233
+ if (isSecretLikeEnvKey(key) || isSecretLikeValue(value)) {
21234
+ throw new CredentialReferenceError(`Refusing to store raw secret-like env value for "${key}". Use a credential reference instead.`);
21235
+ }
21236
+ normalized[key] = value;
21237
+ }
21238
+ return normalized;
21239
+ }
21240
+ function redactEnv(env) {
21241
+ const redacted = {};
21242
+ for (const [key, value] of Object.entries(env)) {
21243
+ redacted[key] = isSecretLikeEnvKey(key) || isSecretLikeValue(value) ? REDACTED_CREDENTIAL_VALUE : value;
21244
+ }
21245
+ return redacted;
21246
+ }
21247
+ function redactServerCredentials(server) {
21248
+ return {
21249
+ ...server,
21250
+ env: redactEnv(server.env),
21251
+ credentialRefs: normalizeCredentialRefs(server.credentialRefs)
21252
+ };
21253
+ }
21254
+ function readLocalVault() {
21255
+ const path = process.env.HASNA_MCPS_CREDENTIAL_VAULT_PATH ?? join7(MCPS_DIR, "credentials.local.json");
21256
+ if (!existsSync6(path))
21257
+ return {};
21258
+ const parsed = JSON.parse(readFileSync2(path, "utf-8"));
21259
+ if (!isRecord(parsed))
21260
+ return {};
21261
+ const values = {};
21262
+ for (const [key, value] of Object.entries(parsed)) {
21263
+ if (typeof value === "string")
21264
+ values[key] = value;
21265
+ }
21266
+ return values;
21267
+ }
21268
+ function resolveCredentialRef(envKey, ref) {
21269
+ if (ref.source === "env") {
21270
+ const value = process.env[ref.name];
21271
+ if (value === undefined && ref.required !== false) {
21272
+ throw new CredentialReferenceError(`Missing required environment credential "${ref.name}" for "${envKey}"`);
21273
+ }
21274
+ return value;
21275
+ }
21276
+ if (ref.source === "local-vault") {
21277
+ const value = readLocalVault()[ref.name];
21278
+ if (value === undefined && ref.required !== false) {
21279
+ throw new CredentialReferenceError(`Missing required local vault credential "${ref.name}" for "${envKey}"`);
21280
+ }
21281
+ return value;
21282
+ }
21283
+ if (ref.required !== false) {
21284
+ throw new CredentialReferenceError(`Hosted credential "${ref.name}" for "${envKey}" cannot be resolved by the local runtime`);
21285
+ }
21286
+ return;
21287
+ }
21288
+ function resolveServerEnv(server) {
21289
+ const resolved = { ...server.env };
21290
+ const refs = normalizeCredentialRefs(server.credentialRefs);
21291
+ for (const [envKey, ref] of Object.entries(refs)) {
21292
+ const value = resolveCredentialRef(envKey, ref);
21293
+ if (value !== undefined)
21294
+ resolved[envKey] = value;
21295
+ }
21296
+ return resolved;
21297
+ }
21298
+ function credentialRefPlaceholders(refs) {
21299
+ const placeholders = {};
21300
+ for (const key of Object.keys(refs ?? {})) {
21301
+ placeholders[key] = REDACTED_CREDENTIAL_VALUE;
21302
+ }
21303
+ return placeholders;
21304
+ }
21305
+
21306
+ // src/lib/registry.ts
21149
21307
  function parseRow(row) {
21150
21308
  return {
21151
21309
  id: row.id,
@@ -21154,6 +21312,7 @@ function parseRow(row) {
21154
21312
  command: row.command,
21155
21313
  args: safeJsonParse(row.args, []),
21156
21314
  env: safeJsonParse(row.env, {}),
21315
+ credentialRefs: parseCredentialRefs(safeJsonParse(row.credential_refs, {})),
21157
21316
  transport: row.transport,
21158
21317
  url: row.url || null,
21159
21318
  source: row.source,
@@ -21221,9 +21380,9 @@ function addServer(opts) {
21221
21380
  if (!id) {
21222
21381
  throw new Error("Unable to generate a valid server ID");
21223
21382
  }
21224
- const row = db2.prepare(`INSERT INTO servers (id, name, description, command, args, env, transport, url, source)
21225
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
21226
- 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");
21383
+ const row = db2.prepare(`INSERT INTO servers (id, name, description, command, args, env, credential_refs, transport, url, source)
21384
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
21385
+ 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");
21227
21386
  return parseRow(row);
21228
21387
  }
21229
21388
  function removeServer(id) {
@@ -21262,7 +21421,11 @@ function updateServer(id, updates) {
21262
21421
  }
21263
21422
  if (updates.env !== undefined) {
21264
21423
  sets.push("env = ?");
21265
- values.push(JSON.stringify(updates.env));
21424
+ values.push(JSON.stringify(normalizeLiteralEnv(updates.env)));
21425
+ }
21426
+ if (updates.credentialRefs !== undefined) {
21427
+ sets.push("credential_refs = ?");
21428
+ values.push(JSON.stringify(normalizeCredentialRefs(updates.credentialRefs)));
21266
21429
  }
21267
21430
  if (updates.transport !== undefined) {
21268
21431
  sets.push("transport = ?");
@@ -21296,7 +21459,7 @@ function setServerEnv(id, key, value) {
21296
21459
  if (!server)
21297
21460
  throw new Error(`Server "${id}" not found`);
21298
21461
  const env = { ...server.env, [key]: value };
21299
- db2.prepare("UPDATE servers SET env = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(env), id);
21462
+ db2.prepare("UPDATE servers SET env = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(normalizeLiteralEnv(env)), id);
21300
21463
  }
21301
21464
  function unsetServerEnv(id, key) {
21302
21465
  const db2 = getDb();
@@ -21307,6 +21470,23 @@ function unsetServerEnv(id, key) {
21307
21470
  delete env[key];
21308
21471
  db2.prepare("UPDATE servers SET env = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(env), id);
21309
21472
  }
21473
+ function setServerCredentialRef(id, key, ref) {
21474
+ const db2 = getDb();
21475
+ const server = getServer(id);
21476
+ if (!server)
21477
+ throw new Error(`Server "${id}" not found`);
21478
+ const credentialRefs = normalizeCredentialRefs({ ...server.credentialRefs ?? {}, [key]: ref });
21479
+ db2.prepare("UPDATE servers SET credential_refs = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(credentialRefs), id);
21480
+ }
21481
+ function unsetServerCredentialRef(id, key) {
21482
+ const db2 = getDb();
21483
+ const server = getServer(id);
21484
+ if (!server)
21485
+ throw new Error(`Server "${id}" not found`);
21486
+ const credentialRefs = { ...server.credentialRefs ?? {} };
21487
+ delete credentialRefs[key];
21488
+ db2.prepare("UPDATE servers SET credential_refs = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(normalizeCredentialRefs(credentialRefs)), id);
21489
+ }
21310
21490
  function cacheTools(serverId, tools) {
21311
21491
  const db2 = getDb();
21312
21492
  const insert = db2.prepare("INSERT INTO tool_cache (server_id, name, description, input_schema) VALUES (?, ?, ?, ?)");
@@ -21346,6 +21526,7 @@ function cloneServer(id, newName) {
21346
21526
  command: server.command,
21347
21527
  args: server.args,
21348
21528
  env: server.env,
21529
+ credentialRefs: server.credentialRefs,
21349
21530
  transport: server.transport,
21350
21531
  url: server.url ?? undefined,
21351
21532
  source: server.source
@@ -35591,6 +35772,185 @@ class StreamableHTTPClientTransport {
35591
35772
  // src/lib/proxy.ts
35592
35773
  init_config2();
35593
35774
  init_db();
35775
+
35776
+ // src/lib/local-command-consent.ts
35777
+ class LocalCommandConsentError extends Error {
35778
+ review;
35779
+ constructor(message, review) {
35780
+ super(message);
35781
+ this.name = "LocalCommandConsentError";
35782
+ this.review = review;
35783
+ }
35784
+ }
35785
+ var SHELL_COMMANDS = new Set(["bash", "sh", "zsh", "fish", "cmd", "cmd.exe", "powershell", "powershell.exe", "pwsh"]);
35786
+ var DESTRUCTIVE_COMMANDS = new Set([
35787
+ "rm",
35788
+ "dd",
35789
+ "mkfs",
35790
+ "shutdown",
35791
+ "reboot",
35792
+ "poweroff",
35793
+ "halt",
35794
+ "killall",
35795
+ "pkill"
35796
+ ]);
35797
+ var SHELL_EVAL_FLAGS = new Set(["-c", "/c", "-Command", "-command", "-EncodedCommand", "-encodedcommand"]);
35798
+ var SECRET_FLAG_PATTERN = /(?:^|[-_])(api[-_]?key|token|secret|password|passwd|credential|auth|private[-_]?key)(?:$|[-_])/i;
35799
+ var SECRET_KEY_PATTERN2 = /(?:^|[_-])(api[_-]?key|token|secret|password|passwd|credential|auth|private[_-]?key)(?:$|[_-])/i;
35800
+ 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_-]+)$/;
35801
+ var SHELL_META_PATTERN = /[;&|`<>]|\$\(/;
35802
+ function commandBase(command) {
35803
+ return command.trim().split(/[\\/]/).pop()?.toLowerCase() || command.trim().toLowerCase();
35804
+ }
35805
+ function normalizeArgs(args) {
35806
+ return (args ?? []).map((arg) => String(arg));
35807
+ }
35808
+ function isSecretKey(key) {
35809
+ return SECRET_KEY_PATTERN2.test(key) || SECRET_FLAG_PATTERN.test(key);
35810
+ }
35811
+ function isSecretValue(value) {
35812
+ return SECRET_VALUE_PATTERN2.test(value.trim());
35813
+ }
35814
+ function isSecretAssignment(arg) {
35815
+ const eqIdx = arg.indexOf("=");
35816
+ if (eqIdx <= 0)
35817
+ return false;
35818
+ const key = arg.slice(0, eqIdx);
35819
+ const value = arg.slice(eqIdx + 1);
35820
+ return isSecretKey(key) || isSecretValue(value);
35821
+ }
35822
+ function isSecretArg(args, index) {
35823
+ const arg = args[index] ?? "";
35824
+ const previous = args[index - 1] ?? "";
35825
+ return isSecretAssignment(arg) || isSecretValue(arg) || SECRET_FLAG_PATTERN.test(previous);
35826
+ }
35827
+ function quoteArg(value) {
35828
+ return JSON.stringify(value);
35829
+ }
35830
+ function displayCommand(command, args) {
35831
+ return [quoteArg(command), ...args.map((arg, index) => quoteArg(isSecretArg(args, index) ? "<redacted>" : arg))].join(" ");
35832
+ }
35833
+ function pushRisk(risks, risk) {
35834
+ if (risks.some((existing) => existing.code === risk.code && existing.evidence === risk.evidence))
35835
+ return;
35836
+ risks.push(risk);
35837
+ }
35838
+ function inspectRisks(command, args, env) {
35839
+ const risks = [];
35840
+ const base = commandBase(command);
35841
+ const joined = [command, ...args].join(" ");
35842
+ if (SHELL_COMMANDS.has(base)) {
35843
+ pushRisk(risks, {
35844
+ code: "shell_interpreter",
35845
+ severity: "warning",
35846
+ message: "Command launches a shell interpreter.",
35847
+ evidence: base
35848
+ });
35849
+ if (args.some((arg) => SHELL_EVAL_FLAGS.has(arg))) {
35850
+ pushRisk(risks, {
35851
+ code: "shell_eval",
35852
+ severity: "danger",
35853
+ message: "Shell command evaluates an inline script.",
35854
+ evidence: base
35855
+ });
35856
+ }
35857
+ }
35858
+ if (base === "sudo") {
35859
+ pushRisk(risks, {
35860
+ code: "privilege_escalation",
35861
+ severity: "danger",
35862
+ message: "Command requests elevated privileges.",
35863
+ evidence: base
35864
+ });
35865
+ }
35866
+ if (DESTRUCTIVE_COMMANDS.has(base) || /\brm\s+-[^\s]*[rf][^\s]*\b/.test(joined) || /--no-preserve-root\b/.test(joined)) {
35867
+ pushRisk(risks, {
35868
+ code: "destructive_command",
35869
+ severity: "danger",
35870
+ message: "Command includes a destructive system operation.",
35871
+ evidence: base
35872
+ });
35873
+ }
35874
+ if (/\b(curl|wget)\b[\s\S]*\|[\s\S]*\b(sh|bash|zsh|fish)\b/.test(joined)) {
35875
+ pushRisk(risks, {
35876
+ code: "download_pipe_shell",
35877
+ severity: "danger",
35878
+ message: "Command downloads remote content and pipes it to a shell."
35879
+ });
35880
+ }
35881
+ if ([command, ...args].some((part) => SHELL_META_PATTERN.test(part))) {
35882
+ pushRisk(risks, {
35883
+ code: "shell_metacharacters",
35884
+ severity: "warning",
35885
+ message: "Command or arguments contain shell metacharacters."
35886
+ });
35887
+ }
35888
+ if (args.some((arg, index) => isSecretArg(args, index))) {
35889
+ pushRisk(risks, {
35890
+ code: "inline_secret",
35891
+ severity: "danger",
35892
+ message: "Command arguments appear to contain inline secret material."
35893
+ });
35894
+ }
35895
+ const secretEnvKeys = Object.keys(env).filter(isSecretKey).sort();
35896
+ if (secretEnvKeys.length > 0) {
35897
+ pushRisk(risks, {
35898
+ code: "secret_env",
35899
+ severity: "warning",
35900
+ message: "Environment contains secret-like keys; values are redacted from consent output.",
35901
+ evidence: secretEnvKeys.join(", ")
35902
+ });
35903
+ }
35904
+ return risks;
35905
+ }
35906
+ function inspectLocalCommand(input) {
35907
+ const args = normalizeArgs(input.args);
35908
+ const env = input.env ?? {};
35909
+ const transport = input.transport ?? "stdio";
35910
+ const risks = inspectRisks(input.command, args, env);
35911
+ return {
35912
+ requiresConsent: transport === "stdio",
35913
+ operation: input.operation ?? "launch",
35914
+ command: input.command,
35915
+ args,
35916
+ displayCommand: displayCommand(input.command, args),
35917
+ envKeys: Object.keys(env).sort(),
35918
+ risks,
35919
+ hasDangerousRisk: risks.some((risk) => risk.severity === "danger")
35920
+ };
35921
+ }
35922
+ function formatLocalCommandReview(review) {
35923
+ const lines = [
35924
+ `Command: ${review.displayCommand}`,
35925
+ review.envKeys.length > 0 ? `Env keys: ${review.envKeys.join(", ")}` : "Env keys: <none>"
35926
+ ];
35927
+ if (review.risks.length > 0) {
35928
+ lines.push("Risks:");
35929
+ for (const risk of review.risks) {
35930
+ lines.push(`- ${risk.severity}: ${risk.code} - ${risk.message}${risk.evidence ? ` (${risk.evidence})` : ""}`);
35931
+ }
35932
+ } else {
35933
+ lines.push("Risks: none detected");
35934
+ }
35935
+ return lines.join(`
35936
+ `);
35937
+ }
35938
+ function assertLocalCommandConsent(input, consent = {}) {
35939
+ const review = inspectLocalCommand(input);
35940
+ if (!review.requiresConsent)
35941
+ return review;
35942
+ if (consent.approved !== true) {
35943
+ throw new LocalCommandConsentError(`local stdio command approval is required before ${review.operation}.
35944
+ ${formatLocalCommandReview(review)}`, review);
35945
+ }
35946
+ if (review.hasDangerousRisk && consent.allowRisky !== true) {
35947
+ throw new LocalCommandConsentError(`risky command approval is required before ${review.operation}.
35948
+ ${formatLocalCommandReview(review)}`, review);
35949
+ }
35950
+ return review;
35951
+ }
35952
+
35953
+ // src/lib/proxy.ts
35594
35954
  var connections = new Map;
35595
35955
  var inflightConnections = new Map;
35596
35956
  var CONNECT_CONCURRENCY = 4;
@@ -35617,7 +35977,7 @@ function requireUrl(entry) {
35617
35977
  throw new Error(`Server "${entry.id}" has an invalid URL: ${entry.url}`);
35618
35978
  }
35619
35979
  }
35620
- async function connectToServer(entry) {
35980
+ async function connectToServer(entry, options = {}) {
35621
35981
  if (connections.has(entry.id)) {
35622
35982
  return connections.get(entry.id);
35623
35983
  }
@@ -35633,10 +35993,17 @@ async function connectToServer(entry) {
35633
35993
  if (!entry.command?.trim()) {
35634
35994
  throw new Error(`Server "${entry.id}" is missing a command`);
35635
35995
  }
35996
+ assertLocalCommandConsent({
35997
+ command: entry.command,
35998
+ args: entry.args,
35999
+ env: { ...entry.env, ...credentialRefPlaceholders(entry.credentialRefs) },
36000
+ transport: entry.transport,
36001
+ operation: "launch"
36002
+ }, options.localCommandConsent);
35636
36003
  transport = new StdioClientTransport({
35637
36004
  command: entry.command,
35638
36005
  args: entry.args,
35639
- env: buildEnv(entry.env)
36006
+ env: buildEnv(resolveServerEnv(entry))
35640
36007
  });
35641
36008
  } else if (entry.transport === "sse") {
35642
36009
  transport = new SSEClientTransport(requireUrl(entry));
@@ -35749,7 +36116,7 @@ async function callTool(prefixedName, args) {
35749
36116
  ]
35750
36117
  };
35751
36118
  }
35752
- async function connectAllEnabled() {
36119
+ async function connectAllEnabled(options = {}) {
35753
36120
  const servers = listServers().filter((s) => s.enabled);
35754
36121
  const results = [];
35755
36122
  let index = 0;
@@ -35761,7 +36128,7 @@ async function connectAllEnabled() {
35761
36128
  return;
35762
36129
  const server = servers[current];
35763
36130
  try {
35764
- const conn = await connectToServer(server);
36131
+ const conn = await connectToServer(server, options);
35765
36132
  results.push(conn);
35766
36133
  } catch (err) {
35767
36134
  console.error(`Failed to connect to ${server.name}: ${err.message}`);
@@ -35773,13 +36140,28 @@ async function connectAllEnabled() {
35773
36140
  }
35774
36141
 
35775
36142
  // src/lib/doctor.ts
35776
- async function diagnoseServer(server) {
36143
+ async function diagnoseServer(server, options = {}) {
35777
36144
  const checks4 = [];
36145
+ let hasLocalConsent = true;
35778
36146
  if (server.transport === "stdio") {
36147
+ try {
36148
+ assertLocalCommandConsent({
36149
+ command: server.command,
36150
+ args: server.args,
36151
+ env: { ...server.env, ...credentialRefPlaceholders(server.credentialRefs) },
36152
+ transport: server.transport,
36153
+ operation: "diagnose"
36154
+ }, options.localCommandConsent);
36155
+ } catch (err) {
36156
+ hasLocalConsent = false;
36157
+ checks4.push({ name: "local command consent", pass: false, message: err.message });
36158
+ }
35779
36159
  try {
35780
36160
  const path = execFileSync("which", [server.command], { stdio: "pipe" }).toString().trim();
35781
36161
  let version2 = "";
35782
36162
  try {
36163
+ if (!hasLocalConsent)
36164
+ throw new Error("local stdio command approval is required before version probing");
35783
36165
  version2 = execFileSync(server.command, ["--version"], { stdio: "pipe" }).toString().trim().split(`
35784
36166
  `)[0];
35785
36167
  } catch {}
@@ -35795,12 +36177,29 @@ async function diagnoseServer(server) {
35795
36177
  }
35796
36178
  }
35797
36179
  const missingEnv = Object.entries(server.env).filter(([, v]) => !v);
35798
- if (Object.keys(server.env).length === 0) {
35799
- checks4.push({ name: "env vars", pass: true, message: "no env vars required" });
35800
- } else if (missingEnv.length > 0) {
35801
- checks4.push({ name: "env vars", pass: false, message: `missing values for: ${missingEnv.map(([k]) => k).join(", ")}` });
36180
+ const credentialRefCount = Object.keys(server.credentialRefs ?? {}).length;
36181
+ let credentialError = null;
36182
+ try {
36183
+ resolveServerEnv(server);
36184
+ } catch (err) {
36185
+ credentialError = err.message;
36186
+ }
36187
+ if (Object.keys(server.env).length === 0 && credentialRefCount === 0) {
36188
+ checks4.push({ name: "env vars", pass: true, message: "no env vars or credential refs required" });
36189
+ } else if (missingEnv.length > 0 || credentialError) {
36190
+ const parts = [];
36191
+ if (missingEnv.length > 0)
36192
+ parts.push(`missing literal values for: ${missingEnv.map(([k]) => k).join(", ")}`);
36193
+ if (credentialError)
36194
+ parts.push(credentialError);
36195
+ checks4.push({ name: "env vars", pass: false, message: parts.join("; ") });
35802
36196
  } else {
35803
- checks4.push({ name: "env vars", pass: true, message: `${Object.keys(server.env).length} env var(s) set` });
36197
+ const literalCount = Object.keys(server.env).length;
36198
+ checks4.push({
36199
+ name: "env vars",
36200
+ pass: true,
36201
+ message: `${literalCount} literal env var(s), ${credentialRefCount} credential ref(s) available`
36202
+ });
35804
36203
  }
35805
36204
  if (server.transport !== "stdio" && server.url) {
35806
36205
  try {
@@ -35810,10 +36209,10 @@ async function diagnoseServer(server) {
35810
36209
  checks4.push({ name: "URL reachable", pass: false, message: `unreachable: ${err.message}` });
35811
36210
  }
35812
36211
  }
35813
- if (server.enabled) {
36212
+ if (server.enabled && hasLocalConsent) {
35814
36213
  try {
35815
36214
  await Promise.race([
35816
- connectToServer(server),
36215
+ connectToServer(server, { localCommandConsent: options.localCommandConsent }),
35817
36216
  new Promise((_, reject) => setTimeout(() => reject(new Error("timeout after 10s")), 1e4))
35818
36217
  ]);
35819
36218
  await disconnectServer(server.id);
@@ -35821,8 +36220,10 @@ async function diagnoseServer(server) {
35821
36220
  } catch (err) {
35822
36221
  checks4.push({ name: "connect & list tools", pass: false, message: err.message });
35823
36222
  }
35824
- } else {
36223
+ } else if (!server.enabled) {
35825
36224
  checks4.push({ name: "connect & list tools", pass: true, message: "skipped (server disabled)" });
36225
+ } else {
36226
+ checks4.push({ name: "connect & list tools", pass: false, message: "skipped until local stdio command is approved" });
35826
36227
  }
35827
36228
  return {
35828
36229
  server,
@@ -35863,7 +36264,7 @@ async function getRegistryServer(id) {
35863
36264
  const all = entries.map(parseRegistryEntry);
35864
36265
  return all.find((s) => s.id === id) || null;
35865
36266
  }
35866
- async function installFromRegistry(id) {
36267
+ async function installFromRegistry(id, options = {}) {
35867
36268
  const server = await getRegistryServer(id);
35868
36269
  if (!server) {
35869
36270
  throw new Error(`Server "${id}" not found in registry`);
@@ -35883,6 +36284,13 @@ async function installFromRegistry(id) {
35883
36284
  transport = pkg.transport.type;
35884
36285
  }
35885
36286
  }
36287
+ assertLocalCommandConsent({
36288
+ command,
36289
+ args,
36290
+ transport,
36291
+ env: {},
36292
+ operation: "register"
36293
+ }, options.localCommandConsent);
35886
36294
  return addServer({
35887
36295
  name: server.name,
35888
36296
  description: server.description,
@@ -35908,8 +36316,8 @@ init_sources();
35908
36316
 
35909
36317
  // src/lib/install.ts
35910
36318
  import { execFileSync as execFileSync2 } from "child_process";
35911
- import { existsSync as existsSync7, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync7 } from "fs";
35912
- import { join as join8 } from "path";
36319
+ import { existsSync as existsSync8, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync7 } from "fs";
36320
+ import { join as join9 } from "path";
35913
36321
  import { homedir as homedir8 } from "os";
35914
36322
  function installToClaude(entry) {
35915
36323
  try {
@@ -35921,7 +36329,7 @@ function installToClaude(entry) {
35921
36329
  "--scope",
35922
36330
  "user"
35923
36331
  ];
35924
- for (const [k, v] of Object.entries(entry.env)) {
36332
+ for (const [k, v] of Object.entries(assertAgentInstallEnv(entry))) {
35925
36333
  args.push("--env", `${k}=${v}`);
35926
36334
  }
35927
36335
  args.push(entry.id, "--", entry.command, ...entry.args);
@@ -35933,9 +36341,9 @@ function installToClaude(entry) {
35933
36341
  }
35934
36342
  function installToCodex(entry) {
35935
36343
  try {
35936
- const configDir = join8(homedir8(), ".codex");
35937
- const configPath = join8(configDir, "config.toml");
35938
- if (!existsSync7(configDir)) {
36344
+ const configDir = join9(homedir8(), ".codex");
36345
+ const configPath = join9(configDir, "config.toml");
36346
+ if (!existsSync8(configDir)) {
35939
36347
  mkdirSync7(configDir, { recursive: true });
35940
36348
  }
35941
36349
  const block = `
@@ -35943,7 +36351,7 @@ function installToCodex(entry) {
35943
36351
  ` + `command = ${JSON.stringify(entry.command)}
35944
36352
  ` + `args = [${entry.args.map((a) => JSON.stringify(a)).join(", ")}]
35945
36353
  `;
35946
- const existing = existsSync7(configPath) ? readFileSync3(configPath, "utf-8") : "";
36354
+ const existing = existsSync8(configPath) ? readFileSync4(configPath, "utf-8") : "";
35947
36355
  if (existing.includes(`[mcp_servers.${entry.id}]`)) {
35948
36356
  return { agent: "codex", success: true };
35949
36357
  }
@@ -35955,21 +36363,22 @@ function installToCodex(entry) {
35955
36363
  }
35956
36364
  function installToGemini(entry) {
35957
36365
  try {
35958
- const configDir = join8(homedir8(), ".gemini");
35959
- const configPath = join8(configDir, "settings.json");
35960
- if (!existsSync7(configDir)) {
36366
+ const configDir = join9(homedir8(), ".gemini");
36367
+ const configPath = join9(configDir, "settings.json");
36368
+ if (!existsSync8(configDir)) {
35961
36369
  mkdirSync7(configDir, { recursive: true });
35962
36370
  }
35963
36371
  let settings = {};
35964
- if (existsSync7(configPath)) {
35965
- settings = JSON.parse(readFileSync3(configPath, "utf-8"));
36372
+ if (existsSync8(configPath)) {
36373
+ settings = JSON.parse(readFileSync4(configPath, "utf-8"));
35966
36374
  }
35967
36375
  if (!settings.mcpServers)
35968
36376
  settings.mcpServers = {};
36377
+ const env = assertAgentInstallEnv(entry);
35969
36378
  settings.mcpServers[entry.id] = {
35970
36379
  command: entry.command,
35971
36380
  args: entry.args,
35972
- ...Object.keys(entry.env).length > 0 ? { env: entry.env } : {}
36381
+ ...Object.keys(env).length > 0 ? { env } : {}
35973
36382
  };
35974
36383
  writeFileSync3(configPath, JSON.stringify(settings, null, 2), "utf-8");
35975
36384
  return { agent: "gemini", success: true };
@@ -35977,7 +36386,43 @@ function installToGemini(entry) {
35977
36386
  return { agent: "gemini", success: false, error: err.message };
35978
36387
  }
35979
36388
  }
35980
- function installToAgents(entry, targets = ["claude", "codex", "gemini"]) {
36389
+ function assertAgentInstallEnv(entry) {
36390
+ const refs = entry.credentialRefs ?? {};
36391
+ if (Object.keys(refs).length > 0) {
36392
+ throw new CredentialReferenceError(`Server "${entry.id}" uses credential references; refusing to materialize secrets into local agent config files`);
36393
+ }
36394
+ for (const [key, value] of Object.entries(entry.env)) {
36395
+ if (isSecretLikeEnvKey(key) || isSecretLikeValue(value)) {
36396
+ throw new CredentialReferenceError(`Server "${entry.id}" has legacy raw secret-like env "${key}"; move it to a credential reference before installing to agents`);
36397
+ }
36398
+ }
36399
+ return entry.env;
36400
+ }
36401
+ function installToAgents(entry, targets = ["claude", "codex", "gemini"], options = {}) {
36402
+ try {
36403
+ assertLocalCommandConsent({
36404
+ command: entry.command,
36405
+ args: entry.args,
36406
+ env: { ...entry.env, ...credentialRefPlaceholders(entry.credentialRefs) },
36407
+ transport: entry.transport,
36408
+ operation: "install"
36409
+ }, options.localCommandConsent);
36410
+ } catch (err) {
36411
+ return targets.map((target) => ({
36412
+ agent: target,
36413
+ success: false,
36414
+ error: err.message
36415
+ }));
36416
+ }
36417
+ try {
36418
+ assertAgentInstallEnv(entry);
36419
+ } catch (err) {
36420
+ return targets.map((target) => ({
36421
+ agent: target,
36422
+ success: false,
36423
+ error: err.message
36424
+ }));
36425
+ }
35981
36426
  return targets.map((target) => {
35982
36427
  if (target === "claude")
35983
36428
  return installToClaude(entry);
@@ -36192,11 +36637,11 @@ function seedDefaultMachines() {
36192
36637
  // src/lib/fleet.ts
36193
36638
  init_config2();
36194
36639
  import { spawn as spawn2 } from "child_process";
36195
- import { existsSync as existsSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
36196
- import { join as join9 } from "path";
36640
+ import { existsSync as existsSync9, mkdirSync as mkdirSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
36641
+ import { join as join10 } from "path";
36197
36642
  var NPM_SEARCH_URL = "https://registry.npmjs.org/-/v1/search";
36198
36643
  var NPM_REGISTRY_URL = "https://registry.npmjs.org";
36199
- var CATALOG_CACHE_PATH = join9(MCPS_DIR, "cache", "hasna-catalog.json");
36644
+ var CATALOG_CACHE_PATH = join10(MCPS_DIR, "cache", "hasna-catalog.json");
36200
36645
  var DEFAULT_CATALOG_CACHE_TTL_MS = 60 * 60 * 1000;
36201
36646
  var DEFAULT_REMOTE_TIMEOUT_MS = 180000;
36202
36647
  var DEFAULT_HANDSHAKE_TIMEOUT_MS = 2500;
@@ -36206,9 +36651,9 @@ function normalizeQueryList(values) {
36206
36651
  }
36207
36652
  function readCatalogCache(maxAgeMs) {
36208
36653
  try {
36209
- if (!existsSync8(CATALOG_CACHE_PATH))
36654
+ if (!existsSync9(CATALOG_CACHE_PATH))
36210
36655
  return null;
36211
- const parsed = JSON.parse(readFileSync4(CATALOG_CACHE_PATH, "utf-8"));
36656
+ const parsed = JSON.parse(readFileSync5(CATALOG_CACHE_PATH, "utf-8"));
36212
36657
  if (!parsed.cachedAt || !Array.isArray(parsed.entries))
36213
36658
  return null;
36214
36659
  if (Date.now() - parsed.cachedAt > maxAgeMs)
@@ -36220,7 +36665,7 @@ function readCatalogCache(maxAgeMs) {
36220
36665
  }
36221
36666
  function writeCatalogCache(entries) {
36222
36667
  try {
36223
- mkdirSync8(join9(MCPS_DIR, "cache"), { recursive: true });
36668
+ mkdirSync8(join10(MCPS_DIR, "cache"), { recursive: true });
36224
36669
  writeFileSync4(CATALOG_CACHE_PATH, JSON.stringify({ cachedAt: Date.now(), entries }, null, 2), "utf-8");
36225
36670
  } catch {}
36226
36671
  }
@@ -36978,11 +37423,19 @@ function installProviderProfile(id, options = {}) {
36978
37423
  if (!command) {
36979
37424
  throw new Error(`Provider profile "${id}" does not define an install fallback command`);
36980
37425
  }
37426
+ assertLocalCommandConsent({
37427
+ command,
37428
+ args,
37429
+ env: fallback?.env ?? {},
37430
+ transport: useFallback ? "stdio" : profile.transport,
37431
+ operation: "register"
37432
+ }, options.localCommandConsent);
36981
37433
  return addServer({
36982
37434
  name: options.name ?? profile.displayName,
36983
37435
  description: profile.description ?? undefined,
36984
37436
  command,
36985
37437
  args,
37438
+ env: fallback?.env,
36986
37439
  transport: useFallback ? "stdio" : profile.transport,
36987
37440
  url: useFallback ? fallback?.url : profile.endpoint ?? undefined,
36988
37441
  source: "provider-profile"
@@ -38216,22 +38669,22 @@ var EMPTY_COMPLETION_RESULT = {
38216
38669
  };
38217
38670
 
38218
38671
  // src/lib/version.ts
38219
- import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
38220
- import { dirname as dirname3, join as join10 } from "path";
38672
+ import { existsSync as existsSync10, readFileSync as readFileSync6 } from "fs";
38673
+ import { dirname as dirname3, join as join11 } from "path";
38221
38674
  import { fileURLToPath } from "url";
38222
38675
  var FALLBACK_VERSION = "0.0.1";
38223
38676
  function readPackageVersion(moduleUrl, fallback = FALLBACK_VERSION) {
38224
38677
  const baseDir = dirname3(fileURLToPath(moduleUrl));
38225
38678
  const candidates = [
38226
- join10(baseDir, "..", "..", "package.json"),
38227
- join10(baseDir, "..", "package.json"),
38228
- join10(baseDir, "package.json")
38679
+ join11(baseDir, "..", "..", "package.json"),
38680
+ join11(baseDir, "..", "package.json"),
38681
+ join11(baseDir, "package.json")
38229
38682
  ];
38230
38683
  for (const candidate of candidates) {
38231
- if (!existsSync9(candidate))
38684
+ if (!existsSync10(candidate))
38232
38685
  continue;
38233
38686
  try {
38234
- const pkg = JSON.parse(readFileSync5(candidate, "utf-8"));
38687
+ const pkg = JSON.parse(readFileSync6(candidate, "utf-8"));
38235
38688
  if (pkg.version)
38236
38689
  return pkg.version;
38237
38690
  } catch {}
@@ -38257,6 +38710,16 @@ function jsonContent(value) {
38257
38710
  function errorContent(text) {
38258
38711
  return { ...textContent(text), isError: true };
38259
38712
  }
38713
+ function localConsent(input) {
38714
+ return {
38715
+ approved: input.allow_local_stdio === true,
38716
+ allowRisky: input.allow_risky_command === true,
38717
+ source: "mcp"
38718
+ };
38719
+ }
38720
+ function readCredentialRefs(input) {
38721
+ return normalizeCredentialRefs(input.credential_refs ?? input.credentialRefs);
38722
+ }
38260
38723
  function buildMcpTools() {
38261
38724
  const definitions = [
38262
38725
  {
@@ -38281,23 +38744,60 @@ function buildMcpTools() {
38281
38744
  description: exports_external2.string().optional().describe("Description"),
38282
38745
  transport: exports_external2.enum(["stdio", "sse", "streamable-http"]).optional().describe("Transport type"),
38283
38746
  url: exports_external2.string().optional().describe("URL for remote transports"),
38284
- env: exports_external2.record(exports_external2.string()).optional().describe("Environment variables")
38747
+ env: exports_external2.record(exports_external2.string()).optional().describe("Environment variables"),
38748
+ credential_refs: exports_external2.record(exports_external2.object({
38749
+ source: exports_external2.enum(["env", "local-vault", "hosted"]),
38750
+ name: exports_external2.string(),
38751
+ required: exports_external2.boolean().optional(),
38752
+ description: exports_external2.string().optional()
38753
+ })).optional().describe("Credential references by server env key"),
38754
+ allow_local_stdio: exports_external2.boolean().optional().describe("Approve registering this local stdio command"),
38755
+ allow_risky_command: exports_external2.boolean().optional().describe("Approve registering risky local command patterns")
38285
38756
  },
38286
- run: ({ command, args, name, description, transport, url: url2, env }) => jsonContent(addServer({
38287
- command: String(command),
38288
- args: Array.isArray(args) ? args.map(String) : [],
38289
- name: typeof name === "string" ? name : undefined,
38290
- description: typeof description === "string" ? description : undefined,
38291
- transport,
38292
- url: typeof url2 === "string" ? url2 : undefined,
38293
- env: isRecordOfStrings(env) ? env : {}
38294
- }))
38757
+ run: (input) => {
38758
+ const command = String(input.command);
38759
+ const args = Array.isArray(input.args) ? input.args.map(String) : [];
38760
+ const env = isRecordOfStrings(input.env) ? input.env : {};
38761
+ const credentialRefs = readCredentialRefs(input);
38762
+ const transport = input.transport;
38763
+ try {
38764
+ assertLocalCommandConsent({
38765
+ command,
38766
+ args,
38767
+ env: { ...env, ...Object.fromEntries(Object.keys(credentialRefs).map((key) => [key, "<credential-ref>"])) },
38768
+ transport,
38769
+ operation: "register"
38770
+ }, localConsent(input));
38771
+ return jsonContent(addServer({
38772
+ command,
38773
+ args,
38774
+ name: typeof input.name === "string" ? input.name : undefined,
38775
+ description: typeof input.description === "string" ? input.description : undefined,
38776
+ transport,
38777
+ url: typeof input.url === "string" ? input.url : undefined,
38778
+ env,
38779
+ credentialRefs
38780
+ }));
38781
+ } catch (err) {
38782
+ return errorContent(err.message);
38783
+ }
38784
+ }
38295
38785
  },
38296
38786
  {
38297
38787
  name: "install_from_registry",
38298
38788
  description: "Install an MCP server from the official registry",
38299
- paramsSchema: { id: exports_external2.string().describe("Registry server ID") },
38300
- run: async ({ id }) => jsonContent(await installFromRegistry(String(id)))
38789
+ paramsSchema: {
38790
+ id: exports_external2.string().describe("Registry server ID"),
38791
+ allow_local_stdio: exports_external2.boolean().optional().describe("Approve registering registry stdio commands"),
38792
+ allow_risky_command: exports_external2.boolean().optional().describe("Approve registering risky local command patterns")
38793
+ },
38794
+ run: async (input) => {
38795
+ try {
38796
+ return jsonContent(await installFromRegistry(String(input.id), { localCommandConsent: localConsent(input) }));
38797
+ } catch (err) {
38798
+ return errorContent(err.message);
38799
+ }
38800
+ }
38301
38801
  },
38302
38802
  {
38303
38803
  name: "remove_server",
@@ -38343,26 +38843,52 @@ function buildMcpTools() {
38343
38843
  command: exports_external2.string().optional().describe("New command"),
38344
38844
  args: exports_external2.array(exports_external2.string()).optional().describe("New args list"),
38345
38845
  transport: exports_external2.enum(["stdio", "sse", "streamable-http"]).optional().describe("New transport type"),
38346
- url: exports_external2.string().optional().describe("New URL for remote transports")
38846
+ url: exports_external2.string().optional().describe("New URL for remote transports"),
38847
+ credential_refs: exports_external2.record(exports_external2.object({
38848
+ source: exports_external2.enum(["env", "local-vault", "hosted"]),
38849
+ name: exports_external2.string(),
38850
+ required: exports_external2.boolean().optional(),
38851
+ description: exports_external2.string().optional()
38852
+ })).optional().describe("Credential references by server env key"),
38853
+ allow_local_stdio: exports_external2.boolean().optional().describe("Approve updating this local stdio command"),
38854
+ allow_risky_command: exports_external2.boolean().optional().describe("Approve risky local command patterns")
38347
38855
  },
38348
- run: ({ id, name, description, command, args, transport, url: url2 }) => {
38349
- const serverId = String(id);
38856
+ run: (input) => {
38857
+ const serverId = String(input.id);
38350
38858
  const existing = getServer(serverId);
38351
38859
  if (!existing)
38352
38860
  return errorContent(`Server "${serverId}" not found.`);
38353
38861
  const fields = {};
38354
- if (typeof name === "string")
38355
- fields.name = name;
38356
- if (typeof description === "string")
38357
- fields.description = description;
38358
- if (typeof command === "string")
38359
- fields.command = command;
38360
- if (Array.isArray(args))
38361
- fields.args = args.map(String);
38362
- if (transport === "stdio" || transport === "sse" || transport === "streamable-http")
38363
- fields.transport = transport;
38364
- if (typeof url2 === "string")
38365
- fields.url = url2;
38862
+ if (typeof input.name === "string")
38863
+ fields.name = input.name;
38864
+ if (typeof input.description === "string")
38865
+ fields.description = input.description;
38866
+ if (typeof input.command === "string")
38867
+ fields.command = input.command;
38868
+ if (Array.isArray(input.args))
38869
+ fields.args = input.args.map(String);
38870
+ if (input.credential_refs !== undefined || input.credentialRefs !== undefined)
38871
+ fields.credentialRefs = readCredentialRefs(input);
38872
+ if (input.transport === "stdio" || input.transport === "sse" || input.transport === "streamable-http")
38873
+ fields.transport = input.transport;
38874
+ if (typeof input.url === "string")
38875
+ fields.url = input.url;
38876
+ if (fields.command !== undefined || fields.args !== undefined || fields.transport !== undefined) {
38877
+ try {
38878
+ assertLocalCommandConsent({
38879
+ command: fields.command ?? existing.command,
38880
+ args: fields.args ?? existing.args,
38881
+ env: {
38882
+ ...existing.env,
38883
+ ...Object.fromEntries(Object.keys(fields.credentialRefs ?? existing.credentialRefs ?? {}).map((key) => [key, "<credential-ref>"]))
38884
+ },
38885
+ transport: fields.transport ?? existing.transport,
38886
+ operation: "register"
38887
+ }, localConsent(input));
38888
+ } catch (err) {
38889
+ return errorContent(err.message);
38890
+ }
38891
+ }
38366
38892
  return jsonContent(redactServerEnv(updateServer(serverId, fields)));
38367
38893
  }
38368
38894
  },
@@ -38444,13 +38970,16 @@ function buildMcpTools() {
38444
38970
  paramsSchema: {
38445
38971
  id: exports_external2.string().describe("Provider profile ID"),
38446
38972
  name: exports_external2.string().optional().describe("Override registered server name"),
38447
- use_fallback: exports_external2.boolean().optional().describe("Install the stdio fallback command instead of the direct remote transport")
38973
+ use_fallback: exports_external2.boolean().optional().describe("Install the stdio fallback command instead of the direct remote transport"),
38974
+ allow_local_stdio: exports_external2.boolean().optional().describe("Approve registering provider stdio fallback commands"),
38975
+ allow_risky_command: exports_external2.boolean().optional().describe("Approve risky local command patterns")
38448
38976
  },
38449
- run: ({ id, name, use_fallback }) => {
38977
+ run: (input) => {
38450
38978
  try {
38451
- return jsonContent(redactServerEnv(installProviderProfile(String(id), {
38452
- name: typeof name === "string" ? name : undefined,
38453
- useFallback: use_fallback === true
38979
+ return jsonContent(redactServerEnv(installProviderProfile(String(input.id), {
38980
+ name: typeof input.name === "string" ? input.name : undefined,
38981
+ useFallback: input.use_fallback === true,
38982
+ localCommandConsent: localConsent(input)
38454
38983
  })));
38455
38984
  } catch (err) {
38456
38985
  return errorContent(err.message);
@@ -38523,15 +39052,19 @@ function buildMcpTools() {
38523
39052
  description: "Install a registered MCP server into Claude Code, Codex, and/or Gemini",
38524
39053
  paramsSchema: {
38525
39054
  id: exports_external2.string().describe("Server ID to install (from list_servers)"),
38526
- targets: exports_external2.array(exports_external2.enum(["claude", "codex", "gemini"])).optional().describe("Target agents to install into (default: all)")
39055
+ targets: exports_external2.array(exports_external2.enum(["claude", "codex", "gemini"])).optional().describe("Target agents to install into (default: all)"),
39056
+ allow_local_stdio: exports_external2.boolean().optional().describe("Approve installing local stdio commands into local agent configs"),
39057
+ allow_risky_command: exports_external2.boolean().optional().describe("Approve installing risky local command patterns")
38527
39058
  },
38528
- run: ({ id, targets }) => {
38529
- const serverId = String(id);
39059
+ run: (input) => {
39060
+ const serverId = String(input.id);
38530
39061
  const entry = getServer(serverId);
38531
39062
  if (!entry)
38532
39063
  return errorContent(`Server "${serverId}" not found.`);
38533
- const agentTargets = Array.isArray(targets) ? targets : undefined;
38534
- return jsonContent(installToAgents(entry, agentTargets ?? ["claude", "codex", "gemini"]));
39064
+ const agentTargets = Array.isArray(input.targets) ? input.targets : undefined;
39065
+ return jsonContent(installToAgents(entry, agentTargets ?? ["claude", "codex", "gemini"], {
39066
+ localCommandConsent: localConsent(input)
39067
+ }));
38535
39068
  }
38536
39069
  },
38537
39070
  {
@@ -38543,11 +39076,14 @@ function buildMcpTools() {
38543
39076
  {
38544
39077
  name: "connect_and_list_tools",
38545
39078
  description: "Connect to all enabled MCP servers and list their available tools",
38546
- paramsSchema: {},
38547
- run: async () => {
39079
+ paramsSchema: {
39080
+ allow_local_stdio: exports_external2.boolean().optional().describe("Approve launching enabled local stdio commands"),
39081
+ allow_risky_command: exports_external2.boolean().optional().describe("Approve launching risky local command patterns")
39082
+ },
39083
+ run: async (input) => {
38548
39084
  let liveTools = [];
38549
39085
  try {
38550
- await connectAllEnabled();
39086
+ await connectAllEnabled({ localCommandConsent: localConsent(input) });
38551
39087
  liveTools = listAllTools();
38552
39088
  } finally {
38553
39089
  await disconnectAll().catch(() => {
@@ -38562,11 +39098,13 @@ function buildMcpTools() {
38562
39098
  description: `Call a tool on a connected upstream MCP server. Tool name format: server_id${TOOL_PREFIX_SEPARATOR}tool_name`,
38563
39099
  paramsSchema: {
38564
39100
  tool_name: exports_external2.string().describe(`Prefixed tool name (server_id${TOOL_PREFIX_SEPARATOR}tool_name)`),
38565
- arguments: exports_external2.record(exports_external2.unknown()).optional().describe("Tool arguments as key-value pairs")
39101
+ arguments: exports_external2.record(exports_external2.unknown()).optional().describe("Tool arguments as key-value pairs"),
39102
+ allow_local_stdio: exports_external2.boolean().optional().describe("Approve launching this local stdio command"),
39103
+ allow_risky_command: exports_external2.boolean().optional().describe("Approve launching risky local command patterns")
38566
39104
  },
38567
- run: async ({ tool_name, arguments: args }) => {
39105
+ run: async (input) => {
38568
39106
  try {
38569
- const toolName = String(tool_name);
39107
+ const toolName = String(input.tool_name);
38570
39108
  const sepIdx = toolName.indexOf(TOOL_PREFIX_SEPARATOR);
38571
39109
  if (sepIdx === -1)
38572
39110
  return errorContent(`Error: Invalid tool name "${toolName}"`);
@@ -38576,8 +39114,8 @@ function buildMcpTools() {
38576
39114
  return errorContent(`Error: Server "${serverId}" not found.`);
38577
39115
  if (!entry.enabled)
38578
39116
  return errorContent(`Error: Server "${serverId}" is disabled.`);
38579
- await connectToServer(entry);
38580
- const result = await callTool(toolName, readRecord(args));
39117
+ await connectToServer(entry, { localCommandConsent: localConsent(input) });
39118
+ const result = await callTool(toolName, readRecord(input.arguments));
38581
39119
  return { content: result.content };
38582
39120
  } catch (error2) {
38583
39121
  return errorContent(`Error: ${error2.message}`);
@@ -38587,13 +39125,17 @@ function buildMcpTools() {
38587
39125
  {
38588
39126
  name: "diagnose_server",
38589
39127
  description: "Run health checks on a registered MCP server",
38590
- paramsSchema: { id: exports_external2.string().describe("Server ID") },
38591
- run: async ({ id }) => {
38592
- const serverId = String(id);
39128
+ paramsSchema: {
39129
+ id: exports_external2.string().describe("Server ID"),
39130
+ allow_local_stdio: exports_external2.boolean().optional().describe("Approve launching local stdio diagnostics"),
39131
+ allow_risky_command: exports_external2.boolean().optional().describe("Approve diagnosing risky local command patterns")
39132
+ },
39133
+ run: async (input) => {
39134
+ const serverId = String(input.id);
38593
39135
  const entry = getServer(serverId);
38594
39136
  if (!entry)
38595
39137
  return errorContent(`Server "${serverId}" not found.`);
38596
- return jsonContent(await diagnoseServer(entry));
39138
+ return jsonContent(await diagnoseServer(entry, { localCommandConsent: localConsent(input) }));
38597
39139
  }
38598
39140
  },
38599
39141
  {
@@ -38903,32 +39445,32 @@ if (isDirectRun) {
38903
39445
  }
38904
39446
 
38905
39447
  // src/server/serve.ts
38906
- import { existsSync as existsSync10 } from "fs";
38907
- import { join as join11, dirname as dirname4, extname, resolve, relative as relative2, sep } from "path";
39448
+ import { existsSync as existsSync11 } from "fs";
39449
+ import { join as join12, dirname as dirname4, extname, resolve, relative as relative2, sep } from "path";
38908
39450
  import { fileURLToPath as fileURLToPath2 } from "url";
38909
39451
  init_sources();
38910
39452
  init_db();
38911
39453
  function redactServer(server) {
38912
- return { ...server, env: {} };
39454
+ return { ...redactServerCredentials(server), env: {} };
38913
39455
  }
38914
39456
  function resolveDashboardDir() {
38915
39457
  const candidates = [];
38916
39458
  try {
38917
39459
  const scriptDir = dirname4(fileURLToPath2(import.meta.url));
38918
- candidates.push(join11(scriptDir, "..", "dashboard", "dist"));
38919
- candidates.push(join11(scriptDir, "..", "..", "dashboard", "dist"));
39460
+ candidates.push(join12(scriptDir, "..", "dashboard", "dist"));
39461
+ candidates.push(join12(scriptDir, "..", "..", "dashboard", "dist"));
38920
39462
  } catch {}
38921
39463
  if (process.argv[1]) {
38922
39464
  const mainDir = dirname4(process.argv[1]);
38923
- candidates.push(join11(mainDir, "..", "dashboard", "dist"));
38924
- candidates.push(join11(mainDir, "..", "..", "dashboard", "dist"));
39465
+ candidates.push(join12(mainDir, "..", "dashboard", "dist"));
39466
+ candidates.push(join12(mainDir, "..", "..", "dashboard", "dist"));
38925
39467
  }
38926
- candidates.push(join11(process.cwd(), "dashboard", "dist"));
39468
+ candidates.push(join12(process.cwd(), "dashboard", "dist"));
38927
39469
  for (const candidate of candidates) {
38928
- if (existsSync10(candidate))
39470
+ if (existsSync11(candidate))
38929
39471
  return candidate;
38930
39472
  }
38931
- return join11(process.cwd(), "dashboard", "dist");
39473
+ return join12(process.cwd(), "dashboard", "dist");
38932
39474
  }
38933
39475
  var MIME_TYPES = {
38934
39476
  ".html": "text/html; charset=utf-8",
@@ -38962,6 +39504,20 @@ function isValidId(id) {
38962
39504
  return /^[a-z0-9-]+$/.test(id);
38963
39505
  }
38964
39506
  var MAX_BODY_SIZE = 1024 * 1024;
39507
+ function consentFromInput(input) {
39508
+ return {
39509
+ approved: input.allow_local_stdio === true || input.allowLocalStdio === true,
39510
+ allowRisky: input.allow_risky_command === true || input.allowRiskyCommand === true,
39511
+ source: "api"
39512
+ };
39513
+ }
39514
+ function consentFromSearchParams(params) {
39515
+ return {
39516
+ approved: params.get("allow_local_stdio") === "1" || params.get("allow_local_stdio") === "true",
39517
+ allowRisky: params.get("allow_risky_command") === "1" || params.get("allow_risky_command") === "true",
39518
+ source: "api"
39519
+ };
39520
+ }
38965
39521
  function isLoopbackHost(hostname3) {
38966
39522
  return hostname3 === "127.0.0.1" || hostname3 === "localhost" || hostname3 === "::1";
38967
39523
  }
@@ -39016,7 +39572,7 @@ function getAllServersWithToolCount() {
39016
39572
  }));
39017
39573
  }
39018
39574
  function serveStaticFile(filePath) {
39019
- if (!existsSync10(filePath))
39575
+ if (!existsSync11(filePath))
39020
39576
  return null;
39021
39577
  const ext = extname(filePath);
39022
39578
  const contentType = MIME_TYPES[ext] || "application/octet-stream";
@@ -39049,7 +39605,7 @@ async function startServer(port, options) {
39049
39605
  const host = options?.host ?? "127.0.0.1";
39050
39606
  getDb();
39051
39607
  const dashboardDir = resolveDashboardDir();
39052
- const dashboardExists = existsSync10(dashboardDir);
39608
+ const dashboardExists = existsSync11(dashboardDir);
39053
39609
  if (!dashboardExists) {
39054
39610
  console.error(`
39055
39611
  Dashboard not found at: ${dashboardDir}`);
@@ -39108,16 +39664,33 @@ Dashboard not found at: ${dashboardDir}`);
39108
39664
  return json({ error: "Invalid 'args' format" }, 400, port);
39109
39665
  }
39110
39666
  const args = body.args || [];
39667
+ const credentialRefs = normalizeCredentialRefs(body.credential_refs ?? body.credentialRefs);
39668
+ const env = body.env && typeof body.env === "object" && !Array.isArray(body.env) ? body.env : {};
39669
+ try {
39670
+ assertLocalCommandConsent({
39671
+ command,
39672
+ args,
39673
+ env: { ...env, ...Object.fromEntries(Object.keys(credentialRefs).map((key) => [key, "<credential-ref>"])) },
39674
+ transport,
39675
+ operation: "register"
39676
+ }, consentFromInput(body));
39677
+ } catch (err) {
39678
+ return json({ error: err.message }, 400, port);
39679
+ }
39111
39680
  const entry = addServer({
39112
39681
  name: body.name,
39113
39682
  command,
39114
39683
  args,
39115
39684
  description: body.description,
39116
39685
  transport,
39117
- url: body.url
39686
+ url: body.url,
39687
+ env,
39688
+ credentialRefs
39118
39689
  });
39119
39690
  return json(entry, 200, port);
39120
39691
  } catch (e) {
39692
+ if (e instanceof CredentialReferenceError)
39693
+ return json({ error: e.message }, 400, port);
39121
39694
  return json({ error: e instanceof Error ? e.message : "Failed to add server" }, 500, port);
39122
39695
  }
39123
39696
  }
@@ -39196,6 +39769,11 @@ Dashboard not found at: ${dashboardDir}`);
39196
39769
  fields.description = body.description;
39197
39770
  if (body.command !== undefined)
39198
39771
  fields.command = body.command;
39772
+ if (body.env !== undefined)
39773
+ fields.env = body.env;
39774
+ if (body.credential_refs !== undefined || body.credentialRefs !== undefined) {
39775
+ fields.credentialRefs = normalizeCredentialRefs(body.credential_refs ?? body.credentialRefs);
39776
+ }
39199
39777
  if (body.transport !== undefined)
39200
39778
  fields.transport = body.transport;
39201
39779
  if (body.url !== undefined)
@@ -39206,6 +39784,22 @@ Dashboard not found at: ${dashboardDir}`);
39206
39784
  }
39207
39785
  fields.args = body.args;
39208
39786
  }
39787
+ if (fields.command !== undefined || fields.args !== undefined || fields.transport !== undefined) {
39788
+ try {
39789
+ assertLocalCommandConsent({
39790
+ command: fields.command ?? existing.command,
39791
+ args: fields.args ?? existing.args,
39792
+ env: {
39793
+ ...fields.env ?? existing.env,
39794
+ ...Object.fromEntries(Object.keys(fields.credentialRefs ?? existing.credentialRefs ?? {}).map((key) => [key, "<credential-ref>"]))
39795
+ },
39796
+ transport: fields.transport ?? existing.transport,
39797
+ operation: "register"
39798
+ }, consentFromInput(body));
39799
+ } catch (err) {
39800
+ return json({ error: err.message }, 400, port);
39801
+ }
39802
+ }
39209
39803
  const updated = updateServer(id, fields);
39210
39804
  return json(redactServer(updated), 200, port);
39211
39805
  } catch (e) {
@@ -39234,6 +39828,8 @@ Dashboard not found at: ${dashboardDir}`);
39234
39828
  setServerEnv(id, body.key, body.value);
39235
39829
  return json({ ok: true }, 200, port);
39236
39830
  } catch (e) {
39831
+ if (e instanceof CredentialReferenceError)
39832
+ return json({ error: e.message }, 400, port);
39237
39833
  return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
39238
39834
  }
39239
39835
  }
@@ -39281,7 +39877,7 @@ Dashboard not found at: ${dashboardDir}`);
39281
39877
  }
39282
39878
  if (!body.tool || typeof body.tool !== "string")
39283
39879
  return json({ error: "Missing 'tool'" }, 400, port);
39284
- await connectToServer(entry);
39880
+ await connectToServer(entry, { localCommandConsent: consentFromInput(body) });
39285
39881
  const toolName = `${id}__${body.tool}`;
39286
39882
  const result = await callTool(toolName, body.args || {});
39287
39883
  await disconnectServer(id).catch(() => {
@@ -39292,6 +39888,9 @@ Dashboard not found at: ${dashboardDir}`);
39292
39888
  await disconnectServer(id).catch(() => {
39293
39889
  return;
39294
39890
  });
39891
+ if (e instanceof LocalCommandConsentError) {
39892
+ return json({ error: e.message }, 400, port);
39893
+ }
39295
39894
  return json({ error: e instanceof Error ? e.message : "Failed to call tool" }, 500, port);
39296
39895
  }
39297
39896
  }
@@ -39304,7 +39903,7 @@ Dashboard not found at: ${dashboardDir}`);
39304
39903
  if (!entry)
39305
39904
  return json({ error: `Server '${id}' not found` }, 404, port);
39306
39905
  try {
39307
- const report = await diagnoseServer(entry);
39906
+ const report = await diagnoseServer(entry, { localCommandConsent: consentFromSearchParams(url2.searchParams) });
39308
39907
  return json(report, 200, port);
39309
39908
  } catch (e) {
39310
39909
  return json({ error: e instanceof Error ? e.message : "Failed to diagnose server" }, 500, port);
@@ -39433,7 +40032,7 @@ Dashboard not found at: ${dashboardDir}`);
39433
40032
  return res2;
39434
40033
  }
39435
40034
  }
39436
- const indexPath = join11(dashboardDir, "index.html");
40035
+ const indexPath = join12(dashboardDir, "index.html");
39437
40036
  const res = serveStaticFile(indexPath);
39438
40037
  if (res)
39439
40038
  return res;
@@ -39959,6 +40558,13 @@ function ServerDetail({ server, onSelectTool, onBack }) {
39959
40558
  const [tools2, setTools] = useState3([]);
39960
40559
  const [loading, setLoading] = useState3(false);
39961
40560
  const [error2, setError] = useState3(null);
40561
+ const commandReview = inspectLocalCommand({
40562
+ command: server.command,
40563
+ args: server.args,
40564
+ env: server.env,
40565
+ transport: server.transport,
40566
+ operation: "launch"
40567
+ });
39962
40568
  const cachedTools = getCachedTools(server.id);
39963
40569
  const cachedKey = cachedTools.map((t) => `${t.name}|${t.description}|${JSON.stringify(t.input_schema)}`).join(";");
39964
40570
  useEffect3(() => {
@@ -39979,7 +40585,9 @@ function ServerDetail({ server, onSelectTool, onBack }) {
39979
40585
  setLoading(true);
39980
40586
  setError(null);
39981
40587
  try {
39982
- const conn = await connectToServer(server);
40588
+ const conn = await connectToServer(server, {
40589
+ localCommandConsent: { approved: true, source: "tui" }
40590
+ });
39983
40591
  setTools(conn.tools);
39984
40592
  } catch (err) {
39985
40593
  setError(err.message);
@@ -40038,6 +40646,21 @@ function ServerDetail({ server, onSelectTool, onBack }) {
40038
40646
  server.args.join(" ")
40039
40647
  ]
40040
40648
  }, undefined, true, undefined, this),
40649
+ commandReview.requiresConsent && /* @__PURE__ */ jsxDEV2(Box4, {
40650
+ marginTop: 1,
40651
+ flexDirection: "column",
40652
+ children: [
40653
+ /* @__PURE__ */ jsxDEV2(Text5, {
40654
+ color: commandReview.hasDangerousRisk ? "red" : "yellow",
40655
+ children: "Local stdio command review"
40656
+ }, undefined, false, undefined, this),
40657
+ formatLocalCommandReview(commandReview).split(`
40658
+ `).map((line, index) => /* @__PURE__ */ jsxDEV2(Text5, {
40659
+ dimColor: true,
40660
+ children: line
40661
+ }, `${index}:${line}`, false, undefined, this))
40662
+ ]
40663
+ }, undefined, true, undefined, this),
40041
40664
  server.description && /* @__PURE__ */ jsxDEV2(Text5, {
40042
40665
  dimColor: true,
40043
40666
  children: server.description
@@ -40206,7 +40829,9 @@ function SearchView({ onBack }) {
40206
40829
  setInstalling(item.value);
40207
40830
  setMessage(null);
40208
40831
  try {
40209
- const server = await installFromRegistry(item.value);
40832
+ const server = await installFromRegistry(item.value, {
40833
+ localCommandConsent: { approved: true, source: "tui" }
40834
+ });
40210
40835
  setMessage(`Installed: ${server.name} [${server.id}]`);
40211
40836
  } catch (err) {
40212
40837
  setMessage(`Install failed: ${err.message}`);
@@ -40317,7 +40942,9 @@ function ToolCall({ server, tool, onBack }) {
40317
40942
  setError(null);
40318
40943
  setResult(null);
40319
40944
  try {
40320
- await connectToServer(server);
40945
+ await connectToServer(server, {
40946
+ localCommandConsent: { approved: true, source: "tui" }
40947
+ });
40321
40948
  const prefixed = `${server.id}${TOOL_PREFIX_SEPARATOR}${tool.name}`;
40322
40949
  const res = await callTool(prefixed, args);
40323
40950
  const text = res.content.map((c) => c.text).join(`
@@ -40537,6 +41164,52 @@ var MACHINE_PLATFORMS = ["linux", "darwin", "unknown"];
40537
41164
  var MACHINE_ARCHES = ["arm64", "x64", "unknown"];
40538
41165
  var MACHINE_INSTALLERS = ["auto", "bun", "npm"];
40539
41166
  var FLEET_INSTALL_MODES = ["missing", "missing-or-outdated", "all"];
41167
+ function localConsentFromOptions(opts, approved = false) {
41168
+ return {
41169
+ approved: approved || opts.yes === true || opts.allowLocalStdio === true,
41170
+ allowRisky: opts.allowRiskyCommand === true,
41171
+ source: "cli"
41172
+ };
41173
+ }
41174
+ function printLocalCommandReviewIfNeeded(input) {
41175
+ const review = inspectLocalCommand(input);
41176
+ if (!review.requiresConsent)
41177
+ return;
41178
+ console.log(chalk2.dim(formatLocalCommandReview(review)));
41179
+ }
41180
+ function assertCliLocalCommandConsent(input, opts, approved = false) {
41181
+ const consent = localConsentFromOptions(opts, approved);
41182
+ const review = inspectLocalCommand(input);
41183
+ if (review.requiresConsent && consent.approved === true && (!review.hasDangerousRisk || consent.allowRisky === true)) {
41184
+ console.log(chalk2.dim(formatLocalCommandReview(review)));
41185
+ }
41186
+ assertLocalCommandConsent(input, consent);
41187
+ return consent;
41188
+ }
41189
+ function parseCredentialPairs(pairs, source) {
41190
+ const refs = {};
41191
+ for (const pair of pairs ?? []) {
41192
+ const eqIdx = pair.indexOf("=");
41193
+ if (eqIdx <= 0)
41194
+ throw new Error(`Credential reference must use KEY=NAME format: ${pair}`);
41195
+ const key = pair.slice(0, eqIdx).trim();
41196
+ const name = pair.slice(eqIdx + 1).trim();
41197
+ if (!key || !name)
41198
+ throw new Error(`Credential reference must use KEY=NAME format: ${pair}`);
41199
+ refs[key] = { source, name, required: true };
41200
+ }
41201
+ return refs;
41202
+ }
41203
+ function credentialRefsFromOptions(opts) {
41204
+ return {
41205
+ ...parseCredentialPairs(opts.credentialEnv, "env"),
41206
+ ...parseCredentialPairs(opts.credentialLocal, "local-vault"),
41207
+ ...parseCredentialPairs(opts.credentialHosted, "hosted")
41208
+ };
41209
+ }
41210
+ function formatCredentialRef(ref) {
41211
+ return `${ref.source}:${ref.name}`;
41212
+ }
40540
41213
  function printProviderProfile(profile) {
40541
41214
  const status = profile.enabled ? chalk2.green("enabled") : chalk2.red("disabled");
40542
41215
  console.log(` ${chalk2.bold(profile.displayName)} ${chalk2.dim(`[${profile.id}]`)} \u2014 ${chalk2.dim(profile.transport)} \u2014 ${status}`);
@@ -40785,11 +41458,12 @@ providersCmd.command("info").argument("<id>", "Provider profile ID").description
40785
41458
  console.log(` Docs: ${chalk2.cyan(profile.docsUrl)}`);
40786
41459
  closeDb();
40787
41460
  });
40788
- providersCmd.command("install").argument("<id>", "Provider profile ID").description("Register a curated provider profile as an MCP server").option("--name <name>", "Override registered server name").option("--fallback", "Install the stdio fallback command instead of direct remote transport").option("--json", "Output as JSON").action((id, opts) => {
41461
+ providersCmd.command("install").argument("<id>", "Provider profile ID").description("Register a curated provider profile as an MCP server").option("--name <name>", "Override registered server name").option("--fallback", "Install the stdio fallback command instead of direct remote transport").option("--yes", "Approve local stdio fallback commands").option("--allow-local-stdio", "Approve local stdio fallback commands").option("--allow-risky-command", "Approve high-risk local command patterns").option("--json", "Output as JSON").action((id, opts) => {
40789
41462
  try {
40790
41463
  const server = installProviderProfile(id, {
40791
41464
  name: opts.name,
40792
- useFallback: opts.fallback === true
41465
+ useFallback: opts.fallback === true,
41466
+ localCommandConsent: localConsentFromOptions(opts)
40793
41467
  });
40794
41468
  if (opts.json) {
40795
41469
  printJson(server);
@@ -40820,11 +41494,13 @@ function detectSourceType(url2) {
40820
41494
  return "mcp-registry";
40821
41495
  return null;
40822
41496
  }
40823
- program2.command("add").passThroughOptions().argument("[command]", "Command to run the MCP server").argument("[args...]", "Arguments for the command").option("--name <name>", "Display name for the server").option("--description <desc>", "Description").option("--from-registry <id>", "Install from official registry by ID").option("--transport <type>", "Transport type: stdio, sse, streamable-http", "stdio").option("--url <url>", "URL for remote transports").option("--env <pairs...>", "Environment variables as KEY=VALUE pairs").option("--wizard", "Interactive setup wizard").option("--force", "Register even if duplicate command exists").description("Add a local MCP server").action(async (command, args, opts) => {
41497
+ program2.command("add").passThroughOptions().argument("[command]", "Command to run the MCP server").argument("[args...]", "Arguments for the command").option("--name <name>", "Display name for the server").option("--description <desc>", "Description").option("--from-registry <id>", "Install from official registry by ID").option("--transport <type>", "Transport type: stdio, sse, streamable-http", "stdio").option("--url <url>", "URL for remote transports").option("--env <pairs...>", "Environment variables as KEY=VALUE pairs").option("--credential-env <pairs...>", "Credential refs as KEY=ENV_VAR pairs").option("--credential-local <pairs...>", "Credential refs as KEY=LOCAL_VAULT_NAME pairs").option("--credential-hosted <pairs...>", "Credential refs as KEY=HOSTED_CREDENTIAL_ID pairs").option("--wizard", "Interactive setup wizard").option("--force", "Register even if duplicate command exists").option("--yes", "Approve registering local stdio commands").option("--allow-local-stdio", "Approve registering local stdio commands").option("--allow-risky-command", "Approve high-risk local command patterns").description("Add a local MCP server").action(async (command, args, opts) => {
40824
41498
  try {
40825
41499
  if (opts.fromRegistry) {
40826
41500
  console.log(chalk2.dim(`Installing "${opts.fromRegistry}" from registry...`));
40827
- const server2 = await installFromRegistry(opts.fromRegistry);
41501
+ const server2 = await installFromRegistry(opts.fromRegistry, {
41502
+ localCommandConsent: localConsentFromOptions(opts)
41503
+ });
40828
41504
  console.log(chalk2.green(`Added server: ${server2.name} [${server2.id}]`));
40829
41505
  console.log(chalk2.dim(` ${server2.command} ${server2.args.join(" ")}`));
40830
41506
  closeDb();
@@ -40863,6 +41539,13 @@ Server to add:`));
40863
41539
  console.log(` Name: ${wizardName}`);
40864
41540
  if (Object.keys(env).length)
40865
41541
  console.log(` Env: ${Object.keys(env).join(", ")}`);
41542
+ printLocalCommandReviewIfNeeded({
41543
+ command: wizardCommand,
41544
+ args: wizardArgs,
41545
+ env,
41546
+ transport,
41547
+ operation: "register"
41548
+ });
40866
41549
  const confirm = await new Promise((resolve2) => {
40867
41550
  const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
40868
41551
  rl2.question(chalk2.bold("Add this server? [Y/n]: "), (ans) => {
@@ -40875,13 +41558,21 @@ Server to add:`));
40875
41558
  closeDb();
40876
41559
  return;
40877
41560
  }
41561
+ assertLocalCommandConsent({
41562
+ command: wizardCommand,
41563
+ args: wizardArgs,
41564
+ env,
41565
+ transport,
41566
+ operation: "register"
41567
+ }, localConsentFromOptions(opts, true));
40878
41568
  const server2 = addServer({
40879
41569
  command: wizardCommand,
40880
41570
  args: wizardArgs,
40881
41571
  name: wizardName || undefined,
40882
41572
  description: wizardDescription || undefined,
40883
41573
  transport,
40884
- env
41574
+ env,
41575
+ credentialRefs: credentialRefsFromOptions(opts)
40885
41576
  });
40886
41577
  console.log(chalk2.green(`Added: ${server2.name} [${server2.id}]`));
40887
41578
  closeDb();
@@ -40911,6 +41602,14 @@ Server to add:`));
40911
41602
  envMap[key] = rest.join("=");
40912
41603
  }
40913
41604
  }
41605
+ const credentialRefs = credentialRefsFromOptions(opts);
41606
+ assertCliLocalCommandConsent({
41607
+ command,
41608
+ args,
41609
+ env: { ...envMap, ...Object.fromEntries(Object.keys(credentialRefs).map((key) => [key, "<credential-ref>"])) },
41610
+ transport: opts.transport,
41611
+ operation: "register"
41612
+ }, opts);
40914
41613
  const server = addServer({
40915
41614
  name: opts.name,
40916
41615
  description: opts.description,
@@ -40918,7 +41617,8 @@ Server to add:`));
40918
41617
  args,
40919
41618
  transport: opts.transport,
40920
41619
  url: opts.url,
40921
- env: envMap
41620
+ env: envMap,
41621
+ credentialRefs
40922
41622
  });
40923
41623
  console.log(chalk2.green(`Added server: ${server.name} [${server.id}]`));
40924
41624
  console.log(chalk2.dim(` ${server.command} ${server.args.join(" ")}`));
@@ -40933,29 +41633,44 @@ Server to add:`));
40933
41633
  }
40934
41634
  closeDb();
40935
41635
  });
40936
- program2.command("update-server").argument("<id>", "Server ID to update").description("Update fields of a registered server").option("--name <name>", "New display name").option("--description <desc>", "New description").option("--command <cmd>", "New command").option("--args <args...>", "New args list").option("--transport <type>", "New transport type").option("--url <url>", "New URL").action((id, opts) => {
40937
- const server = getServer(id);
40938
- if (!server) {
40939
- console.error(chalk2.red(`Server "${id}" not found.`));
41636
+ program2.command("update-server").argument("<id>", "Server ID to update").description("Update fields of a registered server").option("--name <name>", "New display name").option("--description <desc>", "New description").option("--command <cmd>", "New command").option("--args <args...>", "New args list").option("--transport <type>", "New transport type").option("--url <url>", "New URL").option("--yes", "Approve updated local stdio commands").option("--allow-local-stdio", "Approve updated local stdio commands").option("--allow-risky-command", "Approve high-risk local command patterns").action((id, opts) => {
41637
+ try {
41638
+ const server = getServer(id);
41639
+ if (!server) {
41640
+ console.error(chalk2.red(`Server "${id}" not found.`));
41641
+ closeDb();
41642
+ process.exit(1);
41643
+ }
41644
+ const fields = {};
41645
+ if (opts.name !== undefined)
41646
+ fields.name = opts.name;
41647
+ if (opts.description !== undefined)
41648
+ fields.description = opts.description;
41649
+ if (opts.command !== undefined)
41650
+ fields.command = opts.command;
41651
+ if (opts.args !== undefined)
41652
+ fields.args = opts.args;
41653
+ if (opts.transport !== undefined)
41654
+ fields.transport = opts.transport;
41655
+ if (opts.url !== undefined)
41656
+ fields.url = opts.url;
41657
+ if (fields.command !== undefined || fields.args !== undefined || fields.transport !== undefined) {
41658
+ assertCliLocalCommandConsent({
41659
+ command: fields.command ?? server.command,
41660
+ args: fields.args ?? server.args,
41661
+ env: server.env,
41662
+ transport: fields.transport ?? server.transport,
41663
+ operation: "register"
41664
+ }, opts);
41665
+ }
41666
+ const updated = updateServer(id, fields);
41667
+ console.log(chalk2.green(`Updated server: ${updated.name} [${updated.id}]`));
41668
+ closeDb();
41669
+ } catch (err) {
41670
+ console.error(chalk2.red(`Failed to update server: ${err.message}`));
40940
41671
  closeDb();
40941
41672
  process.exit(1);
40942
41673
  }
40943
- const fields = {};
40944
- if (opts.name !== undefined)
40945
- fields.name = opts.name;
40946
- if (opts.description !== undefined)
40947
- fields.description = opts.description;
40948
- if (opts.command !== undefined)
40949
- fields.command = opts.command;
40950
- if (opts.args !== undefined)
40951
- fields.args = opts.args;
40952
- if (opts.transport !== undefined)
40953
- fields.transport = opts.transport;
40954
- if (opts.url !== undefined)
40955
- fields.url = opts.url;
40956
- const updated = updateServer(id, fields);
40957
- console.log(chalk2.green(`Updated server: ${updated.name} [${updated.id}]`));
40958
- closeDb();
40959
41674
  });
40960
41675
  program2.command("clone").argument("<id>", "Server ID to clone").argument("<new-name>", "Name for the cloned server").description("Clone a server with a new name").action((id, newName) => {
40961
41676
  try {
@@ -41002,10 +41717,10 @@ program2.command("disable").argument("<id>", "Server ID to disable").description
41002
41717
  console.log(chalk2.yellow(`Disabled server: ${server.name}`));
41003
41718
  closeDb();
41004
41719
  });
41005
- program2.command("tools").argument("[server-id]", "Optional server ID to filter by").description("List tools (all or per server)").option("--connect", "Connect to servers to fetch live tools").action(async (serverId, opts) => {
41720
+ program2.command("tools").argument("[server-id]", "Optional server ID to filter by").description("List tools (all or per server)").option("--connect", "Connect to servers to fetch live tools").option("--yes", "Approve launching local stdio commands when --connect is used").option("--allow-local-stdio", "Approve launching local stdio commands when --connect is used").option("--allow-risky-command", "Approve high-risk local command patterns").action(async (serverId, opts) => {
41006
41721
  if (opts.connect) {
41007
41722
  console.log(chalk2.dim("Connecting to enabled servers..."));
41008
- await connectAllEnabled();
41723
+ await connectAllEnabled({ localCommandConsent: localConsentFromOptions(opts) });
41009
41724
  const tools2 = listAllTools();
41010
41725
  if (tools2.length === 0) {
41011
41726
  console.log(chalk2.dim("No tools available."));
@@ -41055,7 +41770,7 @@ ${total} tool(s) total.`));
41055
41770
  }
41056
41771
  closeDb();
41057
41772
  });
41058
- program2.command("call").argument("<tool>", "Tool name (server_id__tool_name)").option("--arg <pairs...>", "Arguments as key=value pairs").option("--json <json>", "Arguments as JSON string").description("Call a tool directly").action(async (tool, opts) => {
41773
+ program2.command("call").argument("<tool>", "Tool name (server_id__tool_name)").option("--arg <pairs...>", "Arguments as key=value pairs").option("--json <json>", "Arguments as JSON string").option("--yes", "Approve launching local stdio commands").option("--allow-local-stdio", "Approve launching local stdio commands").option("--allow-risky-command", "Approve high-risk local command patterns").description("Call a tool directly").action(async (tool, opts) => {
41059
41774
  let args = {};
41060
41775
  if (opts.json) {
41061
41776
  try {
@@ -41078,7 +41793,7 @@ program2.command("call").argument("<tool>", "Tool name (server_id__tool_name)").
41078
41793
  let exitCode = 0;
41079
41794
  try {
41080
41795
  console.log(chalk2.dim(`Connecting to servers...`));
41081
- await connectAllEnabled();
41796
+ await connectAllEnabled({ localCommandConsent: localConsentFromOptions(opts) });
41082
41797
  console.log(chalk2.dim(`Calling ${tool}...`));
41083
41798
  const result = await callTool(tool, args);
41084
41799
  for (const c of result.content) {
@@ -41102,20 +41817,24 @@ program2.command("info").argument("<id>", "Server ID").description("Show server
41102
41817
  closeDb();
41103
41818
  process.exit(1);
41104
41819
  }
41105
- console.log(chalk2.bold(server.name) + " " + chalk2.dim(`[${server.id}]`));
41106
- console.log(` Status: ${server.enabled ? chalk2.green("enabled") : chalk2.red("disabled")}`);
41107
- console.log(` Source: ${server.source}`);
41108
- console.log(` Transport: ${server.transport}`);
41109
- console.log(` Command: ${server.command} ${server.args.join(" ")}`);
41110
- if (server.url)
41111
- console.log(` URL: ${server.url}`);
41112
- if (server.description)
41113
- console.log(` Desc: ${server.description}`);
41114
- if (Object.keys(server.env).length > 0) {
41115
- console.log(` Env: ${Object.entries(server.env).map(([k, v]) => `${k}=${v}`).join(", ")}`);
41116
- }
41117
- console.log(` Created: ${server.created_at}`);
41118
- console.log(` Updated: ${server.updated_at}`);
41820
+ const safeServer = redactServerCredentials(server);
41821
+ console.log(chalk2.bold(safeServer.name) + " " + chalk2.dim(`[${safeServer.id}]`));
41822
+ console.log(` Status: ${safeServer.enabled ? chalk2.green("enabled") : chalk2.red("disabled")}`);
41823
+ console.log(` Source: ${safeServer.source}`);
41824
+ console.log(` Transport: ${safeServer.transport}`);
41825
+ console.log(` Command: ${safeServer.command} ${safeServer.args.join(" ")}`);
41826
+ if (safeServer.url)
41827
+ console.log(` URL: ${safeServer.url}`);
41828
+ if (safeServer.description)
41829
+ console.log(` Desc: ${safeServer.description}`);
41830
+ if (Object.keys(safeServer.env).length > 0) {
41831
+ console.log(` Env: ${Object.entries(safeServer.env).map(([k, v]) => `${k}=${v}`).join(", ")}`);
41832
+ }
41833
+ if (Object.keys(safeServer.credentialRefs ?? {}).length > 0) {
41834
+ console.log(` Cred refs: ${Object.entries(safeServer.credentialRefs ?? {}).map(([k, ref]) => `${k}=${formatCredentialRef(ref)}`).join(", ")}`);
41835
+ }
41836
+ console.log(` Created: ${safeServer.created_at}`);
41837
+ console.log(` Updated: ${safeServer.updated_at}`);
41119
41838
  const cached2 = getCachedTools(id);
41120
41839
  if (cached2.length > 0) {
41121
41840
  console.log(chalk2.bold(`
@@ -41146,7 +41865,7 @@ program2.command("status").description("Show registry stats").option("--json", "
41146
41865
  console.log(` Tools: ${totalTools} (cached)`);
41147
41866
  closeDb();
41148
41867
  });
41149
- program2.command("doctor").argument("[id]", "Server ID to check (omit to check all)").description("Diagnose server health \u2014 checks PATH, env vars, connectivity").option("--fix", "Attempt to fix issues automatically").action(async (id, opts) => {
41868
+ program2.command("doctor").argument("[id]", "Server ID to check (omit to check all)").description("Diagnose server health \u2014 checks PATH, env vars, connectivity").option("--fix", "Attempt to fix issues automatically").option("--yes", "Approve probing and launching local stdio commands").option("--allow-local-stdio", "Approve probing and launching local stdio commands").option("--allow-risky-command", "Approve high-risk local command patterns").action(async (id, opts) => {
41150
41869
  const { execFileSync: execFileSync22 } = await import("child_process");
41151
41870
  const servers = id ? [getServer(id)].filter(Boolean) : listServers();
41152
41871
  if (servers.length === 0) {
@@ -41158,7 +41877,7 @@ program2.command("doctor").argument("[id]", "Server ID to check (omit to check a
41158
41877
  for (const server of servers) {
41159
41878
  console.log(chalk2.bold(`
41160
41879
  ${server.name} [${server.id}]`));
41161
- const report = await diagnoseServer(server);
41880
+ const report = await diagnoseServer(server, { localCommandConsent: localConsentFromOptions(opts) });
41162
41881
  for (const check2 of report.checks) {
41163
41882
  const icon = check2.pass ? chalk2.green("\u2713") : chalk2.red("\u2717");
41164
41883
  console.log(` ${icon} ${check2.name}: ${chalk2.dim(check2.message)}`);
@@ -41244,7 +41963,7 @@ program2.command("update").description("Update mcps to the latest version").acti
41244
41963
  process.exit(1);
41245
41964
  }
41246
41965
  });
41247
- program2.command("find").argument("[query]", "Search query (omit to list all from awesome list)").description("Find MCP servers across npm, GitHub, official registry, and awesome lists").option("--source <sources...>", "Source IDs to search (see `mcps sources list`)").option("--limit <n>", "Max results per source", "20").option("--awesome", "List curated servers from punkpeye/awesome-mcp-servers").option("--json", "Output as JSON").option("--install", "After showing results, prompt to select one and install it").option("--yes", "Auto-install without prompting (only when there is exactly 1 result)").option("--no-cache", "Bypass source cache and fetch fresh results").action(async (query, opts) => {
41966
+ program2.command("find").argument("[query]", "Search query (omit to list all from awesome list)").description("Find MCP servers across npm, GitHub, official registry, and awesome lists").option("--source <sources...>", "Source IDs to search (see `mcps sources list`)").option("--limit <n>", "Max results per source", "20").option("--awesome", "List curated servers from punkpeye/awesome-mcp-servers").option("--json", "Output as JSON").option("--install", "After showing results, prompt to select one and install it").option("--yes", "Auto-install without prompting (only when there is exactly 1 result)").option("--allow-risky-command", "Approve high-risk local command patterns").option("--no-cache", "Bypass source cache and fetch fresh results").action(async (query, opts) => {
41248
41967
  try {
41249
41968
  if (opts.awesome) {
41250
41969
  console.log(chalk2.dim("Fetching curated awesome-mcp-servers list..."));
@@ -41354,6 +42073,14 @@ Enter number to install (1-${results.length}), or 0 to cancel: `), resolve2);
41354
42073
  return;
41355
42074
  }
41356
42075
  console.log(chalk2.dim(`Installing ${chosen.name}...`));
42076
+ const localCommandInput = {
42077
+ command: "npx",
42078
+ args: ["-y", pkg],
42079
+ env: {},
42080
+ transport: "stdio",
42081
+ operation: "install"
42082
+ };
42083
+ const localCommandConsent = assertCliLocalCommandConsent(localCommandInput, opts, true);
41357
42084
  const server = addServer({
41358
42085
  command: "npx",
41359
42086
  args: ["-y", pkg],
@@ -41361,7 +42088,7 @@ Enter number to install (1-${results.length}), or 0 to cancel: `), resolve2);
41361
42088
  description: chosen.description,
41362
42089
  transport: "stdio"
41363
42090
  });
41364
- const results2 = installToAgents(server, ["claude", "codex", "gemini"]);
42091
+ const results2 = installToAgents(server, ["claude", "codex", "gemini"], { localCommandConsent });
41365
42092
  for (const r of results2) {
41366
42093
  if (r.success) {
41367
42094
  console.log(chalk2.green(` \u2713 ${r.agent}`));
@@ -41506,7 +42233,7 @@ sourcesCmd.command("test").argument("<id>", "Source ID to test").description("Te
41506
42233
  }
41507
42234
  closeDb();
41508
42235
  });
41509
- program2.command("install").argument("[id]", "Server ID (from `mcps list`) to install into AI agents").description("Install a registered MCP server into Claude Code, Codex, and/or Gemini").option("--claude", "Install to Claude Code").option("--codex", "Install to Codex").option("--gemini", "Install to Gemini").option("--all", "Install to all agents (default if none specified)").option("--to <agents...>", "Agents to install to: claude, codex, gemini").option("--from-registry <id>", "Add from official registry and install in one step").option("--npm <package>", "Add an npm package as a server and install in one step").action(async (id, opts) => {
42236
+ program2.command("install").argument("[id]", "Server ID (from `mcps list`) to install into AI agents").description("Install a registered MCP server into Claude Code, Codex, and/or Gemini").option("--claude", "Install to Claude Code").option("--codex", "Install to Codex").option("--gemini", "Install to Gemini").option("--all", "Install to all agents (default if none specified)").option("--to <agents...>", "Agents to install to: claude, codex, gemini").option("--from-registry <id>", "Add from official registry and install in one step").option("--npm <package>", "Add an npm package as a server and install in one step").option("--yes", "Approve installing local stdio commands into agents").option("--allow-local-stdio", "Approve installing local stdio commands into agents").option("--allow-risky-command", "Approve high-risk local command patterns").action(async (id, opts) => {
41510
42237
  const targets = [];
41511
42238
  if (opts.to) {
41512
42239
  for (const t of opts.to) {
@@ -41532,7 +42259,9 @@ program2.command("install").argument("[id]", "Server ID (from `mcps list`) to in
41532
42259
  if (opts.fromRegistry) {
41533
42260
  console.log(chalk2.dim(`Installing "${opts.fromRegistry}" from registry...`));
41534
42261
  try {
41535
- server = await installFromRegistry(opts.fromRegistry);
42262
+ server = await installFromRegistry(opts.fromRegistry, {
42263
+ localCommandConsent: localConsentFromOptions(opts)
42264
+ });
41536
42265
  console.log(chalk2.green(`Added server: ${server.name} [${server.id}]`));
41537
42266
  } catch (err) {
41538
42267
  console.error(chalk2.red(`Failed to install from registry: ${err.message}`));
@@ -41541,6 +42270,13 @@ program2.command("install").argument("[id]", "Server ID (from `mcps list`) to in
41541
42270
  }
41542
42271
  } else if (opts.npm) {
41543
42272
  const pkg = opts.npm;
42273
+ assertCliLocalCommandConsent({
42274
+ command: "npx",
42275
+ args: ["-y", pkg],
42276
+ env: {},
42277
+ transport: "stdio",
42278
+ operation: "install"
42279
+ }, opts);
41544
42280
  server = addServer({ command: "npx", args: ["-y", pkg], name: pkg, transport: "stdio" });
41545
42281
  console.log(chalk2.green(`Added server: ${server.name} [${server.id}]`));
41546
42282
  } else {
@@ -41557,7 +42293,7 @@ program2.command("install").argument("[id]", "Server ID (from `mcps list`) to in
41557
42293
  }
41558
42294
  }
41559
42295
  console.log(chalk2.dim(`Installing "${server.name}" to: ${targets.join(", ")}...`));
41560
- const results = installToAgents(server, targets);
42296
+ const results = installToAgents(server, targets, { localCommandConsent: localConsentFromOptions(opts) });
41561
42297
  for (const r of results) {
41562
42298
  if (r.success) {
41563
42299
  console.log(chalk2.green(` \u2713 ${r.agent}`));
@@ -41738,7 +42474,7 @@ fleetCmd.command("install").argument("[machineIds...]", "Optional machine IDs to
41738
42474
  }
41739
42475
  });
41740
42476
  program2.command("export").description("Export all servers and sources to a JSON file").option("--file <path>", "Output file path", `${process.env.HOME ?? "~"}/.hasna/mcps/export.json`).option("--stdout", "Write to stdout instead of a file").action((opts) => {
41741
- const servers = listServers();
42477
+ const servers = listServers().map(redactServerCredentials);
41742
42478
  const sources = listSources();
41743
42479
  const payload = {
41744
42480
  version: 1,
@@ -41758,7 +42494,7 @@ program2.command("export").description("Export all servers and sources to a JSON
41758
42494
  program2.command("import").argument("<file>", "Path to the export JSON file").description("Import servers and sources from a JSON export file").option("--overwrite", "Overwrite existing entries with matching IDs").action((file, opts) => {
41759
42495
  let payload;
41760
42496
  try {
41761
- payload = JSON.parse(readFileSync6(file, "utf-8"));
42497
+ payload = JSON.parse(readFileSync7(file, "utf-8"));
41762
42498
  } catch (err) {
41763
42499
  console.error(chalk2.red(`Failed to read file: ${err.message}`));
41764
42500
  closeDb();
@@ -41780,7 +42516,23 @@ program2.command("import").argument("<file>", "Path to the export JSON file").de
41780
42516
  serversSkipped++;
41781
42517
  continue;
41782
42518
  }
41783
- db2.run(`INSERT ${orReplace} INTO servers (id, name, description, command, args, env, transport, url, source, enabled, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`, [s.id, s.name, s.description, s.command, JSON.stringify(s.args ?? []), JSON.stringify(s.env ?? {}), s.transport, s.url, s.source, s.enabled ? 1 : 0, s.created_at, s.updated_at]);
42519
+ const literalEnv = normalizeLiteralEnv(s.env ?? {});
42520
+ const credentialRefs = normalizeCredentialRefs(s.credentialRefs ?? s.credential_refs ?? {});
42521
+ db2.run(`INSERT ${orReplace} INTO servers (id, name, description, command, args, env, credential_refs, transport, url, source, enabled, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`, [
42522
+ s.id,
42523
+ s.name,
42524
+ s.description,
42525
+ s.command,
42526
+ JSON.stringify(s.args ?? []),
42527
+ JSON.stringify(literalEnv),
42528
+ JSON.stringify(credentialRefs),
42529
+ s.transport,
42530
+ s.url,
42531
+ s.source,
42532
+ s.enabled ? 1 : 0,
42533
+ s.created_at,
42534
+ s.updated_at
42535
+ ]);
41784
42536
  serversImported++;
41785
42537
  }
41786
42538
  let sourcesImported = 0;
@@ -41806,14 +42558,17 @@ envCmd.command("list").argument("<id>").description("List env vars for a server"
41806
42558
  closeDb();
41807
42559
  process.exit(1);
41808
42560
  }
41809
- const entries = Object.entries(server.env);
41810
- if (entries.length === 0) {
41811
- console.log(chalk2.dim("No env vars set."));
42561
+ const envEntries = Object.entries(redactEnv(server.env));
42562
+ const refEntries = Object.entries(server.credentialRefs ?? {});
42563
+ if (envEntries.length === 0 && refEntries.length === 0) {
42564
+ console.log(chalk2.dim("No env vars or credential refs set."));
41812
42565
  closeDb();
41813
42566
  return;
41814
42567
  }
41815
- for (const [k, v] of entries)
42568
+ for (const [k, v] of envEntries)
41816
42569
  console.log(` ${chalk2.bold(k)}=${chalk2.dim(v)}`);
42570
+ for (const [k, ref] of refEntries)
42571
+ console.log(` ${chalk2.bold(k)}=${chalk2.dim(formatCredentialRef(ref))}`);
41817
42572
  closeDb();
41818
42573
  });
41819
42574
  envCmd.command("set").argument("<id>").argument("<pair>", "KEY=VALUE").description("Set an env var").action((id, pair) => {
@@ -41835,6 +42590,36 @@ envCmd.command("set").argument("<id>").argument("<pair>", "KEY=VALUE").descripti
41835
42590
  }
41836
42591
  closeDb();
41837
42592
  });
42593
+ envCmd.command("ref").argument("<id>").argument("<pair>", "KEY=NAME").description("Set a credential reference for a server env key").option("--source <source>", "Credential source: env, local-vault, hosted", "env").action((id, pair, opts) => {
42594
+ const source = opts.source;
42595
+ if (source !== "env" && source !== "local-vault" && source !== "hosted") {
42596
+ console.error(chalk2.red("Source must be one of: env, local-vault, hosted"));
42597
+ closeDb();
42598
+ process.exit(1);
42599
+ }
42600
+ try {
42601
+ const refs = parseCredentialPairs([pair], source);
42602
+ const [key, ref] = Object.entries(refs)[0];
42603
+ setServerCredentialRef(id, key, ref);
42604
+ console.log(chalk2.green(`Set credential ref ${key}=${formatCredentialRef(ref)} on ${id}`));
42605
+ } catch (err) {
42606
+ console.error(chalk2.red(err.message));
42607
+ closeDb();
42608
+ process.exit(1);
42609
+ }
42610
+ closeDb();
42611
+ });
42612
+ envCmd.command("unset-ref").argument("<id>").argument("<key>").description("Remove a credential reference").action((id, key) => {
42613
+ try {
42614
+ unsetServerCredentialRef(id, key);
42615
+ console.log(chalk2.green(`Unset credential ref ${key} on ${id}`));
42616
+ } catch (err) {
42617
+ console.error(chalk2.red(err.message));
42618
+ closeDb();
42619
+ process.exit(1);
42620
+ }
42621
+ closeDb();
42622
+ });
41838
42623
  envCmd.command("unset").argument("<id>").argument("<key>").description("Remove an env var").action((id, key) => {
41839
42624
  try {
41840
42625
  unsetServerEnv(id, key);