@hasna/mcps 0.0.14 → 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 +967 -182
- package/bin/mcp.js +605 -95
- package/dist/index.d.ts +5 -2
- package/dist/index.js +523 -53
- package/dist/lib/credentials.d.ts +18 -0
- package/dist/lib/doctor.d.ts +4 -1
- package/dist/lib/install.d.ts +5 -1
- package/dist/lib/local-command-consent.d.ts +38 -0
- package/dist/lib/proxy.d.ts +6 -2
- package/dist/lib/registry.d.ts +4 -2
- package/dist/lib/remote.d.ts +4 -1
- package/dist/mcp/index.js +605 -95
- package/dist/types.d.ts +15 -0
- package/package.json +1 -1
package/dist/mcp/index.js
CHANGED
|
@@ -16673,6 +16673,7 @@ function getDb() {
|
|
|
16673
16673
|
command TEXT NOT NULL,
|
|
16674
16674
|
args TEXT NOT NULL DEFAULT '[]',
|
|
16675
16675
|
env TEXT NOT NULL DEFAULT '{}',
|
|
16676
|
+
credential_refs TEXT NOT NULL DEFAULT '{}',
|
|
16676
16677
|
transport TEXT NOT NULL DEFAULT 'stdio',
|
|
16677
16678
|
url TEXT,
|
|
16678
16679
|
source TEXT NOT NULL DEFAULT 'local',
|
|
@@ -16699,6 +16700,9 @@ function getDb() {
|
|
|
16699
16700
|
try {
|
|
16700
16701
|
db.exec("ALTER TABLE servers ADD COLUMN last_error TEXT");
|
|
16701
16702
|
} catch {}
|
|
16703
|
+
try {
|
|
16704
|
+
db.exec("ALTER TABLE servers ADD COLUMN credential_refs TEXT NOT NULL DEFAULT '{}'");
|
|
16705
|
+
} catch {}
|
|
16702
16706
|
db.exec(`
|
|
16703
16707
|
CREATE TABLE IF NOT EXISTS sources (
|
|
16704
16708
|
id TEXT PRIMARY KEY,
|
|
@@ -16824,17 +16828,17 @@ __export(exports_sources, {
|
|
|
16824
16828
|
clearCache: () => clearCache,
|
|
16825
16829
|
addSource: () => addSource
|
|
16826
16830
|
});
|
|
16827
|
-
import { mkdirSync as mkdirSync6, existsSync as
|
|
16828
|
-
import { join as
|
|
16831
|
+
import { mkdirSync as mkdirSync6, existsSync as existsSync8, readFileSync as readFileSync4, writeFileSync as writeFileSync2, readdirSync as readdirSync3, unlinkSync } from "fs";
|
|
16832
|
+
import { join as join9 } from "path";
|
|
16829
16833
|
function getCacheFile(sourceId) {
|
|
16830
|
-
return
|
|
16834
|
+
return join9(CACHE_DIR, `${sourceId}.json`);
|
|
16831
16835
|
}
|
|
16832
16836
|
function readCache(sourceId) {
|
|
16833
16837
|
try {
|
|
16834
16838
|
const file = getCacheFile(sourceId);
|
|
16835
|
-
if (!
|
|
16839
|
+
if (!existsSync8(file))
|
|
16836
16840
|
return null;
|
|
16837
|
-
const data = JSON.parse(
|
|
16841
|
+
const data = JSON.parse(readFileSync4(file, "utf-8"));
|
|
16838
16842
|
return data;
|
|
16839
16843
|
} catch {
|
|
16840
16844
|
return null;
|
|
@@ -16848,7 +16852,7 @@ function writeCache(sourceId, results) {
|
|
|
16848
16852
|
}
|
|
16849
16853
|
function clearCache(sourceId) {
|
|
16850
16854
|
try {
|
|
16851
|
-
if (!
|
|
16855
|
+
if (!existsSync8(CACHE_DIR))
|
|
16852
16856
|
return;
|
|
16853
16857
|
const files = readdirSync3(CACHE_DIR);
|
|
16854
16858
|
for (const file of files) {
|
|
@@ -16856,7 +16860,7 @@ function clearCache(sourceId) {
|
|
|
16856
16860
|
continue;
|
|
16857
16861
|
if (!sourceId || file.startsWith(`${sourceId}.`)) {
|
|
16858
16862
|
try {
|
|
16859
|
-
unlinkSync(
|
|
16863
|
+
unlinkSync(join9(CACHE_DIR, file));
|
|
16860
16864
|
} catch {}
|
|
16861
16865
|
}
|
|
16862
16866
|
}
|
|
@@ -17102,7 +17106,7 @@ var CACHE_DIR, DEFAULT_TTL_MS;
|
|
|
17102
17106
|
var init_sources = __esm(() => {
|
|
17103
17107
|
init_db();
|
|
17104
17108
|
init_config2();
|
|
17105
|
-
CACHE_DIR =
|
|
17109
|
+
CACHE_DIR = join9(MCPS_DIR, "cache");
|
|
17106
17110
|
DEFAULT_TTL_MS = 10 * 60 * 1000;
|
|
17107
17111
|
});
|
|
17108
17112
|
|
|
@@ -30660,6 +30664,146 @@ function readPackageVersion(moduleUrl, fallback = FALLBACK_VERSION) {
|
|
|
30660
30664
|
|
|
30661
30665
|
// src/lib/registry.ts
|
|
30662
30666
|
init_db();
|
|
30667
|
+
|
|
30668
|
+
// src/lib/credentials.ts
|
|
30669
|
+
init_config2();
|
|
30670
|
+
import { existsSync as existsSync7, readFileSync as readFileSync3 } from "fs";
|
|
30671
|
+
import { join as join8 } from "path";
|
|
30672
|
+
|
|
30673
|
+
class CredentialReferenceError extends Error {
|
|
30674
|
+
constructor(message) {
|
|
30675
|
+
super(message);
|
|
30676
|
+
this.name = "CredentialReferenceError";
|
|
30677
|
+
}
|
|
30678
|
+
}
|
|
30679
|
+
var SECRET_KEY_PATTERN = /(?:^|[_-])(api[_-]?key|token|secret|password|passwd|credential|auth|private[_-]?key)(?:$|[_-])/i;
|
|
30680
|
+
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_-]+)$/;
|
|
30681
|
+
var REDACTED_CREDENTIAL_VALUE = "<redacted>";
|
|
30682
|
+
function normalizeKey(key) {
|
|
30683
|
+
return key.trim();
|
|
30684
|
+
}
|
|
30685
|
+
function isRecord(value) {
|
|
30686
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
30687
|
+
}
|
|
30688
|
+
function isSecretLikeEnvKey(key) {
|
|
30689
|
+
return SECRET_KEY_PATTERN.test(key);
|
|
30690
|
+
}
|
|
30691
|
+
function isSecretLikeValue(value) {
|
|
30692
|
+
return SECRET_VALUE_PATTERN.test(value.trim());
|
|
30693
|
+
}
|
|
30694
|
+
function normalizeCredentialRef(ref) {
|
|
30695
|
+
const source = ref.source;
|
|
30696
|
+
if (source !== "env" && source !== "local-vault" && source !== "hosted") {
|
|
30697
|
+
throw new CredentialReferenceError(`Unsupported credential reference source: ${String(source)}`);
|
|
30698
|
+
}
|
|
30699
|
+
const name = ref.name?.trim();
|
|
30700
|
+
if (!name) {
|
|
30701
|
+
throw new CredentialReferenceError("Credential reference name is required");
|
|
30702
|
+
}
|
|
30703
|
+
return {
|
|
30704
|
+
source,
|
|
30705
|
+
name,
|
|
30706
|
+
required: ref.required !== false,
|
|
30707
|
+
...ref.description ? { description: ref.description } : {}
|
|
30708
|
+
};
|
|
30709
|
+
}
|
|
30710
|
+
function normalizeCredentialRefs(refs) {
|
|
30711
|
+
const normalized = {};
|
|
30712
|
+
for (const [rawKey, ref] of Object.entries(refs ?? {})) {
|
|
30713
|
+
const key = normalizeKey(rawKey);
|
|
30714
|
+
if (!key)
|
|
30715
|
+
throw new CredentialReferenceError("Credential reference env key is required");
|
|
30716
|
+
normalized[key] = normalizeCredentialRef(ref);
|
|
30717
|
+
}
|
|
30718
|
+
return normalized;
|
|
30719
|
+
}
|
|
30720
|
+
function parseCredentialRefs(value) {
|
|
30721
|
+
if (!isRecord(value))
|
|
30722
|
+
return {};
|
|
30723
|
+
const refs = {};
|
|
30724
|
+
for (const [key, ref] of Object.entries(value)) {
|
|
30725
|
+
if (!isRecord(ref))
|
|
30726
|
+
continue;
|
|
30727
|
+
const source = ref.source;
|
|
30728
|
+
const name = ref.name;
|
|
30729
|
+
if ((source === "env" || source === "local-vault" || source === "hosted") && typeof name === "string" && name.trim()) {
|
|
30730
|
+
refs[key] = normalizeCredentialRef({
|
|
30731
|
+
source,
|
|
30732
|
+
name,
|
|
30733
|
+
required: typeof ref.required === "boolean" ? ref.required : true,
|
|
30734
|
+
description: typeof ref.description === "string" ? ref.description : undefined
|
|
30735
|
+
});
|
|
30736
|
+
}
|
|
30737
|
+
}
|
|
30738
|
+
return refs;
|
|
30739
|
+
}
|
|
30740
|
+
function normalizeLiteralEnv(env) {
|
|
30741
|
+
const normalized = {};
|
|
30742
|
+
for (const [rawKey, rawValue] of Object.entries(env ?? {})) {
|
|
30743
|
+
const key = normalizeKey(rawKey);
|
|
30744
|
+
if (!key)
|
|
30745
|
+
continue;
|
|
30746
|
+
const value = String(rawValue);
|
|
30747
|
+
if (isSecretLikeEnvKey(key) || isSecretLikeValue(value)) {
|
|
30748
|
+
throw new CredentialReferenceError(`Refusing to store raw secret-like env value for "${key}". Use a credential reference instead.`);
|
|
30749
|
+
}
|
|
30750
|
+
normalized[key] = value;
|
|
30751
|
+
}
|
|
30752
|
+
return normalized;
|
|
30753
|
+
}
|
|
30754
|
+
function readLocalVault() {
|
|
30755
|
+
const path = process.env.HASNA_MCPS_CREDENTIAL_VAULT_PATH ?? join8(MCPS_DIR, "credentials.local.json");
|
|
30756
|
+
if (!existsSync7(path))
|
|
30757
|
+
return {};
|
|
30758
|
+
const parsed = JSON.parse(readFileSync3(path, "utf-8"));
|
|
30759
|
+
if (!isRecord(parsed))
|
|
30760
|
+
return {};
|
|
30761
|
+
const values = {};
|
|
30762
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
30763
|
+
if (typeof value === "string")
|
|
30764
|
+
values[key] = value;
|
|
30765
|
+
}
|
|
30766
|
+
return values;
|
|
30767
|
+
}
|
|
30768
|
+
function resolveCredentialRef(envKey, ref) {
|
|
30769
|
+
if (ref.source === "env") {
|
|
30770
|
+
const value = process.env[ref.name];
|
|
30771
|
+
if (value === undefined && ref.required !== false) {
|
|
30772
|
+
throw new CredentialReferenceError(`Missing required environment credential "${ref.name}" for "${envKey}"`);
|
|
30773
|
+
}
|
|
30774
|
+
return value;
|
|
30775
|
+
}
|
|
30776
|
+
if (ref.source === "local-vault") {
|
|
30777
|
+
const value = readLocalVault()[ref.name];
|
|
30778
|
+
if (value === undefined && ref.required !== false) {
|
|
30779
|
+
throw new CredentialReferenceError(`Missing required local vault credential "${ref.name}" for "${envKey}"`);
|
|
30780
|
+
}
|
|
30781
|
+
return value;
|
|
30782
|
+
}
|
|
30783
|
+
if (ref.required !== false) {
|
|
30784
|
+
throw new CredentialReferenceError(`Hosted credential "${ref.name}" for "${envKey}" cannot be resolved by the local runtime`);
|
|
30785
|
+
}
|
|
30786
|
+
return;
|
|
30787
|
+
}
|
|
30788
|
+
function resolveServerEnv(server) {
|
|
30789
|
+
const resolved = { ...server.env };
|
|
30790
|
+
const refs = normalizeCredentialRefs(server.credentialRefs);
|
|
30791
|
+
for (const [envKey, ref] of Object.entries(refs)) {
|
|
30792
|
+
const value = resolveCredentialRef(envKey, ref);
|
|
30793
|
+
if (value !== undefined)
|
|
30794
|
+
resolved[envKey] = value;
|
|
30795
|
+
}
|
|
30796
|
+
return resolved;
|
|
30797
|
+
}
|
|
30798
|
+
function credentialRefPlaceholders(refs) {
|
|
30799
|
+
const placeholders = {};
|
|
30800
|
+
for (const key of Object.keys(refs ?? {})) {
|
|
30801
|
+
placeholders[key] = REDACTED_CREDENTIAL_VALUE;
|
|
30802
|
+
}
|
|
30803
|
+
return placeholders;
|
|
30804
|
+
}
|
|
30805
|
+
|
|
30806
|
+
// src/lib/registry.ts
|
|
30663
30807
|
function parseRow(row) {
|
|
30664
30808
|
return {
|
|
30665
30809
|
id: row.id,
|
|
@@ -30668,6 +30812,7 @@ function parseRow(row) {
|
|
|
30668
30812
|
command: row.command,
|
|
30669
30813
|
args: safeJsonParse(row.args, []),
|
|
30670
30814
|
env: safeJsonParse(row.env, {}),
|
|
30815
|
+
credentialRefs: parseCredentialRefs(safeJsonParse(row.credential_refs, {})),
|
|
30671
30816
|
transport: row.transport,
|
|
30672
30817
|
url: row.url || null,
|
|
30673
30818
|
source: row.source,
|
|
@@ -30735,9 +30880,9 @@ function addServer(opts) {
|
|
|
30735
30880
|
if (!id) {
|
|
30736
30881
|
throw new Error("Unable to generate a valid server ID");
|
|
30737
30882
|
}
|
|
30738
|
-
const row = db2.prepare(`INSERT INTO servers (id, name, description, command, args, env, transport, url, source)
|
|
30739
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
30740
|
-
RETURNING *`).get(id, name, opts.description || null, command, JSON.stringify(opts.args || []), JSON.stringify(opts.env
|
|
30883
|
+
const row = db2.prepare(`INSERT INTO servers (id, name, description, command, args, env, credential_refs, transport, url, source)
|
|
30884
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
30885
|
+
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");
|
|
30741
30886
|
return parseRow(row);
|
|
30742
30887
|
}
|
|
30743
30888
|
function removeServer(id) {
|
|
@@ -30776,7 +30921,11 @@ function updateServer(id, updates) {
|
|
|
30776
30921
|
}
|
|
30777
30922
|
if (updates.env !== undefined) {
|
|
30778
30923
|
sets.push("env = ?");
|
|
30779
|
-
values.push(JSON.stringify(updates.env));
|
|
30924
|
+
values.push(JSON.stringify(normalizeLiteralEnv(updates.env)));
|
|
30925
|
+
}
|
|
30926
|
+
if (updates.credentialRefs !== undefined) {
|
|
30927
|
+
sets.push("credential_refs = ?");
|
|
30928
|
+
values.push(JSON.stringify(normalizeCredentialRefs(updates.credentialRefs)));
|
|
30780
30929
|
}
|
|
30781
30930
|
if (updates.transport !== undefined) {
|
|
30782
30931
|
sets.push("transport = ?");
|
|
@@ -30840,6 +30989,185 @@ function getCachedTools(serverId) {
|
|
|
30840
30989
|
|
|
30841
30990
|
// src/lib/remote.ts
|
|
30842
30991
|
init_config2();
|
|
30992
|
+
|
|
30993
|
+
// src/lib/local-command-consent.ts
|
|
30994
|
+
class LocalCommandConsentError extends Error {
|
|
30995
|
+
review;
|
|
30996
|
+
constructor(message, review) {
|
|
30997
|
+
super(message);
|
|
30998
|
+
this.name = "LocalCommandConsentError";
|
|
30999
|
+
this.review = review;
|
|
31000
|
+
}
|
|
31001
|
+
}
|
|
31002
|
+
var SHELL_COMMANDS = new Set(["bash", "sh", "zsh", "fish", "cmd", "cmd.exe", "powershell", "powershell.exe", "pwsh"]);
|
|
31003
|
+
var DESTRUCTIVE_COMMANDS = new Set([
|
|
31004
|
+
"rm",
|
|
31005
|
+
"dd",
|
|
31006
|
+
"mkfs",
|
|
31007
|
+
"shutdown",
|
|
31008
|
+
"reboot",
|
|
31009
|
+
"poweroff",
|
|
31010
|
+
"halt",
|
|
31011
|
+
"killall",
|
|
31012
|
+
"pkill"
|
|
31013
|
+
]);
|
|
31014
|
+
var SHELL_EVAL_FLAGS = new Set(["-c", "/c", "-Command", "-command", "-EncodedCommand", "-encodedcommand"]);
|
|
31015
|
+
var SECRET_FLAG_PATTERN = /(?:^|[-_])(api[-_]?key|token|secret|password|passwd|credential|auth|private[-_]?key)(?:$|[-_])/i;
|
|
31016
|
+
var SECRET_KEY_PATTERN2 = /(?:^|[_-])(api[_-]?key|token|secret|password|passwd|credential|auth|private[_-]?key)(?:$|[_-])/i;
|
|
31017
|
+
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_-]+)$/;
|
|
31018
|
+
var SHELL_META_PATTERN = /[;&|`<>]|\$\(/;
|
|
31019
|
+
function commandBase(command) {
|
|
31020
|
+
return command.trim().split(/[\\/]/).pop()?.toLowerCase() || command.trim().toLowerCase();
|
|
31021
|
+
}
|
|
31022
|
+
function normalizeArgs(args) {
|
|
31023
|
+
return (args ?? []).map((arg) => String(arg));
|
|
31024
|
+
}
|
|
31025
|
+
function isSecretKey(key) {
|
|
31026
|
+
return SECRET_KEY_PATTERN2.test(key) || SECRET_FLAG_PATTERN.test(key);
|
|
31027
|
+
}
|
|
31028
|
+
function isSecretValue(value) {
|
|
31029
|
+
return SECRET_VALUE_PATTERN2.test(value.trim());
|
|
31030
|
+
}
|
|
31031
|
+
function isSecretAssignment(arg) {
|
|
31032
|
+
const eqIdx = arg.indexOf("=");
|
|
31033
|
+
if (eqIdx <= 0)
|
|
31034
|
+
return false;
|
|
31035
|
+
const key = arg.slice(0, eqIdx);
|
|
31036
|
+
const value = arg.slice(eqIdx + 1);
|
|
31037
|
+
return isSecretKey(key) || isSecretValue(value);
|
|
31038
|
+
}
|
|
31039
|
+
function isSecretArg(args, index) {
|
|
31040
|
+
const arg = args[index] ?? "";
|
|
31041
|
+
const previous = args[index - 1] ?? "";
|
|
31042
|
+
return isSecretAssignment(arg) || isSecretValue(arg) || SECRET_FLAG_PATTERN.test(previous);
|
|
31043
|
+
}
|
|
31044
|
+
function quoteArg(value) {
|
|
31045
|
+
return JSON.stringify(value);
|
|
31046
|
+
}
|
|
31047
|
+
function displayCommand(command, args) {
|
|
31048
|
+
return [quoteArg(command), ...args.map((arg, index) => quoteArg(isSecretArg(args, index) ? "<redacted>" : arg))].join(" ");
|
|
31049
|
+
}
|
|
31050
|
+
function pushRisk(risks, risk) {
|
|
31051
|
+
if (risks.some((existing) => existing.code === risk.code && existing.evidence === risk.evidence))
|
|
31052
|
+
return;
|
|
31053
|
+
risks.push(risk);
|
|
31054
|
+
}
|
|
31055
|
+
function inspectRisks(command, args, env) {
|
|
31056
|
+
const risks = [];
|
|
31057
|
+
const base = commandBase(command);
|
|
31058
|
+
const joined = [command, ...args].join(" ");
|
|
31059
|
+
if (SHELL_COMMANDS.has(base)) {
|
|
31060
|
+
pushRisk(risks, {
|
|
31061
|
+
code: "shell_interpreter",
|
|
31062
|
+
severity: "warning",
|
|
31063
|
+
message: "Command launches a shell interpreter.",
|
|
31064
|
+
evidence: base
|
|
31065
|
+
});
|
|
31066
|
+
if (args.some((arg) => SHELL_EVAL_FLAGS.has(arg))) {
|
|
31067
|
+
pushRisk(risks, {
|
|
31068
|
+
code: "shell_eval",
|
|
31069
|
+
severity: "danger",
|
|
31070
|
+
message: "Shell command evaluates an inline script.",
|
|
31071
|
+
evidence: base
|
|
31072
|
+
});
|
|
31073
|
+
}
|
|
31074
|
+
}
|
|
31075
|
+
if (base === "sudo") {
|
|
31076
|
+
pushRisk(risks, {
|
|
31077
|
+
code: "privilege_escalation",
|
|
31078
|
+
severity: "danger",
|
|
31079
|
+
message: "Command requests elevated privileges.",
|
|
31080
|
+
evidence: base
|
|
31081
|
+
});
|
|
31082
|
+
}
|
|
31083
|
+
if (DESTRUCTIVE_COMMANDS.has(base) || /\brm\s+-[^\s]*[rf][^\s]*\b/.test(joined) || /--no-preserve-root\b/.test(joined)) {
|
|
31084
|
+
pushRisk(risks, {
|
|
31085
|
+
code: "destructive_command",
|
|
31086
|
+
severity: "danger",
|
|
31087
|
+
message: "Command includes a destructive system operation.",
|
|
31088
|
+
evidence: base
|
|
31089
|
+
});
|
|
31090
|
+
}
|
|
31091
|
+
if (/\b(curl|wget)\b[\s\S]*\|[\s\S]*\b(sh|bash|zsh|fish)\b/.test(joined)) {
|
|
31092
|
+
pushRisk(risks, {
|
|
31093
|
+
code: "download_pipe_shell",
|
|
31094
|
+
severity: "danger",
|
|
31095
|
+
message: "Command downloads remote content and pipes it to a shell."
|
|
31096
|
+
});
|
|
31097
|
+
}
|
|
31098
|
+
if ([command, ...args].some((part) => SHELL_META_PATTERN.test(part))) {
|
|
31099
|
+
pushRisk(risks, {
|
|
31100
|
+
code: "shell_metacharacters",
|
|
31101
|
+
severity: "warning",
|
|
31102
|
+
message: "Command or arguments contain shell metacharacters."
|
|
31103
|
+
});
|
|
31104
|
+
}
|
|
31105
|
+
if (args.some((arg, index) => isSecretArg(args, index))) {
|
|
31106
|
+
pushRisk(risks, {
|
|
31107
|
+
code: "inline_secret",
|
|
31108
|
+
severity: "danger",
|
|
31109
|
+
message: "Command arguments appear to contain inline secret material."
|
|
31110
|
+
});
|
|
31111
|
+
}
|
|
31112
|
+
const secretEnvKeys = Object.keys(env).filter(isSecretKey).sort();
|
|
31113
|
+
if (secretEnvKeys.length > 0) {
|
|
31114
|
+
pushRisk(risks, {
|
|
31115
|
+
code: "secret_env",
|
|
31116
|
+
severity: "warning",
|
|
31117
|
+
message: "Environment contains secret-like keys; values are redacted from consent output.",
|
|
31118
|
+
evidence: secretEnvKeys.join(", ")
|
|
31119
|
+
});
|
|
31120
|
+
}
|
|
31121
|
+
return risks;
|
|
31122
|
+
}
|
|
31123
|
+
function inspectLocalCommand(input) {
|
|
31124
|
+
const args = normalizeArgs(input.args);
|
|
31125
|
+
const env = input.env ?? {};
|
|
31126
|
+
const transport = input.transport ?? "stdio";
|
|
31127
|
+
const risks = inspectRisks(input.command, args, env);
|
|
31128
|
+
return {
|
|
31129
|
+
requiresConsent: transport === "stdio",
|
|
31130
|
+
operation: input.operation ?? "launch",
|
|
31131
|
+
command: input.command,
|
|
31132
|
+
args,
|
|
31133
|
+
displayCommand: displayCommand(input.command, args),
|
|
31134
|
+
envKeys: Object.keys(env).sort(),
|
|
31135
|
+
risks,
|
|
31136
|
+
hasDangerousRisk: risks.some((risk) => risk.severity === "danger")
|
|
31137
|
+
};
|
|
31138
|
+
}
|
|
31139
|
+
function formatLocalCommandReview(review) {
|
|
31140
|
+
const lines = [
|
|
31141
|
+
`Command: ${review.displayCommand}`,
|
|
31142
|
+
review.envKeys.length > 0 ? `Env keys: ${review.envKeys.join(", ")}` : "Env keys: <none>"
|
|
31143
|
+
];
|
|
31144
|
+
if (review.risks.length > 0) {
|
|
31145
|
+
lines.push("Risks:");
|
|
31146
|
+
for (const risk of review.risks) {
|
|
31147
|
+
lines.push(`- ${risk.severity}: ${risk.code} - ${risk.message}${risk.evidence ? ` (${risk.evidence})` : ""}`);
|
|
31148
|
+
}
|
|
31149
|
+
} else {
|
|
31150
|
+
lines.push("Risks: none detected");
|
|
31151
|
+
}
|
|
31152
|
+
return lines.join(`
|
|
31153
|
+
`);
|
|
31154
|
+
}
|
|
31155
|
+
function assertLocalCommandConsent(input, consent = {}) {
|
|
31156
|
+
const review = inspectLocalCommand(input);
|
|
31157
|
+
if (!review.requiresConsent)
|
|
31158
|
+
return review;
|
|
31159
|
+
if (consent.approved !== true) {
|
|
31160
|
+
throw new LocalCommandConsentError(`local stdio command approval is required before ${review.operation}.
|
|
31161
|
+
${formatLocalCommandReview(review)}`, review);
|
|
31162
|
+
}
|
|
31163
|
+
if (review.hasDangerousRisk && consent.allowRisky !== true) {
|
|
31164
|
+
throw new LocalCommandConsentError(`risky command approval is required before ${review.operation}.
|
|
31165
|
+
${formatLocalCommandReview(review)}`, review);
|
|
31166
|
+
}
|
|
31167
|
+
return review;
|
|
31168
|
+
}
|
|
31169
|
+
|
|
31170
|
+
// src/lib/remote.ts
|
|
30843
31171
|
function parseRegistryEntry(entry) {
|
|
30844
31172
|
const s = entry.server;
|
|
30845
31173
|
return {
|
|
@@ -30870,7 +31198,7 @@ async function getRegistryServer(id) {
|
|
|
30870
31198
|
const all = entries.map(parseRegistryEntry);
|
|
30871
31199
|
return all.find((s) => s.id === id) || null;
|
|
30872
31200
|
}
|
|
30873
|
-
async function installFromRegistry(id) {
|
|
31201
|
+
async function installFromRegistry(id, options = {}) {
|
|
30874
31202
|
const server = await getRegistryServer(id);
|
|
30875
31203
|
if (!server) {
|
|
30876
31204
|
throw new Error(`Server "${id}" not found in registry`);
|
|
@@ -30890,6 +31218,13 @@ async function installFromRegistry(id) {
|
|
|
30890
31218
|
transport = pkg.transport.type;
|
|
30891
31219
|
}
|
|
30892
31220
|
}
|
|
31221
|
+
assertLocalCommandConsent({
|
|
31222
|
+
command,
|
|
31223
|
+
args,
|
|
31224
|
+
transport,
|
|
31225
|
+
env: {},
|
|
31226
|
+
operation: "register"
|
|
31227
|
+
}, options.localCommandConsent);
|
|
30893
31228
|
return addServer({
|
|
30894
31229
|
name: server.name,
|
|
30895
31230
|
description: server.description,
|
|
@@ -30915,8 +31250,8 @@ init_sources();
|
|
|
30915
31250
|
|
|
30916
31251
|
// src/lib/install.ts
|
|
30917
31252
|
import { execFileSync } from "child_process";
|
|
30918
|
-
import { existsSync as
|
|
30919
|
-
import { join as
|
|
31253
|
+
import { existsSync as existsSync9, readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync7 } from "fs";
|
|
31254
|
+
import { join as join10 } from "path";
|
|
30920
31255
|
import { homedir as homedir8 } from "os";
|
|
30921
31256
|
function installToClaude(entry) {
|
|
30922
31257
|
try {
|
|
@@ -30928,7 +31263,7 @@ function installToClaude(entry) {
|
|
|
30928
31263
|
"--scope",
|
|
30929
31264
|
"user"
|
|
30930
31265
|
];
|
|
30931
|
-
for (const [k, v] of Object.entries(entry
|
|
31266
|
+
for (const [k, v] of Object.entries(assertAgentInstallEnv(entry))) {
|
|
30932
31267
|
args.push("--env", `${k}=${v}`);
|
|
30933
31268
|
}
|
|
30934
31269
|
args.push(entry.id, "--", entry.command, ...entry.args);
|
|
@@ -30940,9 +31275,9 @@ function installToClaude(entry) {
|
|
|
30940
31275
|
}
|
|
30941
31276
|
function installToCodex(entry) {
|
|
30942
31277
|
try {
|
|
30943
|
-
const configDir =
|
|
30944
|
-
const configPath =
|
|
30945
|
-
if (!
|
|
31278
|
+
const configDir = join10(homedir8(), ".codex");
|
|
31279
|
+
const configPath = join10(configDir, "config.toml");
|
|
31280
|
+
if (!existsSync9(configDir)) {
|
|
30946
31281
|
mkdirSync7(configDir, { recursive: true });
|
|
30947
31282
|
}
|
|
30948
31283
|
const block = `
|
|
@@ -30950,7 +31285,7 @@ function installToCodex(entry) {
|
|
|
30950
31285
|
` + `command = ${JSON.stringify(entry.command)}
|
|
30951
31286
|
` + `args = [${entry.args.map((a) => JSON.stringify(a)).join(", ")}]
|
|
30952
31287
|
`;
|
|
30953
|
-
const existing =
|
|
31288
|
+
const existing = existsSync9(configPath) ? readFileSync5(configPath, "utf-8") : "";
|
|
30954
31289
|
if (existing.includes(`[mcp_servers.${entry.id}]`)) {
|
|
30955
31290
|
return { agent: "codex", success: true };
|
|
30956
31291
|
}
|
|
@@ -30962,21 +31297,22 @@ function installToCodex(entry) {
|
|
|
30962
31297
|
}
|
|
30963
31298
|
function installToGemini(entry) {
|
|
30964
31299
|
try {
|
|
30965
|
-
const configDir =
|
|
30966
|
-
const configPath =
|
|
30967
|
-
if (!
|
|
31300
|
+
const configDir = join10(homedir8(), ".gemini");
|
|
31301
|
+
const configPath = join10(configDir, "settings.json");
|
|
31302
|
+
if (!existsSync9(configDir)) {
|
|
30968
31303
|
mkdirSync7(configDir, { recursive: true });
|
|
30969
31304
|
}
|
|
30970
31305
|
let settings = {};
|
|
30971
|
-
if (
|
|
30972
|
-
settings = JSON.parse(
|
|
31306
|
+
if (existsSync9(configPath)) {
|
|
31307
|
+
settings = JSON.parse(readFileSync5(configPath, "utf-8"));
|
|
30973
31308
|
}
|
|
30974
31309
|
if (!settings.mcpServers)
|
|
30975
31310
|
settings.mcpServers = {};
|
|
31311
|
+
const env = assertAgentInstallEnv(entry);
|
|
30976
31312
|
settings.mcpServers[entry.id] = {
|
|
30977
31313
|
command: entry.command,
|
|
30978
31314
|
args: entry.args,
|
|
30979
|
-
...Object.keys(
|
|
31315
|
+
...Object.keys(env).length > 0 ? { env } : {}
|
|
30980
31316
|
};
|
|
30981
31317
|
writeFileSync3(configPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
30982
31318
|
return { agent: "gemini", success: true };
|
|
@@ -30984,7 +31320,43 @@ function installToGemini(entry) {
|
|
|
30984
31320
|
return { agent: "gemini", success: false, error: err.message };
|
|
30985
31321
|
}
|
|
30986
31322
|
}
|
|
30987
|
-
function
|
|
31323
|
+
function assertAgentInstallEnv(entry) {
|
|
31324
|
+
const refs = entry.credentialRefs ?? {};
|
|
31325
|
+
if (Object.keys(refs).length > 0) {
|
|
31326
|
+
throw new CredentialReferenceError(`Server "${entry.id}" uses credential references; refusing to materialize secrets into local agent config files`);
|
|
31327
|
+
}
|
|
31328
|
+
for (const [key, value] of Object.entries(entry.env)) {
|
|
31329
|
+
if (isSecretLikeEnvKey(key) || isSecretLikeValue(value)) {
|
|
31330
|
+
throw new CredentialReferenceError(`Server "${entry.id}" has legacy raw secret-like env "${key}"; move it to a credential reference before installing to agents`);
|
|
31331
|
+
}
|
|
31332
|
+
}
|
|
31333
|
+
return entry.env;
|
|
31334
|
+
}
|
|
31335
|
+
function installToAgents(entry, targets = ["claude", "codex", "gemini"], options = {}) {
|
|
31336
|
+
try {
|
|
31337
|
+
assertLocalCommandConsent({
|
|
31338
|
+
command: entry.command,
|
|
31339
|
+
args: entry.args,
|
|
31340
|
+
env: { ...entry.env, ...credentialRefPlaceholders(entry.credentialRefs) },
|
|
31341
|
+
transport: entry.transport,
|
|
31342
|
+
operation: "install"
|
|
31343
|
+
}, options.localCommandConsent);
|
|
31344
|
+
} catch (err) {
|
|
31345
|
+
return targets.map((target) => ({
|
|
31346
|
+
agent: target,
|
|
31347
|
+
success: false,
|
|
31348
|
+
error: err.message
|
|
31349
|
+
}));
|
|
31350
|
+
}
|
|
31351
|
+
try {
|
|
31352
|
+
assertAgentInstallEnv(entry);
|
|
31353
|
+
} catch (err) {
|
|
31354
|
+
return targets.map((target) => ({
|
|
31355
|
+
agent: target,
|
|
31356
|
+
success: false,
|
|
31357
|
+
error: err.message
|
|
31358
|
+
}));
|
|
31359
|
+
}
|
|
30988
31360
|
return targets.map((target) => {
|
|
30989
31361
|
if (target === "claude")
|
|
30990
31362
|
return installToClaude(entry);
|
|
@@ -33401,7 +33773,7 @@ function requireUrl(entry) {
|
|
|
33401
33773
|
throw new Error(`Server "${entry.id}" has an invalid URL: ${entry.url}`);
|
|
33402
33774
|
}
|
|
33403
33775
|
}
|
|
33404
|
-
async function connectToServer(entry) {
|
|
33776
|
+
async function connectToServer(entry, options = {}) {
|
|
33405
33777
|
if (connections.has(entry.id)) {
|
|
33406
33778
|
return connections.get(entry.id);
|
|
33407
33779
|
}
|
|
@@ -33417,10 +33789,17 @@ async function connectToServer(entry) {
|
|
|
33417
33789
|
if (!entry.command?.trim()) {
|
|
33418
33790
|
throw new Error(`Server "${entry.id}" is missing a command`);
|
|
33419
33791
|
}
|
|
33792
|
+
assertLocalCommandConsent({
|
|
33793
|
+
command: entry.command,
|
|
33794
|
+
args: entry.args,
|
|
33795
|
+
env: { ...entry.env, ...credentialRefPlaceholders(entry.credentialRefs) },
|
|
33796
|
+
transport: entry.transport,
|
|
33797
|
+
operation: "launch"
|
|
33798
|
+
}, options.localCommandConsent);
|
|
33420
33799
|
transport = new StdioClientTransport({
|
|
33421
33800
|
command: entry.command,
|
|
33422
33801
|
args: entry.args,
|
|
33423
|
-
env: buildEnv(entry
|
|
33802
|
+
env: buildEnv(resolveServerEnv(entry))
|
|
33424
33803
|
});
|
|
33425
33804
|
} else if (entry.transport === "sse") {
|
|
33426
33805
|
transport = new SSEClientTransport(requireUrl(entry));
|
|
@@ -33533,7 +33912,7 @@ async function callTool(prefixedName, args) {
|
|
|
33533
33912
|
]
|
|
33534
33913
|
};
|
|
33535
33914
|
}
|
|
33536
|
-
async function connectAllEnabled() {
|
|
33915
|
+
async function connectAllEnabled(options = {}) {
|
|
33537
33916
|
const servers = listServers().filter((s) => s.enabled);
|
|
33538
33917
|
const results = [];
|
|
33539
33918
|
let index = 0;
|
|
@@ -33545,7 +33924,7 @@ async function connectAllEnabled() {
|
|
|
33545
33924
|
return;
|
|
33546
33925
|
const server = servers[current];
|
|
33547
33926
|
try {
|
|
33548
|
-
const conn = await connectToServer(server);
|
|
33927
|
+
const conn = await connectToServer(server, options);
|
|
33549
33928
|
results.push(conn);
|
|
33550
33929
|
} catch (err) {
|
|
33551
33930
|
console.error(`Failed to connect to ${server.name}: ${err.message}`);
|
|
@@ -33558,13 +33937,28 @@ async function connectAllEnabled() {
|
|
|
33558
33937
|
|
|
33559
33938
|
// src/lib/doctor.ts
|
|
33560
33939
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
33561
|
-
async function diagnoseServer(server) {
|
|
33940
|
+
async function diagnoseServer(server, options = {}) {
|
|
33562
33941
|
const checks4 = [];
|
|
33942
|
+
let hasLocalConsent = true;
|
|
33563
33943
|
if (server.transport === "stdio") {
|
|
33944
|
+
try {
|
|
33945
|
+
assertLocalCommandConsent({
|
|
33946
|
+
command: server.command,
|
|
33947
|
+
args: server.args,
|
|
33948
|
+
env: { ...server.env, ...credentialRefPlaceholders(server.credentialRefs) },
|
|
33949
|
+
transport: server.transport,
|
|
33950
|
+
operation: "diagnose"
|
|
33951
|
+
}, options.localCommandConsent);
|
|
33952
|
+
} catch (err) {
|
|
33953
|
+
hasLocalConsent = false;
|
|
33954
|
+
checks4.push({ name: "local command consent", pass: false, message: err.message });
|
|
33955
|
+
}
|
|
33564
33956
|
try {
|
|
33565
33957
|
const path = execFileSync2("which", [server.command], { stdio: "pipe" }).toString().trim();
|
|
33566
33958
|
let version2 = "";
|
|
33567
33959
|
try {
|
|
33960
|
+
if (!hasLocalConsent)
|
|
33961
|
+
throw new Error("local stdio command approval is required before version probing");
|
|
33568
33962
|
version2 = execFileSync2(server.command, ["--version"], { stdio: "pipe" }).toString().trim().split(`
|
|
33569
33963
|
`)[0];
|
|
33570
33964
|
} catch {}
|
|
@@ -33580,12 +33974,29 @@ async function diagnoseServer(server) {
|
|
|
33580
33974
|
}
|
|
33581
33975
|
}
|
|
33582
33976
|
const missingEnv = Object.entries(server.env).filter(([, v]) => !v);
|
|
33583
|
-
|
|
33584
|
-
|
|
33585
|
-
|
|
33586
|
-
|
|
33977
|
+
const credentialRefCount = Object.keys(server.credentialRefs ?? {}).length;
|
|
33978
|
+
let credentialError = null;
|
|
33979
|
+
try {
|
|
33980
|
+
resolveServerEnv(server);
|
|
33981
|
+
} catch (err) {
|
|
33982
|
+
credentialError = err.message;
|
|
33983
|
+
}
|
|
33984
|
+
if (Object.keys(server.env).length === 0 && credentialRefCount === 0) {
|
|
33985
|
+
checks4.push({ name: "env vars", pass: true, message: "no env vars or credential refs required" });
|
|
33986
|
+
} else if (missingEnv.length > 0 || credentialError) {
|
|
33987
|
+
const parts = [];
|
|
33988
|
+
if (missingEnv.length > 0)
|
|
33989
|
+
parts.push(`missing literal values for: ${missingEnv.map(([k]) => k).join(", ")}`);
|
|
33990
|
+
if (credentialError)
|
|
33991
|
+
parts.push(credentialError);
|
|
33992
|
+
checks4.push({ name: "env vars", pass: false, message: parts.join("; ") });
|
|
33587
33993
|
} else {
|
|
33588
|
-
|
|
33994
|
+
const literalCount = Object.keys(server.env).length;
|
|
33995
|
+
checks4.push({
|
|
33996
|
+
name: "env vars",
|
|
33997
|
+
pass: true,
|
|
33998
|
+
message: `${literalCount} literal env var(s), ${credentialRefCount} credential ref(s) available`
|
|
33999
|
+
});
|
|
33589
34000
|
}
|
|
33590
34001
|
if (server.transport !== "stdio" && server.url) {
|
|
33591
34002
|
try {
|
|
@@ -33595,10 +34006,10 @@ async function diagnoseServer(server) {
|
|
|
33595
34006
|
checks4.push({ name: "URL reachable", pass: false, message: `unreachable: ${err.message}` });
|
|
33596
34007
|
}
|
|
33597
34008
|
}
|
|
33598
|
-
if (server.enabled) {
|
|
34009
|
+
if (server.enabled && hasLocalConsent) {
|
|
33599
34010
|
try {
|
|
33600
34011
|
await Promise.race([
|
|
33601
|
-
connectToServer(server),
|
|
34012
|
+
connectToServer(server, { localCommandConsent: options.localCommandConsent }),
|
|
33602
34013
|
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout after 10s")), 1e4))
|
|
33603
34014
|
]);
|
|
33604
34015
|
await disconnectServer(server.id);
|
|
@@ -33606,8 +34017,10 @@ async function diagnoseServer(server) {
|
|
|
33606
34017
|
} catch (err) {
|
|
33607
34018
|
checks4.push({ name: "connect & list tools", pass: false, message: err.message });
|
|
33608
34019
|
}
|
|
33609
|
-
} else {
|
|
34020
|
+
} else if (!server.enabled) {
|
|
33610
34021
|
checks4.push({ name: "connect & list tools", pass: true, message: "skipped (server disabled)" });
|
|
34022
|
+
} else {
|
|
34023
|
+
checks4.push({ name: "connect & list tools", pass: false, message: "skipped until local stdio command is approved" });
|
|
33611
34024
|
}
|
|
33612
34025
|
return {
|
|
33613
34026
|
server,
|
|
@@ -33820,11 +34233,11 @@ function seedDefaultMachines() {
|
|
|
33820
34233
|
// src/lib/fleet.ts
|
|
33821
34234
|
init_config2();
|
|
33822
34235
|
import { spawn as spawn2 } from "child_process";
|
|
33823
|
-
import { existsSync as
|
|
33824
|
-
import { join as
|
|
34236
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
34237
|
+
import { join as join11 } from "path";
|
|
33825
34238
|
var NPM_SEARCH_URL = "https://registry.npmjs.org/-/v1/search";
|
|
33826
34239
|
var NPM_REGISTRY_URL = "https://registry.npmjs.org";
|
|
33827
|
-
var CATALOG_CACHE_PATH =
|
|
34240
|
+
var CATALOG_CACHE_PATH = join11(MCPS_DIR, "cache", "hasna-catalog.json");
|
|
33828
34241
|
var DEFAULT_CATALOG_CACHE_TTL_MS = 60 * 60 * 1000;
|
|
33829
34242
|
var DEFAULT_REMOTE_TIMEOUT_MS = 180000;
|
|
33830
34243
|
var DEFAULT_HANDSHAKE_TIMEOUT_MS = 2500;
|
|
@@ -33834,9 +34247,9 @@ function normalizeQueryList(values) {
|
|
|
33834
34247
|
}
|
|
33835
34248
|
function readCatalogCache(maxAgeMs) {
|
|
33836
34249
|
try {
|
|
33837
|
-
if (!
|
|
34250
|
+
if (!existsSync10(CATALOG_CACHE_PATH))
|
|
33838
34251
|
return null;
|
|
33839
|
-
const parsed = JSON.parse(
|
|
34252
|
+
const parsed = JSON.parse(readFileSync6(CATALOG_CACHE_PATH, "utf-8"));
|
|
33840
34253
|
if (!parsed.cachedAt || !Array.isArray(parsed.entries))
|
|
33841
34254
|
return null;
|
|
33842
34255
|
if (Date.now() - parsed.cachedAt > maxAgeMs)
|
|
@@ -33848,7 +34261,7 @@ function readCatalogCache(maxAgeMs) {
|
|
|
33848
34261
|
}
|
|
33849
34262
|
function writeCatalogCache(entries) {
|
|
33850
34263
|
try {
|
|
33851
|
-
mkdirSync8(
|
|
34264
|
+
mkdirSync8(join11(MCPS_DIR, "cache"), { recursive: true });
|
|
33852
34265
|
writeFileSync4(CATALOG_CACHE_PATH, JSON.stringify({ cachedAt: Date.now(), entries }, null, 2), "utf-8");
|
|
33853
34266
|
} catch {}
|
|
33854
34267
|
}
|
|
@@ -34606,11 +35019,19 @@ function installProviderProfile(id, options = {}) {
|
|
|
34606
35019
|
if (!command) {
|
|
34607
35020
|
throw new Error(`Provider profile "${id}" does not define an install fallback command`);
|
|
34608
35021
|
}
|
|
35022
|
+
assertLocalCommandConsent({
|
|
35023
|
+
command,
|
|
35024
|
+
args,
|
|
35025
|
+
env: fallback?.env ?? {},
|
|
35026
|
+
transport: useFallback ? "stdio" : profile.transport,
|
|
35027
|
+
operation: "register"
|
|
35028
|
+
}, options.localCommandConsent);
|
|
34609
35029
|
return addServer({
|
|
34610
35030
|
name: options.name ?? profile.displayName,
|
|
34611
35031
|
description: profile.description ?? undefined,
|
|
34612
35032
|
command,
|
|
34613
35033
|
args,
|
|
35034
|
+
env: fallback?.env,
|
|
34614
35035
|
transport: useFallback ? "stdio" : profile.transport,
|
|
34615
35036
|
url: useFallback ? fallback?.url : profile.endpoint ?? undefined,
|
|
34616
35037
|
source: "provider-profile"
|
|
@@ -34632,6 +35053,16 @@ function jsonContent(value) {
|
|
|
34632
35053
|
function errorContent(text) {
|
|
34633
35054
|
return { ...textContent(text), isError: true };
|
|
34634
35055
|
}
|
|
35056
|
+
function localConsent(input) {
|
|
35057
|
+
return {
|
|
35058
|
+
approved: input.allow_local_stdio === true,
|
|
35059
|
+
allowRisky: input.allow_risky_command === true,
|
|
35060
|
+
source: "mcp"
|
|
35061
|
+
};
|
|
35062
|
+
}
|
|
35063
|
+
function readCredentialRefs(input) {
|
|
35064
|
+
return normalizeCredentialRefs(input.credential_refs ?? input.credentialRefs);
|
|
35065
|
+
}
|
|
34635
35066
|
function buildMcpTools() {
|
|
34636
35067
|
const definitions = [
|
|
34637
35068
|
{
|
|
@@ -34656,23 +35087,60 @@ function buildMcpTools() {
|
|
|
34656
35087
|
description: exports_external2.string().optional().describe("Description"),
|
|
34657
35088
|
transport: exports_external2.enum(["stdio", "sse", "streamable-http"]).optional().describe("Transport type"),
|
|
34658
35089
|
url: exports_external2.string().optional().describe("URL for remote transports"),
|
|
34659
|
-
env: exports_external2.record(exports_external2.string()).optional().describe("Environment variables")
|
|
35090
|
+
env: exports_external2.record(exports_external2.string()).optional().describe("Environment variables"),
|
|
35091
|
+
credential_refs: exports_external2.record(exports_external2.object({
|
|
35092
|
+
source: exports_external2.enum(["env", "local-vault", "hosted"]),
|
|
35093
|
+
name: exports_external2.string(),
|
|
35094
|
+
required: exports_external2.boolean().optional(),
|
|
35095
|
+
description: exports_external2.string().optional()
|
|
35096
|
+
})).optional().describe("Credential references by server env key"),
|
|
35097
|
+
allow_local_stdio: exports_external2.boolean().optional().describe("Approve registering this local stdio command"),
|
|
35098
|
+
allow_risky_command: exports_external2.boolean().optional().describe("Approve registering risky local command patterns")
|
|
34660
35099
|
},
|
|
34661
|
-
run: (
|
|
34662
|
-
command
|
|
34663
|
-
args
|
|
34664
|
-
|
|
34665
|
-
|
|
34666
|
-
transport
|
|
34667
|
-
|
|
34668
|
-
|
|
34669
|
-
|
|
35100
|
+
run: (input) => {
|
|
35101
|
+
const command = String(input.command);
|
|
35102
|
+
const args = Array.isArray(input.args) ? input.args.map(String) : [];
|
|
35103
|
+
const env = isRecordOfStrings(input.env) ? input.env : {};
|
|
35104
|
+
const credentialRefs = readCredentialRefs(input);
|
|
35105
|
+
const transport = input.transport;
|
|
35106
|
+
try {
|
|
35107
|
+
assertLocalCommandConsent({
|
|
35108
|
+
command,
|
|
35109
|
+
args,
|
|
35110
|
+
env: { ...env, ...Object.fromEntries(Object.keys(credentialRefs).map((key) => [key, "<credential-ref>"])) },
|
|
35111
|
+
transport,
|
|
35112
|
+
operation: "register"
|
|
35113
|
+
}, localConsent(input));
|
|
35114
|
+
return jsonContent(addServer({
|
|
35115
|
+
command,
|
|
35116
|
+
args,
|
|
35117
|
+
name: typeof input.name === "string" ? input.name : undefined,
|
|
35118
|
+
description: typeof input.description === "string" ? input.description : undefined,
|
|
35119
|
+
transport,
|
|
35120
|
+
url: typeof input.url === "string" ? input.url : undefined,
|
|
35121
|
+
env,
|
|
35122
|
+
credentialRefs
|
|
35123
|
+
}));
|
|
35124
|
+
} catch (err) {
|
|
35125
|
+
return errorContent(err.message);
|
|
35126
|
+
}
|
|
35127
|
+
}
|
|
34670
35128
|
},
|
|
34671
35129
|
{
|
|
34672
35130
|
name: "install_from_registry",
|
|
34673
35131
|
description: "Install an MCP server from the official registry",
|
|
34674
|
-
paramsSchema: {
|
|
34675
|
-
|
|
35132
|
+
paramsSchema: {
|
|
35133
|
+
id: exports_external2.string().describe("Registry server ID"),
|
|
35134
|
+
allow_local_stdio: exports_external2.boolean().optional().describe("Approve registering registry stdio commands"),
|
|
35135
|
+
allow_risky_command: exports_external2.boolean().optional().describe("Approve registering risky local command patterns")
|
|
35136
|
+
},
|
|
35137
|
+
run: async (input) => {
|
|
35138
|
+
try {
|
|
35139
|
+
return jsonContent(await installFromRegistry(String(input.id), { localCommandConsent: localConsent(input) }));
|
|
35140
|
+
} catch (err) {
|
|
35141
|
+
return errorContent(err.message);
|
|
35142
|
+
}
|
|
35143
|
+
}
|
|
34676
35144
|
},
|
|
34677
35145
|
{
|
|
34678
35146
|
name: "remove_server",
|
|
@@ -34718,26 +35186,52 @@ function buildMcpTools() {
|
|
|
34718
35186
|
command: exports_external2.string().optional().describe("New command"),
|
|
34719
35187
|
args: exports_external2.array(exports_external2.string()).optional().describe("New args list"),
|
|
34720
35188
|
transport: exports_external2.enum(["stdio", "sse", "streamable-http"]).optional().describe("New transport type"),
|
|
34721
|
-
url: exports_external2.string().optional().describe("New URL for remote transports")
|
|
35189
|
+
url: exports_external2.string().optional().describe("New URL for remote transports"),
|
|
35190
|
+
credential_refs: exports_external2.record(exports_external2.object({
|
|
35191
|
+
source: exports_external2.enum(["env", "local-vault", "hosted"]),
|
|
35192
|
+
name: exports_external2.string(),
|
|
35193
|
+
required: exports_external2.boolean().optional(),
|
|
35194
|
+
description: exports_external2.string().optional()
|
|
35195
|
+
})).optional().describe("Credential references by server env key"),
|
|
35196
|
+
allow_local_stdio: exports_external2.boolean().optional().describe("Approve updating this local stdio command"),
|
|
35197
|
+
allow_risky_command: exports_external2.boolean().optional().describe("Approve risky local command patterns")
|
|
34722
35198
|
},
|
|
34723
|
-
run: (
|
|
34724
|
-
const serverId = String(id);
|
|
35199
|
+
run: (input) => {
|
|
35200
|
+
const serverId = String(input.id);
|
|
34725
35201
|
const existing = getServer(serverId);
|
|
34726
35202
|
if (!existing)
|
|
34727
35203
|
return errorContent(`Server "${serverId}" not found.`);
|
|
34728
35204
|
const fields = {};
|
|
34729
|
-
if (typeof name === "string")
|
|
34730
|
-
fields.name = name;
|
|
34731
|
-
if (typeof description === "string")
|
|
34732
|
-
fields.description = description;
|
|
34733
|
-
if (typeof command === "string")
|
|
34734
|
-
fields.command = command;
|
|
34735
|
-
if (Array.isArray(args))
|
|
34736
|
-
fields.args = args.map(String);
|
|
34737
|
-
if (
|
|
34738
|
-
fields.
|
|
34739
|
-
if (
|
|
34740
|
-
fields.
|
|
35205
|
+
if (typeof input.name === "string")
|
|
35206
|
+
fields.name = input.name;
|
|
35207
|
+
if (typeof input.description === "string")
|
|
35208
|
+
fields.description = input.description;
|
|
35209
|
+
if (typeof input.command === "string")
|
|
35210
|
+
fields.command = input.command;
|
|
35211
|
+
if (Array.isArray(input.args))
|
|
35212
|
+
fields.args = input.args.map(String);
|
|
35213
|
+
if (input.credential_refs !== undefined || input.credentialRefs !== undefined)
|
|
35214
|
+
fields.credentialRefs = readCredentialRefs(input);
|
|
35215
|
+
if (input.transport === "stdio" || input.transport === "sse" || input.transport === "streamable-http")
|
|
35216
|
+
fields.transport = input.transport;
|
|
35217
|
+
if (typeof input.url === "string")
|
|
35218
|
+
fields.url = input.url;
|
|
35219
|
+
if (fields.command !== undefined || fields.args !== undefined || fields.transport !== undefined) {
|
|
35220
|
+
try {
|
|
35221
|
+
assertLocalCommandConsent({
|
|
35222
|
+
command: fields.command ?? existing.command,
|
|
35223
|
+
args: fields.args ?? existing.args,
|
|
35224
|
+
env: {
|
|
35225
|
+
...existing.env,
|
|
35226
|
+
...Object.fromEntries(Object.keys(fields.credentialRefs ?? existing.credentialRefs ?? {}).map((key) => [key, "<credential-ref>"]))
|
|
35227
|
+
},
|
|
35228
|
+
transport: fields.transport ?? existing.transport,
|
|
35229
|
+
operation: "register"
|
|
35230
|
+
}, localConsent(input));
|
|
35231
|
+
} catch (err) {
|
|
35232
|
+
return errorContent(err.message);
|
|
35233
|
+
}
|
|
35234
|
+
}
|
|
34741
35235
|
return jsonContent(redactServerEnv(updateServer(serverId, fields)));
|
|
34742
35236
|
}
|
|
34743
35237
|
},
|
|
@@ -34819,13 +35313,16 @@ function buildMcpTools() {
|
|
|
34819
35313
|
paramsSchema: {
|
|
34820
35314
|
id: exports_external2.string().describe("Provider profile ID"),
|
|
34821
35315
|
name: exports_external2.string().optional().describe("Override registered server name"),
|
|
34822
|
-
use_fallback: exports_external2.boolean().optional().describe("Install the stdio fallback command instead of the direct remote transport")
|
|
35316
|
+
use_fallback: exports_external2.boolean().optional().describe("Install the stdio fallback command instead of the direct remote transport"),
|
|
35317
|
+
allow_local_stdio: exports_external2.boolean().optional().describe("Approve registering provider stdio fallback commands"),
|
|
35318
|
+
allow_risky_command: exports_external2.boolean().optional().describe("Approve risky local command patterns")
|
|
34823
35319
|
},
|
|
34824
|
-
run: (
|
|
35320
|
+
run: (input) => {
|
|
34825
35321
|
try {
|
|
34826
|
-
return jsonContent(redactServerEnv(installProviderProfile(String(id), {
|
|
34827
|
-
name: typeof name === "string" ? name : undefined,
|
|
34828
|
-
useFallback: use_fallback === true
|
|
35322
|
+
return jsonContent(redactServerEnv(installProviderProfile(String(input.id), {
|
|
35323
|
+
name: typeof input.name === "string" ? input.name : undefined,
|
|
35324
|
+
useFallback: input.use_fallback === true,
|
|
35325
|
+
localCommandConsent: localConsent(input)
|
|
34829
35326
|
})));
|
|
34830
35327
|
} catch (err) {
|
|
34831
35328
|
return errorContent(err.message);
|
|
@@ -34898,15 +35395,19 @@ function buildMcpTools() {
|
|
|
34898
35395
|
description: "Install a registered MCP server into Claude Code, Codex, and/or Gemini",
|
|
34899
35396
|
paramsSchema: {
|
|
34900
35397
|
id: exports_external2.string().describe("Server ID to install (from list_servers)"),
|
|
34901
|
-
targets: exports_external2.array(exports_external2.enum(["claude", "codex", "gemini"])).optional().describe("Target agents to install into (default: all)")
|
|
35398
|
+
targets: exports_external2.array(exports_external2.enum(["claude", "codex", "gemini"])).optional().describe("Target agents to install into (default: all)"),
|
|
35399
|
+
allow_local_stdio: exports_external2.boolean().optional().describe("Approve installing local stdio commands into local agent configs"),
|
|
35400
|
+
allow_risky_command: exports_external2.boolean().optional().describe("Approve installing risky local command patterns")
|
|
34902
35401
|
},
|
|
34903
|
-
run: (
|
|
34904
|
-
const serverId = String(id);
|
|
35402
|
+
run: (input) => {
|
|
35403
|
+
const serverId = String(input.id);
|
|
34905
35404
|
const entry = getServer(serverId);
|
|
34906
35405
|
if (!entry)
|
|
34907
35406
|
return errorContent(`Server "${serverId}" not found.`);
|
|
34908
|
-
const agentTargets = Array.isArray(targets) ? targets : undefined;
|
|
34909
|
-
return jsonContent(installToAgents(entry, agentTargets ?? ["claude", "codex", "gemini"]
|
|
35407
|
+
const agentTargets = Array.isArray(input.targets) ? input.targets : undefined;
|
|
35408
|
+
return jsonContent(installToAgents(entry, agentTargets ?? ["claude", "codex", "gemini"], {
|
|
35409
|
+
localCommandConsent: localConsent(input)
|
|
35410
|
+
}));
|
|
34910
35411
|
}
|
|
34911
35412
|
},
|
|
34912
35413
|
{
|
|
@@ -34918,11 +35419,14 @@ function buildMcpTools() {
|
|
|
34918
35419
|
{
|
|
34919
35420
|
name: "connect_and_list_tools",
|
|
34920
35421
|
description: "Connect to all enabled MCP servers and list their available tools",
|
|
34921
|
-
paramsSchema: {
|
|
34922
|
-
|
|
35422
|
+
paramsSchema: {
|
|
35423
|
+
allow_local_stdio: exports_external2.boolean().optional().describe("Approve launching enabled local stdio commands"),
|
|
35424
|
+
allow_risky_command: exports_external2.boolean().optional().describe("Approve launching risky local command patterns")
|
|
35425
|
+
},
|
|
35426
|
+
run: async (input) => {
|
|
34923
35427
|
let liveTools = [];
|
|
34924
35428
|
try {
|
|
34925
|
-
await connectAllEnabled();
|
|
35429
|
+
await connectAllEnabled({ localCommandConsent: localConsent(input) });
|
|
34926
35430
|
liveTools = listAllTools();
|
|
34927
35431
|
} finally {
|
|
34928
35432
|
await disconnectAll().catch(() => {
|
|
@@ -34937,11 +35441,13 @@ function buildMcpTools() {
|
|
|
34937
35441
|
description: `Call a tool on a connected upstream MCP server. Tool name format: server_id${TOOL_PREFIX_SEPARATOR}tool_name`,
|
|
34938
35442
|
paramsSchema: {
|
|
34939
35443
|
tool_name: exports_external2.string().describe(`Prefixed tool name (server_id${TOOL_PREFIX_SEPARATOR}tool_name)`),
|
|
34940
|
-
arguments: exports_external2.record(exports_external2.unknown()).optional().describe("Tool arguments as key-value pairs")
|
|
35444
|
+
arguments: exports_external2.record(exports_external2.unknown()).optional().describe("Tool arguments as key-value pairs"),
|
|
35445
|
+
allow_local_stdio: exports_external2.boolean().optional().describe("Approve launching this local stdio command"),
|
|
35446
|
+
allow_risky_command: exports_external2.boolean().optional().describe("Approve launching risky local command patterns")
|
|
34941
35447
|
},
|
|
34942
|
-
run: async (
|
|
35448
|
+
run: async (input) => {
|
|
34943
35449
|
try {
|
|
34944
|
-
const toolName = String(tool_name);
|
|
35450
|
+
const toolName = String(input.tool_name);
|
|
34945
35451
|
const sepIdx = toolName.indexOf(TOOL_PREFIX_SEPARATOR);
|
|
34946
35452
|
if (sepIdx === -1)
|
|
34947
35453
|
return errorContent(`Error: Invalid tool name "${toolName}"`);
|
|
@@ -34951,8 +35457,8 @@ function buildMcpTools() {
|
|
|
34951
35457
|
return errorContent(`Error: Server "${serverId}" not found.`);
|
|
34952
35458
|
if (!entry.enabled)
|
|
34953
35459
|
return errorContent(`Error: Server "${serverId}" is disabled.`);
|
|
34954
|
-
await connectToServer(entry);
|
|
34955
|
-
const result = await callTool(toolName, readRecord(
|
|
35460
|
+
await connectToServer(entry, { localCommandConsent: localConsent(input) });
|
|
35461
|
+
const result = await callTool(toolName, readRecord(input.arguments));
|
|
34956
35462
|
return { content: result.content };
|
|
34957
35463
|
} catch (error2) {
|
|
34958
35464
|
return errorContent(`Error: ${error2.message}`);
|
|
@@ -34962,13 +35468,17 @@ function buildMcpTools() {
|
|
|
34962
35468
|
{
|
|
34963
35469
|
name: "diagnose_server",
|
|
34964
35470
|
description: "Run health checks on a registered MCP server",
|
|
34965
|
-
paramsSchema: {
|
|
34966
|
-
|
|
34967
|
-
|
|
35471
|
+
paramsSchema: {
|
|
35472
|
+
id: exports_external2.string().describe("Server ID"),
|
|
35473
|
+
allow_local_stdio: exports_external2.boolean().optional().describe("Approve launching local stdio diagnostics"),
|
|
35474
|
+
allow_risky_command: exports_external2.boolean().optional().describe("Approve diagnosing risky local command patterns")
|
|
35475
|
+
},
|
|
35476
|
+
run: async (input) => {
|
|
35477
|
+
const serverId = String(input.id);
|
|
34968
35478
|
const entry = getServer(serverId);
|
|
34969
35479
|
if (!entry)
|
|
34970
35480
|
return errorContent(`Server "${serverId}" not found.`);
|
|
34971
|
-
return jsonContent(await diagnoseServer(entry));
|
|
35481
|
+
return jsonContent(await diagnoseServer(entry, { localCommandConsent: localConsent(input) }));
|
|
34972
35482
|
}
|
|
34973
35483
|
},
|
|
34974
35484
|
{
|