@hasna/mcps 0.0.13 → 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/mcp.js CHANGED
@@ -16577,6 +16577,85 @@ var init_config2 = __esm(() => {
16577
16577
  DB_PATH = process.env.HASNA_MCPS_DB_PATH ?? process.env.MCPS_DB_PATH ?? join7(MCPS_DIR, "registry.db");
16578
16578
  });
16579
16579
 
16580
+ // src/lib/provider-profile-seeds.ts
16581
+ var DEFAULT_PROVIDER_PROFILE_SEEDS;
16582
+ var init_provider_profile_seeds = __esm(() => {
16583
+ DEFAULT_PROVIDER_PROFILE_SEEDS = [
16584
+ {
16585
+ id: "notion",
16586
+ displayName: "Notion",
16587
+ description: "Connect a Notion workspace so agents can search, read, create, and update workspace content.",
16588
+ endpoint: "https://mcp.notion.com/mcp",
16589
+ transport: "streamable-http",
16590
+ fallbackEndpoints: [
16591
+ {
16592
+ transport: "sse",
16593
+ url: "https://mcp.notion.com/sse",
16594
+ notes: "Fallback for clients that do not support Streamable HTTP."
16595
+ }
16596
+ ],
16597
+ authType: "oauth2",
16598
+ authMetadata: {
16599
+ oauthVersion: "2.0",
16600
+ pkce: true,
16601
+ dynamicClientRegistration: true,
16602
+ bearerToken: "none",
16603
+ notes: "Remote Notion MCP uses OAuth with PKCE. Bearer-token authentication is only appropriate for self-hosted/local fallback deployments."
16604
+ },
16605
+ tokenMode: "workspace",
16606
+ installFallback: {
16607
+ command: "npx",
16608
+ args: ["-y", "mcp-remote", "https://mcp.notion.com/sse", "--transport", "sse-only"],
16609
+ packageName: "mcp-remote",
16610
+ url: "https://mcp.notion.com/sse"
16611
+ },
16612
+ docsUrl: "https://developers.notion.com/guides/mcp/build-mcp-client",
16613
+ safety: {
16614
+ requiresApproval: true,
16615
+ dataClasses: ["workspace_content", "pages", "databases", "comments"],
16616
+ notes: "Connected agents operate with the authorizing user's workspace access. Human confirmation is recommended for write-capable workflows."
16617
+ },
16618
+ provenance: {
16619
+ source: "curated",
16620
+ sourceUrl: "https://developers.notion.com/guides/mcp/build-mcp-client",
16621
+ verifiedAt: "2026-05-10"
16622
+ }
16623
+ },
16624
+ {
16625
+ id: "linear",
16626
+ displayName: "Linear",
16627
+ description: "Connect Linear so agents can find, create, and update issues, projects, comments, and related workspace objects.",
16628
+ endpoint: "https://mcp.linear.app/mcp",
16629
+ transport: "streamable-http",
16630
+ authType: "oauth2",
16631
+ authMetadata: {
16632
+ oauthVersion: "2.1",
16633
+ dynamicClientRegistration: true,
16634
+ bearerToken: "optional",
16635
+ notes: "Linear supports interactive OAuth 2.1 with dynamic client registration and optional Authorization: Bearer tokens for OAuth tokens or API keys."
16636
+ },
16637
+ tokenMode: "workspace",
16638
+ installFallback: {
16639
+ command: "npx",
16640
+ args: ["-y", "mcp-remote", "https://mcp.linear.app/mcp"],
16641
+ packageName: "mcp-remote",
16642
+ url: "https://mcp.linear.app/mcp"
16643
+ },
16644
+ docsUrl: "https://linear.app/docs/mcp",
16645
+ safety: {
16646
+ requiresApproval: true,
16647
+ dataClasses: ["issues", "projects", "comments", "teams", "users"],
16648
+ notes: "Linear tools can create and update workspace objects, so write actions should be policy-gated by the platform."
16649
+ },
16650
+ provenance: {
16651
+ source: "curated",
16652
+ sourceUrl: "https://linear.app/docs/mcp",
16653
+ verifiedAt: "2026-05-10"
16654
+ }
16655
+ }
16656
+ ];
16657
+ });
16658
+
16580
16659
  // src/lib/db.ts
16581
16660
  import { mkdirSync as mkdirSync5 } from "fs";
16582
16661
  function getDb() {
@@ -16662,6 +16741,50 @@ function getDb() {
16662
16741
  ('github-mcp-topic', 'GitHub MCP Topic', 'github-topic', 'https://api.github.com/search/repositories', 'GitHub repositories tagged with mcp-server topic')
16663
16742
  `);
16664
16743
  }
16744
+ db.exec(`
16745
+ CREATE TABLE IF NOT EXISTS provider_profiles (
16746
+ id TEXT PRIMARY KEY,
16747
+ display_name TEXT NOT NULL,
16748
+ description TEXT,
16749
+ endpoint TEXT,
16750
+ transport TEXT NOT NULL,
16751
+ fallback_endpoints TEXT NOT NULL DEFAULT '[]',
16752
+ auth_type TEXT NOT NULL,
16753
+ auth_metadata TEXT NOT NULL DEFAULT '{}',
16754
+ scopes TEXT NOT NULL DEFAULT '[]',
16755
+ token_mode TEXT NOT NULL DEFAULT 'none',
16756
+ install_fallback TEXT NOT NULL DEFAULT '{}',
16757
+ docs_url TEXT,
16758
+ safety TEXT NOT NULL DEFAULT '{}',
16759
+ provenance TEXT NOT NULL DEFAULT '{"source":"manual"}',
16760
+ enabled INTEGER NOT NULL DEFAULT 1,
16761
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
16762
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
16763
+ )
16764
+ `);
16765
+ try {
16766
+ db.exec("ALTER TABLE provider_profiles ADD COLUMN fallback_endpoints TEXT NOT NULL DEFAULT '[]'");
16767
+ } catch {}
16768
+ try {
16769
+ db.exec("ALTER TABLE provider_profiles ADD COLUMN auth_metadata TEXT NOT NULL DEFAULT '{}'");
16770
+ } catch {}
16771
+ db.exec("CREATE INDEX IF NOT EXISTS idx_provider_profiles_enabled ON provider_profiles(enabled)");
16772
+ const providerProfileCount = db.query("SELECT COUNT(*) as c FROM provider_profiles").get().c;
16773
+ if (providerProfileCount === 0) {
16774
+ const insertProviderProfile = db.prepare(`
16775
+ INSERT OR IGNORE INTO provider_profiles (
16776
+ id, display_name, description, endpoint, transport, fallback_endpoints,
16777
+ auth_type, auth_metadata, scopes, token_mode, install_fallback,
16778
+ docs_url, safety, provenance, enabled
16779
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
16780
+ `);
16781
+ const run = db.transaction(() => {
16782
+ for (const profile of DEFAULT_PROVIDER_PROFILE_SEEDS) {
16783
+ insertProviderProfile.run(profile.id, profile.displayName, profile.description ?? null, profile.endpoint ?? null, profile.transport, JSON.stringify(profile.fallbackEndpoints ?? []), profile.authType, JSON.stringify(profile.authMetadata ?? {}), JSON.stringify(profile.scopes ?? []), profile.tokenMode ?? "none", JSON.stringify(profile.installFallback ?? null), profile.docsUrl ?? null, JSON.stringify(profile.safety ?? {}), JSON.stringify(profile.provenance), profile.enabled === false ? 0 : 1);
16784
+ }
16785
+ });
16786
+ run();
16787
+ }
16665
16788
  db.exec(`
16666
16789
  CREATE TABLE IF NOT EXISTS feedback (
16667
16790
  id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
@@ -16685,6 +16808,7 @@ var db = null, _adapter = null;
16685
16808
  var init_db = __esm(() => {
16686
16809
  init_dist();
16687
16810
  init_config2();
16811
+ init_provider_profile_seeds();
16688
16812
  });
16689
16813
 
16690
16814
  // src/lib/sources.ts
@@ -30716,6 +30840,185 @@ function getCachedTools(serverId) {
30716
30840
 
30717
30841
  // src/lib/remote.ts
30718
30842
  init_config2();
30843
+
30844
+ // src/lib/local-command-consent.ts
30845
+ class LocalCommandConsentError extends Error {
30846
+ review;
30847
+ constructor(message, review) {
30848
+ super(message);
30849
+ this.name = "LocalCommandConsentError";
30850
+ this.review = review;
30851
+ }
30852
+ }
30853
+ var SHELL_COMMANDS = new Set(["bash", "sh", "zsh", "fish", "cmd", "cmd.exe", "powershell", "powershell.exe", "pwsh"]);
30854
+ var DESTRUCTIVE_COMMANDS = new Set([
30855
+ "rm",
30856
+ "dd",
30857
+ "mkfs",
30858
+ "shutdown",
30859
+ "reboot",
30860
+ "poweroff",
30861
+ "halt",
30862
+ "killall",
30863
+ "pkill"
30864
+ ]);
30865
+ var SHELL_EVAL_FLAGS = new Set(["-c", "/c", "-Command", "-command", "-EncodedCommand", "-encodedcommand"]);
30866
+ var SECRET_FLAG_PATTERN = /(?:^|[-_])(api[-_]?key|token|secret|password|passwd|credential|auth|private[-_]?key)(?:$|[-_])/i;
30867
+ var SECRET_KEY_PATTERN = /(?:^|[_-])(api[_-]?key|token|secret|password|passwd|credential|auth|private[_-]?key)(?:$|[_-])/i;
30868
+ 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_-]+)$/;
30869
+ var SHELL_META_PATTERN = /[;&|`<>]|\$\(/;
30870
+ function commandBase(command) {
30871
+ return command.trim().split(/[\\/]/).pop()?.toLowerCase() || command.trim().toLowerCase();
30872
+ }
30873
+ function normalizeArgs(args) {
30874
+ return (args ?? []).map((arg) => String(arg));
30875
+ }
30876
+ function isSecretKey(key) {
30877
+ return SECRET_KEY_PATTERN.test(key) || SECRET_FLAG_PATTERN.test(key);
30878
+ }
30879
+ function isSecretValue(value) {
30880
+ return SECRET_VALUE_PATTERN.test(value.trim());
30881
+ }
30882
+ function isSecretAssignment(arg) {
30883
+ const eqIdx = arg.indexOf("=");
30884
+ if (eqIdx <= 0)
30885
+ return false;
30886
+ const key = arg.slice(0, eqIdx);
30887
+ const value = arg.slice(eqIdx + 1);
30888
+ return isSecretKey(key) || isSecretValue(value);
30889
+ }
30890
+ function isSecretArg(args, index) {
30891
+ const arg = args[index] ?? "";
30892
+ const previous = args[index - 1] ?? "";
30893
+ return isSecretAssignment(arg) || isSecretValue(arg) || SECRET_FLAG_PATTERN.test(previous);
30894
+ }
30895
+ function quoteArg(value) {
30896
+ return JSON.stringify(value);
30897
+ }
30898
+ function displayCommand(command, args) {
30899
+ return [quoteArg(command), ...args.map((arg, index) => quoteArg(isSecretArg(args, index) ? "<redacted>" : arg))].join(" ");
30900
+ }
30901
+ function pushRisk(risks, risk) {
30902
+ if (risks.some((existing) => existing.code === risk.code && existing.evidence === risk.evidence))
30903
+ return;
30904
+ risks.push(risk);
30905
+ }
30906
+ function inspectRisks(command, args, env) {
30907
+ const risks = [];
30908
+ const base = commandBase(command);
30909
+ const joined = [command, ...args].join(" ");
30910
+ if (SHELL_COMMANDS.has(base)) {
30911
+ pushRisk(risks, {
30912
+ code: "shell_interpreter",
30913
+ severity: "warning",
30914
+ message: "Command launches a shell interpreter.",
30915
+ evidence: base
30916
+ });
30917
+ if (args.some((arg) => SHELL_EVAL_FLAGS.has(arg))) {
30918
+ pushRisk(risks, {
30919
+ code: "shell_eval",
30920
+ severity: "danger",
30921
+ message: "Shell command evaluates an inline script.",
30922
+ evidence: base
30923
+ });
30924
+ }
30925
+ }
30926
+ if (base === "sudo") {
30927
+ pushRisk(risks, {
30928
+ code: "privilege_escalation",
30929
+ severity: "danger",
30930
+ message: "Command requests elevated privileges.",
30931
+ evidence: base
30932
+ });
30933
+ }
30934
+ if (DESTRUCTIVE_COMMANDS.has(base) || /\brm\s+-[^\s]*[rf][^\s]*\b/.test(joined) || /--no-preserve-root\b/.test(joined)) {
30935
+ pushRisk(risks, {
30936
+ code: "destructive_command",
30937
+ severity: "danger",
30938
+ message: "Command includes a destructive system operation.",
30939
+ evidence: base
30940
+ });
30941
+ }
30942
+ if (/\b(curl|wget)\b[\s\S]*\|[\s\S]*\b(sh|bash|zsh|fish)\b/.test(joined)) {
30943
+ pushRisk(risks, {
30944
+ code: "download_pipe_shell",
30945
+ severity: "danger",
30946
+ message: "Command downloads remote content and pipes it to a shell."
30947
+ });
30948
+ }
30949
+ if ([command, ...args].some((part) => SHELL_META_PATTERN.test(part))) {
30950
+ pushRisk(risks, {
30951
+ code: "shell_metacharacters",
30952
+ severity: "warning",
30953
+ message: "Command or arguments contain shell metacharacters."
30954
+ });
30955
+ }
30956
+ if (args.some((arg, index) => isSecretArg(args, index))) {
30957
+ pushRisk(risks, {
30958
+ code: "inline_secret",
30959
+ severity: "danger",
30960
+ message: "Command arguments appear to contain inline secret material."
30961
+ });
30962
+ }
30963
+ const secretEnvKeys = Object.keys(env).filter(isSecretKey).sort();
30964
+ if (secretEnvKeys.length > 0) {
30965
+ pushRisk(risks, {
30966
+ code: "secret_env",
30967
+ severity: "warning",
30968
+ message: "Environment contains secret-like keys; values are redacted from consent output.",
30969
+ evidence: secretEnvKeys.join(", ")
30970
+ });
30971
+ }
30972
+ return risks;
30973
+ }
30974
+ function inspectLocalCommand(input) {
30975
+ const args = normalizeArgs(input.args);
30976
+ const env = input.env ?? {};
30977
+ const transport = input.transport ?? "stdio";
30978
+ const risks = inspectRisks(input.command, args, env);
30979
+ return {
30980
+ requiresConsent: transport === "stdio",
30981
+ operation: input.operation ?? "launch",
30982
+ command: input.command,
30983
+ args,
30984
+ displayCommand: displayCommand(input.command, args),
30985
+ envKeys: Object.keys(env).sort(),
30986
+ risks,
30987
+ hasDangerousRisk: risks.some((risk) => risk.severity === "danger")
30988
+ };
30989
+ }
30990
+ function formatLocalCommandReview(review) {
30991
+ const lines = [
30992
+ `Command: ${review.displayCommand}`,
30993
+ review.envKeys.length > 0 ? `Env keys: ${review.envKeys.join(", ")}` : "Env keys: <none>"
30994
+ ];
30995
+ if (review.risks.length > 0) {
30996
+ lines.push("Risks:");
30997
+ for (const risk of review.risks) {
30998
+ lines.push(`- ${risk.severity}: ${risk.code} - ${risk.message}${risk.evidence ? ` (${risk.evidence})` : ""}`);
30999
+ }
31000
+ } else {
31001
+ lines.push("Risks: none detected");
31002
+ }
31003
+ return lines.join(`
31004
+ `);
31005
+ }
31006
+ function assertLocalCommandConsent(input, consent = {}) {
31007
+ const review = inspectLocalCommand(input);
31008
+ if (!review.requiresConsent)
31009
+ return review;
31010
+ if (consent.approved !== true) {
31011
+ throw new LocalCommandConsentError(`local stdio command approval is required before ${review.operation}.
31012
+ ${formatLocalCommandReview(review)}`, review);
31013
+ }
31014
+ if (review.hasDangerousRisk && consent.allowRisky !== true) {
31015
+ throw new LocalCommandConsentError(`risky command approval is required before ${review.operation}.
31016
+ ${formatLocalCommandReview(review)}`, review);
31017
+ }
31018
+ return review;
31019
+ }
31020
+
31021
+ // src/lib/remote.ts
30719
31022
  function parseRegistryEntry(entry) {
30720
31023
  const s = entry.server;
30721
31024
  return {
@@ -30746,7 +31049,7 @@ async function getRegistryServer(id) {
30746
31049
  const all = entries.map(parseRegistryEntry);
30747
31050
  return all.find((s) => s.id === id) || null;
30748
31051
  }
30749
- async function installFromRegistry(id) {
31052
+ async function installFromRegistry(id, options = {}) {
30750
31053
  const server = await getRegistryServer(id);
30751
31054
  if (!server) {
30752
31055
  throw new Error(`Server "${id}" not found in registry`);
@@ -30766,6 +31069,13 @@ async function installFromRegistry(id) {
30766
31069
  transport = pkg.transport.type;
30767
31070
  }
30768
31071
  }
31072
+ assertLocalCommandConsent({
31073
+ command,
31074
+ args,
31075
+ transport,
31076
+ env: {},
31077
+ operation: "register"
31078
+ }, options.localCommandConsent);
30769
31079
  return addServer({
30770
31080
  name: server.name,
30771
31081
  description: server.description,
@@ -30860,7 +31170,22 @@ function installToGemini(entry) {
30860
31170
  return { agent: "gemini", success: false, error: err.message };
30861
31171
  }
30862
31172
  }
30863
- function installToAgents(entry, targets = ["claude", "codex", "gemini"]) {
31173
+ function installToAgents(entry, targets = ["claude", "codex", "gemini"], options = {}) {
31174
+ try {
31175
+ assertLocalCommandConsent({
31176
+ command: entry.command,
31177
+ args: entry.args,
31178
+ env: entry.env,
31179
+ transport: entry.transport,
31180
+ operation: "install"
31181
+ }, options.localCommandConsent);
31182
+ } catch (err) {
31183
+ return targets.map((target) => ({
31184
+ agent: target,
31185
+ success: false,
31186
+ error: err.message
31187
+ }));
31188
+ }
30864
31189
  return targets.map((target) => {
30865
31190
  if (target === "claude")
30866
31191
  return installToClaude(entry);
@@ -33277,7 +33602,7 @@ function requireUrl(entry) {
33277
33602
  throw new Error(`Server "${entry.id}" has an invalid URL: ${entry.url}`);
33278
33603
  }
33279
33604
  }
33280
- async function connectToServer(entry) {
33605
+ async function connectToServer(entry, options = {}) {
33281
33606
  if (connections.has(entry.id)) {
33282
33607
  return connections.get(entry.id);
33283
33608
  }
@@ -33293,6 +33618,13 @@ async function connectToServer(entry) {
33293
33618
  if (!entry.command?.trim()) {
33294
33619
  throw new Error(`Server "${entry.id}" is missing a command`);
33295
33620
  }
33621
+ assertLocalCommandConsent({
33622
+ command: entry.command,
33623
+ args: entry.args,
33624
+ env: entry.env,
33625
+ transport: entry.transport,
33626
+ operation: "launch"
33627
+ }, options.localCommandConsent);
33296
33628
  transport = new StdioClientTransport({
33297
33629
  command: entry.command,
33298
33630
  args: entry.args,
@@ -33409,7 +33741,7 @@ async function callTool(prefixedName, args) {
33409
33741
  ]
33410
33742
  };
33411
33743
  }
33412
- async function connectAllEnabled() {
33744
+ async function connectAllEnabled(options = {}) {
33413
33745
  const servers = listServers().filter((s) => s.enabled);
33414
33746
  const results = [];
33415
33747
  let index = 0;
@@ -33421,7 +33753,7 @@ async function connectAllEnabled() {
33421
33753
  return;
33422
33754
  const server = servers[current];
33423
33755
  try {
33424
- const conn = await connectToServer(server);
33756
+ const conn = await connectToServer(server, options);
33425
33757
  results.push(conn);
33426
33758
  } catch (err) {
33427
33759
  console.error(`Failed to connect to ${server.name}: ${err.message}`);
@@ -33434,13 +33766,28 @@ async function connectAllEnabled() {
33434
33766
 
33435
33767
  // src/lib/doctor.ts
33436
33768
  import { execFileSync as execFileSync2 } from "child_process";
33437
- async function diagnoseServer(server) {
33769
+ async function diagnoseServer(server, options = {}) {
33438
33770
  const checks4 = [];
33771
+ let hasLocalConsent = true;
33439
33772
  if (server.transport === "stdio") {
33773
+ try {
33774
+ assertLocalCommandConsent({
33775
+ command: server.command,
33776
+ args: server.args,
33777
+ env: server.env,
33778
+ transport: server.transport,
33779
+ operation: "diagnose"
33780
+ }, options.localCommandConsent);
33781
+ } catch (err) {
33782
+ hasLocalConsent = false;
33783
+ checks4.push({ name: "local command consent", pass: false, message: err.message });
33784
+ }
33440
33785
  try {
33441
33786
  const path = execFileSync2("which", [server.command], { stdio: "pipe" }).toString().trim();
33442
33787
  let version2 = "";
33443
33788
  try {
33789
+ if (!hasLocalConsent)
33790
+ throw new Error("local stdio command approval is required before version probing");
33444
33791
  version2 = execFileSync2(server.command, ["--version"], { stdio: "pipe" }).toString().trim().split(`
33445
33792
  `)[0];
33446
33793
  } catch {}
@@ -33471,10 +33818,10 @@ async function diagnoseServer(server) {
33471
33818
  checks4.push({ name: "URL reachable", pass: false, message: `unreachable: ${err.message}` });
33472
33819
  }
33473
33820
  }
33474
- if (server.enabled) {
33821
+ if (server.enabled && hasLocalConsent) {
33475
33822
  try {
33476
33823
  await Promise.race([
33477
- connectToServer(server),
33824
+ connectToServer(server, { localCommandConsent: options.localCommandConsent }),
33478
33825
  new Promise((_, reject) => setTimeout(() => reject(new Error("timeout after 10s")), 1e4))
33479
33826
  ]);
33480
33827
  await disconnectServer(server.id);
@@ -33482,8 +33829,10 @@ async function diagnoseServer(server) {
33482
33829
  } catch (err) {
33483
33830
  checks4.push({ name: "connect & list tools", pass: false, message: err.message });
33484
33831
  }
33485
- } else {
33832
+ } else if (!server.enabled) {
33486
33833
  checks4.push({ name: "connect & list tools", pass: true, message: "skipped (server disabled)" });
33834
+ } else {
33835
+ checks4.push({ name: "connect & list tools", pass: false, message: "skipped until local stdio command is approved" });
33487
33836
  }
33488
33837
  return {
33489
33838
  server,
@@ -34395,6 +34744,112 @@ async function runFleetInstall(options = {}, dependencies = {}) {
34395
34744
  }));
34396
34745
  }
34397
34746
 
34747
+ // src/lib/provider-profiles.ts
34748
+ init_db();
34749
+ init_provider_profile_seeds();
34750
+ var TRANSPORTS = new Set(["stdio", "sse", "streamable-http"]);
34751
+ var AUTH_TYPES = new Set(["none", "oauth2", "api_key", "bearer_token", "custom"]);
34752
+ var TOKEN_MODES = new Set(["none", "user", "workspace", "service"]);
34753
+ var BEARER_TOKEN_MODES = new Set(["none", "optional", "required"]);
34754
+ var PROVENANCE_SOURCES = new Set(["curated", "official-registry", "npm", "github", "manual"]);
34755
+ function safeJsonParse2(value, fallback) {
34756
+ if (typeof value !== "string")
34757
+ return fallback;
34758
+ try {
34759
+ return JSON.parse(value);
34760
+ } catch {
34761
+ return fallback;
34762
+ }
34763
+ }
34764
+ function normalizeId(id) {
34765
+ const normalized = id.trim().toLowerCase();
34766
+ if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/.test(normalized)) {
34767
+ throw new Error("Provider profile id must be lowercase kebab-case");
34768
+ }
34769
+ return normalized;
34770
+ }
34771
+ function parseRow3(row) {
34772
+ const installFallback = safeJsonParse2(row.install_fallback, null);
34773
+ return {
34774
+ id: row.id,
34775
+ displayName: row.display_name,
34776
+ description: row.description || null,
34777
+ endpoint: row.endpoint || null,
34778
+ transport: row.transport,
34779
+ fallbackEndpoints: safeJsonParse2(row.fallback_endpoints, []),
34780
+ authType: row.auth_type,
34781
+ authMetadata: safeJsonParse2(row.auth_metadata, {}),
34782
+ scopes: safeJsonParse2(row.scopes, []),
34783
+ tokenMode: row.token_mode,
34784
+ installFallback,
34785
+ docsUrl: row.docs_url || null,
34786
+ safety: safeJsonParse2(row.safety, {}),
34787
+ provenance: safeJsonParse2(row.provenance, { source: "manual" }),
34788
+ enabled: row.enabled === 1 || row.enabled === true,
34789
+ created_at: row.created_at,
34790
+ updated_at: row.updated_at
34791
+ };
34792
+ }
34793
+ function listProviderProfiles(options = {}) {
34794
+ const db2 = getDb();
34795
+ const sql = options.enabledOnly ? "SELECT * FROM provider_profiles WHERE enabled = 1 ORDER BY display_name" : "SELECT * FROM provider_profiles ORDER BY display_name";
34796
+ return db2.prepare(sql).all().map(parseRow3);
34797
+ }
34798
+ function searchProviderProfiles(query, options = {}) {
34799
+ const normalizedQuery = query.trim().toLowerCase();
34800
+ if (!normalizedQuery)
34801
+ return listProviderProfiles(options);
34802
+ return listProviderProfiles(options).filter((profile) => {
34803
+ const searchable = [
34804
+ profile.id,
34805
+ profile.displayName,
34806
+ profile.description ?? "",
34807
+ profile.endpoint ?? "",
34808
+ profile.docsUrl ?? "",
34809
+ profile.provenance.sourceUrl ?? "",
34810
+ profile.provenance.packageName ?? ""
34811
+ ].join(`
34812
+ `).toLowerCase();
34813
+ return searchable.includes(normalizedQuery);
34814
+ });
34815
+ }
34816
+ function getProviderProfile(id) {
34817
+ const db2 = getDb();
34818
+ const row = db2.prepare("SELECT * FROM provider_profiles WHERE id = ?").get(normalizeId(id));
34819
+ return row ? parseRow3(row) : null;
34820
+ }
34821
+ function installProviderProfile(id, options = {}) {
34822
+ const profile = getProviderProfile(id);
34823
+ if (!profile)
34824
+ throw new Error(`Provider profile "${id}" not found`);
34825
+ if (!profile.enabled)
34826
+ throw new Error(`Provider profile "${id}" is disabled`);
34827
+ const fallback = profile.installFallback;
34828
+ const useFallback = options.useFallback || !profile.endpoint;
34829
+ const command = useFallback ? fallback?.command : fallback?.command ?? "npx";
34830
+ const args = useFallback ? fallback?.args ?? [] : fallback?.args ?? [];
34831
+ if (!command) {
34832
+ throw new Error(`Provider profile "${id}" does not define an install fallback command`);
34833
+ }
34834
+ assertLocalCommandConsent({
34835
+ command,
34836
+ args,
34837
+ env: fallback?.env ?? {},
34838
+ transport: useFallback ? "stdio" : profile.transport,
34839
+ operation: "register"
34840
+ }, options.localCommandConsent);
34841
+ return addServer({
34842
+ name: options.name ?? profile.displayName,
34843
+ description: profile.description ?? undefined,
34844
+ command,
34845
+ args,
34846
+ env: fallback?.env,
34847
+ transport: useFallback ? "stdio" : profile.transport,
34848
+ url: useFallback ? fallback?.url : profile.endpoint ?? undefined,
34849
+ source: "provider-profile"
34850
+ });
34851
+ }
34852
+
34398
34853
  // src/mcp/tools.ts
34399
34854
  var VERSION = readPackageVersion(import.meta.url);
34400
34855
  var mcpsAgents = new Map;
@@ -34410,6 +34865,13 @@ function jsonContent(value) {
34410
34865
  function errorContent(text) {
34411
34866
  return { ...textContent(text), isError: true };
34412
34867
  }
34868
+ function localConsent(input) {
34869
+ return {
34870
+ approved: input.allow_local_stdio === true,
34871
+ allowRisky: input.allow_risky_command === true,
34872
+ source: "mcp"
34873
+ };
34874
+ }
34413
34875
  function buildMcpTools() {
34414
34876
  const definitions = [
34415
34877
  {
@@ -34434,23 +34896,46 @@ function buildMcpTools() {
34434
34896
  description: exports_external2.string().optional().describe("Description"),
34435
34897
  transport: exports_external2.enum(["stdio", "sse", "streamable-http"]).optional().describe("Transport type"),
34436
34898
  url: exports_external2.string().optional().describe("URL for remote transports"),
34437
- env: exports_external2.record(exports_external2.string()).optional().describe("Environment variables")
34899
+ env: exports_external2.record(exports_external2.string()).optional().describe("Environment variables"),
34900
+ allow_local_stdio: exports_external2.boolean().optional().describe("Approve registering this local stdio command"),
34901
+ allow_risky_command: exports_external2.boolean().optional().describe("Approve registering risky local command patterns")
34438
34902
  },
34439
- run: ({ command, args, name, description, transport, url: url2, env }) => jsonContent(addServer({
34440
- command: String(command),
34441
- args: Array.isArray(args) ? args.map(String) : [],
34442
- name: typeof name === "string" ? name : undefined,
34443
- description: typeof description === "string" ? description : undefined,
34444
- transport,
34445
- url: typeof url2 === "string" ? url2 : undefined,
34446
- env: isRecordOfStrings(env) ? env : {}
34447
- }))
34903
+ run: (input) => {
34904
+ const command = String(input.command);
34905
+ const args = Array.isArray(input.args) ? input.args.map(String) : [];
34906
+ const env = isRecordOfStrings(input.env) ? input.env : {};
34907
+ const transport = input.transport;
34908
+ try {
34909
+ assertLocalCommandConsent({ command, args, env, transport, operation: "register" }, localConsent(input));
34910
+ return jsonContent(addServer({
34911
+ command,
34912
+ args,
34913
+ name: typeof input.name === "string" ? input.name : undefined,
34914
+ description: typeof input.description === "string" ? input.description : undefined,
34915
+ transport,
34916
+ url: typeof input.url === "string" ? input.url : undefined,
34917
+ env
34918
+ }));
34919
+ } catch (err) {
34920
+ return errorContent(err.message);
34921
+ }
34922
+ }
34448
34923
  },
34449
34924
  {
34450
34925
  name: "install_from_registry",
34451
34926
  description: "Install an MCP server from the official registry",
34452
- paramsSchema: { id: exports_external2.string().describe("Registry server ID") },
34453
- run: async ({ id }) => jsonContent(await installFromRegistry(String(id)))
34927
+ paramsSchema: {
34928
+ id: exports_external2.string().describe("Registry server ID"),
34929
+ allow_local_stdio: exports_external2.boolean().optional().describe("Approve registering registry stdio commands"),
34930
+ allow_risky_command: exports_external2.boolean().optional().describe("Approve registering risky local command patterns")
34931
+ },
34932
+ run: async (input) => {
34933
+ try {
34934
+ return jsonContent(await installFromRegistry(String(input.id), { localCommandConsent: localConsent(input) }));
34935
+ } catch (err) {
34936
+ return errorContent(err.message);
34937
+ }
34938
+ }
34454
34939
  },
34455
34940
  {
34456
34941
  name: "remove_server",
@@ -34496,26 +34981,41 @@ function buildMcpTools() {
34496
34981
  command: exports_external2.string().optional().describe("New command"),
34497
34982
  args: exports_external2.array(exports_external2.string()).optional().describe("New args list"),
34498
34983
  transport: exports_external2.enum(["stdio", "sse", "streamable-http"]).optional().describe("New transport type"),
34499
- url: exports_external2.string().optional().describe("New URL for remote transports")
34984
+ url: exports_external2.string().optional().describe("New URL for remote transports"),
34985
+ allow_local_stdio: exports_external2.boolean().optional().describe("Approve updating this local stdio command"),
34986
+ allow_risky_command: exports_external2.boolean().optional().describe("Approve risky local command patterns")
34500
34987
  },
34501
- run: ({ id, name, description, command, args, transport, url: url2 }) => {
34502
- const serverId = String(id);
34988
+ run: (input) => {
34989
+ const serverId = String(input.id);
34503
34990
  const existing = getServer(serverId);
34504
34991
  if (!existing)
34505
34992
  return errorContent(`Server "${serverId}" not found.`);
34506
34993
  const fields = {};
34507
- if (typeof name === "string")
34508
- fields.name = name;
34509
- if (typeof description === "string")
34510
- fields.description = description;
34511
- if (typeof command === "string")
34512
- fields.command = command;
34513
- if (Array.isArray(args))
34514
- fields.args = args.map(String);
34515
- if (transport === "stdio" || transport === "sse" || transport === "streamable-http")
34516
- fields.transport = transport;
34517
- if (typeof url2 === "string")
34518
- fields.url = url2;
34994
+ if (typeof input.name === "string")
34995
+ fields.name = input.name;
34996
+ if (typeof input.description === "string")
34997
+ fields.description = input.description;
34998
+ if (typeof input.command === "string")
34999
+ fields.command = input.command;
35000
+ if (Array.isArray(input.args))
35001
+ fields.args = input.args.map(String);
35002
+ if (input.transport === "stdio" || input.transport === "sse" || input.transport === "streamable-http")
35003
+ fields.transport = input.transport;
35004
+ if (typeof input.url === "string")
35005
+ fields.url = input.url;
35006
+ if (fields.command !== undefined || fields.args !== undefined || fields.transport !== undefined) {
35007
+ try {
35008
+ assertLocalCommandConsent({
35009
+ command: fields.command ?? existing.command,
35010
+ args: fields.args ?? existing.args,
35011
+ env: existing.env,
35012
+ transport: fields.transport ?? existing.transport,
35013
+ operation: "register"
35014
+ }, localConsent(input));
35015
+ } catch (err) {
35016
+ return errorContent(err.message);
35017
+ }
35018
+ }
34519
35019
  return jsonContent(redactServerEnv(updateServer(serverId, fields)));
34520
35020
  }
34521
35021
  },
@@ -34561,6 +35061,58 @@ function buildMcpTools() {
34561
35061
  limit: typeof limit === "number" ? limit : undefined
34562
35062
  }))
34563
35063
  },
35064
+ {
35065
+ name: "list_provider_profiles",
35066
+ description: "List curated provider profiles for hosted/common MCP integrations such as Notion and Linear.",
35067
+ paramsSchema: {
35068
+ enabled_only: exports_external2.boolean().optional().describe("Only include enabled provider profiles")
35069
+ },
35070
+ run: ({ enabled_only }) => jsonContent(listProviderProfiles({ enabledOnly: enabled_only === true }))
35071
+ },
35072
+ {
35073
+ name: "search_provider_profiles",
35074
+ description: "Search curated provider profiles separately from raw MCP registry/source search.",
35075
+ paramsSchema: {
35076
+ query: exports_external2.string().describe("Search query such as 'notion', 'linear', or an endpoint URL"),
35077
+ enabled_only: exports_external2.boolean().optional().describe("Only include enabled provider profiles")
35078
+ },
35079
+ run: ({ query, enabled_only }) => jsonContent(searchProviderProfiles(String(query), { enabledOnly: enabled_only === true }))
35080
+ },
35081
+ {
35082
+ name: "get_provider_profile",
35083
+ description: "Get one curated provider profile by ID.",
35084
+ paramsSchema: {
35085
+ id: exports_external2.string().describe("Provider profile ID")
35086
+ },
35087
+ run: ({ id }) => {
35088
+ const profile = getProviderProfile(String(id));
35089
+ if (!profile)
35090
+ return errorContent(`Provider profile "${String(id)}" not found.`);
35091
+ return jsonContent(profile);
35092
+ }
35093
+ },
35094
+ {
35095
+ name: "install_provider_profile",
35096
+ description: "Register a curated provider profile as an MCP server.",
35097
+ paramsSchema: {
35098
+ id: exports_external2.string().describe("Provider profile ID"),
35099
+ name: exports_external2.string().optional().describe("Override registered server name"),
35100
+ use_fallback: exports_external2.boolean().optional().describe("Install the stdio fallback command instead of the direct remote transport"),
35101
+ allow_local_stdio: exports_external2.boolean().optional().describe("Approve registering provider stdio fallback commands"),
35102
+ allow_risky_command: exports_external2.boolean().optional().describe("Approve risky local command patterns")
35103
+ },
35104
+ run: (input) => {
35105
+ try {
35106
+ return jsonContent(redactServerEnv(installProviderProfile(String(input.id), {
35107
+ name: typeof input.name === "string" ? input.name : undefined,
35108
+ useFallback: input.use_fallback === true,
35109
+ localCommandConsent: localConsent(input)
35110
+ })));
35111
+ } catch (err) {
35112
+ return errorContent(err.message);
35113
+ }
35114
+ }
35115
+ },
34564
35116
  {
34565
35117
  name: "list_sources",
34566
35118
  description: "List all configured search sources for finding MCP servers",
@@ -34627,15 +35179,19 @@ function buildMcpTools() {
34627
35179
  description: "Install a registered MCP server into Claude Code, Codex, and/or Gemini",
34628
35180
  paramsSchema: {
34629
35181
  id: exports_external2.string().describe("Server ID to install (from list_servers)"),
34630
- targets: exports_external2.array(exports_external2.enum(["claude", "codex", "gemini"])).optional().describe("Target agents to install into (default: all)")
35182
+ targets: exports_external2.array(exports_external2.enum(["claude", "codex", "gemini"])).optional().describe("Target agents to install into (default: all)"),
35183
+ allow_local_stdio: exports_external2.boolean().optional().describe("Approve installing local stdio commands into local agent configs"),
35184
+ allow_risky_command: exports_external2.boolean().optional().describe("Approve installing risky local command patterns")
34631
35185
  },
34632
- run: ({ id, targets }) => {
34633
- const serverId = String(id);
35186
+ run: (input) => {
35187
+ const serverId = String(input.id);
34634
35188
  const entry = getServer(serverId);
34635
35189
  if (!entry)
34636
35190
  return errorContent(`Server "${serverId}" not found.`);
34637
- const agentTargets = Array.isArray(targets) ? targets : undefined;
34638
- return jsonContent(installToAgents(entry, agentTargets ?? ["claude", "codex", "gemini"]));
35191
+ const agentTargets = Array.isArray(input.targets) ? input.targets : undefined;
35192
+ return jsonContent(installToAgents(entry, agentTargets ?? ["claude", "codex", "gemini"], {
35193
+ localCommandConsent: localConsent(input)
35194
+ }));
34639
35195
  }
34640
35196
  },
34641
35197
  {
@@ -34647,11 +35203,14 @@ function buildMcpTools() {
34647
35203
  {
34648
35204
  name: "connect_and_list_tools",
34649
35205
  description: "Connect to all enabled MCP servers and list their available tools",
34650
- paramsSchema: {},
34651
- run: async () => {
35206
+ paramsSchema: {
35207
+ allow_local_stdio: exports_external2.boolean().optional().describe("Approve launching enabled local stdio commands"),
35208
+ allow_risky_command: exports_external2.boolean().optional().describe("Approve launching risky local command patterns")
35209
+ },
35210
+ run: async (input) => {
34652
35211
  let liveTools = [];
34653
35212
  try {
34654
- await connectAllEnabled();
35213
+ await connectAllEnabled({ localCommandConsent: localConsent(input) });
34655
35214
  liveTools = listAllTools();
34656
35215
  } finally {
34657
35216
  await disconnectAll().catch(() => {
@@ -34666,11 +35225,13 @@ function buildMcpTools() {
34666
35225
  description: `Call a tool on a connected upstream MCP server. Tool name format: server_id${TOOL_PREFIX_SEPARATOR}tool_name`,
34667
35226
  paramsSchema: {
34668
35227
  tool_name: exports_external2.string().describe(`Prefixed tool name (server_id${TOOL_PREFIX_SEPARATOR}tool_name)`),
34669
- arguments: exports_external2.record(exports_external2.unknown()).optional().describe("Tool arguments as key-value pairs")
35228
+ arguments: exports_external2.record(exports_external2.unknown()).optional().describe("Tool arguments as key-value pairs"),
35229
+ allow_local_stdio: exports_external2.boolean().optional().describe("Approve launching this local stdio command"),
35230
+ allow_risky_command: exports_external2.boolean().optional().describe("Approve launching risky local command patterns")
34670
35231
  },
34671
- run: async ({ tool_name, arguments: args }) => {
35232
+ run: async (input) => {
34672
35233
  try {
34673
- const toolName = String(tool_name);
35234
+ const toolName = String(input.tool_name);
34674
35235
  const sepIdx = toolName.indexOf(TOOL_PREFIX_SEPARATOR);
34675
35236
  if (sepIdx === -1)
34676
35237
  return errorContent(`Error: Invalid tool name "${toolName}"`);
@@ -34680,8 +35241,8 @@ function buildMcpTools() {
34680
35241
  return errorContent(`Error: Server "${serverId}" not found.`);
34681
35242
  if (!entry.enabled)
34682
35243
  return errorContent(`Error: Server "${serverId}" is disabled.`);
34683
- await connectToServer(entry);
34684
- const result = await callTool(toolName, readRecord(args));
35244
+ await connectToServer(entry, { localCommandConsent: localConsent(input) });
35245
+ const result = await callTool(toolName, readRecord(input.arguments));
34685
35246
  return { content: result.content };
34686
35247
  } catch (error2) {
34687
35248
  return errorContent(`Error: ${error2.message}`);
@@ -34691,13 +35252,17 @@ function buildMcpTools() {
34691
35252
  {
34692
35253
  name: "diagnose_server",
34693
35254
  description: "Run health checks on a registered MCP server",
34694
- paramsSchema: { id: exports_external2.string().describe("Server ID") },
34695
- run: async ({ id }) => {
34696
- const serverId = String(id);
35255
+ paramsSchema: {
35256
+ id: exports_external2.string().describe("Server ID"),
35257
+ allow_local_stdio: exports_external2.boolean().optional().describe("Approve launching local stdio diagnostics"),
35258
+ allow_risky_command: exports_external2.boolean().optional().describe("Approve diagnosing risky local command patterns")
35259
+ },
35260
+ run: async (input) => {
35261
+ const serverId = String(input.id);
34697
35262
  const entry = getServer(serverId);
34698
35263
  if (!entry)
34699
35264
  return errorContent(`Server "${serverId}" not found.`);
34700
- return jsonContent(await diagnoseServer(entry));
35265
+ return jsonContent(await diagnoseServer(entry, { localCommandConsent: localConsent(input) }));
34701
35266
  }
34702
35267
  },
34703
35268
  {