@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/README.md +21 -0
- package/bin/index.js +448 -98
- package/bin/mcp.js +263 -47
- package/dist/index.d.ts +3 -2
- package/dist/index.js +286 -53
- package/dist/lib/credentials.d.ts +18 -0
- package/dist/lib/registry.d.ts +4 -2
- package/dist/mcp/index.js +263 -47
- package/dist/types.d.ts +10 -0
- package/package.json +1 -1
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
|
|
19130
|
-
import { join as
|
|
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
|
|
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 (!
|
|
19141
|
+
if (!existsSync7(file))
|
|
19138
19142
|
return null;
|
|
19139
|
-
const data = JSON.parse(
|
|
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 (!
|
|
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(
|
|
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 =
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
35619
|
-
var
|
|
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
|
|
35809
|
+
return SECRET_KEY_PATTERN2.test(key) || SECRET_FLAG_PATTERN.test(key);
|
|
35629
35810
|
}
|
|
35630
35811
|
function isSecretValue(value) {
|
|
35631
|
-
return
|
|
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
|
|
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
|
-
|
|
36000
|
-
|
|
36001
|
-
|
|
36002
|
-
|
|
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
|
-
|
|
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
|
|
36122
|
-
import { join as
|
|
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
|
|
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 =
|
|
36147
|
-
const configPath =
|
|
36148
|
-
if (!
|
|
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 =
|
|
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 =
|
|
36169
|
-
const configPath =
|
|
36170
|
-
if (!
|
|
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 (
|
|
36175
|
-
settings = JSON.parse(
|
|
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(
|
|
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
|
|
36421
|
-
import { join as
|
|
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 =
|
|
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 (!
|
|
36654
|
+
if (!existsSync9(CATALOG_CACHE_PATH))
|
|
36435
36655
|
return null;
|
|
36436
|
-
const parsed = JSON.parse(
|
|
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(
|
|
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
|
|
38453
|
-
import { dirname as dirname3, join as
|
|
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
|
-
|
|
38460
|
-
|
|
38461
|
-
|
|
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 (!
|
|
38684
|
+
if (!existsSync10(candidate))
|
|
38465
38685
|
continue;
|
|
38466
38686
|
try {
|
|
38467
|
-
const pkg = JSON.parse(
|
|
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({
|
|
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:
|
|
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
|
|
39201
|
-
import { join as
|
|
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(
|
|
39213
|
-
candidates.push(
|
|
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(
|
|
39218
|
-
candidates.push(
|
|
39465
|
+
candidates.push(join12(mainDir, "..", "dashboard", "dist"));
|
|
39466
|
+
candidates.push(join12(mainDir, "..", "..", "dashboard", "dist"));
|
|
39219
39467
|
}
|
|
39220
|
-
candidates.push(
|
|
39468
|
+
candidates.push(join12(process.cwd(), "dashboard", "dist"));
|
|
39221
39469
|
for (const candidate of candidates) {
|
|
39222
|
-
if (
|
|
39470
|
+
if (existsSync11(candidate))
|
|
39223
39471
|
return candidate;
|
|
39224
39472
|
}
|
|
39225
|
-
return
|
|
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 (!
|
|
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 =
|
|
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({
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
41524
|
-
console.log(
|
|
41525
|
-
console.log(`
|
|
41526
|
-
console.log(`
|
|
41527
|
-
console.log(`
|
|
41528
|
-
|
|
41529
|
-
|
|
41530
|
-
|
|
41531
|
-
|
|
41532
|
-
|
|
41533
|
-
|
|
41534
|
-
}
|
|
41535
|
-
|
|
41536
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
42245
|
-
|
|
42246
|
-
|
|
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
|
|
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);
|