@hasna/mcps 0.0.14 → 0.0.15

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
@@ -19412,7 +19412,7 @@ var init_sources = __esm(() => {
19412
19412
  var require_package = __commonJS((exports, module) => {
19413
19413
  module.exports = {
19414
19414
  name: "@hasna/mcps",
19415
- version: "0.0.14",
19415
+ version: "0.0.15",
19416
19416
  description: "Meta-MCP registry & CLI \u2014 discover, manage, and proxy MCP servers",
19417
19417
  type: "module",
19418
19418
  repository: {
@@ -35591,6 +35591,185 @@ class StreamableHTTPClientTransport {
35591
35591
  // src/lib/proxy.ts
35592
35592
  init_config2();
35593
35593
  init_db();
35594
+
35595
+ // src/lib/local-command-consent.ts
35596
+ class LocalCommandConsentError extends Error {
35597
+ review;
35598
+ constructor(message, review) {
35599
+ super(message);
35600
+ this.name = "LocalCommandConsentError";
35601
+ this.review = review;
35602
+ }
35603
+ }
35604
+ var SHELL_COMMANDS = new Set(["bash", "sh", "zsh", "fish", "cmd", "cmd.exe", "powershell", "powershell.exe", "pwsh"]);
35605
+ var DESTRUCTIVE_COMMANDS = new Set([
35606
+ "rm",
35607
+ "dd",
35608
+ "mkfs",
35609
+ "shutdown",
35610
+ "reboot",
35611
+ "poweroff",
35612
+ "halt",
35613
+ "killall",
35614
+ "pkill"
35615
+ ]);
35616
+ var SHELL_EVAL_FLAGS = new Set(["-c", "/c", "-Command", "-command", "-EncodedCommand", "-encodedcommand"]);
35617
+ var SECRET_FLAG_PATTERN = /(?:^|[-_])(api[-_]?key|token|secret|password|passwd|credential|auth|private[-_]?key)(?:$|[-_])/i;
35618
+ var SECRET_KEY_PATTERN = /(?:^|[_-])(api[_-]?key|token|secret|password|passwd|credential|auth|private[_-]?key)(?:$|[_-])/i;
35619
+ 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_-]+)$/;
35620
+ var SHELL_META_PATTERN = /[;&|`<>]|\$\(/;
35621
+ function commandBase(command) {
35622
+ return command.trim().split(/[\\/]/).pop()?.toLowerCase() || command.trim().toLowerCase();
35623
+ }
35624
+ function normalizeArgs(args) {
35625
+ return (args ?? []).map((arg) => String(arg));
35626
+ }
35627
+ function isSecretKey(key) {
35628
+ return SECRET_KEY_PATTERN.test(key) || SECRET_FLAG_PATTERN.test(key);
35629
+ }
35630
+ function isSecretValue(value) {
35631
+ return SECRET_VALUE_PATTERN.test(value.trim());
35632
+ }
35633
+ function isSecretAssignment(arg) {
35634
+ const eqIdx = arg.indexOf("=");
35635
+ if (eqIdx <= 0)
35636
+ return false;
35637
+ const key = arg.slice(0, eqIdx);
35638
+ const value = arg.slice(eqIdx + 1);
35639
+ return isSecretKey(key) || isSecretValue(value);
35640
+ }
35641
+ function isSecretArg(args, index) {
35642
+ const arg = args[index] ?? "";
35643
+ const previous = args[index - 1] ?? "";
35644
+ return isSecretAssignment(arg) || isSecretValue(arg) || SECRET_FLAG_PATTERN.test(previous);
35645
+ }
35646
+ function quoteArg(value) {
35647
+ return JSON.stringify(value);
35648
+ }
35649
+ function displayCommand(command, args) {
35650
+ return [quoteArg(command), ...args.map((arg, index) => quoteArg(isSecretArg(args, index) ? "<redacted>" : arg))].join(" ");
35651
+ }
35652
+ function pushRisk(risks, risk) {
35653
+ if (risks.some((existing) => existing.code === risk.code && existing.evidence === risk.evidence))
35654
+ return;
35655
+ risks.push(risk);
35656
+ }
35657
+ function inspectRisks(command, args, env) {
35658
+ const risks = [];
35659
+ const base = commandBase(command);
35660
+ const joined = [command, ...args].join(" ");
35661
+ if (SHELL_COMMANDS.has(base)) {
35662
+ pushRisk(risks, {
35663
+ code: "shell_interpreter",
35664
+ severity: "warning",
35665
+ message: "Command launches a shell interpreter.",
35666
+ evidence: base
35667
+ });
35668
+ if (args.some((arg) => SHELL_EVAL_FLAGS.has(arg))) {
35669
+ pushRisk(risks, {
35670
+ code: "shell_eval",
35671
+ severity: "danger",
35672
+ message: "Shell command evaluates an inline script.",
35673
+ evidence: base
35674
+ });
35675
+ }
35676
+ }
35677
+ if (base === "sudo") {
35678
+ pushRisk(risks, {
35679
+ code: "privilege_escalation",
35680
+ severity: "danger",
35681
+ message: "Command requests elevated privileges.",
35682
+ evidence: base
35683
+ });
35684
+ }
35685
+ if (DESTRUCTIVE_COMMANDS.has(base) || /\brm\s+-[^\s]*[rf][^\s]*\b/.test(joined) || /--no-preserve-root\b/.test(joined)) {
35686
+ pushRisk(risks, {
35687
+ code: "destructive_command",
35688
+ severity: "danger",
35689
+ message: "Command includes a destructive system operation.",
35690
+ evidence: base
35691
+ });
35692
+ }
35693
+ if (/\b(curl|wget)\b[\s\S]*\|[\s\S]*\b(sh|bash|zsh|fish)\b/.test(joined)) {
35694
+ pushRisk(risks, {
35695
+ code: "download_pipe_shell",
35696
+ severity: "danger",
35697
+ message: "Command downloads remote content and pipes it to a shell."
35698
+ });
35699
+ }
35700
+ if ([command, ...args].some((part) => SHELL_META_PATTERN.test(part))) {
35701
+ pushRisk(risks, {
35702
+ code: "shell_metacharacters",
35703
+ severity: "warning",
35704
+ message: "Command or arguments contain shell metacharacters."
35705
+ });
35706
+ }
35707
+ if (args.some((arg, index) => isSecretArg(args, index))) {
35708
+ pushRisk(risks, {
35709
+ code: "inline_secret",
35710
+ severity: "danger",
35711
+ message: "Command arguments appear to contain inline secret material."
35712
+ });
35713
+ }
35714
+ const secretEnvKeys = Object.keys(env).filter(isSecretKey).sort();
35715
+ if (secretEnvKeys.length > 0) {
35716
+ pushRisk(risks, {
35717
+ code: "secret_env",
35718
+ severity: "warning",
35719
+ message: "Environment contains secret-like keys; values are redacted from consent output.",
35720
+ evidence: secretEnvKeys.join(", ")
35721
+ });
35722
+ }
35723
+ return risks;
35724
+ }
35725
+ function inspectLocalCommand(input) {
35726
+ const args = normalizeArgs(input.args);
35727
+ const env = input.env ?? {};
35728
+ const transport = input.transport ?? "stdio";
35729
+ const risks = inspectRisks(input.command, args, env);
35730
+ return {
35731
+ requiresConsent: transport === "stdio",
35732
+ operation: input.operation ?? "launch",
35733
+ command: input.command,
35734
+ args,
35735
+ displayCommand: displayCommand(input.command, args),
35736
+ envKeys: Object.keys(env).sort(),
35737
+ risks,
35738
+ hasDangerousRisk: risks.some((risk) => risk.severity === "danger")
35739
+ };
35740
+ }
35741
+ function formatLocalCommandReview(review) {
35742
+ const lines = [
35743
+ `Command: ${review.displayCommand}`,
35744
+ review.envKeys.length > 0 ? `Env keys: ${review.envKeys.join(", ")}` : "Env keys: <none>"
35745
+ ];
35746
+ if (review.risks.length > 0) {
35747
+ lines.push("Risks:");
35748
+ for (const risk of review.risks) {
35749
+ lines.push(`- ${risk.severity}: ${risk.code} - ${risk.message}${risk.evidence ? ` (${risk.evidence})` : ""}`);
35750
+ }
35751
+ } else {
35752
+ lines.push("Risks: none detected");
35753
+ }
35754
+ return lines.join(`
35755
+ `);
35756
+ }
35757
+ function assertLocalCommandConsent(input, consent = {}) {
35758
+ const review = inspectLocalCommand(input);
35759
+ if (!review.requiresConsent)
35760
+ return review;
35761
+ if (consent.approved !== true) {
35762
+ throw new LocalCommandConsentError(`local stdio command approval is required before ${review.operation}.
35763
+ ${formatLocalCommandReview(review)}`, review);
35764
+ }
35765
+ if (review.hasDangerousRisk && consent.allowRisky !== true) {
35766
+ throw new LocalCommandConsentError(`risky command approval is required before ${review.operation}.
35767
+ ${formatLocalCommandReview(review)}`, review);
35768
+ }
35769
+ return review;
35770
+ }
35771
+
35772
+ // src/lib/proxy.ts
35594
35773
  var connections = new Map;
35595
35774
  var inflightConnections = new Map;
35596
35775
  var CONNECT_CONCURRENCY = 4;
@@ -35617,7 +35796,7 @@ function requireUrl(entry) {
35617
35796
  throw new Error(`Server "${entry.id}" has an invalid URL: ${entry.url}`);
35618
35797
  }
35619
35798
  }
35620
- async function connectToServer(entry) {
35799
+ async function connectToServer(entry, options = {}) {
35621
35800
  if (connections.has(entry.id)) {
35622
35801
  return connections.get(entry.id);
35623
35802
  }
@@ -35633,6 +35812,13 @@ async function connectToServer(entry) {
35633
35812
  if (!entry.command?.trim()) {
35634
35813
  throw new Error(`Server "${entry.id}" is missing a command`);
35635
35814
  }
35815
+ assertLocalCommandConsent({
35816
+ command: entry.command,
35817
+ args: entry.args,
35818
+ env: entry.env,
35819
+ transport: entry.transport,
35820
+ operation: "launch"
35821
+ }, options.localCommandConsent);
35636
35822
  transport = new StdioClientTransport({
35637
35823
  command: entry.command,
35638
35824
  args: entry.args,
@@ -35749,7 +35935,7 @@ async function callTool(prefixedName, args) {
35749
35935
  ]
35750
35936
  };
35751
35937
  }
35752
- async function connectAllEnabled() {
35938
+ async function connectAllEnabled(options = {}) {
35753
35939
  const servers = listServers().filter((s) => s.enabled);
35754
35940
  const results = [];
35755
35941
  let index = 0;
@@ -35761,7 +35947,7 @@ async function connectAllEnabled() {
35761
35947
  return;
35762
35948
  const server = servers[current];
35763
35949
  try {
35764
- const conn = await connectToServer(server);
35950
+ const conn = await connectToServer(server, options);
35765
35951
  results.push(conn);
35766
35952
  } catch (err) {
35767
35953
  console.error(`Failed to connect to ${server.name}: ${err.message}`);
@@ -35773,13 +35959,28 @@ async function connectAllEnabled() {
35773
35959
  }
35774
35960
 
35775
35961
  // src/lib/doctor.ts
35776
- async function diagnoseServer(server) {
35962
+ async function diagnoseServer(server, options = {}) {
35777
35963
  const checks4 = [];
35964
+ let hasLocalConsent = true;
35778
35965
  if (server.transport === "stdio") {
35966
+ try {
35967
+ assertLocalCommandConsent({
35968
+ command: server.command,
35969
+ args: server.args,
35970
+ env: server.env,
35971
+ transport: server.transport,
35972
+ operation: "diagnose"
35973
+ }, options.localCommandConsent);
35974
+ } catch (err) {
35975
+ hasLocalConsent = false;
35976
+ checks4.push({ name: "local command consent", pass: false, message: err.message });
35977
+ }
35779
35978
  try {
35780
35979
  const path = execFileSync("which", [server.command], { stdio: "pipe" }).toString().trim();
35781
35980
  let version2 = "";
35782
35981
  try {
35982
+ if (!hasLocalConsent)
35983
+ throw new Error("local stdio command approval is required before version probing");
35783
35984
  version2 = execFileSync(server.command, ["--version"], { stdio: "pipe" }).toString().trim().split(`
35784
35985
  `)[0];
35785
35986
  } catch {}
@@ -35810,10 +36011,10 @@ async function diagnoseServer(server) {
35810
36011
  checks4.push({ name: "URL reachable", pass: false, message: `unreachable: ${err.message}` });
35811
36012
  }
35812
36013
  }
35813
- if (server.enabled) {
36014
+ if (server.enabled && hasLocalConsent) {
35814
36015
  try {
35815
36016
  await Promise.race([
35816
- connectToServer(server),
36017
+ connectToServer(server, { localCommandConsent: options.localCommandConsent }),
35817
36018
  new Promise((_, reject) => setTimeout(() => reject(new Error("timeout after 10s")), 1e4))
35818
36019
  ]);
35819
36020
  await disconnectServer(server.id);
@@ -35821,8 +36022,10 @@ async function diagnoseServer(server) {
35821
36022
  } catch (err) {
35822
36023
  checks4.push({ name: "connect & list tools", pass: false, message: err.message });
35823
36024
  }
35824
- } else {
36025
+ } else if (!server.enabled) {
35825
36026
  checks4.push({ name: "connect & list tools", pass: true, message: "skipped (server disabled)" });
36027
+ } else {
36028
+ checks4.push({ name: "connect & list tools", pass: false, message: "skipped until local stdio command is approved" });
35826
36029
  }
35827
36030
  return {
35828
36031
  server,
@@ -35863,7 +36066,7 @@ async function getRegistryServer(id) {
35863
36066
  const all = entries.map(parseRegistryEntry);
35864
36067
  return all.find((s) => s.id === id) || null;
35865
36068
  }
35866
- async function installFromRegistry(id) {
36069
+ async function installFromRegistry(id, options = {}) {
35867
36070
  const server = await getRegistryServer(id);
35868
36071
  if (!server) {
35869
36072
  throw new Error(`Server "${id}" not found in registry`);
@@ -35883,6 +36086,13 @@ async function installFromRegistry(id) {
35883
36086
  transport = pkg.transport.type;
35884
36087
  }
35885
36088
  }
36089
+ assertLocalCommandConsent({
36090
+ command,
36091
+ args,
36092
+ transport,
36093
+ env: {},
36094
+ operation: "register"
36095
+ }, options.localCommandConsent);
35886
36096
  return addServer({
35887
36097
  name: server.name,
35888
36098
  description: server.description,
@@ -35977,7 +36187,22 @@ function installToGemini(entry) {
35977
36187
  return { agent: "gemini", success: false, error: err.message };
35978
36188
  }
35979
36189
  }
35980
- function installToAgents(entry, targets = ["claude", "codex", "gemini"]) {
36190
+ function installToAgents(entry, targets = ["claude", "codex", "gemini"], options = {}) {
36191
+ try {
36192
+ assertLocalCommandConsent({
36193
+ command: entry.command,
36194
+ args: entry.args,
36195
+ env: entry.env,
36196
+ transport: entry.transport,
36197
+ operation: "install"
36198
+ }, options.localCommandConsent);
36199
+ } catch (err) {
36200
+ return targets.map((target) => ({
36201
+ agent: target,
36202
+ success: false,
36203
+ error: err.message
36204
+ }));
36205
+ }
35981
36206
  return targets.map((target) => {
35982
36207
  if (target === "claude")
35983
36208
  return installToClaude(entry);
@@ -36978,11 +37203,19 @@ function installProviderProfile(id, options = {}) {
36978
37203
  if (!command) {
36979
37204
  throw new Error(`Provider profile "${id}" does not define an install fallback command`);
36980
37205
  }
37206
+ assertLocalCommandConsent({
37207
+ command,
37208
+ args,
37209
+ env: fallback?.env ?? {},
37210
+ transport: useFallback ? "stdio" : profile.transport,
37211
+ operation: "register"
37212
+ }, options.localCommandConsent);
36981
37213
  return addServer({
36982
37214
  name: options.name ?? profile.displayName,
36983
37215
  description: profile.description ?? undefined,
36984
37216
  command,
36985
37217
  args,
37218
+ env: fallback?.env,
36986
37219
  transport: useFallback ? "stdio" : profile.transport,
36987
37220
  url: useFallback ? fallback?.url : profile.endpoint ?? undefined,
36988
37221
  source: "provider-profile"
@@ -38257,6 +38490,13 @@ function jsonContent(value) {
38257
38490
  function errorContent(text) {
38258
38491
  return { ...textContent(text), isError: true };
38259
38492
  }
38493
+ function localConsent(input) {
38494
+ return {
38495
+ approved: input.allow_local_stdio === true,
38496
+ allowRisky: input.allow_risky_command === true,
38497
+ source: "mcp"
38498
+ };
38499
+ }
38260
38500
  function buildMcpTools() {
38261
38501
  const definitions = [
38262
38502
  {
@@ -38281,23 +38521,46 @@ function buildMcpTools() {
38281
38521
  description: exports_external2.string().optional().describe("Description"),
38282
38522
  transport: exports_external2.enum(["stdio", "sse", "streamable-http"]).optional().describe("Transport type"),
38283
38523
  url: exports_external2.string().optional().describe("URL for remote transports"),
38284
- env: exports_external2.record(exports_external2.string()).optional().describe("Environment variables")
38524
+ env: exports_external2.record(exports_external2.string()).optional().describe("Environment variables"),
38525
+ allow_local_stdio: exports_external2.boolean().optional().describe("Approve registering this local stdio command"),
38526
+ allow_risky_command: exports_external2.boolean().optional().describe("Approve registering risky local command patterns")
38285
38527
  },
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
- }))
38528
+ run: (input) => {
38529
+ const command = String(input.command);
38530
+ const args = Array.isArray(input.args) ? input.args.map(String) : [];
38531
+ const env = isRecordOfStrings(input.env) ? input.env : {};
38532
+ const transport = input.transport;
38533
+ try {
38534
+ assertLocalCommandConsent({ command, args, env, transport, operation: "register" }, localConsent(input));
38535
+ return jsonContent(addServer({
38536
+ command,
38537
+ args,
38538
+ name: typeof input.name === "string" ? input.name : undefined,
38539
+ description: typeof input.description === "string" ? input.description : undefined,
38540
+ transport,
38541
+ url: typeof input.url === "string" ? input.url : undefined,
38542
+ env
38543
+ }));
38544
+ } catch (err) {
38545
+ return errorContent(err.message);
38546
+ }
38547
+ }
38295
38548
  },
38296
38549
  {
38297
38550
  name: "install_from_registry",
38298
38551
  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)))
38552
+ paramsSchema: {
38553
+ id: exports_external2.string().describe("Registry server ID"),
38554
+ allow_local_stdio: exports_external2.boolean().optional().describe("Approve registering registry stdio commands"),
38555
+ allow_risky_command: exports_external2.boolean().optional().describe("Approve registering risky local command patterns")
38556
+ },
38557
+ run: async (input) => {
38558
+ try {
38559
+ return jsonContent(await installFromRegistry(String(input.id), { localCommandConsent: localConsent(input) }));
38560
+ } catch (err) {
38561
+ return errorContent(err.message);
38562
+ }
38563
+ }
38301
38564
  },
38302
38565
  {
38303
38566
  name: "remove_server",
@@ -38343,26 +38606,41 @@ function buildMcpTools() {
38343
38606
  command: exports_external2.string().optional().describe("New command"),
38344
38607
  args: exports_external2.array(exports_external2.string()).optional().describe("New args list"),
38345
38608
  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")
38609
+ url: exports_external2.string().optional().describe("New URL for remote transports"),
38610
+ allow_local_stdio: exports_external2.boolean().optional().describe("Approve updating this local stdio command"),
38611
+ allow_risky_command: exports_external2.boolean().optional().describe("Approve risky local command patterns")
38347
38612
  },
38348
- run: ({ id, name, description, command, args, transport, url: url2 }) => {
38349
- const serverId = String(id);
38613
+ run: (input) => {
38614
+ const serverId = String(input.id);
38350
38615
  const existing = getServer(serverId);
38351
38616
  if (!existing)
38352
38617
  return errorContent(`Server "${serverId}" not found.`);
38353
38618
  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;
38619
+ if (typeof input.name === "string")
38620
+ fields.name = input.name;
38621
+ if (typeof input.description === "string")
38622
+ fields.description = input.description;
38623
+ if (typeof input.command === "string")
38624
+ fields.command = input.command;
38625
+ if (Array.isArray(input.args))
38626
+ fields.args = input.args.map(String);
38627
+ if (input.transport === "stdio" || input.transport === "sse" || input.transport === "streamable-http")
38628
+ fields.transport = input.transport;
38629
+ if (typeof input.url === "string")
38630
+ fields.url = input.url;
38631
+ if (fields.command !== undefined || fields.args !== undefined || fields.transport !== undefined) {
38632
+ try {
38633
+ assertLocalCommandConsent({
38634
+ command: fields.command ?? existing.command,
38635
+ args: fields.args ?? existing.args,
38636
+ env: existing.env,
38637
+ transport: fields.transport ?? existing.transport,
38638
+ operation: "register"
38639
+ }, localConsent(input));
38640
+ } catch (err) {
38641
+ return errorContent(err.message);
38642
+ }
38643
+ }
38366
38644
  return jsonContent(redactServerEnv(updateServer(serverId, fields)));
38367
38645
  }
38368
38646
  },
@@ -38444,13 +38722,16 @@ function buildMcpTools() {
38444
38722
  paramsSchema: {
38445
38723
  id: exports_external2.string().describe("Provider profile ID"),
38446
38724
  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")
38725
+ use_fallback: exports_external2.boolean().optional().describe("Install the stdio fallback command instead of the direct remote transport"),
38726
+ allow_local_stdio: exports_external2.boolean().optional().describe("Approve registering provider stdio fallback commands"),
38727
+ allow_risky_command: exports_external2.boolean().optional().describe("Approve risky local command patterns")
38448
38728
  },
38449
- run: ({ id, name, use_fallback }) => {
38729
+ run: (input) => {
38450
38730
  try {
38451
- return jsonContent(redactServerEnv(installProviderProfile(String(id), {
38452
- name: typeof name === "string" ? name : undefined,
38453
- useFallback: use_fallback === true
38731
+ return jsonContent(redactServerEnv(installProviderProfile(String(input.id), {
38732
+ name: typeof input.name === "string" ? input.name : undefined,
38733
+ useFallback: input.use_fallback === true,
38734
+ localCommandConsent: localConsent(input)
38454
38735
  })));
38455
38736
  } catch (err) {
38456
38737
  return errorContent(err.message);
@@ -38523,15 +38804,19 @@ function buildMcpTools() {
38523
38804
  description: "Install a registered MCP server into Claude Code, Codex, and/or Gemini",
38524
38805
  paramsSchema: {
38525
38806
  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)")
38807
+ targets: exports_external2.array(exports_external2.enum(["claude", "codex", "gemini"])).optional().describe("Target agents to install into (default: all)"),
38808
+ allow_local_stdio: exports_external2.boolean().optional().describe("Approve installing local stdio commands into local agent configs"),
38809
+ allow_risky_command: exports_external2.boolean().optional().describe("Approve installing risky local command patterns")
38527
38810
  },
38528
- run: ({ id, targets }) => {
38529
- const serverId = String(id);
38811
+ run: (input) => {
38812
+ const serverId = String(input.id);
38530
38813
  const entry = getServer(serverId);
38531
38814
  if (!entry)
38532
38815
  return errorContent(`Server "${serverId}" not found.`);
38533
- const agentTargets = Array.isArray(targets) ? targets : undefined;
38534
- return jsonContent(installToAgents(entry, agentTargets ?? ["claude", "codex", "gemini"]));
38816
+ const agentTargets = Array.isArray(input.targets) ? input.targets : undefined;
38817
+ return jsonContent(installToAgents(entry, agentTargets ?? ["claude", "codex", "gemini"], {
38818
+ localCommandConsent: localConsent(input)
38819
+ }));
38535
38820
  }
38536
38821
  },
38537
38822
  {
@@ -38543,11 +38828,14 @@ function buildMcpTools() {
38543
38828
  {
38544
38829
  name: "connect_and_list_tools",
38545
38830
  description: "Connect to all enabled MCP servers and list their available tools",
38546
- paramsSchema: {},
38547
- run: async () => {
38831
+ paramsSchema: {
38832
+ allow_local_stdio: exports_external2.boolean().optional().describe("Approve launching enabled local stdio commands"),
38833
+ allow_risky_command: exports_external2.boolean().optional().describe("Approve launching risky local command patterns")
38834
+ },
38835
+ run: async (input) => {
38548
38836
  let liveTools = [];
38549
38837
  try {
38550
- await connectAllEnabled();
38838
+ await connectAllEnabled({ localCommandConsent: localConsent(input) });
38551
38839
  liveTools = listAllTools();
38552
38840
  } finally {
38553
38841
  await disconnectAll().catch(() => {
@@ -38562,11 +38850,13 @@ function buildMcpTools() {
38562
38850
  description: `Call a tool on a connected upstream MCP server. Tool name format: server_id${TOOL_PREFIX_SEPARATOR}tool_name`,
38563
38851
  paramsSchema: {
38564
38852
  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")
38853
+ arguments: exports_external2.record(exports_external2.unknown()).optional().describe("Tool arguments as key-value pairs"),
38854
+ allow_local_stdio: exports_external2.boolean().optional().describe("Approve launching this local stdio command"),
38855
+ allow_risky_command: exports_external2.boolean().optional().describe("Approve launching risky local command patterns")
38566
38856
  },
38567
- run: async ({ tool_name, arguments: args }) => {
38857
+ run: async (input) => {
38568
38858
  try {
38569
- const toolName = String(tool_name);
38859
+ const toolName = String(input.tool_name);
38570
38860
  const sepIdx = toolName.indexOf(TOOL_PREFIX_SEPARATOR);
38571
38861
  if (sepIdx === -1)
38572
38862
  return errorContent(`Error: Invalid tool name "${toolName}"`);
@@ -38576,8 +38866,8 @@ function buildMcpTools() {
38576
38866
  return errorContent(`Error: Server "${serverId}" not found.`);
38577
38867
  if (!entry.enabled)
38578
38868
  return errorContent(`Error: Server "${serverId}" is disabled.`);
38579
- await connectToServer(entry);
38580
- const result = await callTool(toolName, readRecord(args));
38869
+ await connectToServer(entry, { localCommandConsent: localConsent(input) });
38870
+ const result = await callTool(toolName, readRecord(input.arguments));
38581
38871
  return { content: result.content };
38582
38872
  } catch (error2) {
38583
38873
  return errorContent(`Error: ${error2.message}`);
@@ -38587,13 +38877,17 @@ function buildMcpTools() {
38587
38877
  {
38588
38878
  name: "diagnose_server",
38589
38879
  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);
38880
+ paramsSchema: {
38881
+ id: exports_external2.string().describe("Server ID"),
38882
+ allow_local_stdio: exports_external2.boolean().optional().describe("Approve launching local stdio diagnostics"),
38883
+ allow_risky_command: exports_external2.boolean().optional().describe("Approve diagnosing risky local command patterns")
38884
+ },
38885
+ run: async (input) => {
38886
+ const serverId = String(input.id);
38593
38887
  const entry = getServer(serverId);
38594
38888
  if (!entry)
38595
38889
  return errorContent(`Server "${serverId}" not found.`);
38596
- return jsonContent(await diagnoseServer(entry));
38890
+ return jsonContent(await diagnoseServer(entry, { localCommandConsent: localConsent(input) }));
38597
38891
  }
38598
38892
  },
38599
38893
  {
@@ -38962,6 +39256,20 @@ function isValidId(id) {
38962
39256
  return /^[a-z0-9-]+$/.test(id);
38963
39257
  }
38964
39258
  var MAX_BODY_SIZE = 1024 * 1024;
39259
+ function consentFromInput(input) {
39260
+ return {
39261
+ approved: input.allow_local_stdio === true || input.allowLocalStdio === true,
39262
+ allowRisky: input.allow_risky_command === true || input.allowRiskyCommand === true,
39263
+ source: "api"
39264
+ };
39265
+ }
39266
+ function consentFromSearchParams(params) {
39267
+ return {
39268
+ approved: params.get("allow_local_stdio") === "1" || params.get("allow_local_stdio") === "true",
39269
+ allowRisky: params.get("allow_risky_command") === "1" || params.get("allow_risky_command") === "true",
39270
+ source: "api"
39271
+ };
39272
+ }
38965
39273
  function isLoopbackHost(hostname3) {
38966
39274
  return hostname3 === "127.0.0.1" || hostname3 === "localhost" || hostname3 === "::1";
38967
39275
  }
@@ -39108,6 +39416,11 @@ Dashboard not found at: ${dashboardDir}`);
39108
39416
  return json({ error: "Invalid 'args' format" }, 400, port);
39109
39417
  }
39110
39418
  const args = body.args || [];
39419
+ try {
39420
+ assertLocalCommandConsent({ command, args, env: {}, transport, operation: "register" }, consentFromInput(body));
39421
+ } catch (err) {
39422
+ return json({ error: err.message }, 400, port);
39423
+ }
39111
39424
  const entry = addServer({
39112
39425
  name: body.name,
39113
39426
  command,
@@ -39206,6 +39519,19 @@ Dashboard not found at: ${dashboardDir}`);
39206
39519
  }
39207
39520
  fields.args = body.args;
39208
39521
  }
39522
+ if (fields.command !== undefined || fields.args !== undefined || fields.transport !== undefined) {
39523
+ try {
39524
+ assertLocalCommandConsent({
39525
+ command: fields.command ?? existing.command,
39526
+ args: fields.args ?? existing.args,
39527
+ env: existing.env,
39528
+ transport: fields.transport ?? existing.transport,
39529
+ operation: "register"
39530
+ }, consentFromInput(body));
39531
+ } catch (err) {
39532
+ return json({ error: err.message }, 400, port);
39533
+ }
39534
+ }
39209
39535
  const updated = updateServer(id, fields);
39210
39536
  return json(redactServer(updated), 200, port);
39211
39537
  } catch (e) {
@@ -39281,7 +39607,7 @@ Dashboard not found at: ${dashboardDir}`);
39281
39607
  }
39282
39608
  if (!body.tool || typeof body.tool !== "string")
39283
39609
  return json({ error: "Missing 'tool'" }, 400, port);
39284
- await connectToServer(entry);
39610
+ await connectToServer(entry, { localCommandConsent: consentFromInput(body) });
39285
39611
  const toolName = `${id}__${body.tool}`;
39286
39612
  const result = await callTool(toolName, body.args || {});
39287
39613
  await disconnectServer(id).catch(() => {
@@ -39292,6 +39618,9 @@ Dashboard not found at: ${dashboardDir}`);
39292
39618
  await disconnectServer(id).catch(() => {
39293
39619
  return;
39294
39620
  });
39621
+ if (e instanceof LocalCommandConsentError) {
39622
+ return json({ error: e.message }, 400, port);
39623
+ }
39295
39624
  return json({ error: e instanceof Error ? e.message : "Failed to call tool" }, 500, port);
39296
39625
  }
39297
39626
  }
@@ -39304,7 +39633,7 @@ Dashboard not found at: ${dashboardDir}`);
39304
39633
  if (!entry)
39305
39634
  return json({ error: `Server '${id}' not found` }, 404, port);
39306
39635
  try {
39307
- const report = await diagnoseServer(entry);
39636
+ const report = await diagnoseServer(entry, { localCommandConsent: consentFromSearchParams(url2.searchParams) });
39308
39637
  return json(report, 200, port);
39309
39638
  } catch (e) {
39310
39639
  return json({ error: e instanceof Error ? e.message : "Failed to diagnose server" }, 500, port);
@@ -39959,6 +40288,13 @@ function ServerDetail({ server, onSelectTool, onBack }) {
39959
40288
  const [tools2, setTools] = useState3([]);
39960
40289
  const [loading, setLoading] = useState3(false);
39961
40290
  const [error2, setError] = useState3(null);
40291
+ const commandReview = inspectLocalCommand({
40292
+ command: server.command,
40293
+ args: server.args,
40294
+ env: server.env,
40295
+ transport: server.transport,
40296
+ operation: "launch"
40297
+ });
39962
40298
  const cachedTools = getCachedTools(server.id);
39963
40299
  const cachedKey = cachedTools.map((t) => `${t.name}|${t.description}|${JSON.stringify(t.input_schema)}`).join(";");
39964
40300
  useEffect3(() => {
@@ -39979,7 +40315,9 @@ function ServerDetail({ server, onSelectTool, onBack }) {
39979
40315
  setLoading(true);
39980
40316
  setError(null);
39981
40317
  try {
39982
- const conn = await connectToServer(server);
40318
+ const conn = await connectToServer(server, {
40319
+ localCommandConsent: { approved: true, source: "tui" }
40320
+ });
39983
40321
  setTools(conn.tools);
39984
40322
  } catch (err) {
39985
40323
  setError(err.message);
@@ -40038,6 +40376,21 @@ function ServerDetail({ server, onSelectTool, onBack }) {
40038
40376
  server.args.join(" ")
40039
40377
  ]
40040
40378
  }, undefined, true, undefined, this),
40379
+ commandReview.requiresConsent && /* @__PURE__ */ jsxDEV2(Box4, {
40380
+ marginTop: 1,
40381
+ flexDirection: "column",
40382
+ children: [
40383
+ /* @__PURE__ */ jsxDEV2(Text5, {
40384
+ color: commandReview.hasDangerousRisk ? "red" : "yellow",
40385
+ children: "Local stdio command review"
40386
+ }, undefined, false, undefined, this),
40387
+ formatLocalCommandReview(commandReview).split(`
40388
+ `).map((line, index) => /* @__PURE__ */ jsxDEV2(Text5, {
40389
+ dimColor: true,
40390
+ children: line
40391
+ }, `${index}:${line}`, false, undefined, this))
40392
+ ]
40393
+ }, undefined, true, undefined, this),
40041
40394
  server.description && /* @__PURE__ */ jsxDEV2(Text5, {
40042
40395
  dimColor: true,
40043
40396
  children: server.description
@@ -40206,7 +40559,9 @@ function SearchView({ onBack }) {
40206
40559
  setInstalling(item.value);
40207
40560
  setMessage(null);
40208
40561
  try {
40209
- const server = await installFromRegistry(item.value);
40562
+ const server = await installFromRegistry(item.value, {
40563
+ localCommandConsent: { approved: true, source: "tui" }
40564
+ });
40210
40565
  setMessage(`Installed: ${server.name} [${server.id}]`);
40211
40566
  } catch (err) {
40212
40567
  setMessage(`Install failed: ${err.message}`);
@@ -40317,7 +40672,9 @@ function ToolCall({ server, tool, onBack }) {
40317
40672
  setError(null);
40318
40673
  setResult(null);
40319
40674
  try {
40320
- await connectToServer(server);
40675
+ await connectToServer(server, {
40676
+ localCommandConsent: { approved: true, source: "tui" }
40677
+ });
40321
40678
  const prefixed = `${server.id}${TOOL_PREFIX_SEPARATOR}${tool.name}`;
40322
40679
  const res = await callTool(prefixed, args);
40323
40680
  const text = res.content.map((c) => c.text).join(`
@@ -40537,6 +40894,28 @@ var MACHINE_PLATFORMS = ["linux", "darwin", "unknown"];
40537
40894
  var MACHINE_ARCHES = ["arm64", "x64", "unknown"];
40538
40895
  var MACHINE_INSTALLERS = ["auto", "bun", "npm"];
40539
40896
  var FLEET_INSTALL_MODES = ["missing", "missing-or-outdated", "all"];
40897
+ function localConsentFromOptions(opts, approved = false) {
40898
+ return {
40899
+ approved: approved || opts.yes === true || opts.allowLocalStdio === true,
40900
+ allowRisky: opts.allowRiskyCommand === true,
40901
+ source: "cli"
40902
+ };
40903
+ }
40904
+ function printLocalCommandReviewIfNeeded(input) {
40905
+ const review = inspectLocalCommand(input);
40906
+ if (!review.requiresConsent)
40907
+ return;
40908
+ console.log(chalk2.dim(formatLocalCommandReview(review)));
40909
+ }
40910
+ function assertCliLocalCommandConsent(input, opts, approved = false) {
40911
+ const consent = localConsentFromOptions(opts, approved);
40912
+ const review = inspectLocalCommand(input);
40913
+ if (review.requiresConsent && consent.approved === true && (!review.hasDangerousRisk || consent.allowRisky === true)) {
40914
+ console.log(chalk2.dim(formatLocalCommandReview(review)));
40915
+ }
40916
+ assertLocalCommandConsent(input, consent);
40917
+ return consent;
40918
+ }
40540
40919
  function printProviderProfile(profile) {
40541
40920
  const status = profile.enabled ? chalk2.green("enabled") : chalk2.red("disabled");
40542
40921
  console.log(` ${chalk2.bold(profile.displayName)} ${chalk2.dim(`[${profile.id}]`)} \u2014 ${chalk2.dim(profile.transport)} \u2014 ${status}`);
@@ -40785,11 +41164,12 @@ providersCmd.command("info").argument("<id>", "Provider profile ID").description
40785
41164
  console.log(` Docs: ${chalk2.cyan(profile.docsUrl)}`);
40786
41165
  closeDb();
40787
41166
  });
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) => {
41167
+ 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
41168
  try {
40790
41169
  const server = installProviderProfile(id, {
40791
41170
  name: opts.name,
40792
- useFallback: opts.fallback === true
41171
+ useFallback: opts.fallback === true,
41172
+ localCommandConsent: localConsentFromOptions(opts)
40793
41173
  });
40794
41174
  if (opts.json) {
40795
41175
  printJson(server);
@@ -40820,11 +41200,13 @@ function detectSourceType(url2) {
40820
41200
  return "mcp-registry";
40821
41201
  return null;
40822
41202
  }
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) => {
41203
+ 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").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
41204
  try {
40825
41205
  if (opts.fromRegistry) {
40826
41206
  console.log(chalk2.dim(`Installing "${opts.fromRegistry}" from registry...`));
40827
- const server2 = await installFromRegistry(opts.fromRegistry);
41207
+ const server2 = await installFromRegistry(opts.fromRegistry, {
41208
+ localCommandConsent: localConsentFromOptions(opts)
41209
+ });
40828
41210
  console.log(chalk2.green(`Added server: ${server2.name} [${server2.id}]`));
40829
41211
  console.log(chalk2.dim(` ${server2.command} ${server2.args.join(" ")}`));
40830
41212
  closeDb();
@@ -40863,6 +41245,13 @@ Server to add:`));
40863
41245
  console.log(` Name: ${wizardName}`);
40864
41246
  if (Object.keys(env).length)
40865
41247
  console.log(` Env: ${Object.keys(env).join(", ")}`);
41248
+ printLocalCommandReviewIfNeeded({
41249
+ command: wizardCommand,
41250
+ args: wizardArgs,
41251
+ env,
41252
+ transport,
41253
+ operation: "register"
41254
+ });
40866
41255
  const confirm = await new Promise((resolve2) => {
40867
41256
  const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
40868
41257
  rl2.question(chalk2.bold("Add this server? [Y/n]: "), (ans) => {
@@ -40875,6 +41264,13 @@ Server to add:`));
40875
41264
  closeDb();
40876
41265
  return;
40877
41266
  }
41267
+ assertLocalCommandConsent({
41268
+ command: wizardCommand,
41269
+ args: wizardArgs,
41270
+ env,
41271
+ transport,
41272
+ operation: "register"
41273
+ }, localConsentFromOptions(opts, true));
40878
41274
  const server2 = addServer({
40879
41275
  command: wizardCommand,
40880
41276
  args: wizardArgs,
@@ -40911,6 +41307,13 @@ Server to add:`));
40911
41307
  envMap[key] = rest.join("=");
40912
41308
  }
40913
41309
  }
41310
+ assertCliLocalCommandConsent({
41311
+ command,
41312
+ args,
41313
+ env: envMap,
41314
+ transport: opts.transport,
41315
+ operation: "register"
41316
+ }, opts);
40914
41317
  const server = addServer({
40915
41318
  name: opts.name,
40916
41319
  description: opts.description,
@@ -40933,29 +41336,44 @@ Server to add:`));
40933
41336
  }
40934
41337
  closeDb();
40935
41338
  });
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.`));
41339
+ 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) => {
41340
+ try {
41341
+ const server = getServer(id);
41342
+ if (!server) {
41343
+ console.error(chalk2.red(`Server "${id}" not found.`));
41344
+ closeDb();
41345
+ process.exit(1);
41346
+ }
41347
+ const fields = {};
41348
+ if (opts.name !== undefined)
41349
+ fields.name = opts.name;
41350
+ if (opts.description !== undefined)
41351
+ fields.description = opts.description;
41352
+ if (opts.command !== undefined)
41353
+ fields.command = opts.command;
41354
+ if (opts.args !== undefined)
41355
+ fields.args = opts.args;
41356
+ if (opts.transport !== undefined)
41357
+ fields.transport = opts.transport;
41358
+ if (opts.url !== undefined)
41359
+ fields.url = opts.url;
41360
+ if (fields.command !== undefined || fields.args !== undefined || fields.transport !== undefined) {
41361
+ assertCliLocalCommandConsent({
41362
+ command: fields.command ?? server.command,
41363
+ args: fields.args ?? server.args,
41364
+ env: server.env,
41365
+ transport: fields.transport ?? server.transport,
41366
+ operation: "register"
41367
+ }, opts);
41368
+ }
41369
+ const updated = updateServer(id, fields);
41370
+ console.log(chalk2.green(`Updated server: ${updated.name} [${updated.id}]`));
41371
+ closeDb();
41372
+ } catch (err) {
41373
+ console.error(chalk2.red(`Failed to update server: ${err.message}`));
40940
41374
  closeDb();
40941
41375
  process.exit(1);
40942
41376
  }
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
41377
  });
40960
41378
  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
41379
  try {
@@ -41002,10 +41420,10 @@ program2.command("disable").argument("<id>", "Server ID to disable").description
41002
41420
  console.log(chalk2.yellow(`Disabled server: ${server.name}`));
41003
41421
  closeDb();
41004
41422
  });
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) => {
41423
+ 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
41424
  if (opts.connect) {
41007
41425
  console.log(chalk2.dim("Connecting to enabled servers..."));
41008
- await connectAllEnabled();
41426
+ await connectAllEnabled({ localCommandConsent: localConsentFromOptions(opts) });
41009
41427
  const tools2 = listAllTools();
41010
41428
  if (tools2.length === 0) {
41011
41429
  console.log(chalk2.dim("No tools available."));
@@ -41055,7 +41473,7 @@ ${total} tool(s) total.`));
41055
41473
  }
41056
41474
  closeDb();
41057
41475
  });
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) => {
41476
+ 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
41477
  let args = {};
41060
41478
  if (opts.json) {
41061
41479
  try {
@@ -41078,7 +41496,7 @@ program2.command("call").argument("<tool>", "Tool name (server_id__tool_name)").
41078
41496
  let exitCode = 0;
41079
41497
  try {
41080
41498
  console.log(chalk2.dim(`Connecting to servers...`));
41081
- await connectAllEnabled();
41499
+ await connectAllEnabled({ localCommandConsent: localConsentFromOptions(opts) });
41082
41500
  console.log(chalk2.dim(`Calling ${tool}...`));
41083
41501
  const result = await callTool(tool, args);
41084
41502
  for (const c of result.content) {
@@ -41146,7 +41564,7 @@ program2.command("status").description("Show registry stats").option("--json", "
41146
41564
  console.log(` Tools: ${totalTools} (cached)`);
41147
41565
  closeDb();
41148
41566
  });
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) => {
41567
+ 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
41568
  const { execFileSync: execFileSync22 } = await import("child_process");
41151
41569
  const servers = id ? [getServer(id)].filter(Boolean) : listServers();
41152
41570
  if (servers.length === 0) {
@@ -41158,7 +41576,7 @@ program2.command("doctor").argument("[id]", "Server ID to check (omit to check a
41158
41576
  for (const server of servers) {
41159
41577
  console.log(chalk2.bold(`
41160
41578
  ${server.name} [${server.id}]`));
41161
- const report = await diagnoseServer(server);
41579
+ const report = await diagnoseServer(server, { localCommandConsent: localConsentFromOptions(opts) });
41162
41580
  for (const check2 of report.checks) {
41163
41581
  const icon = check2.pass ? chalk2.green("\u2713") : chalk2.red("\u2717");
41164
41582
  console.log(` ${icon} ${check2.name}: ${chalk2.dim(check2.message)}`);
@@ -41244,7 +41662,7 @@ program2.command("update").description("Update mcps to the latest version").acti
41244
41662
  process.exit(1);
41245
41663
  }
41246
41664
  });
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) => {
41665
+ 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
41666
  try {
41249
41667
  if (opts.awesome) {
41250
41668
  console.log(chalk2.dim("Fetching curated awesome-mcp-servers list..."));
@@ -41354,6 +41772,14 @@ Enter number to install (1-${results.length}), or 0 to cancel: `), resolve2);
41354
41772
  return;
41355
41773
  }
41356
41774
  console.log(chalk2.dim(`Installing ${chosen.name}...`));
41775
+ const localCommandInput = {
41776
+ command: "npx",
41777
+ args: ["-y", pkg],
41778
+ env: {},
41779
+ transport: "stdio",
41780
+ operation: "install"
41781
+ };
41782
+ const localCommandConsent = assertCliLocalCommandConsent(localCommandInput, opts, true);
41357
41783
  const server = addServer({
41358
41784
  command: "npx",
41359
41785
  args: ["-y", pkg],
@@ -41361,7 +41787,7 @@ Enter number to install (1-${results.length}), or 0 to cancel: `), resolve2);
41361
41787
  description: chosen.description,
41362
41788
  transport: "stdio"
41363
41789
  });
41364
- const results2 = installToAgents(server, ["claude", "codex", "gemini"]);
41790
+ const results2 = installToAgents(server, ["claude", "codex", "gemini"], { localCommandConsent });
41365
41791
  for (const r of results2) {
41366
41792
  if (r.success) {
41367
41793
  console.log(chalk2.green(` \u2713 ${r.agent}`));
@@ -41506,7 +41932,7 @@ sourcesCmd.command("test").argument("<id>", "Source ID to test").description("Te
41506
41932
  }
41507
41933
  closeDb();
41508
41934
  });
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) => {
41935
+ 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
41936
  const targets = [];
41511
41937
  if (opts.to) {
41512
41938
  for (const t of opts.to) {
@@ -41532,7 +41958,9 @@ program2.command("install").argument("[id]", "Server ID (from `mcps list`) to in
41532
41958
  if (opts.fromRegistry) {
41533
41959
  console.log(chalk2.dim(`Installing "${opts.fromRegistry}" from registry...`));
41534
41960
  try {
41535
- server = await installFromRegistry(opts.fromRegistry);
41961
+ server = await installFromRegistry(opts.fromRegistry, {
41962
+ localCommandConsent: localConsentFromOptions(opts)
41963
+ });
41536
41964
  console.log(chalk2.green(`Added server: ${server.name} [${server.id}]`));
41537
41965
  } catch (err) {
41538
41966
  console.error(chalk2.red(`Failed to install from registry: ${err.message}`));
@@ -41541,6 +41969,13 @@ program2.command("install").argument("[id]", "Server ID (from `mcps list`) to in
41541
41969
  }
41542
41970
  } else if (opts.npm) {
41543
41971
  const pkg = opts.npm;
41972
+ assertCliLocalCommandConsent({
41973
+ command: "npx",
41974
+ args: ["-y", pkg],
41975
+ env: {},
41976
+ transport: "stdio",
41977
+ operation: "install"
41978
+ }, opts);
41544
41979
  server = addServer({ command: "npx", args: ["-y", pkg], name: pkg, transport: "stdio" });
41545
41980
  console.log(chalk2.green(`Added server: ${server.name} [${server.id}]`));
41546
41981
  } else {
@@ -41557,7 +41992,7 @@ program2.command("install").argument("[id]", "Server ID (from `mcps list`) to in
41557
41992
  }
41558
41993
  }
41559
41994
  console.log(chalk2.dim(`Installing "${server.name}" to: ${targets.join(", ")}...`));
41560
- const results = installToAgents(server, targets);
41995
+ const results = installToAgents(server, targets, { localCommandConsent: localConsentFromOptions(opts) });
41561
41996
  for (const r of results) {
41562
41997
  if (r.success) {
41563
41998
  console.log(chalk2.green(` \u2713 ${r.agent}`));