@proofofwork-agency/toolpin 0.2.3

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.
Files changed (61) hide show
  1. package/CONTRIBUTING.md +117 -0
  2. package/LICENSE +183 -0
  3. package/README.md +323 -0
  4. package/SECURITY.md +61 -0
  5. package/action.yml +134 -0
  6. package/dist/canonicalJson.js +38 -0
  7. package/dist/capabilities.js +139 -0
  8. package/dist/ci.js +26 -0
  9. package/dist/cli.js +1843 -0
  10. package/dist/clientSupport.js +76 -0
  11. package/dist/codexToml.js +213 -0
  12. package/dist/config.js +337 -0
  13. package/dist/constants.js +3 -0
  14. package/dist/continueYaml.js +76 -0
  15. package/dist/doctor.js +163 -0
  16. package/dist/install.js +191 -0
  17. package/dist/installed.js +405 -0
  18. package/dist/integrity.js +14 -0
  19. package/dist/inventory.js +169 -0
  20. package/dist/packageIntegrity.js +153 -0
  21. package/dist/plan.js +595 -0
  22. package/dist/policy.js +310 -0
  23. package/dist/registry.js +1610 -0
  24. package/dist/runtimeAdvisory.js +80 -0
  25. package/dist/safeFetch.js +157 -0
  26. package/dist/sarif.js +162 -0
  27. package/dist/scan.js +113 -0
  28. package/dist/search.js +44 -0
  29. package/dist/secrets.js +165 -0
  30. package/dist/signing.js +146 -0
  31. package/dist/tester.js +240 -0
  32. package/dist/trust.js +528 -0
  33. package/dist/tui/app.js +1731 -0
  34. package/dist/tui/command.js +50 -0
  35. package/dist/tui/configSnippet.js +11 -0
  36. package/dist/tui/constants.js +37 -0
  37. package/dist/tui/format.js +31 -0
  38. package/dist/tui/installedState.js +23 -0
  39. package/dist/tui/layout.js +65 -0
  40. package/dist/tui/selectors.js +282 -0
  41. package/dist/tui/types.js +1 -0
  42. package/dist/tui/ui/trust.js +77 -0
  43. package/dist/tui/views/installed.js +82 -0
  44. package/dist/tui/views/panels.js +637 -0
  45. package/dist/tui.js +12 -0
  46. package/dist/types.js +1 -0
  47. package/dist/verificationTrust.js +103 -0
  48. package/dist/verify.js +537 -0
  49. package/dist/version.js +1 -0
  50. package/dist/versions.js +127 -0
  51. package/docs/assets/readme/terminal-demo.svg +174 -0
  52. package/docs/assets/readme/tui-browse-overview.jpg +0 -0
  53. package/docs/assets/readme/tui-config-preview.jpg +0 -0
  54. package/docs/assets/readme/tui-help.jpg +0 -0
  55. package/docs/assets/readme/tui-installed-inventory.jpg +0 -0
  56. package/docs/how-to/catch-drift-in-ci.md +189 -0
  57. package/docs/how-to/custom-registries.md +156 -0
  58. package/docs/how-to/toolpin-curated-registry.md +153 -0
  59. package/package.json +76 -0
  60. package/registry/README.md +92 -0
  61. package/registry/v0/servers +115 -0
@@ -0,0 +1,165 @@
1
+ import { readInstalledServerConfig } from "./doctor.js";
2
+ import { resolveConfigTarget } from "./install.js";
3
+ import { readLockfile } from "./plan.js";
4
+ const SECRET_PREFIXES = [
5
+ { label: "GitHub token", pattern: /^(ghp_|github_pat_)/ },
6
+ { label: "OpenAI-style token", pattern: /^sk-[A-Za-z0-9_-]{8,}/ },
7
+ { label: "AWS access key", pattern: /^AKIA[A-Z0-9]{12,}/ },
8
+ { label: "Slack token", pattern: /^xox[baprs]-/ },
9
+ { label: "Google API key", pattern: /^AIza[0-9A-Za-z_-]{8,}/ },
10
+ { label: "private key", pattern: /^-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
11
+ ];
12
+ export async function auditSecrets(lockfilePath = "mcp-lock.json", scope = "all") {
13
+ const lockfile = await readLockfile(lockfilePath);
14
+ const findings = [];
15
+ const entries = Object.entries(lockfile.servers);
16
+ for (const [key, plan] of entries) {
17
+ const missing = [];
18
+ const invalidScopes = [];
19
+ let foundConfig = false;
20
+ let foundUnreadable = false;
21
+ for (const currentScope of scopesToAudit(scope)) {
22
+ let target;
23
+ try {
24
+ target = resolveConfigTarget(plan.client, currentScope);
25
+ }
26
+ catch (error) {
27
+ invalidScopes.push(`${currentScope}: ${error instanceof Error ? error.message : String(error)}`);
28
+ continue;
29
+ }
30
+ const actual = await readInstalledServerConfig(target.file, plan.name, plan.client);
31
+ if (actual.kind === "missing") {
32
+ missing.push({ scope: currentScope, file: target.file });
33
+ continue;
34
+ }
35
+ if (actual.kind === "unreadable") {
36
+ foundUnreadable = true;
37
+ findings.push({
38
+ kind: "unreadable_config",
39
+ key,
40
+ client: plan.client,
41
+ serverName: plan.name,
42
+ file: target.file,
43
+ scope: currentScope,
44
+ message: actual.message,
45
+ });
46
+ continue;
47
+ }
48
+ foundConfig = true;
49
+ findings.push(...auditConfigSecrets(key, plan.client, plan.name, target.file, currentScope, actual.config, plan.capabilityManifest?.secrets ?? []));
50
+ }
51
+ if (!foundConfig && !foundUnreadable && missing.length > 0) {
52
+ findings.push({
53
+ kind: "missing_config",
54
+ key,
55
+ client: plan.client,
56
+ serverName: plan.name,
57
+ file: missing.map((entry) => entry.file).join(", "),
58
+ scope: scope === "all" ? undefined : missing[0]?.scope,
59
+ message: scope === "all"
60
+ ? `missing ${plan.client} config entry for ${plan.name} in checked scopes: ${missing.map((entry) => entry.scope).join(", ")}`
61
+ : `missing ${plan.client} config entry for ${plan.name}`,
62
+ });
63
+ continue;
64
+ }
65
+ if (!foundConfig && !foundUnreadable && missing.length === 0 && invalidScopes.length > 0) {
66
+ findings.push({
67
+ kind: "invalid_scope",
68
+ key,
69
+ client: plan.client,
70
+ serverName: plan.name,
71
+ file: "",
72
+ message: `cannot audit ${plan.client} at ${scope} scope: ${invalidScopes.join("; ")}`,
73
+ });
74
+ }
75
+ }
76
+ return {
77
+ ok: findings.length === 0,
78
+ checked: entries.length,
79
+ findings,
80
+ };
81
+ }
82
+ function auditConfigSecrets(key, client, serverName, file, scope, config, secretHints) {
83
+ const findings = [];
84
+ const plaintextKeys = new Set();
85
+ for (const secret of secretHints) {
86
+ const value = secretValue(config, secret);
87
+ if (typeof value !== "string" || !value)
88
+ continue;
89
+ if (!isSecretReference(value, secret.name)) {
90
+ plaintextKeys.add(`${secret.source}:${secret.name}`);
91
+ findings.push({
92
+ kind: "plaintext_secret",
93
+ key,
94
+ client,
95
+ serverName,
96
+ file,
97
+ scope,
98
+ secretName: secret.name,
99
+ secretSource: secret.source,
100
+ message: `${secret.source}:${secret.name} is stored as a plaintext value; replace it with a placeholder or external secret reference`,
101
+ redactedValue: redact(value),
102
+ });
103
+ }
104
+ }
105
+ for (const candidate of collectSecretCandidates(config)) {
106
+ if (plaintextKeys.has(`${candidate.source}:${candidate.name}`))
107
+ continue;
108
+ const matched = SECRET_PREFIXES.find((prefix) => prefix.pattern.test(candidate.value));
109
+ if (matched) {
110
+ findings.push({
111
+ kind: "secret_prefix",
112
+ key,
113
+ client,
114
+ serverName,
115
+ file,
116
+ scope,
117
+ secretName: candidate.name,
118
+ secretSource: candidate.source,
119
+ message: `${candidate.source}:${candidate.name} resembles a ${matched.label}; replace it with a placeholder or external secret reference`,
120
+ redactedValue: redact(candidate.value),
121
+ });
122
+ }
123
+ }
124
+ return findings;
125
+ }
126
+ function scopesToAudit(scope) {
127
+ return scope === "all" ? ["project", "global"] : [scope];
128
+ }
129
+ function secretValue(config, secret) {
130
+ const root = asRecord(config);
131
+ if (secret.source === "env")
132
+ return asRecord(root.env)[secret.name] ?? asRecord(root.environment)[secret.name];
133
+ return asRecord(root.headers)[secret.name] ?? asRecord(root.http_headers)[secret.name] ?? asRecord(asRecord(root.requestOptions).headers)[secret.name];
134
+ }
135
+ function collectSecretCandidates(config) {
136
+ const root = asRecord(config);
137
+ return [
138
+ ...valuesFromObject(asRecord(root.env), "env"),
139
+ ...valuesFromObject(asRecord(root.environment), "env"),
140
+ ...valuesFromObject(asRecord(root.headers), "header"),
141
+ ...valuesFromObject(asRecord(root.http_headers), "header"),
142
+ ...valuesFromObject(asRecord(asRecord(root.requestOptions).headers), "header"),
143
+ ];
144
+ }
145
+ function valuesFromObject(value, source) {
146
+ return Object.entries(value).flatMap(([name, child]) => (typeof child === "string" ? [{ source, name, value: child }] : []));
147
+ }
148
+ function isSecretReference(value, name) {
149
+ const trimmed = value.trim();
150
+ const escaped = escapeRegExp(name);
151
+ return (trimmed === `<${name}>` ||
152
+ new RegExp(`^\\$\\{env:${escaped}\\}$`).test(trimmed) ||
153
+ new RegExp(`^\\$\\{${escaped}\\}$`).test(trimmed) ||
154
+ new RegExp(`^\\$\\{\\{\\s*secrets\\.${escaped}\\s*\\}\\}$`).test(trimmed) ||
155
+ /^(op|vault|doppler):\/\//.test(trimmed));
156
+ }
157
+ function redact(_value) {
158
+ return "[REDACTED]";
159
+ }
160
+ function escapeRegExp(value) {
161
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
162
+ }
163
+ function asRecord(value) {
164
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value) ? value : {};
165
+ }
@@ -0,0 +1,146 @@
1
+ import { createHash, createPrivateKey, createPublicKey, sign, verify } from "node:crypto";
2
+ import { readFile, writeFile } from "node:fs/promises";
3
+ import { readLockfileDigest } from "./plan.js";
4
+ import { readPolicyDigest } from "./policy.js";
5
+ import { canonicalJson } from "./canonicalJson.js";
6
+ export async function signLockfile(lockfilePath = "mcp-lock.json", privateKeyPath, signaturePath = "mcp-lock.sig", options = {}) {
7
+ const lockfileDigest = await readLockfileDigest(lockfilePath);
8
+ const policyDigest = options.policyPath ? await readPolicyDigest(options.policyPath) : undefined;
9
+ const key = createPrivateKey(await readFile(privateKeyPath, "utf8"));
10
+ const publicKeyFingerprint = keyFingerprint(createPublicKey(key));
11
+ const payload = signingPayload({
12
+ lockfileDigest,
13
+ policyDigest,
14
+ publicKeyFingerprint,
15
+ signedAt: new Date().toISOString(),
16
+ });
17
+ const signature = sign(null, Buffer.from(canonicalJson(payload), "utf8"), key).toString("base64");
18
+ const envelope = {
19
+ schema: "dev.toolpin.lock-signature",
20
+ version: 2,
21
+ algorithm: "ed25519",
22
+ lockfileDigest,
23
+ ...(policyDigest ? { policyDigest } : {}),
24
+ publicKeyFingerprint,
25
+ signedAt: payload.signedAt,
26
+ signature,
27
+ };
28
+ await writeFile(signaturePath, `${JSON.stringify(envelope, null, 2)}\n`, "utf8");
29
+ return envelope;
30
+ }
31
+ export async function verifyLockfileSignature(lockfilePath = "mcp-lock.json", publicKeyPath, signaturePath = "mcp-lock.sig", options = {}) {
32
+ const actualDigest = await readLockfileDigest(lockfilePath);
33
+ const actualPolicyDigest = options.policyPath ? await readPolicyDigest(options.policyPath) : undefined;
34
+ const envelope = await readSignatureEnvelope(signaturePath);
35
+ if (envelope.lockfileDigest !== actualDigest) {
36
+ return {
37
+ ok: false,
38
+ lockfileDigest: actualDigest,
39
+ signatureDigest: envelope.lockfileDigest,
40
+ message: `Lockfile digest mismatch: signature covers ${envelope.lockfileDigest}, current lockfile is ${actualDigest}`,
41
+ };
42
+ }
43
+ if (envelope.policyDigest !== actualPolicyDigest) {
44
+ return {
45
+ ok: false,
46
+ lockfileDigest: actualDigest,
47
+ signatureDigest: envelope.lockfileDigest,
48
+ policyDigest: actualPolicyDigest,
49
+ message: `Policy digest mismatch: signature covers ${envelope.policyDigest ?? "no policy"}, current policy is ${actualPolicyDigest ?? "no policy"}`,
50
+ };
51
+ }
52
+ const key = createPublicKey(await readFile(publicKeyPath, "utf8"));
53
+ const actualFingerprint = keyFingerprint(key);
54
+ if (envelope.publicKeyFingerprint !== actualFingerprint) {
55
+ return {
56
+ ok: false,
57
+ lockfileDigest: actualDigest,
58
+ signatureDigest: envelope.lockfileDigest,
59
+ policyDigest: actualPolicyDigest,
60
+ publicKeyFingerprint: actualFingerprint,
61
+ message: `Public key fingerprint mismatch: signature requires ${envelope.publicKeyFingerprint}, provided key is ${actualFingerprint}`,
62
+ };
63
+ }
64
+ const payload = signingPayload({
65
+ lockfileDigest: envelope.lockfileDigest,
66
+ policyDigest: envelope.policyDigest,
67
+ publicKeyFingerprint: envelope.publicKeyFingerprint,
68
+ signedAt: envelope.signedAt,
69
+ });
70
+ const valid = verify(null, Buffer.from(canonicalJson(payload), "utf8"), key, Buffer.from(envelope.signature, "base64"));
71
+ return {
72
+ ok: valid,
73
+ lockfileDigest: actualDigest,
74
+ signatureDigest: envelope.lockfileDigest,
75
+ policyDigest: actualPolicyDigest,
76
+ publicKeyFingerprint: actualFingerprint,
77
+ message: valid ? "Signature valid." : "Signature verification failed.",
78
+ };
79
+ }
80
+ async function readSignatureEnvelope(path) {
81
+ let parsed;
82
+ try {
83
+ parsed = JSON.parse(await readFile(path, "utf8"));
84
+ }
85
+ catch (error) {
86
+ if (error instanceof SyntaxError) {
87
+ throw new Error(`Invalid lock signature JSON in ${path}: ${error.message}`);
88
+ }
89
+ throw error;
90
+ }
91
+ return parseSignatureEnvelope(parsed, path);
92
+ }
93
+ function parseSignatureEnvelope(value, path) {
94
+ if (!isRecord(value))
95
+ throw new Error(`Invalid lock signature schema in ${path}: expected object`);
96
+ if (value.schema !== "dev.toolpin.lock-signature")
97
+ throw new Error(`Invalid lock signature schema in ${path}: unsupported schema`);
98
+ if (value.version !== 2)
99
+ throw new Error(`Invalid lock signature schema in ${path}: unsupported version`);
100
+ if (value.algorithm !== "ed25519")
101
+ throw new Error(`Invalid lock signature schema in ${path}: unsupported algorithm`);
102
+ if (typeof value.lockfileDigest !== "string" || !value.lockfileDigest.startsWith("sha256-")) {
103
+ throw new Error(`Invalid lock signature schema in ${path}: invalid lockfileDigest`);
104
+ }
105
+ if (value.policyDigest !== undefined && (typeof value.policyDigest !== "string" || !value.policyDigest.startsWith("sha256-"))) {
106
+ throw new Error(`Invalid lock signature schema in ${path}: invalid policyDigest`);
107
+ }
108
+ if (typeof value.publicKeyFingerprint !== "string" || !value.publicKeyFingerprint.startsWith("sha256-")) {
109
+ throw new Error(`Invalid lock signature schema in ${path}: invalid publicKeyFingerprint`);
110
+ }
111
+ if (typeof value.signedAt !== "string")
112
+ throw new Error(`Invalid lock signature schema in ${path}: invalid signedAt`);
113
+ if (typeof value.signature !== "string" || !value.signature)
114
+ throw new Error(`Invalid lock signature schema in ${path}: invalid signature`);
115
+ return {
116
+ schema: "dev.toolpin.lock-signature",
117
+ version: 2,
118
+ algorithm: "ed25519",
119
+ lockfileDigest: value.lockfileDigest,
120
+ policyDigest: value.policyDigest,
121
+ publicKeyFingerprint: value.publicKeyFingerprint,
122
+ signedAt: value.signedAt,
123
+ signature: value.signature,
124
+ };
125
+ }
126
+ function signingPayload(input) {
127
+ return {
128
+ schema: "dev.toolpin.lock-signature",
129
+ version: 2,
130
+ algorithm: "ed25519",
131
+ lockfileDigest: input.lockfileDigest,
132
+ ...(input.policyDigest ? { policyDigest: input.policyDigest } : {}),
133
+ publicKeyFingerprint: input.publicKeyFingerprint,
134
+ signedAt: input.signedAt,
135
+ };
136
+ }
137
+ function keyFingerprint(key) {
138
+ const der = key.export({ type: "spki", format: "der" });
139
+ return `sha256-${createHash("sha256").update(der).digest("base64")}`;
140
+ }
141
+ export async function readPublicKeyFingerprint(publicKeyPath) {
142
+ return keyFingerprint(createPublicKey(await readFile(publicKeyPath, "utf8")));
143
+ }
144
+ function isRecord(value) {
145
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
146
+ }
package/dist/tester.js ADDED
@@ -0,0 +1,240 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
3
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
4
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
5
+ import { selectLaunchTarget } from "./config.js";
6
+ import { TOOLPIN_VERSION } from "./version.js";
7
+ export async function testServer(server, timeoutMs = 15000) {
8
+ const startedAt = Date.now();
9
+ const launch = selectLaunchTarget(server);
10
+ if (!launch) {
11
+ return fail(server, "none", startedAt, `No launch target is available for ${server.name}.`);
12
+ }
13
+ let client;
14
+ try {
15
+ if (launch.kind === "remote") {
16
+ const headers = resolveRemoteHeaders(launch.remote);
17
+ if (headers.missing.length) {
18
+ return fail(server, `remote:${launch.remote.type}`, startedAt, `Missing required header/env value: ${headers.missing.join(", ")}`);
19
+ }
20
+ const transport = launch.remote.type === "sse"
21
+ ? new SSEClientTransport(new URL(launch.remote.url), { requestInit: { headers: headers.values } })
22
+ : new StreamableHTTPClientTransport(new URL(launch.remote.url), { requestInit: { headers: headers.values } });
23
+ client = new Client({ name: "toolpin", version: TOOLPIN_VERSION });
24
+ await withTimeout(client.connect(transport), timeoutMs, "Timed out connecting to remote MCP server.");
25
+ }
26
+ else {
27
+ const local = packageToStdio(launch.pkg);
28
+ if (local.missing.length) {
29
+ return fail(server, `stdio:${local.command}`, startedAt, `Missing required env value: ${local.missing.join(", ")}`);
30
+ }
31
+ const transport = new StdioClientTransport({
32
+ command: local.command,
33
+ args: local.args,
34
+ env: { ...definedProcessEnv(), ...local.env },
35
+ stderr: "pipe",
36
+ });
37
+ client = new Client({ name: "toolpin", version: TOOLPIN_VERSION });
38
+ await withTimeout(client.connect(transport), timeoutMs, "Timed out starting local MCP server.");
39
+ }
40
+ const response = await withTimeout(client.listTools(), timeoutMs, "Timed out listing MCP tools.");
41
+ const tools = response.tools.map((tool) => ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema }));
42
+ return {
43
+ ok: true,
44
+ serverName: server.name,
45
+ target: launch.kind === "remote" ? `remote:${launch.remote.type}` : `stdio:${launch.pkg.registryType}`,
46
+ durationMs: Date.now() - startedAt,
47
+ tools,
48
+ message: `Connected and listed ${tools.length} tool(s).`,
49
+ };
50
+ }
51
+ catch (error) {
52
+ return fail(server, launch.kind === "remote" ? `remote:${launch.remote.type}` : `stdio:${launch.pkg.registryType}`, startedAt, error instanceof Error ? error.message : String(error));
53
+ }
54
+ finally {
55
+ await client?.close().catch(() => undefined);
56
+ }
57
+ }
58
+ export async function testInstalledClientConfig(serverName, config, timeoutMs = 15000) {
59
+ const startedAt = Date.now();
60
+ const launch = installedConfigToLaunch(config);
61
+ if (!launch) {
62
+ return {
63
+ ok: false,
64
+ serverName,
65
+ target: "installed-config",
66
+ durationMs: Date.now() - startedAt,
67
+ tools: [],
68
+ message: `No stdio or remote launch target is available in the installed config for ${serverName}.`,
69
+ };
70
+ }
71
+ let client;
72
+ try {
73
+ if (launch.kind === "remote") {
74
+ const transport = launch.type === "sse"
75
+ ? new SSEClientTransport(new URL(launch.url), { requestInit: { headers: launch.headers } })
76
+ : new StreamableHTTPClientTransport(new URL(launch.url), { requestInit: { headers: launch.headers } });
77
+ client = new Client({ name: "toolpin", version: TOOLPIN_VERSION });
78
+ await withTimeout(client.connect(transport), timeoutMs, "Timed out connecting to installed remote MCP server.");
79
+ }
80
+ else {
81
+ const transport = new StdioClientTransport({
82
+ command: launch.command,
83
+ args: launch.args,
84
+ env: { ...definedProcessEnv(), ...launch.env },
85
+ stderr: "pipe",
86
+ });
87
+ client = new Client({ name: "toolpin", version: TOOLPIN_VERSION });
88
+ await withTimeout(client.connect(transport), timeoutMs, "Timed out starting installed local MCP server.");
89
+ }
90
+ const response = await withTimeout(client.listTools(), timeoutMs, "Timed out listing MCP tools.");
91
+ const tools = response.tools.map((tool) => ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema }));
92
+ return {
93
+ ok: true,
94
+ serverName,
95
+ target: launch.kind === "remote" ? `installed-remote:${launch.type}` : `installed-stdio:${launch.command}`,
96
+ durationMs: Date.now() - startedAt,
97
+ tools,
98
+ message: `Connected and listed ${tools.length} tool(s).`,
99
+ };
100
+ }
101
+ catch (error) {
102
+ return {
103
+ ok: false,
104
+ serverName,
105
+ target: launch.kind === "remote" ? `installed-remote:${launch.type}` : `installed-stdio:${launch.command}`,
106
+ durationMs: Date.now() - startedAt,
107
+ tools: [],
108
+ message: error instanceof Error ? error.message : String(error),
109
+ };
110
+ }
111
+ finally {
112
+ await client?.close().catch(() => undefined);
113
+ }
114
+ }
115
+ function installedConfigToLaunch(config) {
116
+ const record = asRecord(config);
117
+ const url = firstString(record.url, record.httpUrl, record.serverUrl);
118
+ if (url) {
119
+ const headers = asStringRecord(record.headers) ?? asStringRecord(record.http_headers) ?? asStringRecord(asRecord(record.requestOptions).headers) ?? {};
120
+ return { kind: "remote", type: typeof record.type === "string" ? record.type : "streamable-http", url, headers };
121
+ }
122
+ const commandArray = Array.isArray(record.command) ? record.command.filter((value) => typeof value === "string") : undefined;
123
+ const command = typeof record.command === "string" ? record.command : commandArray?.[0];
124
+ if (!command)
125
+ return undefined;
126
+ const args = Array.isArray(record.args)
127
+ ? record.args.filter((value) => typeof value === "string")
128
+ : commandArray?.slice(1) ?? [];
129
+ const env = asStringRecord(record.env) ?? asStringRecord(record.environment) ?? {};
130
+ return { kind: "stdio", command, args, env };
131
+ }
132
+ function packageToStdio(pkg) {
133
+ const env = resolvePackageEnv(pkg.environmentVariables ?? []);
134
+ if (typeof pkg.command === "string" && pkg.command.length > 0) {
135
+ return { command: pkg.command, args: Array.isArray(pkg.args) ? pkg.args.filter(isNonEmptyString) : [], env: env.values, missing: env.missing };
136
+ }
137
+ const packageArgs = Array.isArray(pkg.packageArguments) ? pkg.packageArguments.filter(isNonEmptyString) : [];
138
+ switch (pkg.registryType) {
139
+ case "npm": {
140
+ const spec = pkg.version ? `${pkg.identifier}@${pkg.version}` : pkg.identifier;
141
+ return pkg.runtimeHint === "bun"
142
+ ? { command: "bunx", args: [spec, ...packageArgs], env: env.values, missing: env.missing }
143
+ : { command: "npx", args: ["-y", spec, ...packageArgs], env: env.values, missing: env.missing };
144
+ }
145
+ case "pypi": {
146
+ const spec = pkg.version ? `${pkg.identifier}==${pkg.version}` : pkg.identifier;
147
+ return { command: "uvx", args: [spec], env: env.values, missing: env.missing };
148
+ }
149
+ case "nuget": {
150
+ const spec = pkg.version ? `${pkg.identifier}@${pkg.version}` : pkg.identifier;
151
+ return { command: "dnx", args: [spec], env: env.values, missing: env.missing };
152
+ }
153
+ case "cargo":
154
+ return { command: pkg.identifier, args: packageArgs, env: env.values, missing: env.missing };
155
+ case "oci":
156
+ return { command: "docker", args: ["run", "--rm", "-i", pkg.identifier], env: env.values, missing: env.missing };
157
+ case "mcpb":
158
+ return { command: "mcpb", args: ["run", pkg.identifier], env: env.values, missing: env.missing };
159
+ default:
160
+ return { command: pkg.identifier, args: packageArgs, env: env.values, missing: env.missing };
161
+ }
162
+ }
163
+ function isNonEmptyString(value) {
164
+ return typeof value === "string" && value.length > 0;
165
+ }
166
+ function resolvePackageEnv(variables) {
167
+ const values = {};
168
+ const missing = [];
169
+ for (const variable of variables) {
170
+ const current = process.env[variable.name] ?? variable.default;
171
+ if (current) {
172
+ values[variable.name] = current;
173
+ }
174
+ else if (variable.isRequired !== false) {
175
+ missing.push(variable.name);
176
+ }
177
+ }
178
+ return { values, missing };
179
+ }
180
+ function resolveRemoteHeaders(remote) {
181
+ const values = {};
182
+ const missing = [];
183
+ for (const header of remote.headers ?? []) {
184
+ const rawValue = typeof header.value === "string" ? header.value : undefined;
185
+ const envName = typeof header.env === "string" ? header.env : extractEnvName(rawValue);
186
+ const envValue = envName ? process.env[envName] : process.env[header.name];
187
+ const resolved = rawValue && envName && envValue ? rawValue.replace(`\${${envName}}`, envValue) : envValue;
188
+ if (resolved) {
189
+ values[header.name] = resolved;
190
+ }
191
+ else if (header.isRequired !== false) {
192
+ missing.push(envName ?? header.name);
193
+ }
194
+ }
195
+ return { values, missing };
196
+ }
197
+ function extractEnvName(value) {
198
+ const match = value?.match(/\$\{([^}]+)\}/);
199
+ return match?.[1];
200
+ }
201
+ function definedProcessEnv() {
202
+ return Object.fromEntries(Object.entries(process.env).filter((entry) => typeof entry[1] === "string"));
203
+ }
204
+ function firstString(...values) {
205
+ return values.find((value) => typeof value === "string" && value.length > 0);
206
+ }
207
+ function asStringRecord(value) {
208
+ if (!value || typeof value !== "object" || Array.isArray(value))
209
+ return undefined;
210
+ const entries = Object.entries(value).filter((entry) => typeof entry[1] === "string");
211
+ return entries.length ? Object.fromEntries(entries) : {};
212
+ }
213
+ function asRecord(value) {
214
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
215
+ }
216
+ async function withTimeout(promise, timeoutMs, message) {
217
+ let timer;
218
+ try {
219
+ return await Promise.race([
220
+ promise,
221
+ new Promise((_, reject) => {
222
+ timer = setTimeout(() => reject(new Error(message)), timeoutMs);
223
+ }),
224
+ ]);
225
+ }
226
+ finally {
227
+ if (timer)
228
+ clearTimeout(timer);
229
+ }
230
+ }
231
+ function fail(server, target, startedAt, message) {
232
+ return {
233
+ ok: false,
234
+ serverName: server.name,
235
+ target,
236
+ durationMs: Date.now() - startedAt,
237
+ tools: [],
238
+ message,
239
+ };
240
+ }