@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/dist/index.js CHANGED
@@ -25242,6 +25242,185 @@ class StreamableHTTPClientTransport {
25242
25242
  // src/lib/proxy.ts
25243
25243
  init_config2();
25244
25244
  init_db();
25245
+
25246
+ // src/lib/local-command-consent.ts
25247
+ class LocalCommandConsentError extends Error {
25248
+ review;
25249
+ constructor(message, review) {
25250
+ super(message);
25251
+ this.name = "LocalCommandConsentError";
25252
+ this.review = review;
25253
+ }
25254
+ }
25255
+ var SHELL_COMMANDS = new Set(["bash", "sh", "zsh", "fish", "cmd", "cmd.exe", "powershell", "powershell.exe", "pwsh"]);
25256
+ var DESTRUCTIVE_COMMANDS = new Set([
25257
+ "rm",
25258
+ "dd",
25259
+ "mkfs",
25260
+ "shutdown",
25261
+ "reboot",
25262
+ "poweroff",
25263
+ "halt",
25264
+ "killall",
25265
+ "pkill"
25266
+ ]);
25267
+ var SHELL_EVAL_FLAGS = new Set(["-c", "/c", "-Command", "-command", "-EncodedCommand", "-encodedcommand"]);
25268
+ var SECRET_FLAG_PATTERN = /(?:^|[-_])(api[-_]?key|token|secret|password|passwd|credential|auth|private[-_]?key)(?:$|[-_])/i;
25269
+ var SECRET_KEY_PATTERN = /(?:^|[_-])(api[_-]?key|token|secret|password|passwd|credential|auth|private[_-]?key)(?:$|[_-])/i;
25270
+ 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_-]+)$/;
25271
+ var SHELL_META_PATTERN = /[;&|`<>]|\$\(/;
25272
+ function commandBase(command) {
25273
+ return command.trim().split(/[\\/]/).pop()?.toLowerCase() || command.trim().toLowerCase();
25274
+ }
25275
+ function normalizeArgs(args) {
25276
+ return (args ?? []).map((arg) => String(arg));
25277
+ }
25278
+ function isSecretKey(key) {
25279
+ return SECRET_KEY_PATTERN.test(key) || SECRET_FLAG_PATTERN.test(key);
25280
+ }
25281
+ function isSecretValue(value) {
25282
+ return SECRET_VALUE_PATTERN.test(value.trim());
25283
+ }
25284
+ function isSecretAssignment(arg) {
25285
+ const eqIdx = arg.indexOf("=");
25286
+ if (eqIdx <= 0)
25287
+ return false;
25288
+ const key = arg.slice(0, eqIdx);
25289
+ const value = arg.slice(eqIdx + 1);
25290
+ return isSecretKey(key) || isSecretValue(value);
25291
+ }
25292
+ function isSecretArg(args, index) {
25293
+ const arg = args[index] ?? "";
25294
+ const previous = args[index - 1] ?? "";
25295
+ return isSecretAssignment(arg) || isSecretValue(arg) || SECRET_FLAG_PATTERN.test(previous);
25296
+ }
25297
+ function quoteArg(value) {
25298
+ return JSON.stringify(value);
25299
+ }
25300
+ function displayCommand(command, args) {
25301
+ return [quoteArg(command), ...args.map((arg, index) => quoteArg(isSecretArg(args, index) ? "<redacted>" : arg))].join(" ");
25302
+ }
25303
+ function pushRisk(risks, risk) {
25304
+ if (risks.some((existing) => existing.code === risk.code && existing.evidence === risk.evidence))
25305
+ return;
25306
+ risks.push(risk);
25307
+ }
25308
+ function inspectRisks(command, args, env) {
25309
+ const risks = [];
25310
+ const base = commandBase(command);
25311
+ const joined = [command, ...args].join(" ");
25312
+ if (SHELL_COMMANDS.has(base)) {
25313
+ pushRisk(risks, {
25314
+ code: "shell_interpreter",
25315
+ severity: "warning",
25316
+ message: "Command launches a shell interpreter.",
25317
+ evidence: base
25318
+ });
25319
+ if (args.some((arg) => SHELL_EVAL_FLAGS.has(arg))) {
25320
+ pushRisk(risks, {
25321
+ code: "shell_eval",
25322
+ severity: "danger",
25323
+ message: "Shell command evaluates an inline script.",
25324
+ evidence: base
25325
+ });
25326
+ }
25327
+ }
25328
+ if (base === "sudo") {
25329
+ pushRisk(risks, {
25330
+ code: "privilege_escalation",
25331
+ severity: "danger",
25332
+ message: "Command requests elevated privileges.",
25333
+ evidence: base
25334
+ });
25335
+ }
25336
+ if (DESTRUCTIVE_COMMANDS.has(base) || /\brm\s+-[^\s]*[rf][^\s]*\b/.test(joined) || /--no-preserve-root\b/.test(joined)) {
25337
+ pushRisk(risks, {
25338
+ code: "destructive_command",
25339
+ severity: "danger",
25340
+ message: "Command includes a destructive system operation.",
25341
+ evidence: base
25342
+ });
25343
+ }
25344
+ if (/\b(curl|wget)\b[\s\S]*\|[\s\S]*\b(sh|bash|zsh|fish)\b/.test(joined)) {
25345
+ pushRisk(risks, {
25346
+ code: "download_pipe_shell",
25347
+ severity: "danger",
25348
+ message: "Command downloads remote content and pipes it to a shell."
25349
+ });
25350
+ }
25351
+ if ([command, ...args].some((part) => SHELL_META_PATTERN.test(part))) {
25352
+ pushRisk(risks, {
25353
+ code: "shell_metacharacters",
25354
+ severity: "warning",
25355
+ message: "Command or arguments contain shell metacharacters."
25356
+ });
25357
+ }
25358
+ if (args.some((arg, index) => isSecretArg(args, index))) {
25359
+ pushRisk(risks, {
25360
+ code: "inline_secret",
25361
+ severity: "danger",
25362
+ message: "Command arguments appear to contain inline secret material."
25363
+ });
25364
+ }
25365
+ const secretEnvKeys = Object.keys(env).filter(isSecretKey).sort();
25366
+ if (secretEnvKeys.length > 0) {
25367
+ pushRisk(risks, {
25368
+ code: "secret_env",
25369
+ severity: "warning",
25370
+ message: "Environment contains secret-like keys; values are redacted from consent output.",
25371
+ evidence: secretEnvKeys.join(", ")
25372
+ });
25373
+ }
25374
+ return risks;
25375
+ }
25376
+ function inspectLocalCommand(input) {
25377
+ const args = normalizeArgs(input.args);
25378
+ const env = input.env ?? {};
25379
+ const transport = input.transport ?? "stdio";
25380
+ const risks = inspectRisks(input.command, args, env);
25381
+ return {
25382
+ requiresConsent: transport === "stdio",
25383
+ operation: input.operation ?? "launch",
25384
+ command: input.command,
25385
+ args,
25386
+ displayCommand: displayCommand(input.command, args),
25387
+ envKeys: Object.keys(env).sort(),
25388
+ risks,
25389
+ hasDangerousRisk: risks.some((risk) => risk.severity === "danger")
25390
+ };
25391
+ }
25392
+ function formatLocalCommandReview(review) {
25393
+ const lines = [
25394
+ `Command: ${review.displayCommand}`,
25395
+ review.envKeys.length > 0 ? `Env keys: ${review.envKeys.join(", ")}` : "Env keys: <none>"
25396
+ ];
25397
+ if (review.risks.length > 0) {
25398
+ lines.push("Risks:");
25399
+ for (const risk of review.risks) {
25400
+ lines.push(`- ${risk.severity}: ${risk.code} - ${risk.message}${risk.evidence ? ` (${risk.evidence})` : ""}`);
25401
+ }
25402
+ } else {
25403
+ lines.push("Risks: none detected");
25404
+ }
25405
+ return lines.join(`
25406
+ `);
25407
+ }
25408
+ function assertLocalCommandConsent(input, consent = {}) {
25409
+ const review = inspectLocalCommand(input);
25410
+ if (!review.requiresConsent)
25411
+ return review;
25412
+ if (consent.approved !== true) {
25413
+ throw new LocalCommandConsentError(`local stdio command approval is required before ${review.operation}.
25414
+ ${formatLocalCommandReview(review)}`, review);
25415
+ }
25416
+ if (review.hasDangerousRisk && consent.allowRisky !== true) {
25417
+ throw new LocalCommandConsentError(`risky command approval is required before ${review.operation}.
25418
+ ${formatLocalCommandReview(review)}`, review);
25419
+ }
25420
+ return review;
25421
+ }
25422
+
25423
+ // src/lib/proxy.ts
25245
25424
  var connections = new Map;
25246
25425
  var inflightConnections = new Map;
25247
25426
  function buildEnv(extra) {
@@ -25267,7 +25446,7 @@ function requireUrl(entry) {
25267
25446
  throw new Error(`Server "${entry.id}" has an invalid URL: ${entry.url}`);
25268
25447
  }
25269
25448
  }
25270
- async function connectToServer(entry) {
25449
+ async function connectToServer(entry, options = {}) {
25271
25450
  if (connections.has(entry.id)) {
25272
25451
  return connections.get(entry.id);
25273
25452
  }
@@ -25283,6 +25462,13 @@ async function connectToServer(entry) {
25283
25462
  if (!entry.command?.trim()) {
25284
25463
  throw new Error(`Server "${entry.id}" is missing a command`);
25285
25464
  }
25465
+ assertLocalCommandConsent({
25466
+ command: entry.command,
25467
+ args: entry.args,
25468
+ env: entry.env,
25469
+ transport: entry.transport,
25470
+ operation: "launch"
25471
+ }, options.localCommandConsent);
25286
25472
  transport = new StdioClientTransport({
25287
25473
  command: entry.command,
25288
25474
  args: entry.args,
@@ -25418,13 +25604,28 @@ async function refreshTools(id) {
25418
25604
  }
25419
25605
 
25420
25606
  // src/lib/doctor.ts
25421
- async function diagnoseServer(server) {
25607
+ async function diagnoseServer(server, options = {}) {
25422
25608
  const checks3 = [];
25609
+ let hasLocalConsent = true;
25423
25610
  if (server.transport === "stdio") {
25611
+ try {
25612
+ assertLocalCommandConsent({
25613
+ command: server.command,
25614
+ args: server.args,
25615
+ env: server.env,
25616
+ transport: server.transport,
25617
+ operation: "diagnose"
25618
+ }, options.localCommandConsent);
25619
+ } catch (err) {
25620
+ hasLocalConsent = false;
25621
+ checks3.push({ name: "local command consent", pass: false, message: err.message });
25622
+ }
25424
25623
  try {
25425
25624
  const path = execFileSync("which", [server.command], { stdio: "pipe" }).toString().trim();
25426
25625
  let version2 = "";
25427
25626
  try {
25627
+ if (!hasLocalConsent)
25628
+ throw new Error("local stdio command approval is required before version probing");
25428
25629
  version2 = execFileSync(server.command, ["--version"], { stdio: "pipe" }).toString().trim().split(`
25429
25630
  `)[0];
25430
25631
  } catch {}
@@ -25455,10 +25656,10 @@ async function diagnoseServer(server) {
25455
25656
  checks3.push({ name: "URL reachable", pass: false, message: `unreachable: ${err.message}` });
25456
25657
  }
25457
25658
  }
25458
- if (server.enabled) {
25659
+ if (server.enabled && hasLocalConsent) {
25459
25660
  try {
25460
25661
  await Promise.race([
25461
- connectToServer(server),
25662
+ connectToServer(server, { localCommandConsent: options.localCommandConsent }),
25462
25663
  new Promise((_, reject) => setTimeout(() => reject(new Error("timeout after 10s")), 1e4))
25463
25664
  ]);
25464
25665
  await disconnectServer(server.id);
@@ -25466,8 +25667,10 @@ async function diagnoseServer(server) {
25466
25667
  } catch (err) {
25467
25668
  checks3.push({ name: "connect & list tools", pass: false, message: err.message });
25468
25669
  }
25469
- } else {
25670
+ } else if (!server.enabled) {
25470
25671
  checks3.push({ name: "connect & list tools", pass: true, message: "skipped (server disabled)" });
25672
+ } else {
25673
+ checks3.push({ name: "connect & list tools", pass: false, message: "skipped until local stdio command is approved" });
25471
25674
  }
25472
25675
  return {
25473
25676
  server,
@@ -25507,7 +25710,7 @@ async function getRegistryServer(id) {
25507
25710
  const all = entries.map(parseRegistryEntry);
25508
25711
  return all.find((s) => s.id === id) || null;
25509
25712
  }
25510
- async function installFromRegistry(id) {
25713
+ async function installFromRegistry(id, options = {}) {
25511
25714
  const server = await getRegistryServer(id);
25512
25715
  if (!server) {
25513
25716
  throw new Error(`Server "${id}" not found in registry`);
@@ -25527,6 +25730,13 @@ async function installFromRegistry(id) {
25527
25730
  transport = pkg.transport.type;
25528
25731
  }
25529
25732
  }
25733
+ assertLocalCommandConsent({
25734
+ command,
25735
+ args,
25736
+ transport,
25737
+ env: {},
25738
+ operation: "register"
25739
+ }, options.localCommandConsent);
25530
25740
  return addServer({
25531
25741
  name: server.name,
25532
25742
  description: server.description,
@@ -25763,11 +25973,19 @@ function installProviderProfile(id, options = {}) {
25763
25973
  if (!command) {
25764
25974
  throw new Error(`Provider profile "${id}" does not define an install fallback command`);
25765
25975
  }
25976
+ assertLocalCommandConsent({
25977
+ command,
25978
+ args,
25979
+ env: fallback?.env ?? {},
25980
+ transport: useFallback ? "stdio" : profile.transport,
25981
+ operation: "register"
25982
+ }, options.localCommandConsent);
25766
25983
  return addServer({
25767
25984
  name: options.name ?? profile.displayName,
25768
25985
  description: profile.description ?? undefined,
25769
25986
  command,
25770
25987
  args,
25988
+ env: fallback?.env,
25771
25989
  transport: useFallback ? "stdio" : profile.transport,
25772
25990
  url: useFallback ? fallback?.url : profile.endpoint ?? undefined,
25773
25991
  source: "provider-profile"
@@ -25856,7 +26074,22 @@ function installToGemini(entry) {
25856
26074
  return { agent: "gemini", success: false, error: err.message };
25857
26075
  }
25858
26076
  }
25859
- function installToAgents(entry, targets = ["claude", "codex", "gemini"]) {
26077
+ function installToAgents(entry, targets = ["claude", "codex", "gemini"], options = {}) {
26078
+ try {
26079
+ assertLocalCommandConsent({
26080
+ command: entry.command,
26081
+ args: entry.args,
26082
+ env: entry.env,
26083
+ transport: entry.transport,
26084
+ operation: "install"
26085
+ }, options.localCommandConsent);
26086
+ } catch (err) {
26087
+ return targets.map((target) => ({
26088
+ agent: target,
26089
+ success: false,
26090
+ error: err.message
26091
+ }));
26092
+ }
25860
26093
  return targets.map((target) => {
25861
26094
  if (target === "claude")
25862
26095
  return installToClaude(entry);
@@ -26819,6 +27052,7 @@ export {
26819
27052
  installToAgents,
26820
27053
  installProviderProfile,
26821
27054
  installFromRegistry,
27055
+ inspectLocalCommand,
26822
27056
  getToolCounts,
26823
27057
  getSource,
26824
27058
  getServer,
@@ -26827,6 +27061,7 @@ export {
26827
27061
  getMachine,
26828
27062
  getDb,
26829
27063
  getCachedTools,
27064
+ formatLocalCommandReview,
26830
27065
  findServers,
26831
27066
  enableSource,
26832
27067
  enableServer,
@@ -26841,9 +27076,11 @@ export {
26841
27076
  closeDb,
26842
27077
  cloneServer,
26843
27078
  callTool,
27079
+ assertLocalCommandConsent,
26844
27080
  addSource,
26845
27081
  addServer,
26846
27082
  addMachine,
27083
+ LocalCommandConsentError,
26847
27084
  DEFAULT_PROVIDER_PROFILE_SEEDS,
26848
27085
  DEFAULT_MACHINE_SEEDS
26849
27086
  };
@@ -1,4 +1,5 @@
1
1
  import type { McpServerEntry } from "../types.js";
2
+ import { type LocalCommandConsent } from "./local-command-consent.js";
2
3
  export interface DoctorCheck {
3
4
  name: string;
4
5
  pass: boolean;
@@ -11,4 +12,6 @@ export interface DoctorReport {
11
12
  checks: DoctorCheck[];
12
13
  healthy: boolean;
13
14
  }
14
- export declare function diagnoseServer(server: McpServerEntry): Promise<DoctorReport>;
15
+ export declare function diagnoseServer(server: McpServerEntry, options?: {
16
+ localCommandConsent?: LocalCommandConsent;
17
+ }): Promise<DoctorReport>;
@@ -1,8 +1,12 @@
1
1
  import type { McpServerEntry } from "../types.js";
2
+ import { type LocalCommandConsent } from "./local-command-consent.js";
2
3
  export type AgentTarget = "claude" | "codex" | "gemini";
3
4
  export interface InstallResult {
4
5
  agent: AgentTarget;
5
6
  success: boolean;
6
7
  error?: string;
7
8
  }
8
- export declare function installToAgents(entry: McpServerEntry, targets?: AgentTarget[]): InstallResult[];
9
+ export interface InstallToAgentsOptions {
10
+ localCommandConsent?: LocalCommandConsent;
11
+ }
12
+ export declare function installToAgents(entry: McpServerEntry, targets?: AgentTarget[], options?: InstallToAgentsOptions): InstallResult[];
@@ -0,0 +1,38 @@
1
+ import type { McpServerEntry } from "../types.js";
2
+ export type LocalCommandOperation = "register" | "install" | "launch" | "diagnose";
3
+ export type LocalCommandRiskSeverity = "warning" | "danger";
4
+ export interface LocalCommandInput {
5
+ command: string;
6
+ args?: string[];
7
+ env?: Record<string, string>;
8
+ transport?: McpServerEntry["transport"];
9
+ operation?: LocalCommandOperation;
10
+ }
11
+ export interface LocalCommandConsent {
12
+ approved?: boolean;
13
+ allowRisky?: boolean;
14
+ source?: string;
15
+ }
16
+ export interface LocalCommandRisk {
17
+ code: string;
18
+ severity: LocalCommandRiskSeverity;
19
+ message: string;
20
+ evidence?: string;
21
+ }
22
+ export interface LocalCommandReview {
23
+ requiresConsent: boolean;
24
+ operation: LocalCommandOperation;
25
+ command: string;
26
+ args: string[];
27
+ displayCommand: string;
28
+ envKeys: string[];
29
+ risks: LocalCommandRisk[];
30
+ hasDangerousRisk: boolean;
31
+ }
32
+ export declare class LocalCommandConsentError extends Error {
33
+ readonly review: LocalCommandReview;
34
+ constructor(message: string, review: LocalCommandReview);
35
+ }
36
+ export declare function inspectLocalCommand(input: LocalCommandInput): LocalCommandReview;
37
+ export declare function formatLocalCommandReview(review: LocalCommandReview): string;
38
+ export declare function assertLocalCommandConsent(input: LocalCommandInput, consent?: LocalCommandConsent): LocalCommandReview;
@@ -1,5 +1,9 @@
1
+ import { type LocalCommandConsent } from "./local-command-consent.js";
1
2
  import type { McpServerEntry, McpTool, ConnectedServer } from "../types.js";
2
- export declare function connectToServer(entry: McpServerEntry): Promise<ConnectedServer>;
3
+ export interface ConnectOptions {
4
+ localCommandConsent?: LocalCommandConsent;
5
+ }
6
+ export declare function connectToServer(entry: McpServerEntry, options?: ConnectOptions): Promise<ConnectedServer>;
3
7
  export declare function disconnectServer(id: string): Promise<void>;
4
8
  export declare function disconnectAll(): Promise<void>;
5
9
  export declare function listAllTools(): McpTool[];
@@ -10,4 +14,4 @@ export declare function callTool(prefixedName: string, args: Record<string, unkn
10
14
  }>;
11
15
  }>;
12
16
  export declare function refreshTools(id: string): Promise<McpTool[]>;
13
- export declare function connectAllEnabled(): Promise<ConnectedServer[]>;
17
+ export declare function connectAllEnabled(options?: ConnectOptions): Promise<ConnectedServer[]>;
@@ -1,4 +1,7 @@
1
+ import { type LocalCommandConsent } from "./local-command-consent.js";
1
2
  import type { RegistryServer, McpServerEntry } from "../types.js";
2
3
  export declare function searchRegistry(query: string): Promise<RegistryServer[]>;
3
4
  export declare function getRegistryServer(id: string): Promise<RegistryServer | null>;
4
- export declare function installFromRegistry(id: string): Promise<McpServerEntry>;
5
+ export declare function installFromRegistry(id: string, options?: {
6
+ localCommandConsent?: LocalCommandConsent;
7
+ }): Promise<McpServerEntry>;