@hasna/mcps 0.0.15 → 0.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/index.js CHANGED
@@ -12040,6 +12040,7 @@ function getDb() {
12040
12040
  command TEXT NOT NULL,
12041
12041
  args TEXT NOT NULL DEFAULT '[]',
12042
12042
  env TEXT NOT NULL DEFAULT '{}',
12043
+ credential_refs TEXT NOT NULL DEFAULT '{}',
12043
12044
  transport TEXT NOT NULL DEFAULT 'stdio',
12044
12045
  url TEXT,
12045
12046
  source TEXT NOT NULL DEFAULT 'local',
@@ -12066,6 +12067,9 @@ function getDb() {
12066
12067
  try {
12067
12068
  db.exec("ALTER TABLE servers ADD COLUMN last_error TEXT");
12068
12069
  } catch {}
12070
+ try {
12071
+ db.exec("ALTER TABLE servers ADD COLUMN credential_refs TEXT NOT NULL DEFAULT '{}'");
12072
+ } catch {}
12069
12073
  db.exec(`
12070
12074
  CREATE TABLE IF NOT EXISTS sources (
12071
12075
  id TEXT PRIMARY KEY,
@@ -19126,17 +19130,17 @@ __export(exports_sources, {
19126
19130
  clearCache: () => clearCache,
19127
19131
  addSource: () => addSource
19128
19132
  });
19129
- import { mkdirSync as mkdirSync6, existsSync as existsSync6, readFileSync as readFileSync2, writeFileSync as writeFileSync2, readdirSync as readdirSync3, unlinkSync } from "fs";
19130
- import { join as join7 } from "path";
19133
+ import { mkdirSync as mkdirSync6, existsSync as existsSync7, readFileSync as readFileSync3, writeFileSync as writeFileSync2, readdirSync as readdirSync3, unlinkSync } from "fs";
19134
+ import { join as join8 } from "path";
19131
19135
  function getCacheFile(sourceId) {
19132
- return join7(CACHE_DIR, `${sourceId}.json`);
19136
+ return join8(CACHE_DIR, `${sourceId}.json`);
19133
19137
  }
19134
19138
  function readCache(sourceId) {
19135
19139
  try {
19136
19140
  const file = getCacheFile(sourceId);
19137
- if (!existsSync6(file))
19141
+ if (!existsSync7(file))
19138
19142
  return null;
19139
- const data = JSON.parse(readFileSync2(file, "utf-8"));
19143
+ const data = JSON.parse(readFileSync3(file, "utf-8"));
19140
19144
  return data;
19141
19145
  } catch {
19142
19146
  return null;
@@ -19150,7 +19154,7 @@ function writeCache(sourceId, results) {
19150
19154
  }
19151
19155
  function clearCache(sourceId) {
19152
19156
  try {
19153
- if (!existsSync6(CACHE_DIR))
19157
+ if (!existsSync7(CACHE_DIR))
19154
19158
  return;
19155
19159
  const files = readdirSync3(CACHE_DIR);
19156
19160
  for (const file of files) {
@@ -19158,7 +19162,7 @@ function clearCache(sourceId) {
19158
19162
  continue;
19159
19163
  if (!sourceId || file.startsWith(`${sourceId}.`)) {
19160
19164
  try {
19161
- unlinkSync(join7(CACHE_DIR, file));
19165
+ unlinkSync(join8(CACHE_DIR, file));
19162
19166
  } catch {}
19163
19167
  }
19164
19168
  }
@@ -19404,7 +19408,7 @@ var CACHE_DIR, DEFAULT_TTL_MS;
19404
19408
  var init_sources = __esm(() => {
19405
19409
  init_db();
19406
19410
  init_config2();
19407
- CACHE_DIR = join7(MCPS_DIR, "cache");
19411
+ CACHE_DIR = join8(MCPS_DIR, "cache");
19408
19412
  DEFAULT_TTL_MS = 10 * 60 * 1000;
19409
19413
  });
19410
19414
 
@@ -19412,7 +19416,7 @@ var init_sources = __esm(() => {
19412
19416
  var require_package = __commonJS((exports, module) => {
19413
19417
  module.exports = {
19414
19418
  name: "@hasna/mcps",
19415
- version: "0.0.15",
19419
+ version: "0.0.16",
19416
19420
  description: "Meta-MCP registry & CLI \u2014 discover, manage, and proxy MCP servers",
19417
19421
  type: "module",
19418
19422
  repository: {
@@ -21142,10 +21146,164 @@ var {
21142
21146
  import React10 from "react";
21143
21147
  import { render } from "ink";
21144
21148
  import chalk2 from "chalk";
21145
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
21149
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
21146
21150
 
21147
21151
  // src/lib/registry.ts
21148
21152
  init_db();
21153
+
21154
+ // src/lib/credentials.ts
21155
+ init_config2();
21156
+ import { existsSync as existsSync6, readFileSync as readFileSync2 } from "fs";
21157
+ import { join as join7 } from "path";
21158
+
21159
+ class CredentialReferenceError extends Error {
21160
+ constructor(message) {
21161
+ super(message);
21162
+ this.name = "CredentialReferenceError";
21163
+ }
21164
+ }
21165
+ var SECRET_KEY_PATTERN = /(?:^|[_-])(api[_-]?key|token|secret|password|passwd|credential|auth|private[_-]?key)(?:$|[_-])/i;
21166
+ var SECRET_VALUE_PATTERN = /^(sk_(?:live|test)_[A-Za-z0-9_]+|ghp_[A-Za-z0-9_]+|github_pat_[A-Za-z0-9_]+|xox[baprs]-[A-Za-z0-9-]+|AIza[A-Za-z0-9_-]{20,}|eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)$/;
21167
+ var REDACTED_CREDENTIAL_VALUE = "<redacted>";
21168
+ function normalizeKey(key) {
21169
+ return key.trim();
21170
+ }
21171
+ function isRecord(value) {
21172
+ return typeof value === "object" && value !== null && !Array.isArray(value);
21173
+ }
21174
+ function isSecretLikeEnvKey(key) {
21175
+ return SECRET_KEY_PATTERN.test(key);
21176
+ }
21177
+ function isSecretLikeValue(value) {
21178
+ return SECRET_VALUE_PATTERN.test(value.trim());
21179
+ }
21180
+ function normalizeCredentialRef(ref) {
21181
+ const source = ref.source;
21182
+ if (source !== "env" && source !== "local-vault" && source !== "hosted") {
21183
+ throw new CredentialReferenceError(`Unsupported credential reference source: ${String(source)}`);
21184
+ }
21185
+ const name = ref.name?.trim();
21186
+ if (!name) {
21187
+ throw new CredentialReferenceError("Credential reference name is required");
21188
+ }
21189
+ return {
21190
+ source,
21191
+ name,
21192
+ required: ref.required !== false,
21193
+ ...ref.description ? { description: ref.description } : {}
21194
+ };
21195
+ }
21196
+ function normalizeCredentialRefs(refs) {
21197
+ const normalized = {};
21198
+ for (const [rawKey, ref] of Object.entries(refs ?? {})) {
21199
+ const key = normalizeKey(rawKey);
21200
+ if (!key)
21201
+ throw new CredentialReferenceError("Credential reference env key is required");
21202
+ normalized[key] = normalizeCredentialRef(ref);
21203
+ }
21204
+ return normalized;
21205
+ }
21206
+ function parseCredentialRefs(value) {
21207
+ if (!isRecord(value))
21208
+ return {};
21209
+ const refs = {};
21210
+ for (const [key, ref] of Object.entries(value)) {
21211
+ if (!isRecord(ref))
21212
+ continue;
21213
+ const source = ref.source;
21214
+ const name = ref.name;
21215
+ if ((source === "env" || source === "local-vault" || source === "hosted") && typeof name === "string" && name.trim()) {
21216
+ refs[key] = normalizeCredentialRef({
21217
+ source,
21218
+ name,
21219
+ required: typeof ref.required === "boolean" ? ref.required : true,
21220
+ description: typeof ref.description === "string" ? ref.description : undefined
21221
+ });
21222
+ }
21223
+ }
21224
+ return refs;
21225
+ }
21226
+ function normalizeLiteralEnv(env) {
21227
+ const normalized = {};
21228
+ for (const [rawKey, rawValue] of Object.entries(env ?? {})) {
21229
+ const key = normalizeKey(rawKey);
21230
+ if (!key)
21231
+ continue;
21232
+ const value = String(rawValue);
21233
+ if (isSecretLikeEnvKey(key) || isSecretLikeValue(value)) {
21234
+ throw new CredentialReferenceError(`Refusing to store raw secret-like env value for "${key}". Use a credential reference instead.`);
21235
+ }
21236
+ normalized[key] = value;
21237
+ }
21238
+ return normalized;
21239
+ }
21240
+ function redactEnv(env) {
21241
+ const redacted = {};
21242
+ for (const [key, value] of Object.entries(env)) {
21243
+ redacted[key] = isSecretLikeEnvKey(key) || isSecretLikeValue(value) ? REDACTED_CREDENTIAL_VALUE : value;
21244
+ }
21245
+ return redacted;
21246
+ }
21247
+ function redactServerCredentials(server) {
21248
+ return {
21249
+ ...server,
21250
+ env: redactEnv(server.env),
21251
+ credentialRefs: normalizeCredentialRefs(server.credentialRefs)
21252
+ };
21253
+ }
21254
+ function readLocalVault() {
21255
+ const path = process.env.HASNA_MCPS_CREDENTIAL_VAULT_PATH ?? join7(MCPS_DIR, "credentials.local.json");
21256
+ if (!existsSync6(path))
21257
+ return {};
21258
+ const parsed = JSON.parse(readFileSync2(path, "utf-8"));
21259
+ if (!isRecord(parsed))
21260
+ return {};
21261
+ const values = {};
21262
+ for (const [key, value] of Object.entries(parsed)) {
21263
+ if (typeof value === "string")
21264
+ values[key] = value;
21265
+ }
21266
+ return values;
21267
+ }
21268
+ function resolveCredentialRef(envKey, ref) {
21269
+ if (ref.source === "env") {
21270
+ const value = process.env[ref.name];
21271
+ if (value === undefined && ref.required !== false) {
21272
+ throw new CredentialReferenceError(`Missing required environment credential "${ref.name}" for "${envKey}"`);
21273
+ }
21274
+ return value;
21275
+ }
21276
+ if (ref.source === "local-vault") {
21277
+ const value = readLocalVault()[ref.name];
21278
+ if (value === undefined && ref.required !== false) {
21279
+ throw new CredentialReferenceError(`Missing required local vault credential "${ref.name}" for "${envKey}"`);
21280
+ }
21281
+ return value;
21282
+ }
21283
+ if (ref.required !== false) {
21284
+ throw new CredentialReferenceError(`Hosted credential "${ref.name}" for "${envKey}" cannot be resolved by the local runtime`);
21285
+ }
21286
+ return;
21287
+ }
21288
+ function resolveServerEnv(server) {
21289
+ const resolved = { ...server.env };
21290
+ const refs = normalizeCredentialRefs(server.credentialRefs);
21291
+ for (const [envKey, ref] of Object.entries(refs)) {
21292
+ const value = resolveCredentialRef(envKey, ref);
21293
+ if (value !== undefined)
21294
+ resolved[envKey] = value;
21295
+ }
21296
+ return resolved;
21297
+ }
21298
+ function credentialRefPlaceholders(refs) {
21299
+ const placeholders = {};
21300
+ for (const key of Object.keys(refs ?? {})) {
21301
+ placeholders[key] = REDACTED_CREDENTIAL_VALUE;
21302
+ }
21303
+ return placeholders;
21304
+ }
21305
+
21306
+ // src/lib/registry.ts
21149
21307
  function parseRow(row) {
21150
21308
  return {
21151
21309
  id: row.id,
@@ -21154,6 +21312,7 @@ function parseRow(row) {
21154
21312
  command: row.command,
21155
21313
  args: safeJsonParse(row.args, []),
21156
21314
  env: safeJsonParse(row.env, {}),
21315
+ credentialRefs: parseCredentialRefs(safeJsonParse(row.credential_refs, {})),
21157
21316
  transport: row.transport,
21158
21317
  url: row.url || null,
21159
21318
  source: row.source,
@@ -21221,9 +21380,9 @@ function addServer(opts) {
21221
21380
  if (!id) {
21222
21381
  throw new Error("Unable to generate a valid server ID");
21223
21382
  }
21224
- const row = db2.prepare(`INSERT INTO servers (id, name, description, command, args, env, transport, url, source)
21225
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
21226
- RETURNING *`).get(id, name, opts.description || null, command, JSON.stringify(opts.args || []), JSON.stringify(opts.env || {}), opts.transport || "stdio", opts.url || null, opts.source || "local");
21383
+ const row = db2.prepare(`INSERT INTO servers (id, name, description, command, args, env, credential_refs, transport, url, source)
21384
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
21385
+ RETURNING *`).get(id, name, opts.description || null, command, JSON.stringify(opts.args || []), JSON.stringify(normalizeLiteralEnv(opts.env)), JSON.stringify(normalizeCredentialRefs(opts.credentialRefs)), opts.transport || "stdio", opts.url || null, opts.source || "local");
21227
21386
  return parseRow(row);
21228
21387
  }
21229
21388
  function removeServer(id) {
@@ -21262,7 +21421,11 @@ function updateServer(id, updates) {
21262
21421
  }
21263
21422
  if (updates.env !== undefined) {
21264
21423
  sets.push("env = ?");
21265
- values.push(JSON.stringify(updates.env));
21424
+ values.push(JSON.stringify(normalizeLiteralEnv(updates.env)));
21425
+ }
21426
+ if (updates.credentialRefs !== undefined) {
21427
+ sets.push("credential_refs = ?");
21428
+ values.push(JSON.stringify(normalizeCredentialRefs(updates.credentialRefs)));
21266
21429
  }
21267
21430
  if (updates.transport !== undefined) {
21268
21431
  sets.push("transport = ?");
@@ -21296,7 +21459,7 @@ function setServerEnv(id, key, value) {
21296
21459
  if (!server)
21297
21460
  throw new Error(`Server "${id}" not found`);
21298
21461
  const env = { ...server.env, [key]: value };
21299
- db2.prepare("UPDATE servers SET env = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(env), id);
21462
+ db2.prepare("UPDATE servers SET env = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(normalizeLiteralEnv(env)), id);
21300
21463
  }
21301
21464
  function unsetServerEnv(id, key) {
21302
21465
  const db2 = getDb();
@@ -21307,6 +21470,23 @@ function unsetServerEnv(id, key) {
21307
21470
  delete env[key];
21308
21471
  db2.prepare("UPDATE servers SET env = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(env), id);
21309
21472
  }
21473
+ function setServerCredentialRef(id, key, ref) {
21474
+ const db2 = getDb();
21475
+ const server = getServer(id);
21476
+ if (!server)
21477
+ throw new Error(`Server "${id}" not found`);
21478
+ const credentialRefs = normalizeCredentialRefs({ ...server.credentialRefs ?? {}, [key]: ref });
21479
+ db2.prepare("UPDATE servers SET credential_refs = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(credentialRefs), id);
21480
+ }
21481
+ function unsetServerCredentialRef(id, key) {
21482
+ const db2 = getDb();
21483
+ const server = getServer(id);
21484
+ if (!server)
21485
+ throw new Error(`Server "${id}" not found`);
21486
+ const credentialRefs = { ...server.credentialRefs ?? {} };
21487
+ delete credentialRefs[key];
21488
+ db2.prepare("UPDATE servers SET credential_refs = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(normalizeCredentialRefs(credentialRefs)), id);
21489
+ }
21310
21490
  function cacheTools(serverId, tools) {
21311
21491
  const db2 = getDb();
21312
21492
  const insert = db2.prepare("INSERT INTO tool_cache (server_id, name, description, input_schema) VALUES (?, ?, ?, ?)");
@@ -21346,6 +21526,7 @@ function cloneServer(id, newName) {
21346
21526
  command: server.command,
21347
21527
  args: server.args,
21348
21528
  env: server.env,
21529
+ credentialRefs: server.credentialRefs,
21349
21530
  transport: server.transport,
21350
21531
  url: server.url ?? undefined,
21351
21532
  source: server.source
@@ -35615,8 +35796,8 @@ var DESTRUCTIVE_COMMANDS = new Set([
35615
35796
  ]);
35616
35797
  var SHELL_EVAL_FLAGS = new Set(["-c", "/c", "-Command", "-command", "-EncodedCommand", "-encodedcommand"]);
35617
35798
  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_-]+)$/;
35799
+ var SECRET_KEY_PATTERN2 = /(?:^|[_-])(api[_-]?key|token|secret|password|passwd|credential|auth|private[_-]?key)(?:$|[_-])/i;
35800
+ var SECRET_VALUE_PATTERN2 = /^(sk_(?:live|test)_[A-Za-z0-9_]+|ghp_[A-Za-z0-9_]+|github_pat_[A-Za-z0-9_]+|xox[baprs]-[A-Za-z0-9-]+|AIza[A-Za-z0-9_-]{20,}|eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)$/;
35620
35801
  var SHELL_META_PATTERN = /[;&|`<>]|\$\(/;
35621
35802
  function commandBase(command) {
35622
35803
  return command.trim().split(/[\\/]/).pop()?.toLowerCase() || command.trim().toLowerCase();
@@ -35625,10 +35806,10 @@ function normalizeArgs(args) {
35625
35806
  return (args ?? []).map((arg) => String(arg));
35626
35807
  }
35627
35808
  function isSecretKey(key) {
35628
- return SECRET_KEY_PATTERN.test(key) || SECRET_FLAG_PATTERN.test(key);
35809
+ return SECRET_KEY_PATTERN2.test(key) || SECRET_FLAG_PATTERN.test(key);
35629
35810
  }
35630
35811
  function isSecretValue(value) {
35631
- return SECRET_VALUE_PATTERN.test(value.trim());
35812
+ return SECRET_VALUE_PATTERN2.test(value.trim());
35632
35813
  }
35633
35814
  function isSecretAssignment(arg) {
35634
35815
  const eqIdx = arg.indexOf("=");
@@ -35815,14 +35996,14 @@ async function connectToServer(entry, options = {}) {
35815
35996
  assertLocalCommandConsent({
35816
35997
  command: entry.command,
35817
35998
  args: entry.args,
35818
- env: entry.env,
35999
+ env: { ...entry.env, ...credentialRefPlaceholders(entry.credentialRefs) },
35819
36000
  transport: entry.transport,
35820
36001
  operation: "launch"
35821
36002
  }, options.localCommandConsent);
35822
36003
  transport = new StdioClientTransport({
35823
36004
  command: entry.command,
35824
36005
  args: entry.args,
35825
- env: buildEnv(entry.env)
36006
+ env: buildEnv(resolveServerEnv(entry))
35826
36007
  });
35827
36008
  } else if (entry.transport === "sse") {
35828
36009
  transport = new SSEClientTransport(requireUrl(entry));
@@ -35967,7 +36148,7 @@ async function diagnoseServer(server, options = {}) {
35967
36148
  assertLocalCommandConsent({
35968
36149
  command: server.command,
35969
36150
  args: server.args,
35970
- env: server.env,
36151
+ env: { ...server.env, ...credentialRefPlaceholders(server.credentialRefs) },
35971
36152
  transport: server.transport,
35972
36153
  operation: "diagnose"
35973
36154
  }, options.localCommandConsent);
@@ -35996,12 +36177,29 @@ async function diagnoseServer(server, options = {}) {
35996
36177
  }
35997
36178
  }
35998
36179
  const missingEnv = Object.entries(server.env).filter(([, v]) => !v);
35999
- if (Object.keys(server.env).length === 0) {
36000
- checks4.push({ name: "env vars", pass: true, message: "no env vars required" });
36001
- } else if (missingEnv.length > 0) {
36002
- checks4.push({ name: "env vars", pass: false, message: `missing values for: ${missingEnv.map(([k]) => k).join(", ")}` });
36180
+ const credentialRefCount = Object.keys(server.credentialRefs ?? {}).length;
36181
+ let credentialError = null;
36182
+ try {
36183
+ resolveServerEnv(server);
36184
+ } catch (err) {
36185
+ credentialError = err.message;
36186
+ }
36187
+ if (Object.keys(server.env).length === 0 && credentialRefCount === 0) {
36188
+ checks4.push({ name: "env vars", pass: true, message: "no env vars or credential refs required" });
36189
+ } else if (missingEnv.length > 0 || credentialError) {
36190
+ const parts = [];
36191
+ if (missingEnv.length > 0)
36192
+ parts.push(`missing literal values for: ${missingEnv.map(([k]) => k).join(", ")}`);
36193
+ if (credentialError)
36194
+ parts.push(credentialError);
36195
+ checks4.push({ name: "env vars", pass: false, message: parts.join("; ") });
36003
36196
  } else {
36004
- checks4.push({ name: "env vars", pass: true, message: `${Object.keys(server.env).length} env var(s) set` });
36197
+ const literalCount = Object.keys(server.env).length;
36198
+ checks4.push({
36199
+ name: "env vars",
36200
+ pass: true,
36201
+ message: `${literalCount} literal env var(s), ${credentialRefCount} credential ref(s) available`
36202
+ });
36005
36203
  }
36006
36204
  if (server.transport !== "stdio" && server.url) {
36007
36205
  try {
@@ -36118,8 +36316,8 @@ init_sources();
36118
36316
 
36119
36317
  // src/lib/install.ts
36120
36318
  import { execFileSync as execFileSync2 } from "child_process";
36121
- import { existsSync as existsSync7, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync7 } from "fs";
36122
- import { join as join8 } from "path";
36319
+ import { existsSync as existsSync8, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync7 } from "fs";
36320
+ import { join as join9 } from "path";
36123
36321
  import { homedir as homedir8 } from "os";
36124
36322
  function installToClaude(entry) {
36125
36323
  try {
@@ -36131,7 +36329,7 @@ function installToClaude(entry) {
36131
36329
  "--scope",
36132
36330
  "user"
36133
36331
  ];
36134
- for (const [k, v] of Object.entries(entry.env)) {
36332
+ for (const [k, v] of Object.entries(assertAgentInstallEnv(entry))) {
36135
36333
  args.push("--env", `${k}=${v}`);
36136
36334
  }
36137
36335
  args.push(entry.id, "--", entry.command, ...entry.args);
@@ -36143,9 +36341,9 @@ function installToClaude(entry) {
36143
36341
  }
36144
36342
  function installToCodex(entry) {
36145
36343
  try {
36146
- const configDir = join8(homedir8(), ".codex");
36147
- const configPath = join8(configDir, "config.toml");
36148
- if (!existsSync7(configDir)) {
36344
+ const configDir = join9(homedir8(), ".codex");
36345
+ const configPath = join9(configDir, "config.toml");
36346
+ if (!existsSync8(configDir)) {
36149
36347
  mkdirSync7(configDir, { recursive: true });
36150
36348
  }
36151
36349
  const block = `
@@ -36153,7 +36351,7 @@ function installToCodex(entry) {
36153
36351
  ` + `command = ${JSON.stringify(entry.command)}
36154
36352
  ` + `args = [${entry.args.map((a) => JSON.stringify(a)).join(", ")}]
36155
36353
  `;
36156
- const existing = existsSync7(configPath) ? readFileSync3(configPath, "utf-8") : "";
36354
+ const existing = existsSync8(configPath) ? readFileSync4(configPath, "utf-8") : "";
36157
36355
  if (existing.includes(`[mcp_servers.${entry.id}]`)) {
36158
36356
  return { agent: "codex", success: true };
36159
36357
  }
@@ -36165,21 +36363,22 @@ function installToCodex(entry) {
36165
36363
  }
36166
36364
  function installToGemini(entry) {
36167
36365
  try {
36168
- const configDir = join8(homedir8(), ".gemini");
36169
- const configPath = join8(configDir, "settings.json");
36170
- if (!existsSync7(configDir)) {
36366
+ const configDir = join9(homedir8(), ".gemini");
36367
+ const configPath = join9(configDir, "settings.json");
36368
+ if (!existsSync8(configDir)) {
36171
36369
  mkdirSync7(configDir, { recursive: true });
36172
36370
  }
36173
36371
  let settings = {};
36174
- if (existsSync7(configPath)) {
36175
- settings = JSON.parse(readFileSync3(configPath, "utf-8"));
36372
+ if (existsSync8(configPath)) {
36373
+ settings = JSON.parse(readFileSync4(configPath, "utf-8"));
36176
36374
  }
36177
36375
  if (!settings.mcpServers)
36178
36376
  settings.mcpServers = {};
36377
+ const env = assertAgentInstallEnv(entry);
36179
36378
  settings.mcpServers[entry.id] = {
36180
36379
  command: entry.command,
36181
36380
  args: entry.args,
36182
- ...Object.keys(entry.env).length > 0 ? { env: entry.env } : {}
36381
+ ...Object.keys(env).length > 0 ? { env } : {}
36183
36382
  };
36184
36383
  writeFileSync3(configPath, JSON.stringify(settings, null, 2), "utf-8");
36185
36384
  return { agent: "gemini", success: true };
@@ -36187,12 +36386,24 @@ function installToGemini(entry) {
36187
36386
  return { agent: "gemini", success: false, error: err.message };
36188
36387
  }
36189
36388
  }
36389
+ function assertAgentInstallEnv(entry) {
36390
+ const refs = entry.credentialRefs ?? {};
36391
+ if (Object.keys(refs).length > 0) {
36392
+ throw new CredentialReferenceError(`Server "${entry.id}" uses credential references; refusing to materialize secrets into local agent config files`);
36393
+ }
36394
+ for (const [key, value] of Object.entries(entry.env)) {
36395
+ if (isSecretLikeEnvKey(key) || isSecretLikeValue(value)) {
36396
+ throw new CredentialReferenceError(`Server "${entry.id}" has legacy raw secret-like env "${key}"; move it to a credential reference before installing to agents`);
36397
+ }
36398
+ }
36399
+ return entry.env;
36400
+ }
36190
36401
  function installToAgents(entry, targets = ["claude", "codex", "gemini"], options = {}) {
36191
36402
  try {
36192
36403
  assertLocalCommandConsent({
36193
36404
  command: entry.command,
36194
36405
  args: entry.args,
36195
- env: entry.env,
36406
+ env: { ...entry.env, ...credentialRefPlaceholders(entry.credentialRefs) },
36196
36407
  transport: entry.transport,
36197
36408
  operation: "install"
36198
36409
  }, options.localCommandConsent);
@@ -36203,6 +36414,15 @@ function installToAgents(entry, targets = ["claude", "codex", "gemini"], options
36203
36414
  error: err.message
36204
36415
  }));
36205
36416
  }
36417
+ try {
36418
+ assertAgentInstallEnv(entry);
36419
+ } catch (err) {
36420
+ return targets.map((target) => ({
36421
+ agent: target,
36422
+ success: false,
36423
+ error: err.message
36424
+ }));
36425
+ }
36206
36426
  return targets.map((target) => {
36207
36427
  if (target === "claude")
36208
36428
  return installToClaude(entry);
@@ -36417,11 +36637,11 @@ function seedDefaultMachines() {
36417
36637
  // src/lib/fleet.ts
36418
36638
  init_config2();
36419
36639
  import { spawn as spawn2 } from "child_process";
36420
- import { existsSync as existsSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
36421
- import { join as join9 } from "path";
36640
+ import { existsSync as existsSync9, mkdirSync as mkdirSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
36641
+ import { join as join10 } from "path";
36422
36642
  var NPM_SEARCH_URL = "https://registry.npmjs.org/-/v1/search";
36423
36643
  var NPM_REGISTRY_URL = "https://registry.npmjs.org";
36424
- var CATALOG_CACHE_PATH = join9(MCPS_DIR, "cache", "hasna-catalog.json");
36644
+ var CATALOG_CACHE_PATH = join10(MCPS_DIR, "cache", "hasna-catalog.json");
36425
36645
  var DEFAULT_CATALOG_CACHE_TTL_MS = 60 * 60 * 1000;
36426
36646
  var DEFAULT_REMOTE_TIMEOUT_MS = 180000;
36427
36647
  var DEFAULT_HANDSHAKE_TIMEOUT_MS = 2500;
@@ -36431,9 +36651,9 @@ function normalizeQueryList(values) {
36431
36651
  }
36432
36652
  function readCatalogCache(maxAgeMs) {
36433
36653
  try {
36434
- if (!existsSync8(CATALOG_CACHE_PATH))
36654
+ if (!existsSync9(CATALOG_CACHE_PATH))
36435
36655
  return null;
36436
- const parsed = JSON.parse(readFileSync4(CATALOG_CACHE_PATH, "utf-8"));
36656
+ const parsed = JSON.parse(readFileSync5(CATALOG_CACHE_PATH, "utf-8"));
36437
36657
  if (!parsed.cachedAt || !Array.isArray(parsed.entries))
36438
36658
  return null;
36439
36659
  if (Date.now() - parsed.cachedAt > maxAgeMs)
@@ -36445,7 +36665,7 @@ function readCatalogCache(maxAgeMs) {
36445
36665
  }
36446
36666
  function writeCatalogCache(entries) {
36447
36667
  try {
36448
- mkdirSync8(join9(MCPS_DIR, "cache"), { recursive: true });
36668
+ mkdirSync8(join10(MCPS_DIR, "cache"), { recursive: true });
36449
36669
  writeFileSync4(CATALOG_CACHE_PATH, JSON.stringify({ cachedAt: Date.now(), entries }, null, 2), "utf-8");
36450
36670
  } catch {}
36451
36671
  }
@@ -38449,22 +38669,22 @@ var EMPTY_COMPLETION_RESULT = {
38449
38669
  };
38450
38670
 
38451
38671
  // src/lib/version.ts
38452
- import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
38453
- import { dirname as dirname3, join as join10 } from "path";
38672
+ import { existsSync as existsSync10, readFileSync as readFileSync6 } from "fs";
38673
+ import { dirname as dirname3, join as join11 } from "path";
38454
38674
  import { fileURLToPath } from "url";
38455
38675
  var FALLBACK_VERSION = "0.0.1";
38456
38676
  function readPackageVersion(moduleUrl, fallback = FALLBACK_VERSION) {
38457
38677
  const baseDir = dirname3(fileURLToPath(moduleUrl));
38458
38678
  const candidates = [
38459
- join10(baseDir, "..", "..", "package.json"),
38460
- join10(baseDir, "..", "package.json"),
38461
- join10(baseDir, "package.json")
38679
+ join11(baseDir, "..", "..", "package.json"),
38680
+ join11(baseDir, "..", "package.json"),
38681
+ join11(baseDir, "package.json")
38462
38682
  ];
38463
38683
  for (const candidate of candidates) {
38464
- if (!existsSync9(candidate))
38684
+ if (!existsSync10(candidate))
38465
38685
  continue;
38466
38686
  try {
38467
- const pkg = JSON.parse(readFileSync5(candidate, "utf-8"));
38687
+ const pkg = JSON.parse(readFileSync6(candidate, "utf-8"));
38468
38688
  if (pkg.version)
38469
38689
  return pkg.version;
38470
38690
  } catch {}
@@ -38497,6 +38717,9 @@ function localConsent(input) {
38497
38717
  source: "mcp"
38498
38718
  };
38499
38719
  }
38720
+ function readCredentialRefs(input) {
38721
+ return normalizeCredentialRefs(input.credential_refs ?? input.credentialRefs);
38722
+ }
38500
38723
  function buildMcpTools() {
38501
38724
  const definitions = [
38502
38725
  {
@@ -38522,6 +38745,12 @@ function buildMcpTools() {
38522
38745
  transport: exports_external2.enum(["stdio", "sse", "streamable-http"]).optional().describe("Transport type"),
38523
38746
  url: exports_external2.string().optional().describe("URL for remote transports"),
38524
38747
  env: exports_external2.record(exports_external2.string()).optional().describe("Environment variables"),
38748
+ credential_refs: exports_external2.record(exports_external2.object({
38749
+ source: exports_external2.enum(["env", "local-vault", "hosted"]),
38750
+ name: exports_external2.string(),
38751
+ required: exports_external2.boolean().optional(),
38752
+ description: exports_external2.string().optional()
38753
+ })).optional().describe("Credential references by server env key"),
38525
38754
  allow_local_stdio: exports_external2.boolean().optional().describe("Approve registering this local stdio command"),
38526
38755
  allow_risky_command: exports_external2.boolean().optional().describe("Approve registering risky local command patterns")
38527
38756
  },
@@ -38529,9 +38758,16 @@ function buildMcpTools() {
38529
38758
  const command = String(input.command);
38530
38759
  const args = Array.isArray(input.args) ? input.args.map(String) : [];
38531
38760
  const env = isRecordOfStrings(input.env) ? input.env : {};
38761
+ const credentialRefs = readCredentialRefs(input);
38532
38762
  const transport = input.transport;
38533
38763
  try {
38534
- assertLocalCommandConsent({ command, args, env, transport, operation: "register" }, localConsent(input));
38764
+ assertLocalCommandConsent({
38765
+ command,
38766
+ args,
38767
+ env: { ...env, ...Object.fromEntries(Object.keys(credentialRefs).map((key) => [key, "<credential-ref>"])) },
38768
+ transport,
38769
+ operation: "register"
38770
+ }, localConsent(input));
38535
38771
  return jsonContent(addServer({
38536
38772
  command,
38537
38773
  args,
@@ -38539,7 +38775,8 @@ function buildMcpTools() {
38539
38775
  description: typeof input.description === "string" ? input.description : undefined,
38540
38776
  transport,
38541
38777
  url: typeof input.url === "string" ? input.url : undefined,
38542
- env
38778
+ env,
38779
+ credentialRefs
38543
38780
  }));
38544
38781
  } catch (err) {
38545
38782
  return errorContent(err.message);
@@ -38607,6 +38844,12 @@ function buildMcpTools() {
38607
38844
  args: exports_external2.array(exports_external2.string()).optional().describe("New args list"),
38608
38845
  transport: exports_external2.enum(["stdio", "sse", "streamable-http"]).optional().describe("New transport type"),
38609
38846
  url: exports_external2.string().optional().describe("New URL for remote transports"),
38847
+ credential_refs: exports_external2.record(exports_external2.object({
38848
+ source: exports_external2.enum(["env", "local-vault", "hosted"]),
38849
+ name: exports_external2.string(),
38850
+ required: exports_external2.boolean().optional(),
38851
+ description: exports_external2.string().optional()
38852
+ })).optional().describe("Credential references by server env key"),
38610
38853
  allow_local_stdio: exports_external2.boolean().optional().describe("Approve updating this local stdio command"),
38611
38854
  allow_risky_command: exports_external2.boolean().optional().describe("Approve risky local command patterns")
38612
38855
  },
@@ -38624,6 +38867,8 @@ function buildMcpTools() {
38624
38867
  fields.command = input.command;
38625
38868
  if (Array.isArray(input.args))
38626
38869
  fields.args = input.args.map(String);
38870
+ if (input.credential_refs !== undefined || input.credentialRefs !== undefined)
38871
+ fields.credentialRefs = readCredentialRefs(input);
38627
38872
  if (input.transport === "stdio" || input.transport === "sse" || input.transport === "streamable-http")
38628
38873
  fields.transport = input.transport;
38629
38874
  if (typeof input.url === "string")
@@ -38633,7 +38878,10 @@ function buildMcpTools() {
38633
38878
  assertLocalCommandConsent({
38634
38879
  command: fields.command ?? existing.command,
38635
38880
  args: fields.args ?? existing.args,
38636
- env: existing.env,
38881
+ env: {
38882
+ ...existing.env,
38883
+ ...Object.fromEntries(Object.keys(fields.credentialRefs ?? existing.credentialRefs ?? {}).map((key) => [key, "<credential-ref>"]))
38884
+ },
38637
38885
  transport: fields.transport ?? existing.transport,
38638
38886
  operation: "register"
38639
38887
  }, localConsent(input));
@@ -39197,32 +39445,32 @@ if (isDirectRun) {
39197
39445
  }
39198
39446
 
39199
39447
  // src/server/serve.ts
39200
- import { existsSync as existsSync10 } from "fs";
39201
- import { join as join11, dirname as dirname4, extname, resolve, relative as relative2, sep } from "path";
39448
+ import { existsSync as existsSync11 } from "fs";
39449
+ import { join as join12, dirname as dirname4, extname, resolve, relative as relative2, sep } from "path";
39202
39450
  import { fileURLToPath as fileURLToPath2 } from "url";
39203
39451
  init_sources();
39204
39452
  init_db();
39205
39453
  function redactServer(server) {
39206
- return { ...server, env: {} };
39454
+ return { ...redactServerCredentials(server), env: {} };
39207
39455
  }
39208
39456
  function resolveDashboardDir() {
39209
39457
  const candidates = [];
39210
39458
  try {
39211
39459
  const scriptDir = dirname4(fileURLToPath2(import.meta.url));
39212
- candidates.push(join11(scriptDir, "..", "dashboard", "dist"));
39213
- candidates.push(join11(scriptDir, "..", "..", "dashboard", "dist"));
39460
+ candidates.push(join12(scriptDir, "..", "dashboard", "dist"));
39461
+ candidates.push(join12(scriptDir, "..", "..", "dashboard", "dist"));
39214
39462
  } catch {}
39215
39463
  if (process.argv[1]) {
39216
39464
  const mainDir = dirname4(process.argv[1]);
39217
- candidates.push(join11(mainDir, "..", "dashboard", "dist"));
39218
- candidates.push(join11(mainDir, "..", "..", "dashboard", "dist"));
39465
+ candidates.push(join12(mainDir, "..", "dashboard", "dist"));
39466
+ candidates.push(join12(mainDir, "..", "..", "dashboard", "dist"));
39219
39467
  }
39220
- candidates.push(join11(process.cwd(), "dashboard", "dist"));
39468
+ candidates.push(join12(process.cwd(), "dashboard", "dist"));
39221
39469
  for (const candidate of candidates) {
39222
- if (existsSync10(candidate))
39470
+ if (existsSync11(candidate))
39223
39471
  return candidate;
39224
39472
  }
39225
- return join11(process.cwd(), "dashboard", "dist");
39473
+ return join12(process.cwd(), "dashboard", "dist");
39226
39474
  }
39227
39475
  var MIME_TYPES = {
39228
39476
  ".html": "text/html; charset=utf-8",
@@ -39324,7 +39572,7 @@ function getAllServersWithToolCount() {
39324
39572
  }));
39325
39573
  }
39326
39574
  function serveStaticFile(filePath) {
39327
- if (!existsSync10(filePath))
39575
+ if (!existsSync11(filePath))
39328
39576
  return null;
39329
39577
  const ext = extname(filePath);
39330
39578
  const contentType = MIME_TYPES[ext] || "application/octet-stream";
@@ -39357,7 +39605,7 @@ async function startServer(port, options) {
39357
39605
  const host = options?.host ?? "127.0.0.1";
39358
39606
  getDb();
39359
39607
  const dashboardDir = resolveDashboardDir();
39360
- const dashboardExists = existsSync10(dashboardDir);
39608
+ const dashboardExists = existsSync11(dashboardDir);
39361
39609
  if (!dashboardExists) {
39362
39610
  console.error(`
39363
39611
  Dashboard not found at: ${dashboardDir}`);
@@ -39416,8 +39664,16 @@ Dashboard not found at: ${dashboardDir}`);
39416
39664
  return json({ error: "Invalid 'args' format" }, 400, port);
39417
39665
  }
39418
39666
  const args = body.args || [];
39667
+ const credentialRefs = normalizeCredentialRefs(body.credential_refs ?? body.credentialRefs);
39668
+ const env = body.env && typeof body.env === "object" && !Array.isArray(body.env) ? body.env : {};
39419
39669
  try {
39420
- assertLocalCommandConsent({ command, args, env: {}, transport, operation: "register" }, consentFromInput(body));
39670
+ assertLocalCommandConsent({
39671
+ command,
39672
+ args,
39673
+ env: { ...env, ...Object.fromEntries(Object.keys(credentialRefs).map((key) => [key, "<credential-ref>"])) },
39674
+ transport,
39675
+ operation: "register"
39676
+ }, consentFromInput(body));
39421
39677
  } catch (err) {
39422
39678
  return json({ error: err.message }, 400, port);
39423
39679
  }
@@ -39427,10 +39683,14 @@ Dashboard not found at: ${dashboardDir}`);
39427
39683
  args,
39428
39684
  description: body.description,
39429
39685
  transport,
39430
- url: body.url
39686
+ url: body.url,
39687
+ env,
39688
+ credentialRefs
39431
39689
  });
39432
39690
  return json(entry, 200, port);
39433
39691
  } catch (e) {
39692
+ if (e instanceof CredentialReferenceError)
39693
+ return json({ error: e.message }, 400, port);
39434
39694
  return json({ error: e instanceof Error ? e.message : "Failed to add server" }, 500, port);
39435
39695
  }
39436
39696
  }
@@ -39509,6 +39769,11 @@ Dashboard not found at: ${dashboardDir}`);
39509
39769
  fields.description = body.description;
39510
39770
  if (body.command !== undefined)
39511
39771
  fields.command = body.command;
39772
+ if (body.env !== undefined)
39773
+ fields.env = body.env;
39774
+ if (body.credential_refs !== undefined || body.credentialRefs !== undefined) {
39775
+ fields.credentialRefs = normalizeCredentialRefs(body.credential_refs ?? body.credentialRefs);
39776
+ }
39512
39777
  if (body.transport !== undefined)
39513
39778
  fields.transport = body.transport;
39514
39779
  if (body.url !== undefined)
@@ -39524,7 +39789,10 @@ Dashboard not found at: ${dashboardDir}`);
39524
39789
  assertLocalCommandConsent({
39525
39790
  command: fields.command ?? existing.command,
39526
39791
  args: fields.args ?? existing.args,
39527
- env: existing.env,
39792
+ env: {
39793
+ ...fields.env ?? existing.env,
39794
+ ...Object.fromEntries(Object.keys(fields.credentialRefs ?? existing.credentialRefs ?? {}).map((key) => [key, "<credential-ref>"]))
39795
+ },
39528
39796
  transport: fields.transport ?? existing.transport,
39529
39797
  operation: "register"
39530
39798
  }, consentFromInput(body));
@@ -39560,6 +39828,8 @@ Dashboard not found at: ${dashboardDir}`);
39560
39828
  setServerEnv(id, body.key, body.value);
39561
39829
  return json({ ok: true }, 200, port);
39562
39830
  } catch (e) {
39831
+ if (e instanceof CredentialReferenceError)
39832
+ return json({ error: e.message }, 400, port);
39563
39833
  return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
39564
39834
  }
39565
39835
  }
@@ -39762,7 +40032,7 @@ Dashboard not found at: ${dashboardDir}`);
39762
40032
  return res2;
39763
40033
  }
39764
40034
  }
39765
- const indexPath = join11(dashboardDir, "index.html");
40035
+ const indexPath = join12(dashboardDir, "index.html");
39766
40036
  const res = serveStaticFile(indexPath);
39767
40037
  if (res)
39768
40038
  return res;
@@ -40916,6 +41186,30 @@ function assertCliLocalCommandConsent(input, opts, approved = false) {
40916
41186
  assertLocalCommandConsent(input, consent);
40917
41187
  return consent;
40918
41188
  }
41189
+ function parseCredentialPairs(pairs, source) {
41190
+ const refs = {};
41191
+ for (const pair of pairs ?? []) {
41192
+ const eqIdx = pair.indexOf("=");
41193
+ if (eqIdx <= 0)
41194
+ throw new Error(`Credential reference must use KEY=NAME format: ${pair}`);
41195
+ const key = pair.slice(0, eqIdx).trim();
41196
+ const name = pair.slice(eqIdx + 1).trim();
41197
+ if (!key || !name)
41198
+ throw new Error(`Credential reference must use KEY=NAME format: ${pair}`);
41199
+ refs[key] = { source, name, required: true };
41200
+ }
41201
+ return refs;
41202
+ }
41203
+ function credentialRefsFromOptions(opts) {
41204
+ return {
41205
+ ...parseCredentialPairs(opts.credentialEnv, "env"),
41206
+ ...parseCredentialPairs(opts.credentialLocal, "local-vault"),
41207
+ ...parseCredentialPairs(opts.credentialHosted, "hosted")
41208
+ };
41209
+ }
41210
+ function formatCredentialRef(ref) {
41211
+ return `${ref.source}:${ref.name}`;
41212
+ }
40919
41213
  function printProviderProfile(profile) {
40920
41214
  const status = profile.enabled ? chalk2.green("enabled") : chalk2.red("disabled");
40921
41215
  console.log(` ${chalk2.bold(profile.displayName)} ${chalk2.dim(`[${profile.id}]`)} \u2014 ${chalk2.dim(profile.transport)} \u2014 ${status}`);
@@ -41200,7 +41494,7 @@ function detectSourceType(url2) {
41200
41494
  return "mcp-registry";
41201
41495
  return null;
41202
41496
  }
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) => {
41497
+ program2.command("add").passThroughOptions().argument("[command]", "Command to run the MCP server").argument("[args...]", "Arguments for the command").option("--name <name>", "Display name for the server").option("--description <desc>", "Description").option("--from-registry <id>", "Install from official registry by ID").option("--transport <type>", "Transport type: stdio, sse, streamable-http", "stdio").option("--url <url>", "URL for remote transports").option("--env <pairs...>", "Environment variables as KEY=VALUE pairs").option("--credential-env <pairs...>", "Credential refs as KEY=ENV_VAR pairs").option("--credential-local <pairs...>", "Credential refs as KEY=LOCAL_VAULT_NAME pairs").option("--credential-hosted <pairs...>", "Credential refs as KEY=HOSTED_CREDENTIAL_ID pairs").option("--wizard", "Interactive setup wizard").option("--force", "Register even if duplicate command exists").option("--yes", "Approve registering local stdio commands").option("--allow-local-stdio", "Approve registering local stdio commands").option("--allow-risky-command", "Approve high-risk local command patterns").description("Add a local MCP server").action(async (command, args, opts) => {
41204
41498
  try {
41205
41499
  if (opts.fromRegistry) {
41206
41500
  console.log(chalk2.dim(`Installing "${opts.fromRegistry}" from registry...`));
@@ -41277,7 +41571,8 @@ Server to add:`));
41277
41571
  name: wizardName || undefined,
41278
41572
  description: wizardDescription || undefined,
41279
41573
  transport,
41280
- env
41574
+ env,
41575
+ credentialRefs: credentialRefsFromOptions(opts)
41281
41576
  });
41282
41577
  console.log(chalk2.green(`Added: ${server2.name} [${server2.id}]`));
41283
41578
  closeDb();
@@ -41307,10 +41602,11 @@ Server to add:`));
41307
41602
  envMap[key] = rest.join("=");
41308
41603
  }
41309
41604
  }
41605
+ const credentialRefs = credentialRefsFromOptions(opts);
41310
41606
  assertCliLocalCommandConsent({
41311
41607
  command,
41312
41608
  args,
41313
- env: envMap,
41609
+ env: { ...envMap, ...Object.fromEntries(Object.keys(credentialRefs).map((key) => [key, "<credential-ref>"])) },
41314
41610
  transport: opts.transport,
41315
41611
  operation: "register"
41316
41612
  }, opts);
@@ -41321,7 +41617,8 @@ Server to add:`));
41321
41617
  args,
41322
41618
  transport: opts.transport,
41323
41619
  url: opts.url,
41324
- env: envMap
41620
+ env: envMap,
41621
+ credentialRefs
41325
41622
  });
41326
41623
  console.log(chalk2.green(`Added server: ${server.name} [${server.id}]`));
41327
41624
  console.log(chalk2.dim(` ${server.command} ${server.args.join(" ")}`));
@@ -41520,20 +41817,24 @@ program2.command("info").argument("<id>", "Server ID").description("Show server
41520
41817
  closeDb();
41521
41818
  process.exit(1);
41522
41819
  }
41523
- console.log(chalk2.bold(server.name) + " " + chalk2.dim(`[${server.id}]`));
41524
- console.log(` Status: ${server.enabled ? chalk2.green("enabled") : chalk2.red("disabled")}`);
41525
- console.log(` Source: ${server.source}`);
41526
- console.log(` Transport: ${server.transport}`);
41527
- console.log(` Command: ${server.command} ${server.args.join(" ")}`);
41528
- if (server.url)
41529
- console.log(` URL: ${server.url}`);
41530
- if (server.description)
41531
- console.log(` Desc: ${server.description}`);
41532
- if (Object.keys(server.env).length > 0) {
41533
- console.log(` Env: ${Object.entries(server.env).map(([k, v]) => `${k}=${v}`).join(", ")}`);
41534
- }
41535
- console.log(` Created: ${server.created_at}`);
41536
- console.log(` Updated: ${server.updated_at}`);
41820
+ const safeServer = redactServerCredentials(server);
41821
+ console.log(chalk2.bold(safeServer.name) + " " + chalk2.dim(`[${safeServer.id}]`));
41822
+ console.log(` Status: ${safeServer.enabled ? chalk2.green("enabled") : chalk2.red("disabled")}`);
41823
+ console.log(` Source: ${safeServer.source}`);
41824
+ console.log(` Transport: ${safeServer.transport}`);
41825
+ console.log(` Command: ${safeServer.command} ${safeServer.args.join(" ")}`);
41826
+ if (safeServer.url)
41827
+ console.log(` URL: ${safeServer.url}`);
41828
+ if (safeServer.description)
41829
+ console.log(` Desc: ${safeServer.description}`);
41830
+ if (Object.keys(safeServer.env).length > 0) {
41831
+ console.log(` Env: ${Object.entries(safeServer.env).map(([k, v]) => `${k}=${v}`).join(", ")}`);
41832
+ }
41833
+ if (Object.keys(safeServer.credentialRefs ?? {}).length > 0) {
41834
+ console.log(` Cred refs: ${Object.entries(safeServer.credentialRefs ?? {}).map(([k, ref]) => `${k}=${formatCredentialRef(ref)}`).join(", ")}`);
41835
+ }
41836
+ console.log(` Created: ${safeServer.created_at}`);
41837
+ console.log(` Updated: ${safeServer.updated_at}`);
41537
41838
  const cached2 = getCachedTools(id);
41538
41839
  if (cached2.length > 0) {
41539
41840
  console.log(chalk2.bold(`
@@ -42173,7 +42474,7 @@ fleetCmd.command("install").argument("[machineIds...]", "Optional machine IDs to
42173
42474
  }
42174
42475
  });
42175
42476
  program2.command("export").description("Export all servers and sources to a JSON file").option("--file <path>", "Output file path", `${process.env.HOME ?? "~"}/.hasna/mcps/export.json`).option("--stdout", "Write to stdout instead of a file").action((opts) => {
42176
- const servers = listServers();
42477
+ const servers = listServers().map(redactServerCredentials);
42177
42478
  const sources = listSources();
42178
42479
  const payload = {
42179
42480
  version: 1,
@@ -42193,7 +42494,7 @@ program2.command("export").description("Export all servers and sources to a JSON
42193
42494
  program2.command("import").argument("<file>", "Path to the export JSON file").description("Import servers and sources from a JSON export file").option("--overwrite", "Overwrite existing entries with matching IDs").action((file, opts) => {
42194
42495
  let payload;
42195
42496
  try {
42196
- payload = JSON.parse(readFileSync6(file, "utf-8"));
42497
+ payload = JSON.parse(readFileSync7(file, "utf-8"));
42197
42498
  } catch (err) {
42198
42499
  console.error(chalk2.red(`Failed to read file: ${err.message}`));
42199
42500
  closeDb();
@@ -42215,7 +42516,23 @@ program2.command("import").argument("<file>", "Path to the export JSON file").de
42215
42516
  serversSkipped++;
42216
42517
  continue;
42217
42518
  }
42218
- db2.run(`INSERT ${orReplace} INTO servers (id, name, description, command, args, env, transport, url, source, enabled, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`, [s.id, s.name, s.description, s.command, JSON.stringify(s.args ?? []), JSON.stringify(s.env ?? {}), s.transport, s.url, s.source, s.enabled ? 1 : 0, s.created_at, s.updated_at]);
42519
+ const literalEnv = normalizeLiteralEnv(s.env ?? {});
42520
+ const credentialRefs = normalizeCredentialRefs(s.credentialRefs ?? s.credential_refs ?? {});
42521
+ db2.run(`INSERT ${orReplace} INTO servers (id, name, description, command, args, env, credential_refs, transport, url, source, enabled, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`, [
42522
+ s.id,
42523
+ s.name,
42524
+ s.description,
42525
+ s.command,
42526
+ JSON.stringify(s.args ?? []),
42527
+ JSON.stringify(literalEnv),
42528
+ JSON.stringify(credentialRefs),
42529
+ s.transport,
42530
+ s.url,
42531
+ s.source,
42532
+ s.enabled ? 1 : 0,
42533
+ s.created_at,
42534
+ s.updated_at
42535
+ ]);
42219
42536
  serversImported++;
42220
42537
  }
42221
42538
  let sourcesImported = 0;
@@ -42241,14 +42558,17 @@ envCmd.command("list").argument("<id>").description("List env vars for a server"
42241
42558
  closeDb();
42242
42559
  process.exit(1);
42243
42560
  }
42244
- const entries = Object.entries(server.env);
42245
- if (entries.length === 0) {
42246
- console.log(chalk2.dim("No env vars set."));
42561
+ const envEntries = Object.entries(redactEnv(server.env));
42562
+ const refEntries = Object.entries(server.credentialRefs ?? {});
42563
+ if (envEntries.length === 0 && refEntries.length === 0) {
42564
+ console.log(chalk2.dim("No env vars or credential refs set."));
42247
42565
  closeDb();
42248
42566
  return;
42249
42567
  }
42250
- for (const [k, v] of entries)
42568
+ for (const [k, v] of envEntries)
42251
42569
  console.log(` ${chalk2.bold(k)}=${chalk2.dim(v)}`);
42570
+ for (const [k, ref] of refEntries)
42571
+ console.log(` ${chalk2.bold(k)}=${chalk2.dim(formatCredentialRef(ref))}`);
42252
42572
  closeDb();
42253
42573
  });
42254
42574
  envCmd.command("set").argument("<id>").argument("<pair>", "KEY=VALUE").description("Set an env var").action((id, pair) => {
@@ -42270,6 +42590,36 @@ envCmd.command("set").argument("<id>").argument("<pair>", "KEY=VALUE").descripti
42270
42590
  }
42271
42591
  closeDb();
42272
42592
  });
42593
+ envCmd.command("ref").argument("<id>").argument("<pair>", "KEY=NAME").description("Set a credential reference for a server env key").option("--source <source>", "Credential source: env, local-vault, hosted", "env").action((id, pair, opts) => {
42594
+ const source = opts.source;
42595
+ if (source !== "env" && source !== "local-vault" && source !== "hosted") {
42596
+ console.error(chalk2.red("Source must be one of: env, local-vault, hosted"));
42597
+ closeDb();
42598
+ process.exit(1);
42599
+ }
42600
+ try {
42601
+ const refs = parseCredentialPairs([pair], source);
42602
+ const [key, ref] = Object.entries(refs)[0];
42603
+ setServerCredentialRef(id, key, ref);
42604
+ console.log(chalk2.green(`Set credential ref ${key}=${formatCredentialRef(ref)} on ${id}`));
42605
+ } catch (err) {
42606
+ console.error(chalk2.red(err.message));
42607
+ closeDb();
42608
+ process.exit(1);
42609
+ }
42610
+ closeDb();
42611
+ });
42612
+ envCmd.command("unset-ref").argument("<id>").argument("<key>").description("Remove a credential reference").action((id, key) => {
42613
+ try {
42614
+ unsetServerCredentialRef(id, key);
42615
+ console.log(chalk2.green(`Unset credential ref ${key} on ${id}`));
42616
+ } catch (err) {
42617
+ console.error(chalk2.red(err.message));
42618
+ closeDb();
42619
+ process.exit(1);
42620
+ }
42621
+ closeDb();
42622
+ });
42273
42623
  envCmd.command("unset").argument("<id>").argument("<key>").description("Remove an env var").action((id, key) => {
42274
42624
  try {
42275
42625
  unsetServerEnv(id, key);