@hasna/mcps 0.0.15 → 0.0.17

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
@@ -11985,7 +11985,7 @@ var init_provider_profile_seeds = __esm(() => {
11985
11985
  provenance: {
11986
11986
  source: "curated",
11987
11987
  sourceUrl: "https://developers.notion.com/guides/mcp/build-mcp-client",
11988
- verifiedAt: "2026-05-10"
11988
+ verifiedAt: "2026-05-11"
11989
11989
  }
11990
11990
  },
11991
11991
  {
@@ -12017,7 +12017,384 @@ var init_provider_profile_seeds = __esm(() => {
12017
12017
  provenance: {
12018
12018
  source: "curated",
12019
12019
  sourceUrl: "https://linear.app/docs/mcp",
12020
- verifiedAt: "2026-05-10"
12020
+ verifiedAt: "2026-05-11"
12021
+ }
12022
+ },
12023
+ {
12024
+ id: "github",
12025
+ displayName: "GitHub",
12026
+ description: "Connect GitHub so agents can inspect repositories, manage issues and pull requests, analyze Actions runs, and work with project metadata.",
12027
+ endpoint: "https://api.githubcopilot.com/mcp/",
12028
+ transport: "streamable-http",
12029
+ authType: "oauth2",
12030
+ authMetadata: {
12031
+ oauthVersion: "2.0",
12032
+ pkce: true,
12033
+ dynamicClientRegistration: false,
12034
+ bearerToken: "optional",
12035
+ notes: "GitHub hosts the remote server. OAuth requires host-side GitHub App/OAuth App setup; clients that cannot complete OAuth can use an Authorization bearer GitHub PAT."
12036
+ },
12037
+ scopes: ["repo", "read:org", "read:user", "user:email", "workflow", "notifications", "project"],
12038
+ tokenMode: "user",
12039
+ installFallback: {
12040
+ command: "docker",
12041
+ args: ["run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", "ghcr.io/github/github-mcp-server"],
12042
+ env: { GITHUB_PERSONAL_ACCESS_TOKEN: "GITHUB_PERSONAL_ACCESS_TOKEN" },
12043
+ packageName: "ghcr.io/github/github-mcp-server",
12044
+ url: "https://api.githubcopilot.com/mcp/"
12045
+ },
12046
+ docsUrl: "https://github.com/github/github-mcp-server",
12047
+ safety: {
12048
+ requiresApproval: true,
12049
+ sensitiveScopes: ["repo", "workflow", "project"],
12050
+ dataClasses: ["repositories", "issues", "pull_requests", "actions", "security_alerts", "organization_members"],
12051
+ notes: "GitHub tools can read private source and mutate repository objects. Prefer least-privilege OAuth scopes or fine-grained PATs and approval-gate write operations."
12052
+ },
12053
+ provenance: {
12054
+ source: "curated",
12055
+ sourceUrl: "https://github.com/github/github-mcp-server",
12056
+ repositoryUrl: "https://github.com/github/github-mcp-server",
12057
+ packageName: "ghcr.io/github/github-mcp-server",
12058
+ verifiedAt: "2026-05-11"
12059
+ }
12060
+ },
12061
+ {
12062
+ id: "slack",
12063
+ displayName: "Slack",
12064
+ description: "Connect Slack so agents can search workspace messages and files, inspect conversations, read user context, and draft or send approved messages.",
12065
+ endpoint: "https://mcp.slack.com/mcp",
12066
+ transport: "streamable-http",
12067
+ authType: "oauth2",
12068
+ authMetadata: {
12069
+ oauthVersion: "2.0",
12070
+ pkce: true,
12071
+ dynamicClientRegistration: false,
12072
+ bearerToken: "none",
12073
+ notes: "Slack's remote MCP endpoint uses Streamable HTTP, requires a registered Slack app identity, and does not support SSE or dynamic client registration."
12074
+ },
12075
+ scopes: [
12076
+ "search:read.public",
12077
+ "search:read.private",
12078
+ "search:read.mpim",
12079
+ "search:read.im",
12080
+ "search:read.files",
12081
+ "search:read.users",
12082
+ "channels:history",
12083
+ "groups:history",
12084
+ "mpim:history",
12085
+ "im:history",
12086
+ "chat:write",
12087
+ "canvases:read",
12088
+ "canvases:write",
12089
+ "users:read",
12090
+ "users:read.email"
12091
+ ],
12092
+ tokenMode: "user",
12093
+ docsUrl: "https://docs.slack.dev/ai/slack-mcp-server/",
12094
+ safety: {
12095
+ requiresApproval: true,
12096
+ sensitiveScopes: ["chat:write", "canvases:write", "users:read.email"],
12097
+ dataClasses: ["messages", "files", "channels", "users", "canvases"],
12098
+ notes: "Slack data often includes confidential team communication. Search/read access should stay tenant-scoped and message/canvas writes should require explicit approval."
12099
+ },
12100
+ provenance: {
12101
+ source: "curated",
12102
+ sourceUrl: "https://docs.slack.dev/ai/slack-mcp-server/",
12103
+ verifiedAt: "2026-05-11"
12104
+ }
12105
+ },
12106
+ {
12107
+ id: "gmail",
12108
+ displayName: "Gmail",
12109
+ description: "Connect Gmail through the open Workspace MCP server for mail search, message retrieval, drafts, labels, filters, and approved send workflows.",
12110
+ transport: "stdio",
12111
+ authType: "oauth2",
12112
+ authMetadata: {
12113
+ oauthVersion: "2.1",
12114
+ pkce: true,
12115
+ dynamicClientRegistration: false,
12116
+ bearerToken: "optional",
12117
+ notes: "Workspace MCP is an independent open-source Google Workspace server. Configure your own Google OAuth client and storage backend for Gmail access."
12118
+ },
12119
+ scopes: [
12120
+ "https://www.googleapis.com/auth/gmail.readonly",
12121
+ "https://www.googleapis.com/auth/gmail.modify",
12122
+ "https://www.googleapis.com/auth/gmail.compose",
12123
+ "https://www.googleapis.com/auth/gmail.send"
12124
+ ],
12125
+ tokenMode: "user",
12126
+ installFallback: {
12127
+ command: "uvx",
12128
+ args: ["workspace-mcp"],
12129
+ env: { WORKSPACE_MCP_PERMISSIONS: "gmail:send" },
12130
+ packageName: "workspace-mcp",
12131
+ url: "http://127.0.0.1:8000/mcp"
12132
+ },
12133
+ docsUrl: "https://workspacemcp.com/docs",
12134
+ safety: {
12135
+ requiresApproval: true,
12136
+ sensitiveScopes: [
12137
+ "https://www.googleapis.com/auth/gmail.modify",
12138
+ "https://www.googleapis.com/auth/gmail.compose",
12139
+ "https://www.googleapis.com/auth/gmail.send"
12140
+ ],
12141
+ dataClasses: ["email_messages", "threads", "attachments", "labels", "contacts"],
12142
+ notes: "Email content and sending are high-impact actions. Prefer read-only permissions until a user intentionally enables drafts or sends."
12143
+ },
12144
+ provenance: {
12145
+ source: "github",
12146
+ sourceUrl: "https://workspacemcp.com/docs",
12147
+ repositoryUrl: "https://github.com/taylorwilsdon/google_workspace_mcp",
12148
+ packageName: "workspace-mcp",
12149
+ verifiedAt: "2026-05-11"
12150
+ }
12151
+ },
12152
+ {
12153
+ id: "google-drive",
12154
+ displayName: "Google Drive",
12155
+ description: "Connect Google Drive through the open Workspace MCP server for file search, folder navigation, content reads, and approved file operations.",
12156
+ transport: "stdio",
12157
+ authType: "oauth2",
12158
+ authMetadata: {
12159
+ oauthVersion: "2.1",
12160
+ pkce: true,
12161
+ dynamicClientRegistration: false,
12162
+ bearerToken: "optional",
12163
+ notes: "Workspace MCP is an independent open-source Google Workspace server. Configure your own Google OAuth client and storage backend for Drive access."
12164
+ },
12165
+ scopes: [
12166
+ "https://www.googleapis.com/auth/drive.readonly",
12167
+ "https://www.googleapis.com/auth/drive.file",
12168
+ "https://www.googleapis.com/auth/drive"
12169
+ ],
12170
+ tokenMode: "user",
12171
+ installFallback: {
12172
+ command: "uvx",
12173
+ args: ["workspace-mcp"],
12174
+ env: { WORKSPACE_MCP_PERMISSIONS: "drive:readonly" },
12175
+ packageName: "workspace-mcp",
12176
+ url: "http://127.0.0.1:8000/mcp"
12177
+ },
12178
+ docsUrl: "https://workspacemcp.com/docs",
12179
+ safety: {
12180
+ requiresApproval: true,
12181
+ sensitiveScopes: [
12182
+ "https://www.googleapis.com/auth/drive.file",
12183
+ "https://www.googleapis.com/auth/drive"
12184
+ ],
12185
+ dataClasses: ["drive_files", "folders", "documents", "spreadsheets", "attachments", "sharing_permissions"],
12186
+ notes: "Drive access can expose broad business documents. Keep tenant allowlists and approval-gate creation, movement, and permission changes."
12187
+ },
12188
+ provenance: {
12189
+ source: "github",
12190
+ sourceUrl: "https://workspacemcp.com/docs",
12191
+ repositoryUrl: "https://github.com/taylorwilsdon/google_workspace_mcp",
12192
+ packageName: "workspace-mcp",
12193
+ verifiedAt: "2026-05-11"
12194
+ }
12195
+ },
12196
+ {
12197
+ id: "google-calendar",
12198
+ displayName: "Google Calendar",
12199
+ description: "Connect Google Calendar through the open Workspace MCP server for calendar discovery, event reads, and approved scheduling changes.",
12200
+ transport: "stdio",
12201
+ authType: "oauth2",
12202
+ authMetadata: {
12203
+ oauthVersion: "2.1",
12204
+ pkce: true,
12205
+ dynamicClientRegistration: false,
12206
+ bearerToken: "optional",
12207
+ notes: "Workspace MCP is an independent open-source Google Workspace server. Configure your own Google OAuth client and storage backend for Calendar access."
12208
+ },
12209
+ scopes: [
12210
+ "https://www.googleapis.com/auth/calendar.readonly",
12211
+ "https://www.googleapis.com/auth/calendar.events",
12212
+ "https://www.googleapis.com/auth/calendar"
12213
+ ],
12214
+ tokenMode: "user",
12215
+ installFallback: {
12216
+ command: "uvx",
12217
+ args: ["workspace-mcp"],
12218
+ env: { WORKSPACE_MCP_PERMISSIONS: "calendar:readonly" },
12219
+ packageName: "workspace-mcp",
12220
+ url: "http://127.0.0.1:8000/mcp"
12221
+ },
12222
+ docsUrl: "https://workspacemcp.com/docs",
12223
+ safety: {
12224
+ requiresApproval: true,
12225
+ sensitiveScopes: [
12226
+ "https://www.googleapis.com/auth/calendar.events",
12227
+ "https://www.googleapis.com/auth/calendar"
12228
+ ],
12229
+ dataClasses: ["calendars", "events", "attendees", "attachments", "availability"],
12230
+ notes: "Calendar writes can invite people, reveal availability, and alter operational schedules. Gate creates, updates, and deletes with user approval."
12231
+ },
12232
+ provenance: {
12233
+ source: "github",
12234
+ sourceUrl: "https://workspacemcp.com/docs",
12235
+ repositoryUrl: "https://github.com/taylorwilsdon/google_workspace_mcp",
12236
+ packageName: "workspace-mcp",
12237
+ verifiedAt: "2026-05-11"
12238
+ }
12239
+ },
12240
+ {
12241
+ id: "stripe",
12242
+ displayName: "Stripe",
12243
+ description: "Connect Stripe so agents can inspect accounts, balances, customers, prices, subscriptions, invoices, disputes, and perform approved Stripe API operations.",
12244
+ endpoint: "https://mcp.stripe.com",
12245
+ transport: "streamable-http",
12246
+ authType: "oauth2",
12247
+ authMetadata: {
12248
+ oauthVersion: "2.0",
12249
+ pkce: true,
12250
+ dynamicClientRegistration: false,
12251
+ bearerToken: "optional",
12252
+ notes: "Stripe supports OAuth MCP sessions and restricted API keys as Authorization bearer tokens for clients that do not support OAuth."
12253
+ },
12254
+ tokenMode: "workspace",
12255
+ installFallback: {
12256
+ command: "npx",
12257
+ args: ["-y", "@stripe/mcp"],
12258
+ env: { STRIPE_SECRET_KEY: "STRIPE_SECRET_KEY" },
12259
+ packageName: "@stripe/mcp",
12260
+ url: "https://mcp.stripe.com"
12261
+ },
12262
+ docsUrl: "https://docs.stripe.com/mcp",
12263
+ safety: {
12264
+ requiresApproval: true,
12265
+ sensitiveScopes: ["restricted_api_key", "live_mode"],
12266
+ dataClasses: ["payments", "customers", "subscriptions", "invoices", "disputes", "account_balances"],
12267
+ notes: "Use restricted keys, separate sandbox/live mode, and require approval for any operation that creates, updates, refunds, cancels, or exposes customer data."
12268
+ },
12269
+ provenance: {
12270
+ source: "curated",
12271
+ sourceUrl: "https://docs.stripe.com/mcp",
12272
+ repositoryUrl: "https://github.com/stripe/ai",
12273
+ packageName: "@stripe/mcp",
12274
+ verifiedAt: "2026-05-11"
12275
+ }
12276
+ },
12277
+ {
12278
+ id: "cloudflare",
12279
+ displayName: "Cloudflare",
12280
+ description: "Connect Cloudflare's remote MCP server for account, zone, developer platform, logs, analytics, and approved infrastructure operations.",
12281
+ endpoint: "https://mcp.cloudflare.com/mcp",
12282
+ transport: "streamable-http",
12283
+ authType: "oauth2",
12284
+ authMetadata: {
12285
+ oauthVersion: "2.0",
12286
+ pkce: true,
12287
+ dynamicClientRegistration: false,
12288
+ bearerToken: "none",
12289
+ notes: "Cloudflare's remote MCP server redirects users through Cloudflare authorization and permission selection."
12290
+ },
12291
+ tokenMode: "workspace",
12292
+ docsUrl: "https://developers.cloudflare.com/agents/model-context-protocol/mcp-servers-for-cloudflare/",
12293
+ safety: {
12294
+ requiresApproval: true,
12295
+ sensitiveScopes: ["account_admin", "zone_write", "workers_write", "dns_write"],
12296
+ dataClasses: ["accounts", "zones", "dns_records", "workers", "logs", "analytics", "security_events"],
12297
+ notes: "Cloudflare tools can affect live infrastructure. Require account scoping, least-privilege permissions, and approval for write operations."
12298
+ },
12299
+ provenance: {
12300
+ source: "curated",
12301
+ sourceUrl: "https://github.com/cloudflare/mcp",
12302
+ repositoryUrl: "https://github.com/cloudflare/mcp",
12303
+ verifiedAt: "2026-05-11"
12304
+ }
12305
+ },
12306
+ {
12307
+ id: "postgres",
12308
+ displayName: "PostgreSQL",
12309
+ description: "Connect a PostgreSQL database through the reference read-only MCP server for schema inspection and SQL query analysis.",
12310
+ transport: "stdio",
12311
+ authType: "api_key",
12312
+ authMetadata: {
12313
+ bearerToken: "none",
12314
+ notes: "The stdio server takes a PostgreSQL connection URL argument. Store the URL in a secret manager or environment variable and grant read-only database credentials."
12315
+ },
12316
+ tokenMode: "service",
12317
+ installFallback: {
12318
+ command: "npx",
12319
+ args: ["-y", "@modelcontextprotocol/server-postgres", "${POSTGRES_URL}"],
12320
+ packageName: "@modelcontextprotocol/server-postgres"
12321
+ },
12322
+ docsUrl: "https://www.npmjs.com/package/@modelcontextprotocol/server-postgres",
12323
+ safety: {
12324
+ readOnly: true,
12325
+ requiresApproval: false,
12326
+ sensitiveScopes: ["database_connection_url"],
12327
+ dataClasses: ["database_schema", "table_rows", "query_results"],
12328
+ notes: "Use read-only database users and network allowlists. Treat query results as sensitive even when the server enforces read-only transactions."
12329
+ },
12330
+ provenance: {
12331
+ source: "npm",
12332
+ sourceUrl: "https://www.npmjs.com/package/@modelcontextprotocol/server-postgres",
12333
+ repositoryUrl: "https://github.com/modelcontextprotocol/servers",
12334
+ packageName: "@modelcontextprotocol/server-postgres",
12335
+ verifiedAt: "2026-05-11"
12336
+ }
12337
+ },
12338
+ {
12339
+ id: "filesystem",
12340
+ displayName: "Filesystem",
12341
+ description: "Connect the reference filesystem MCP server for scoped local file and directory reads, writes, searches, metadata, and move operations.",
12342
+ transport: "stdio",
12343
+ authType: "none",
12344
+ authMetadata: {
12345
+ bearerToken: "none",
12346
+ notes: "Filesystem access is bounded by explicit root directories passed to the local stdio server."
12347
+ },
12348
+ tokenMode: "none",
12349
+ installFallback: {
12350
+ command: "npx",
12351
+ args: ["-y", "@modelcontextprotocol/server-filesystem", "${workspaceFolder}"],
12352
+ packageName: "@modelcontextprotocol/server-filesystem"
12353
+ },
12354
+ docsUrl: "https://www.npmjs.com/package/@modelcontextprotocol/server-filesystem",
12355
+ safety: {
12356
+ requiresApproval: true,
12357
+ destructiveTools: ["write_file", "delete_file", "move_file", "create_directory"],
12358
+ dataClasses: ["local_files", "source_code", "documents", "directory_metadata"],
12359
+ notes: "Always constrain roots to intended project directories and approval-gate write, delete, and move operations."
12360
+ },
12361
+ provenance: {
12362
+ source: "npm",
12363
+ sourceUrl: "https://www.npmjs.com/package/@modelcontextprotocol/server-filesystem",
12364
+ repositoryUrl: "https://github.com/modelcontextprotocol/servers",
12365
+ packageName: "@modelcontextprotocol/server-filesystem",
12366
+ verifiedAt: "2026-05-11"
12367
+ }
12368
+ },
12369
+ {
12370
+ id: "browser",
12371
+ displayName: "Browser Automation",
12372
+ description: "Connect Playwright MCP so agents can navigate pages, inspect accessibility snapshots, fill forms, capture screenshots, and run browser automation.",
12373
+ transport: "stdio",
12374
+ authType: "none",
12375
+ authMetadata: {
12376
+ bearerToken: "none",
12377
+ notes: "Playwright MCP runs browser automation locally and may reuse browser profile state unless configured for isolation."
12378
+ },
12379
+ tokenMode: "none",
12380
+ installFallback: {
12381
+ command: "npx",
12382
+ args: ["-y", "@playwright/mcp"],
12383
+ packageName: "@playwright/mcp"
12384
+ },
12385
+ docsUrl: "https://playwright.dev/docs/getting-started-mcp",
12386
+ safety: {
12387
+ requiresApproval: true,
12388
+ destructiveTools: ["browser_click", "browser_type", "browser_file_upload", "browser_run_code"],
12389
+ dataClasses: ["web_pages", "forms", "cookies", "local_storage", "screenshots", "browser_profiles"],
12390
+ notes: "Browser automation can submit forms, alter accounts, and expose logged-in sessions. Prefer isolated profiles and require approval for side-effecting actions."
12391
+ },
12392
+ provenance: {
12393
+ source: "curated",
12394
+ sourceUrl: "https://playwright.dev/docs/getting-started-mcp",
12395
+ repositoryUrl: "https://github.com/microsoft/playwright-mcp",
12396
+ packageName: "@playwright/mcp",
12397
+ verifiedAt: "2026-05-11"
12021
12398
  }
12022
12399
  }
12023
12400
  ];
@@ -12040,6 +12417,7 @@ function getDb() {
12040
12417
  command TEXT NOT NULL,
12041
12418
  args TEXT NOT NULL DEFAULT '[]',
12042
12419
  env TEXT NOT NULL DEFAULT '{}',
12420
+ credential_refs TEXT NOT NULL DEFAULT '{}',
12043
12421
  transport TEXT NOT NULL DEFAULT 'stdio',
12044
12422
  url TEXT,
12045
12423
  source TEXT NOT NULL DEFAULT 'local',
@@ -12066,6 +12444,9 @@ function getDb() {
12066
12444
  try {
12067
12445
  db.exec("ALTER TABLE servers ADD COLUMN last_error TEXT");
12068
12446
  } catch {}
12447
+ try {
12448
+ db.exec("ALTER TABLE servers ADD COLUMN credential_refs TEXT NOT NULL DEFAULT '{}'");
12449
+ } catch {}
12069
12450
  db.exec(`
12070
12451
  CREATE TABLE IF NOT EXISTS sources (
12071
12452
  id TEXT PRIMARY KEY,
@@ -12136,22 +12517,19 @@ function getDb() {
12136
12517
  db.exec("ALTER TABLE provider_profiles ADD COLUMN auth_metadata TEXT NOT NULL DEFAULT '{}'");
12137
12518
  } catch {}
12138
12519
  db.exec("CREATE INDEX IF NOT EXISTS idx_provider_profiles_enabled ON provider_profiles(enabled)");
12139
- const providerProfileCount = db.query("SELECT COUNT(*) as c FROM provider_profiles").get().c;
12140
- if (providerProfileCount === 0) {
12141
- const insertProviderProfile = db.prepare(`
12142
- INSERT OR IGNORE INTO provider_profiles (
12143
- id, display_name, description, endpoint, transport, fallback_endpoints,
12144
- auth_type, auth_metadata, scopes, token_mode, install_fallback,
12145
- docs_url, safety, provenance, enabled
12146
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
12147
- `);
12148
- const run = db.transaction(() => {
12149
- for (const profile of DEFAULT_PROVIDER_PROFILE_SEEDS) {
12150
- 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);
12151
- }
12152
- });
12153
- run();
12154
- }
12520
+ const insertProviderProfile = db.prepare(`
12521
+ INSERT OR IGNORE INTO provider_profiles (
12522
+ id, display_name, description, endpoint, transport, fallback_endpoints,
12523
+ auth_type, auth_metadata, scopes, token_mode, install_fallback,
12524
+ docs_url, safety, provenance, enabled
12525
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
12526
+ `);
12527
+ const seedProviderProfiles = db.transaction(() => {
12528
+ for (const profile of DEFAULT_PROVIDER_PROFILE_SEEDS) {
12529
+ 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);
12530
+ }
12531
+ });
12532
+ seedProviderProfiles();
12155
12533
  db.exec(`
12156
12534
  CREATE TABLE IF NOT EXISTS feedback (
12157
12535
  id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
@@ -19126,17 +19504,17 @@ __export(exports_sources, {
19126
19504
  clearCache: () => clearCache,
19127
19505
  addSource: () => addSource
19128
19506
  });
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";
19507
+ import { mkdirSync as mkdirSync6, existsSync as existsSync7, readFileSync as readFileSync3, writeFileSync as writeFileSync2, readdirSync as readdirSync3, unlinkSync } from "fs";
19508
+ import { join as join8 } from "path";
19131
19509
  function getCacheFile(sourceId) {
19132
- return join7(CACHE_DIR, `${sourceId}.json`);
19510
+ return join8(CACHE_DIR, `${sourceId}.json`);
19133
19511
  }
19134
19512
  function readCache(sourceId) {
19135
19513
  try {
19136
19514
  const file = getCacheFile(sourceId);
19137
- if (!existsSync6(file))
19515
+ if (!existsSync7(file))
19138
19516
  return null;
19139
- const data = JSON.parse(readFileSync2(file, "utf-8"));
19517
+ const data = JSON.parse(readFileSync3(file, "utf-8"));
19140
19518
  return data;
19141
19519
  } catch {
19142
19520
  return null;
@@ -19150,7 +19528,7 @@ function writeCache(sourceId, results) {
19150
19528
  }
19151
19529
  function clearCache(sourceId) {
19152
19530
  try {
19153
- if (!existsSync6(CACHE_DIR))
19531
+ if (!existsSync7(CACHE_DIR))
19154
19532
  return;
19155
19533
  const files = readdirSync3(CACHE_DIR);
19156
19534
  for (const file of files) {
@@ -19158,7 +19536,7 @@ function clearCache(sourceId) {
19158
19536
  continue;
19159
19537
  if (!sourceId || file.startsWith(`${sourceId}.`)) {
19160
19538
  try {
19161
- unlinkSync(join7(CACHE_DIR, file));
19539
+ unlinkSync(join8(CACHE_DIR, file));
19162
19540
  } catch {}
19163
19541
  }
19164
19542
  }
@@ -19404,7 +19782,7 @@ var CACHE_DIR, DEFAULT_TTL_MS;
19404
19782
  var init_sources = __esm(() => {
19405
19783
  init_db();
19406
19784
  init_config2();
19407
- CACHE_DIR = join7(MCPS_DIR, "cache");
19785
+ CACHE_DIR = join8(MCPS_DIR, "cache");
19408
19786
  DEFAULT_TTL_MS = 10 * 60 * 1000;
19409
19787
  });
19410
19788
 
@@ -19412,7 +19790,7 @@ var init_sources = __esm(() => {
19412
19790
  var require_package = __commonJS((exports, module) => {
19413
19791
  module.exports = {
19414
19792
  name: "@hasna/mcps",
19415
- version: "0.0.15",
19793
+ version: "0.0.17",
19416
19794
  description: "Meta-MCP registry & CLI \u2014 discover, manage, and proxy MCP servers",
19417
19795
  type: "module",
19418
19796
  repository: {
@@ -21142,10 +21520,164 @@ var {
21142
21520
  import React10 from "react";
21143
21521
  import { render } from "ink";
21144
21522
  import chalk2 from "chalk";
21145
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
21523
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
21146
21524
 
21147
21525
  // src/lib/registry.ts
21148
21526
  init_db();
21527
+
21528
+ // src/lib/credentials.ts
21529
+ init_config2();
21530
+ import { existsSync as existsSync6, readFileSync as readFileSync2 } from "fs";
21531
+ import { join as join7 } from "path";
21532
+
21533
+ class CredentialReferenceError extends Error {
21534
+ constructor(message) {
21535
+ super(message);
21536
+ this.name = "CredentialReferenceError";
21537
+ }
21538
+ }
21539
+ var SECRET_KEY_PATTERN = /(?:^|[_-])(api[_-]?key|token|secret|password|passwd|credential|auth|private[_-]?key)(?:$|[_-])/i;
21540
+ 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_-]+)$/;
21541
+ var REDACTED_CREDENTIAL_VALUE = "<redacted>";
21542
+ function normalizeKey(key) {
21543
+ return key.trim();
21544
+ }
21545
+ function isRecord(value) {
21546
+ return typeof value === "object" && value !== null && !Array.isArray(value);
21547
+ }
21548
+ function isSecretLikeEnvKey(key) {
21549
+ return SECRET_KEY_PATTERN.test(key);
21550
+ }
21551
+ function isSecretLikeValue(value) {
21552
+ return SECRET_VALUE_PATTERN.test(value.trim());
21553
+ }
21554
+ function normalizeCredentialRef(ref) {
21555
+ const source = ref.source;
21556
+ if (source !== "env" && source !== "local-vault" && source !== "hosted") {
21557
+ throw new CredentialReferenceError(`Unsupported credential reference source: ${String(source)}`);
21558
+ }
21559
+ const name = ref.name?.trim();
21560
+ if (!name) {
21561
+ throw new CredentialReferenceError("Credential reference name is required");
21562
+ }
21563
+ return {
21564
+ source,
21565
+ name,
21566
+ required: ref.required !== false,
21567
+ ...ref.description ? { description: ref.description } : {}
21568
+ };
21569
+ }
21570
+ function normalizeCredentialRefs(refs) {
21571
+ const normalized = {};
21572
+ for (const [rawKey, ref] of Object.entries(refs ?? {})) {
21573
+ const key = normalizeKey(rawKey);
21574
+ if (!key)
21575
+ throw new CredentialReferenceError("Credential reference env key is required");
21576
+ normalized[key] = normalizeCredentialRef(ref);
21577
+ }
21578
+ return normalized;
21579
+ }
21580
+ function parseCredentialRefs(value) {
21581
+ if (!isRecord(value))
21582
+ return {};
21583
+ const refs = {};
21584
+ for (const [key, ref] of Object.entries(value)) {
21585
+ if (!isRecord(ref))
21586
+ continue;
21587
+ const source = ref.source;
21588
+ const name = ref.name;
21589
+ if ((source === "env" || source === "local-vault" || source === "hosted") && typeof name === "string" && name.trim()) {
21590
+ refs[key] = normalizeCredentialRef({
21591
+ source,
21592
+ name,
21593
+ required: typeof ref.required === "boolean" ? ref.required : true,
21594
+ description: typeof ref.description === "string" ? ref.description : undefined
21595
+ });
21596
+ }
21597
+ }
21598
+ return refs;
21599
+ }
21600
+ function normalizeLiteralEnv(env) {
21601
+ const normalized = {};
21602
+ for (const [rawKey, rawValue] of Object.entries(env ?? {})) {
21603
+ const key = normalizeKey(rawKey);
21604
+ if (!key)
21605
+ continue;
21606
+ const value = String(rawValue);
21607
+ if (isSecretLikeEnvKey(key) || isSecretLikeValue(value)) {
21608
+ throw new CredentialReferenceError(`Refusing to store raw secret-like env value for "${key}". Use a credential reference instead.`);
21609
+ }
21610
+ normalized[key] = value;
21611
+ }
21612
+ return normalized;
21613
+ }
21614
+ function redactEnv(env) {
21615
+ const redacted = {};
21616
+ for (const [key, value] of Object.entries(env)) {
21617
+ redacted[key] = isSecretLikeEnvKey(key) || isSecretLikeValue(value) ? REDACTED_CREDENTIAL_VALUE : value;
21618
+ }
21619
+ return redacted;
21620
+ }
21621
+ function redactServerCredentials(server) {
21622
+ return {
21623
+ ...server,
21624
+ env: redactEnv(server.env),
21625
+ credentialRefs: normalizeCredentialRefs(server.credentialRefs)
21626
+ };
21627
+ }
21628
+ function readLocalVault() {
21629
+ const path = process.env.HASNA_MCPS_CREDENTIAL_VAULT_PATH ?? join7(MCPS_DIR, "credentials.local.json");
21630
+ if (!existsSync6(path))
21631
+ return {};
21632
+ const parsed = JSON.parse(readFileSync2(path, "utf-8"));
21633
+ if (!isRecord(parsed))
21634
+ return {};
21635
+ const values = {};
21636
+ for (const [key, value] of Object.entries(parsed)) {
21637
+ if (typeof value === "string")
21638
+ values[key] = value;
21639
+ }
21640
+ return values;
21641
+ }
21642
+ function resolveCredentialRef(envKey, ref) {
21643
+ if (ref.source === "env") {
21644
+ const value = process.env[ref.name];
21645
+ if (value === undefined && ref.required !== false) {
21646
+ throw new CredentialReferenceError(`Missing required environment credential "${ref.name}" for "${envKey}"`);
21647
+ }
21648
+ return value;
21649
+ }
21650
+ if (ref.source === "local-vault") {
21651
+ const value = readLocalVault()[ref.name];
21652
+ if (value === undefined && ref.required !== false) {
21653
+ throw new CredentialReferenceError(`Missing required local vault credential "${ref.name}" for "${envKey}"`);
21654
+ }
21655
+ return value;
21656
+ }
21657
+ if (ref.required !== false) {
21658
+ throw new CredentialReferenceError(`Hosted credential "${ref.name}" for "${envKey}" cannot be resolved by the local runtime`);
21659
+ }
21660
+ return;
21661
+ }
21662
+ function resolveServerEnv(server) {
21663
+ const resolved = { ...server.env };
21664
+ const refs = normalizeCredentialRefs(server.credentialRefs);
21665
+ for (const [envKey, ref] of Object.entries(refs)) {
21666
+ const value = resolveCredentialRef(envKey, ref);
21667
+ if (value !== undefined)
21668
+ resolved[envKey] = value;
21669
+ }
21670
+ return resolved;
21671
+ }
21672
+ function credentialRefPlaceholders(refs) {
21673
+ const placeholders = {};
21674
+ for (const key of Object.keys(refs ?? {})) {
21675
+ placeholders[key] = REDACTED_CREDENTIAL_VALUE;
21676
+ }
21677
+ return placeholders;
21678
+ }
21679
+
21680
+ // src/lib/registry.ts
21149
21681
  function parseRow(row) {
21150
21682
  return {
21151
21683
  id: row.id,
@@ -21154,6 +21686,7 @@ function parseRow(row) {
21154
21686
  command: row.command,
21155
21687
  args: safeJsonParse(row.args, []),
21156
21688
  env: safeJsonParse(row.env, {}),
21689
+ credentialRefs: parseCredentialRefs(safeJsonParse(row.credential_refs, {})),
21157
21690
  transport: row.transport,
21158
21691
  url: row.url || null,
21159
21692
  source: row.source,
@@ -21221,9 +21754,9 @@ function addServer(opts) {
21221
21754
  if (!id) {
21222
21755
  throw new Error("Unable to generate a valid server ID");
21223
21756
  }
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");
21757
+ const row = db2.prepare(`INSERT INTO servers (id, name, description, command, args, env, credential_refs, transport, url, source)
21758
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
21759
+ 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
21760
  return parseRow(row);
21228
21761
  }
21229
21762
  function removeServer(id) {
@@ -21262,7 +21795,11 @@ function updateServer(id, updates) {
21262
21795
  }
21263
21796
  if (updates.env !== undefined) {
21264
21797
  sets.push("env = ?");
21265
- values.push(JSON.stringify(updates.env));
21798
+ values.push(JSON.stringify(normalizeLiteralEnv(updates.env)));
21799
+ }
21800
+ if (updates.credentialRefs !== undefined) {
21801
+ sets.push("credential_refs = ?");
21802
+ values.push(JSON.stringify(normalizeCredentialRefs(updates.credentialRefs)));
21266
21803
  }
21267
21804
  if (updates.transport !== undefined) {
21268
21805
  sets.push("transport = ?");
@@ -21296,7 +21833,7 @@ function setServerEnv(id, key, value) {
21296
21833
  if (!server)
21297
21834
  throw new Error(`Server "${id}" not found`);
21298
21835
  const env = { ...server.env, [key]: value };
21299
- db2.prepare("UPDATE servers SET env = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(env), id);
21836
+ db2.prepare("UPDATE servers SET env = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(normalizeLiteralEnv(env)), id);
21300
21837
  }
21301
21838
  function unsetServerEnv(id, key) {
21302
21839
  const db2 = getDb();
@@ -21307,6 +21844,23 @@ function unsetServerEnv(id, key) {
21307
21844
  delete env[key];
21308
21845
  db2.prepare("UPDATE servers SET env = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(env), id);
21309
21846
  }
21847
+ function setServerCredentialRef(id, key, ref) {
21848
+ const db2 = getDb();
21849
+ const server = getServer(id);
21850
+ if (!server)
21851
+ throw new Error(`Server "${id}" not found`);
21852
+ const credentialRefs = normalizeCredentialRefs({ ...server.credentialRefs ?? {}, [key]: ref });
21853
+ db2.prepare("UPDATE servers SET credential_refs = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(credentialRefs), id);
21854
+ }
21855
+ function unsetServerCredentialRef(id, key) {
21856
+ const db2 = getDb();
21857
+ const server = getServer(id);
21858
+ if (!server)
21859
+ throw new Error(`Server "${id}" not found`);
21860
+ const credentialRefs = { ...server.credentialRefs ?? {} };
21861
+ delete credentialRefs[key];
21862
+ db2.prepare("UPDATE servers SET credential_refs = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(normalizeCredentialRefs(credentialRefs)), id);
21863
+ }
21310
21864
  function cacheTools(serverId, tools) {
21311
21865
  const db2 = getDb();
21312
21866
  const insert = db2.prepare("INSERT INTO tool_cache (server_id, name, description, input_schema) VALUES (?, ?, ?, ?)");
@@ -21346,6 +21900,7 @@ function cloneServer(id, newName) {
21346
21900
  command: server.command,
21347
21901
  args: server.args,
21348
21902
  env: server.env,
21903
+ credentialRefs: server.credentialRefs,
21349
21904
  transport: server.transport,
21350
21905
  url: server.url ?? undefined,
21351
21906
  source: server.source
@@ -35615,8 +36170,8 @@ var DESTRUCTIVE_COMMANDS = new Set([
35615
36170
  ]);
35616
36171
  var SHELL_EVAL_FLAGS = new Set(["-c", "/c", "-Command", "-command", "-EncodedCommand", "-encodedcommand"]);
35617
36172
  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_-]+)$/;
36173
+ var SECRET_KEY_PATTERN2 = /(?:^|[_-])(api[_-]?key|token|secret|password|passwd|credential|auth|private[_-]?key)(?:$|[_-])/i;
36174
+ 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
36175
  var SHELL_META_PATTERN = /[;&|`<>]|\$\(/;
35621
36176
  function commandBase(command) {
35622
36177
  return command.trim().split(/[\\/]/).pop()?.toLowerCase() || command.trim().toLowerCase();
@@ -35625,10 +36180,10 @@ function normalizeArgs(args) {
35625
36180
  return (args ?? []).map((arg) => String(arg));
35626
36181
  }
35627
36182
  function isSecretKey(key) {
35628
- return SECRET_KEY_PATTERN.test(key) || SECRET_FLAG_PATTERN.test(key);
36183
+ return SECRET_KEY_PATTERN2.test(key) || SECRET_FLAG_PATTERN.test(key);
35629
36184
  }
35630
36185
  function isSecretValue(value) {
35631
- return SECRET_VALUE_PATTERN.test(value.trim());
36186
+ return SECRET_VALUE_PATTERN2.test(value.trim());
35632
36187
  }
35633
36188
  function isSecretAssignment(arg) {
35634
36189
  const eqIdx = arg.indexOf("=");
@@ -35815,14 +36370,14 @@ async function connectToServer(entry, options = {}) {
35815
36370
  assertLocalCommandConsent({
35816
36371
  command: entry.command,
35817
36372
  args: entry.args,
35818
- env: entry.env,
36373
+ env: { ...entry.env, ...credentialRefPlaceholders(entry.credentialRefs) },
35819
36374
  transport: entry.transport,
35820
36375
  operation: "launch"
35821
36376
  }, options.localCommandConsent);
35822
36377
  transport = new StdioClientTransport({
35823
36378
  command: entry.command,
35824
36379
  args: entry.args,
35825
- env: buildEnv(entry.env)
36380
+ env: buildEnv(resolveServerEnv(entry))
35826
36381
  });
35827
36382
  } else if (entry.transport === "sse") {
35828
36383
  transport = new SSEClientTransport(requireUrl(entry));
@@ -35967,7 +36522,7 @@ async function diagnoseServer(server, options = {}) {
35967
36522
  assertLocalCommandConsent({
35968
36523
  command: server.command,
35969
36524
  args: server.args,
35970
- env: server.env,
36525
+ env: { ...server.env, ...credentialRefPlaceholders(server.credentialRefs) },
35971
36526
  transport: server.transport,
35972
36527
  operation: "diagnose"
35973
36528
  }, options.localCommandConsent);
@@ -35996,12 +36551,29 @@ async function diagnoseServer(server, options = {}) {
35996
36551
  }
35997
36552
  }
35998
36553
  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(", ")}` });
36554
+ const credentialRefCount = Object.keys(server.credentialRefs ?? {}).length;
36555
+ let credentialError = null;
36556
+ try {
36557
+ resolveServerEnv(server);
36558
+ } catch (err) {
36559
+ credentialError = err.message;
36560
+ }
36561
+ if (Object.keys(server.env).length === 0 && credentialRefCount === 0) {
36562
+ checks4.push({ name: "env vars", pass: true, message: "no env vars or credential refs required" });
36563
+ } else if (missingEnv.length > 0 || credentialError) {
36564
+ const parts = [];
36565
+ if (missingEnv.length > 0)
36566
+ parts.push(`missing literal values for: ${missingEnv.map(([k]) => k).join(", ")}`);
36567
+ if (credentialError)
36568
+ parts.push(credentialError);
36569
+ checks4.push({ name: "env vars", pass: false, message: parts.join("; ") });
36003
36570
  } else {
36004
- checks4.push({ name: "env vars", pass: true, message: `${Object.keys(server.env).length} env var(s) set` });
36571
+ const literalCount = Object.keys(server.env).length;
36572
+ checks4.push({
36573
+ name: "env vars",
36574
+ pass: true,
36575
+ message: `${literalCount} literal env var(s), ${credentialRefCount} credential ref(s) available`
36576
+ });
36005
36577
  }
36006
36578
  if (server.transport !== "stdio" && server.url) {
36007
36579
  try {
@@ -36118,8 +36690,8 @@ init_sources();
36118
36690
 
36119
36691
  // src/lib/install.ts
36120
36692
  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";
36693
+ import { existsSync as existsSync8, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync7 } from "fs";
36694
+ import { join as join9 } from "path";
36123
36695
  import { homedir as homedir8 } from "os";
36124
36696
  function installToClaude(entry) {
36125
36697
  try {
@@ -36131,7 +36703,7 @@ function installToClaude(entry) {
36131
36703
  "--scope",
36132
36704
  "user"
36133
36705
  ];
36134
- for (const [k, v] of Object.entries(entry.env)) {
36706
+ for (const [k, v] of Object.entries(assertAgentInstallEnv(entry))) {
36135
36707
  args.push("--env", `${k}=${v}`);
36136
36708
  }
36137
36709
  args.push(entry.id, "--", entry.command, ...entry.args);
@@ -36143,9 +36715,9 @@ function installToClaude(entry) {
36143
36715
  }
36144
36716
  function installToCodex(entry) {
36145
36717
  try {
36146
- const configDir = join8(homedir8(), ".codex");
36147
- const configPath = join8(configDir, "config.toml");
36148
- if (!existsSync7(configDir)) {
36718
+ const configDir = join9(homedir8(), ".codex");
36719
+ const configPath = join9(configDir, "config.toml");
36720
+ if (!existsSync8(configDir)) {
36149
36721
  mkdirSync7(configDir, { recursive: true });
36150
36722
  }
36151
36723
  const block = `
@@ -36153,7 +36725,7 @@ function installToCodex(entry) {
36153
36725
  ` + `command = ${JSON.stringify(entry.command)}
36154
36726
  ` + `args = [${entry.args.map((a) => JSON.stringify(a)).join(", ")}]
36155
36727
  `;
36156
- const existing = existsSync7(configPath) ? readFileSync3(configPath, "utf-8") : "";
36728
+ const existing = existsSync8(configPath) ? readFileSync4(configPath, "utf-8") : "";
36157
36729
  if (existing.includes(`[mcp_servers.${entry.id}]`)) {
36158
36730
  return { agent: "codex", success: true };
36159
36731
  }
@@ -36165,21 +36737,22 @@ function installToCodex(entry) {
36165
36737
  }
36166
36738
  function installToGemini(entry) {
36167
36739
  try {
36168
- const configDir = join8(homedir8(), ".gemini");
36169
- const configPath = join8(configDir, "settings.json");
36170
- if (!existsSync7(configDir)) {
36740
+ const configDir = join9(homedir8(), ".gemini");
36741
+ const configPath = join9(configDir, "settings.json");
36742
+ if (!existsSync8(configDir)) {
36171
36743
  mkdirSync7(configDir, { recursive: true });
36172
36744
  }
36173
36745
  let settings = {};
36174
- if (existsSync7(configPath)) {
36175
- settings = JSON.parse(readFileSync3(configPath, "utf-8"));
36746
+ if (existsSync8(configPath)) {
36747
+ settings = JSON.parse(readFileSync4(configPath, "utf-8"));
36176
36748
  }
36177
36749
  if (!settings.mcpServers)
36178
36750
  settings.mcpServers = {};
36751
+ const env = assertAgentInstallEnv(entry);
36179
36752
  settings.mcpServers[entry.id] = {
36180
36753
  command: entry.command,
36181
36754
  args: entry.args,
36182
- ...Object.keys(entry.env).length > 0 ? { env: entry.env } : {}
36755
+ ...Object.keys(env).length > 0 ? { env } : {}
36183
36756
  };
36184
36757
  writeFileSync3(configPath, JSON.stringify(settings, null, 2), "utf-8");
36185
36758
  return { agent: "gemini", success: true };
@@ -36187,12 +36760,24 @@ function installToGemini(entry) {
36187
36760
  return { agent: "gemini", success: false, error: err.message };
36188
36761
  }
36189
36762
  }
36763
+ function assertAgentInstallEnv(entry) {
36764
+ const refs = entry.credentialRefs ?? {};
36765
+ if (Object.keys(refs).length > 0) {
36766
+ throw new CredentialReferenceError(`Server "${entry.id}" uses credential references; refusing to materialize secrets into local agent config files`);
36767
+ }
36768
+ for (const [key, value] of Object.entries(entry.env)) {
36769
+ if (isSecretLikeEnvKey(key) || isSecretLikeValue(value)) {
36770
+ throw new CredentialReferenceError(`Server "${entry.id}" has legacy raw secret-like env "${key}"; move it to a credential reference before installing to agents`);
36771
+ }
36772
+ }
36773
+ return entry.env;
36774
+ }
36190
36775
  function installToAgents(entry, targets = ["claude", "codex", "gemini"], options = {}) {
36191
36776
  try {
36192
36777
  assertLocalCommandConsent({
36193
36778
  command: entry.command,
36194
36779
  args: entry.args,
36195
- env: entry.env,
36780
+ env: { ...entry.env, ...credentialRefPlaceholders(entry.credentialRefs) },
36196
36781
  transport: entry.transport,
36197
36782
  operation: "install"
36198
36783
  }, options.localCommandConsent);
@@ -36203,6 +36788,15 @@ function installToAgents(entry, targets = ["claude", "codex", "gemini"], options
36203
36788
  error: err.message
36204
36789
  }));
36205
36790
  }
36791
+ try {
36792
+ assertAgentInstallEnv(entry);
36793
+ } catch (err) {
36794
+ return targets.map((target) => ({
36795
+ agent: target,
36796
+ success: false,
36797
+ error: err.message
36798
+ }));
36799
+ }
36206
36800
  return targets.map((target) => {
36207
36801
  if (target === "claude")
36208
36802
  return installToClaude(entry);
@@ -36417,11 +37011,11 @@ function seedDefaultMachines() {
36417
37011
  // src/lib/fleet.ts
36418
37012
  init_config2();
36419
37013
  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";
37014
+ import { existsSync as existsSync9, mkdirSync as mkdirSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
37015
+ import { join as join10 } from "path";
36422
37016
  var NPM_SEARCH_URL = "https://registry.npmjs.org/-/v1/search";
36423
37017
  var NPM_REGISTRY_URL = "https://registry.npmjs.org";
36424
- var CATALOG_CACHE_PATH = join9(MCPS_DIR, "cache", "hasna-catalog.json");
37018
+ var CATALOG_CACHE_PATH = join10(MCPS_DIR, "cache", "hasna-catalog.json");
36425
37019
  var DEFAULT_CATALOG_CACHE_TTL_MS = 60 * 60 * 1000;
36426
37020
  var DEFAULT_REMOTE_TIMEOUT_MS = 180000;
36427
37021
  var DEFAULT_HANDSHAKE_TIMEOUT_MS = 2500;
@@ -36431,9 +37025,9 @@ function normalizeQueryList(values) {
36431
37025
  }
36432
37026
  function readCatalogCache(maxAgeMs) {
36433
37027
  try {
36434
- if (!existsSync8(CATALOG_CACHE_PATH))
37028
+ if (!existsSync9(CATALOG_CACHE_PATH))
36435
37029
  return null;
36436
- const parsed = JSON.parse(readFileSync4(CATALOG_CACHE_PATH, "utf-8"));
37030
+ const parsed = JSON.parse(readFileSync5(CATALOG_CACHE_PATH, "utf-8"));
36437
37031
  if (!parsed.cachedAt || !Array.isArray(parsed.entries))
36438
37032
  return null;
36439
37033
  if (Date.now() - parsed.cachedAt > maxAgeMs)
@@ -36445,7 +37039,7 @@ function readCatalogCache(maxAgeMs) {
36445
37039
  }
36446
37040
  function writeCatalogCache(entries) {
36447
37041
  try {
36448
- mkdirSync8(join9(MCPS_DIR, "cache"), { recursive: true });
37042
+ mkdirSync8(join10(MCPS_DIR, "cache"), { recursive: true });
36449
37043
  writeFileSync4(CATALOG_CACHE_PATH, JSON.stringify({ cachedAt: Date.now(), entries }, null, 2), "utf-8");
36450
37044
  } catch {}
36451
37045
  }
@@ -38449,22 +39043,22 @@ var EMPTY_COMPLETION_RESULT = {
38449
39043
  };
38450
39044
 
38451
39045
  // src/lib/version.ts
38452
- import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
38453
- import { dirname as dirname3, join as join10 } from "path";
39046
+ import { existsSync as existsSync10, readFileSync as readFileSync6 } from "fs";
39047
+ import { dirname as dirname3, join as join11 } from "path";
38454
39048
  import { fileURLToPath } from "url";
38455
39049
  var FALLBACK_VERSION = "0.0.1";
38456
39050
  function readPackageVersion(moduleUrl, fallback = FALLBACK_VERSION) {
38457
39051
  const baseDir = dirname3(fileURLToPath(moduleUrl));
38458
39052
  const candidates = [
38459
- join10(baseDir, "..", "..", "package.json"),
38460
- join10(baseDir, "..", "package.json"),
38461
- join10(baseDir, "package.json")
39053
+ join11(baseDir, "..", "..", "package.json"),
39054
+ join11(baseDir, "..", "package.json"),
39055
+ join11(baseDir, "package.json")
38462
39056
  ];
38463
39057
  for (const candidate of candidates) {
38464
- if (!existsSync9(candidate))
39058
+ if (!existsSync10(candidate))
38465
39059
  continue;
38466
39060
  try {
38467
- const pkg = JSON.parse(readFileSync5(candidate, "utf-8"));
39061
+ const pkg = JSON.parse(readFileSync6(candidate, "utf-8"));
38468
39062
  if (pkg.version)
38469
39063
  return pkg.version;
38470
39064
  } catch {}
@@ -38497,6 +39091,9 @@ function localConsent(input) {
38497
39091
  source: "mcp"
38498
39092
  };
38499
39093
  }
39094
+ function readCredentialRefs(input) {
39095
+ return normalizeCredentialRefs(input.credential_refs ?? input.credentialRefs);
39096
+ }
38500
39097
  function buildMcpTools() {
38501
39098
  const definitions = [
38502
39099
  {
@@ -38522,6 +39119,12 @@ function buildMcpTools() {
38522
39119
  transport: exports_external2.enum(["stdio", "sse", "streamable-http"]).optional().describe("Transport type"),
38523
39120
  url: exports_external2.string().optional().describe("URL for remote transports"),
38524
39121
  env: exports_external2.record(exports_external2.string()).optional().describe("Environment variables"),
39122
+ credential_refs: exports_external2.record(exports_external2.object({
39123
+ source: exports_external2.enum(["env", "local-vault", "hosted"]),
39124
+ name: exports_external2.string(),
39125
+ required: exports_external2.boolean().optional(),
39126
+ description: exports_external2.string().optional()
39127
+ })).optional().describe("Credential references by server env key"),
38525
39128
  allow_local_stdio: exports_external2.boolean().optional().describe("Approve registering this local stdio command"),
38526
39129
  allow_risky_command: exports_external2.boolean().optional().describe("Approve registering risky local command patterns")
38527
39130
  },
@@ -38529,9 +39132,16 @@ function buildMcpTools() {
38529
39132
  const command = String(input.command);
38530
39133
  const args = Array.isArray(input.args) ? input.args.map(String) : [];
38531
39134
  const env = isRecordOfStrings(input.env) ? input.env : {};
39135
+ const credentialRefs = readCredentialRefs(input);
38532
39136
  const transport = input.transport;
38533
39137
  try {
38534
- assertLocalCommandConsent({ command, args, env, transport, operation: "register" }, localConsent(input));
39138
+ assertLocalCommandConsent({
39139
+ command,
39140
+ args,
39141
+ env: { ...env, ...Object.fromEntries(Object.keys(credentialRefs).map((key) => [key, "<credential-ref>"])) },
39142
+ transport,
39143
+ operation: "register"
39144
+ }, localConsent(input));
38535
39145
  return jsonContent(addServer({
38536
39146
  command,
38537
39147
  args,
@@ -38539,7 +39149,8 @@ function buildMcpTools() {
38539
39149
  description: typeof input.description === "string" ? input.description : undefined,
38540
39150
  transport,
38541
39151
  url: typeof input.url === "string" ? input.url : undefined,
38542
- env
39152
+ env,
39153
+ credentialRefs
38543
39154
  }));
38544
39155
  } catch (err) {
38545
39156
  return errorContent(err.message);
@@ -38607,6 +39218,12 @@ function buildMcpTools() {
38607
39218
  args: exports_external2.array(exports_external2.string()).optional().describe("New args list"),
38608
39219
  transport: exports_external2.enum(["stdio", "sse", "streamable-http"]).optional().describe("New transport type"),
38609
39220
  url: exports_external2.string().optional().describe("New URL for remote transports"),
39221
+ credential_refs: exports_external2.record(exports_external2.object({
39222
+ source: exports_external2.enum(["env", "local-vault", "hosted"]),
39223
+ name: exports_external2.string(),
39224
+ required: exports_external2.boolean().optional(),
39225
+ description: exports_external2.string().optional()
39226
+ })).optional().describe("Credential references by server env key"),
38610
39227
  allow_local_stdio: exports_external2.boolean().optional().describe("Approve updating this local stdio command"),
38611
39228
  allow_risky_command: exports_external2.boolean().optional().describe("Approve risky local command patterns")
38612
39229
  },
@@ -38624,6 +39241,8 @@ function buildMcpTools() {
38624
39241
  fields.command = input.command;
38625
39242
  if (Array.isArray(input.args))
38626
39243
  fields.args = input.args.map(String);
39244
+ if (input.credential_refs !== undefined || input.credentialRefs !== undefined)
39245
+ fields.credentialRefs = readCredentialRefs(input);
38627
39246
  if (input.transport === "stdio" || input.transport === "sse" || input.transport === "streamable-http")
38628
39247
  fields.transport = input.transport;
38629
39248
  if (typeof input.url === "string")
@@ -38633,7 +39252,10 @@ function buildMcpTools() {
38633
39252
  assertLocalCommandConsent({
38634
39253
  command: fields.command ?? existing.command,
38635
39254
  args: fields.args ?? existing.args,
38636
- env: existing.env,
39255
+ env: {
39256
+ ...existing.env,
39257
+ ...Object.fromEntries(Object.keys(fields.credentialRefs ?? existing.credentialRefs ?? {}).map((key) => [key, "<credential-ref>"]))
39258
+ },
38637
39259
  transport: fields.transport ?? existing.transport,
38638
39260
  operation: "register"
38639
39261
  }, localConsent(input));
@@ -38688,7 +39310,7 @@ function buildMcpTools() {
38688
39310
  },
38689
39311
  {
38690
39312
  name: "list_provider_profiles",
38691
- description: "List curated provider profiles for hosted/common MCP integrations such as Notion and Linear.",
39313
+ description: "List curated provider profiles for hosted/common MCP integrations such as GitHub, Slack, Google Workspace, Stripe, Cloudflare, Postgres, filesystem, and browser automation.",
38692
39314
  paramsSchema: {
38693
39315
  enabled_only: exports_external2.boolean().optional().describe("Only include enabled provider profiles")
38694
39316
  },
@@ -38698,7 +39320,7 @@ function buildMcpTools() {
38698
39320
  name: "search_provider_profiles",
38699
39321
  description: "Search curated provider profiles separately from raw MCP registry/source search.",
38700
39322
  paramsSchema: {
38701
- query: exports_external2.string().describe("Search query such as 'notion', 'linear', or an endpoint URL"),
39323
+ query: exports_external2.string().describe("Search query such as 'github', 'slack', 'postgres', or an endpoint URL"),
38702
39324
  enabled_only: exports_external2.boolean().optional().describe("Only include enabled provider profiles")
38703
39325
  },
38704
39326
  run: ({ query, enabled_only }) => jsonContent(searchProviderProfiles(String(query), { enabledOnly: enabled_only === true }))
@@ -39197,32 +39819,32 @@ if (isDirectRun) {
39197
39819
  }
39198
39820
 
39199
39821
  // 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";
39822
+ import { existsSync as existsSync11 } from "fs";
39823
+ import { join as join12, dirname as dirname4, extname, resolve, relative as relative2, sep } from "path";
39202
39824
  import { fileURLToPath as fileURLToPath2 } from "url";
39203
39825
  init_sources();
39204
39826
  init_db();
39205
39827
  function redactServer(server) {
39206
- return { ...server, env: {} };
39828
+ return { ...redactServerCredentials(server), env: {} };
39207
39829
  }
39208
39830
  function resolveDashboardDir() {
39209
39831
  const candidates = [];
39210
39832
  try {
39211
39833
  const scriptDir = dirname4(fileURLToPath2(import.meta.url));
39212
- candidates.push(join11(scriptDir, "..", "dashboard", "dist"));
39213
- candidates.push(join11(scriptDir, "..", "..", "dashboard", "dist"));
39834
+ candidates.push(join12(scriptDir, "..", "dashboard", "dist"));
39835
+ candidates.push(join12(scriptDir, "..", "..", "dashboard", "dist"));
39214
39836
  } catch {}
39215
39837
  if (process.argv[1]) {
39216
39838
  const mainDir = dirname4(process.argv[1]);
39217
- candidates.push(join11(mainDir, "..", "dashboard", "dist"));
39218
- candidates.push(join11(mainDir, "..", "..", "dashboard", "dist"));
39839
+ candidates.push(join12(mainDir, "..", "dashboard", "dist"));
39840
+ candidates.push(join12(mainDir, "..", "..", "dashboard", "dist"));
39219
39841
  }
39220
- candidates.push(join11(process.cwd(), "dashboard", "dist"));
39842
+ candidates.push(join12(process.cwd(), "dashboard", "dist"));
39221
39843
  for (const candidate of candidates) {
39222
- if (existsSync10(candidate))
39844
+ if (existsSync11(candidate))
39223
39845
  return candidate;
39224
39846
  }
39225
- return join11(process.cwd(), "dashboard", "dist");
39847
+ return join12(process.cwd(), "dashboard", "dist");
39226
39848
  }
39227
39849
  var MIME_TYPES = {
39228
39850
  ".html": "text/html; charset=utf-8",
@@ -39324,7 +39946,7 @@ function getAllServersWithToolCount() {
39324
39946
  }));
39325
39947
  }
39326
39948
  function serveStaticFile(filePath) {
39327
- if (!existsSync10(filePath))
39949
+ if (!existsSync11(filePath))
39328
39950
  return null;
39329
39951
  const ext = extname(filePath);
39330
39952
  const contentType = MIME_TYPES[ext] || "application/octet-stream";
@@ -39357,7 +39979,7 @@ async function startServer(port, options) {
39357
39979
  const host = options?.host ?? "127.0.0.1";
39358
39980
  getDb();
39359
39981
  const dashboardDir = resolveDashboardDir();
39360
- const dashboardExists = existsSync10(dashboardDir);
39982
+ const dashboardExists = existsSync11(dashboardDir);
39361
39983
  if (!dashboardExists) {
39362
39984
  console.error(`
39363
39985
  Dashboard not found at: ${dashboardDir}`);
@@ -39416,8 +40038,16 @@ Dashboard not found at: ${dashboardDir}`);
39416
40038
  return json({ error: "Invalid 'args' format" }, 400, port);
39417
40039
  }
39418
40040
  const args = body.args || [];
40041
+ const credentialRefs = normalizeCredentialRefs(body.credential_refs ?? body.credentialRefs);
40042
+ const env = body.env && typeof body.env === "object" && !Array.isArray(body.env) ? body.env : {};
39419
40043
  try {
39420
- assertLocalCommandConsent({ command, args, env: {}, transport, operation: "register" }, consentFromInput(body));
40044
+ assertLocalCommandConsent({
40045
+ command,
40046
+ args,
40047
+ env: { ...env, ...Object.fromEntries(Object.keys(credentialRefs).map((key) => [key, "<credential-ref>"])) },
40048
+ transport,
40049
+ operation: "register"
40050
+ }, consentFromInput(body));
39421
40051
  } catch (err) {
39422
40052
  return json({ error: err.message }, 400, port);
39423
40053
  }
@@ -39427,10 +40057,14 @@ Dashboard not found at: ${dashboardDir}`);
39427
40057
  args,
39428
40058
  description: body.description,
39429
40059
  transport,
39430
- url: body.url
40060
+ url: body.url,
40061
+ env,
40062
+ credentialRefs
39431
40063
  });
39432
40064
  return json(entry, 200, port);
39433
40065
  } catch (e) {
40066
+ if (e instanceof CredentialReferenceError)
40067
+ return json({ error: e.message }, 400, port);
39434
40068
  return json({ error: e instanceof Error ? e.message : "Failed to add server" }, 500, port);
39435
40069
  }
39436
40070
  }
@@ -39509,6 +40143,11 @@ Dashboard not found at: ${dashboardDir}`);
39509
40143
  fields.description = body.description;
39510
40144
  if (body.command !== undefined)
39511
40145
  fields.command = body.command;
40146
+ if (body.env !== undefined)
40147
+ fields.env = body.env;
40148
+ if (body.credential_refs !== undefined || body.credentialRefs !== undefined) {
40149
+ fields.credentialRefs = normalizeCredentialRefs(body.credential_refs ?? body.credentialRefs);
40150
+ }
39512
40151
  if (body.transport !== undefined)
39513
40152
  fields.transport = body.transport;
39514
40153
  if (body.url !== undefined)
@@ -39524,7 +40163,10 @@ Dashboard not found at: ${dashboardDir}`);
39524
40163
  assertLocalCommandConsent({
39525
40164
  command: fields.command ?? existing.command,
39526
40165
  args: fields.args ?? existing.args,
39527
- env: existing.env,
40166
+ env: {
40167
+ ...fields.env ?? existing.env,
40168
+ ...Object.fromEntries(Object.keys(fields.credentialRefs ?? existing.credentialRefs ?? {}).map((key) => [key, "<credential-ref>"]))
40169
+ },
39528
40170
  transport: fields.transport ?? existing.transport,
39529
40171
  operation: "register"
39530
40172
  }, consentFromInput(body));
@@ -39560,6 +40202,8 @@ Dashboard not found at: ${dashboardDir}`);
39560
40202
  setServerEnv(id, body.key, body.value);
39561
40203
  return json({ ok: true }, 200, port);
39562
40204
  } catch (e) {
40205
+ if (e instanceof CredentialReferenceError)
40206
+ return json({ error: e.message }, 400, port);
39563
40207
  return json({ error: e instanceof Error ? e.message : "Failed" }, 500, port);
39564
40208
  }
39565
40209
  }
@@ -39762,7 +40406,7 @@ Dashboard not found at: ${dashboardDir}`);
39762
40406
  return res2;
39763
40407
  }
39764
40408
  }
39765
- const indexPath = join11(dashboardDir, "index.html");
40409
+ const indexPath = join12(dashboardDir, "index.html");
39766
40410
  const res = serveStaticFile(indexPath);
39767
40411
  if (res)
39768
40412
  return res;
@@ -40916,6 +41560,30 @@ function assertCliLocalCommandConsent(input, opts, approved = false) {
40916
41560
  assertLocalCommandConsent(input, consent);
40917
41561
  return consent;
40918
41562
  }
41563
+ function parseCredentialPairs(pairs, source) {
41564
+ const refs = {};
41565
+ for (const pair of pairs ?? []) {
41566
+ const eqIdx = pair.indexOf("=");
41567
+ if (eqIdx <= 0)
41568
+ throw new Error(`Credential reference must use KEY=NAME format: ${pair}`);
41569
+ const key = pair.slice(0, eqIdx).trim();
41570
+ const name = pair.slice(eqIdx + 1).trim();
41571
+ if (!key || !name)
41572
+ throw new Error(`Credential reference must use KEY=NAME format: ${pair}`);
41573
+ refs[key] = { source, name, required: true };
41574
+ }
41575
+ return refs;
41576
+ }
41577
+ function credentialRefsFromOptions(opts) {
41578
+ return {
41579
+ ...parseCredentialPairs(opts.credentialEnv, "env"),
41580
+ ...parseCredentialPairs(opts.credentialLocal, "local-vault"),
41581
+ ...parseCredentialPairs(opts.credentialHosted, "hosted")
41582
+ };
41583
+ }
41584
+ function formatCredentialRef(ref) {
41585
+ return `${ref.source}:${ref.name}`;
41586
+ }
40919
41587
  function printProviderProfile(profile) {
40920
41588
  const status = profile.enabled ? chalk2.green("enabled") : chalk2.red("disabled");
40921
41589
  console.log(` ${chalk2.bold(profile.displayName)} ${chalk2.dim(`[${profile.id}]`)} \u2014 ${chalk2.dim(profile.transport)} \u2014 ${status}`);
@@ -41200,7 +41868,7 @@ function detectSourceType(url2) {
41200
41868
  return "mcp-registry";
41201
41869
  return null;
41202
41870
  }
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) => {
41871
+ 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
41872
  try {
41205
41873
  if (opts.fromRegistry) {
41206
41874
  console.log(chalk2.dim(`Installing "${opts.fromRegistry}" from registry...`));
@@ -41277,7 +41945,8 @@ Server to add:`));
41277
41945
  name: wizardName || undefined,
41278
41946
  description: wizardDescription || undefined,
41279
41947
  transport,
41280
- env
41948
+ env,
41949
+ credentialRefs: credentialRefsFromOptions(opts)
41281
41950
  });
41282
41951
  console.log(chalk2.green(`Added: ${server2.name} [${server2.id}]`));
41283
41952
  closeDb();
@@ -41307,10 +41976,11 @@ Server to add:`));
41307
41976
  envMap[key] = rest.join("=");
41308
41977
  }
41309
41978
  }
41979
+ const credentialRefs = credentialRefsFromOptions(opts);
41310
41980
  assertCliLocalCommandConsent({
41311
41981
  command,
41312
41982
  args,
41313
- env: envMap,
41983
+ env: { ...envMap, ...Object.fromEntries(Object.keys(credentialRefs).map((key) => [key, "<credential-ref>"])) },
41314
41984
  transport: opts.transport,
41315
41985
  operation: "register"
41316
41986
  }, opts);
@@ -41321,7 +41991,8 @@ Server to add:`));
41321
41991
  args,
41322
41992
  transport: opts.transport,
41323
41993
  url: opts.url,
41324
- env: envMap
41994
+ env: envMap,
41995
+ credentialRefs
41325
41996
  });
41326
41997
  console.log(chalk2.green(`Added server: ${server.name} [${server.id}]`));
41327
41998
  console.log(chalk2.dim(` ${server.command} ${server.args.join(" ")}`));
@@ -41520,20 +42191,24 @@ program2.command("info").argument("<id>", "Server ID").description("Show server
41520
42191
  closeDb();
41521
42192
  process.exit(1);
41522
42193
  }
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}`);
42194
+ const safeServer = redactServerCredentials(server);
42195
+ console.log(chalk2.bold(safeServer.name) + " " + chalk2.dim(`[${safeServer.id}]`));
42196
+ console.log(` Status: ${safeServer.enabled ? chalk2.green("enabled") : chalk2.red("disabled")}`);
42197
+ console.log(` Source: ${safeServer.source}`);
42198
+ console.log(` Transport: ${safeServer.transport}`);
42199
+ console.log(` Command: ${safeServer.command} ${safeServer.args.join(" ")}`);
42200
+ if (safeServer.url)
42201
+ console.log(` URL: ${safeServer.url}`);
42202
+ if (safeServer.description)
42203
+ console.log(` Desc: ${safeServer.description}`);
42204
+ if (Object.keys(safeServer.env).length > 0) {
42205
+ console.log(` Env: ${Object.entries(safeServer.env).map(([k, v]) => `${k}=${v}`).join(", ")}`);
42206
+ }
42207
+ if (Object.keys(safeServer.credentialRefs ?? {}).length > 0) {
42208
+ console.log(` Cred refs: ${Object.entries(safeServer.credentialRefs ?? {}).map(([k, ref]) => `${k}=${formatCredentialRef(ref)}`).join(", ")}`);
42209
+ }
42210
+ console.log(` Created: ${safeServer.created_at}`);
42211
+ console.log(` Updated: ${safeServer.updated_at}`);
41537
42212
  const cached2 = getCachedTools(id);
41538
42213
  if (cached2.length > 0) {
41539
42214
  console.log(chalk2.bold(`
@@ -42173,7 +42848,7 @@ fleetCmd.command("install").argument("[machineIds...]", "Optional machine IDs to
42173
42848
  }
42174
42849
  });
42175
42850
  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();
42851
+ const servers = listServers().map(redactServerCredentials);
42177
42852
  const sources = listSources();
42178
42853
  const payload = {
42179
42854
  version: 1,
@@ -42193,7 +42868,7 @@ program2.command("export").description("Export all servers and sources to a JSON
42193
42868
  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
42869
  let payload;
42195
42870
  try {
42196
- payload = JSON.parse(readFileSync6(file, "utf-8"));
42871
+ payload = JSON.parse(readFileSync7(file, "utf-8"));
42197
42872
  } catch (err) {
42198
42873
  console.error(chalk2.red(`Failed to read file: ${err.message}`));
42199
42874
  closeDb();
@@ -42215,7 +42890,23 @@ program2.command("import").argument("<file>", "Path to the export JSON file").de
42215
42890
  serversSkipped++;
42216
42891
  continue;
42217
42892
  }
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]);
42893
+ const literalEnv = normalizeLiteralEnv(s.env ?? {});
42894
+ const credentialRefs = normalizeCredentialRefs(s.credentialRefs ?? s.credential_refs ?? {});
42895
+ db2.run(`INSERT ${orReplace} INTO servers (id, name, description, command, args, env, credential_refs, transport, url, source, enabled, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`, [
42896
+ s.id,
42897
+ s.name,
42898
+ s.description,
42899
+ s.command,
42900
+ JSON.stringify(s.args ?? []),
42901
+ JSON.stringify(literalEnv),
42902
+ JSON.stringify(credentialRefs),
42903
+ s.transport,
42904
+ s.url,
42905
+ s.source,
42906
+ s.enabled ? 1 : 0,
42907
+ s.created_at,
42908
+ s.updated_at
42909
+ ]);
42219
42910
  serversImported++;
42220
42911
  }
42221
42912
  let sourcesImported = 0;
@@ -42241,14 +42932,17 @@ envCmd.command("list").argument("<id>").description("List env vars for a server"
42241
42932
  closeDb();
42242
42933
  process.exit(1);
42243
42934
  }
42244
- const entries = Object.entries(server.env);
42245
- if (entries.length === 0) {
42246
- console.log(chalk2.dim("No env vars set."));
42935
+ const envEntries = Object.entries(redactEnv(server.env));
42936
+ const refEntries = Object.entries(server.credentialRefs ?? {});
42937
+ if (envEntries.length === 0 && refEntries.length === 0) {
42938
+ console.log(chalk2.dim("No env vars or credential refs set."));
42247
42939
  closeDb();
42248
42940
  return;
42249
42941
  }
42250
- for (const [k, v] of entries)
42942
+ for (const [k, v] of envEntries)
42251
42943
  console.log(` ${chalk2.bold(k)}=${chalk2.dim(v)}`);
42944
+ for (const [k, ref] of refEntries)
42945
+ console.log(` ${chalk2.bold(k)}=${chalk2.dim(formatCredentialRef(ref))}`);
42252
42946
  closeDb();
42253
42947
  });
42254
42948
  envCmd.command("set").argument("<id>").argument("<pair>", "KEY=VALUE").description("Set an env var").action((id, pair) => {
@@ -42270,6 +42964,36 @@ envCmd.command("set").argument("<id>").argument("<pair>", "KEY=VALUE").descripti
42270
42964
  }
42271
42965
  closeDb();
42272
42966
  });
42967
+ 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) => {
42968
+ const source = opts.source;
42969
+ if (source !== "env" && source !== "local-vault" && source !== "hosted") {
42970
+ console.error(chalk2.red("Source must be one of: env, local-vault, hosted"));
42971
+ closeDb();
42972
+ process.exit(1);
42973
+ }
42974
+ try {
42975
+ const refs = parseCredentialPairs([pair], source);
42976
+ const [key, ref] = Object.entries(refs)[0];
42977
+ setServerCredentialRef(id, key, ref);
42978
+ console.log(chalk2.green(`Set credential ref ${key}=${formatCredentialRef(ref)} on ${id}`));
42979
+ } catch (err) {
42980
+ console.error(chalk2.red(err.message));
42981
+ closeDb();
42982
+ process.exit(1);
42983
+ }
42984
+ closeDb();
42985
+ });
42986
+ envCmd.command("unset-ref").argument("<id>").argument("<key>").description("Remove a credential reference").action((id, key) => {
42987
+ try {
42988
+ unsetServerCredentialRef(id, key);
42989
+ console.log(chalk2.green(`Unset credential ref ${key} on ${id}`));
42990
+ } catch (err) {
42991
+ console.error(chalk2.red(err.message));
42992
+ closeDb();
42993
+ process.exit(1);
42994
+ }
42995
+ closeDb();
42996
+ });
42273
42997
  envCmd.command("unset").argument("<id>").argument("<key>").description("Remove an env var").action((id, key) => {
42274
42998
  try {
42275
42999
  unsetServerEnv(id, key);