@mcp-rating/gateway 0.2.0

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 (168) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +130 -0
  3. package/dist/ads/ad-tracker.d.ts +63 -0
  4. package/dist/ads/ad-tracker.js +144 -0
  5. package/dist/ads/ad-tracker.js.map +1 -0
  6. package/dist/analytics/usage-tracker.d.ts +67 -0
  7. package/dist/analytics/usage-tracker.js +148 -0
  8. package/dist/analytics/usage-tracker.js.map +1 -0
  9. package/dist/audit/audit-log.d.ts +71 -0
  10. package/dist/audit/audit-log.js +72 -0
  11. package/dist/audit/audit-log.js.map +1 -0
  12. package/dist/config/gateway-config.d.ts +6 -0
  13. package/dist/config/gateway-config.js +135 -0
  14. package/dist/config/gateway-config.js.map +1 -0
  15. package/dist/config/groups.d.ts +29 -0
  16. package/dist/config/groups.js +102 -0
  17. package/dist/config/groups.js.map +1 -0
  18. package/dist/config/profiles.d.ts +42 -0
  19. package/dist/config/profiles.js +167 -0
  20. package/dist/config/profiles.js.map +1 -0
  21. package/dist/config/types.d.ts +76 -0
  22. package/dist/config/types.js +2 -0
  23. package/dist/config/types.js.map +1 -0
  24. package/dist/connection/auto-installer.d.ts +13 -0
  25. package/dist/connection/auto-installer.js +61 -0
  26. package/dist/connection/auto-installer.js.map +1 -0
  27. package/dist/connection/connection-manager.d.ts +106 -0
  28. package/dist/connection/connection-manager.js +845 -0
  29. package/dist/connection/connection-manager.js.map +1 -0
  30. package/dist/connection/downstream-client.d.ts +50 -0
  31. package/dist/connection/downstream-client.js +170 -0
  32. package/dist/connection/downstream-client.js.map +1 -0
  33. package/dist/connection/http-client.d.ts +12 -0
  34. package/dist/connection/http-client.js +26 -0
  35. package/dist/connection/http-client.js.map +1 -0
  36. package/dist/connection/sse-client.d.ts +12 -0
  37. package/dist/connection/sse-client.js +26 -0
  38. package/dist/connection/sse-client.js.map +1 -0
  39. package/dist/connection/transport-factory.d.ts +31 -0
  40. package/dist/connection/transport-factory.js +67 -0
  41. package/dist/connection/transport-factory.js.map +1 -0
  42. package/dist/connection/types.d.ts +134 -0
  43. package/dist/connection/types.js +5 -0
  44. package/dist/connection/types.js.map +1 -0
  45. package/dist/connection/ws-client.d.ts +12 -0
  46. package/dist/connection/ws-client.js +26 -0
  47. package/dist/connection/ws-client.js.map +1 -0
  48. package/dist/discovery/auto-recommender.d.ts +51 -0
  49. package/dist/discovery/auto-recommender.js +176 -0
  50. package/dist/discovery/auto-recommender.js.map +1 -0
  51. package/dist/gateway-server.d.ts +42 -0
  52. package/dist/gateway-server.js +135 -0
  53. package/dist/gateway-server.js.map +1 -0
  54. package/dist/http-daemon.d.ts +26 -0
  55. package/dist/http-daemon.js +302 -0
  56. package/dist/http-daemon.js.map +1 -0
  57. package/dist/index.d.ts +2 -0
  58. package/dist/index.js +83 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/meta-tools/ad-status.d.ts +8 -0
  61. package/dist/meta-tools/ad-status.js +41 -0
  62. package/dist/meta-tools/ad-status.js.map +1 -0
  63. package/dist/meta-tools/audit.d.ts +12 -0
  64. package/dist/meta-tools/audit.js +98 -0
  65. package/dist/meta-tools/audit.js.map +1 -0
  66. package/dist/meta-tools/call-tool.d.ts +11 -0
  67. package/dist/meta-tools/call-tool.js +65 -0
  68. package/dist/meta-tools/call-tool.js.map +1 -0
  69. package/dist/meta-tools/connect.d.ts +3 -0
  70. package/dist/meta-tools/connect.js +105 -0
  71. package/dist/meta-tools/connect.js.map +1 -0
  72. package/dist/meta-tools/disconnect.d.ts +3 -0
  73. package/dist/meta-tools/disconnect.js +30 -0
  74. package/dist/meta-tools/disconnect.js.map +1 -0
  75. package/dist/meta-tools/discover.d.ts +3 -0
  76. package/dist/meta-tools/discover.js +97 -0
  77. package/dist/meta-tools/discover.js.map +1 -0
  78. package/dist/meta-tools/groups.d.ts +7 -0
  79. package/dist/meta-tools/groups.js +237 -0
  80. package/dist/meta-tools/groups.js.map +1 -0
  81. package/dist/meta-tools/health.d.ts +4 -0
  82. package/dist/meta-tools/health.js +76 -0
  83. package/dist/meta-tools/health.js.map +1 -0
  84. package/dist/meta-tools/index.d.ts +28 -0
  85. package/dist/meta-tools/index.js +56 -0
  86. package/dist/meta-tools/index.js.map +1 -0
  87. package/dist/meta-tools/list-active.d.ts +3 -0
  88. package/dist/meta-tools/list-active.js +50 -0
  89. package/dist/meta-tools/list-active.js.map +1 -0
  90. package/dist/meta-tools/profiles.d.ts +6 -0
  91. package/dist/meta-tools/profiles.js +197 -0
  92. package/dist/meta-tools/profiles.js.map +1 -0
  93. package/dist/meta-tools/recommend.d.ts +14 -0
  94. package/dist/meta-tools/recommend.js +74 -0
  95. package/dist/meta-tools/recommend.js.map +1 -0
  96. package/dist/meta-tools/sandbox.d.ts +12 -0
  97. package/dist/meta-tools/sandbox.js +125 -0
  98. package/dist/meta-tools/sandbox.js.map +1 -0
  99. package/dist/meta-tools/server-info.d.ts +4 -0
  100. package/dist/meta-tools/server-info.js +156 -0
  101. package/dist/meta-tools/server-info.js.map +1 -0
  102. package/dist/meta-tools/usage.d.ts +6 -0
  103. package/dist/meta-tools/usage.js +149 -0
  104. package/dist/meta-tools/usage.js.map +1 -0
  105. package/dist/permissions/permission-manager.d.ts +26 -0
  106. package/dist/permissions/permission-manager.js +73 -0
  107. package/dist/permissions/permission-manager.js.map +1 -0
  108. package/dist/permissions/trust-tiers.d.ts +18 -0
  109. package/dist/permissions/trust-tiers.js +42 -0
  110. package/dist/permissions/trust-tiers.js.map +1 -0
  111. package/dist/plugins/builtin/logging-plugin.d.ts +6 -0
  112. package/dist/plugins/builtin/logging-plugin.js +41 -0
  113. package/dist/plugins/builtin/logging-plugin.js.map +1 -0
  114. package/dist/plugins/plugin-manager.d.ts +65 -0
  115. package/dist/plugins/plugin-manager.js +137 -0
  116. package/dist/plugins/plugin-manager.js.map +1 -0
  117. package/dist/proxy/prime-handlers.d.ts +27 -0
  118. package/dist/proxy/prime-handlers.js +47 -0
  119. package/dist/proxy/prime-handlers.js.map +1 -0
  120. package/dist/proxy/progress-forwarder.d.ts +44 -0
  121. package/dist/proxy/progress-forwarder.js +40 -0
  122. package/dist/proxy/progress-forwarder.js.map +1 -0
  123. package/dist/proxy/prompt-router.d.ts +25 -0
  124. package/dist/proxy/prompt-router.js +110 -0
  125. package/dist/proxy/prompt-router.js.map +1 -0
  126. package/dist/proxy/resource-router.d.ts +25 -0
  127. package/dist/proxy/resource-router.js +91 -0
  128. package/dist/proxy/resource-router.js.map +1 -0
  129. package/dist/proxy/schema-adapter.d.ts +11 -0
  130. package/dist/proxy/schema-adapter.js +36 -0
  131. package/dist/proxy/schema-adapter.js.map +1 -0
  132. package/dist/proxy/task-proxy.d.ts +28 -0
  133. package/dist/proxy/task-proxy.js +186 -0
  134. package/dist/proxy/task-proxy.js.map +1 -0
  135. package/dist/proxy/tool-router.d.ts +38 -0
  136. package/dist/proxy/tool-router.js +229 -0
  137. package/dist/proxy/tool-router.js.map +1 -0
  138. package/dist/proxy/ui-detect.d.ts +22 -0
  139. package/dist/proxy/ui-detect.js +30 -0
  140. package/dist/proxy/ui-detect.js.map +1 -0
  141. package/dist/registry/registry-client.d.ts +55 -0
  142. package/dist/registry/registry-client.js +211 -0
  143. package/dist/registry/registry-client.js.map +1 -0
  144. package/dist/registry/types.d.ts +69 -0
  145. package/dist/registry/types.js +2 -0
  146. package/dist/registry/types.js.map +1 -0
  147. package/dist/sandbox/container-runtime.d.ts +91 -0
  148. package/dist/sandbox/container-runtime.js +178 -0
  149. package/dist/sandbox/container-runtime.js.map +1 -0
  150. package/dist/sandbox/egress-proxy.d.ts +51 -0
  151. package/dist/sandbox/egress-proxy.js +146 -0
  152. package/dist/sandbox/egress-proxy.js.map +1 -0
  153. package/dist/sandbox/env-scoper.d.ts +29 -0
  154. package/dist/sandbox/env-scoper.js +60 -0
  155. package/dist/sandbox/env-scoper.js.map +1 -0
  156. package/dist/sandbox/manifest-resolver.d.ts +40 -0
  157. package/dist/sandbox/manifest-resolver.js +185 -0
  158. package/dist/sandbox/manifest-resolver.js.map +1 -0
  159. package/dist/sandbox/types.d.ts +88 -0
  160. package/dist/sandbox/types.js +21 -0
  161. package/dist/sandbox/types.js.map +1 -0
  162. package/dist/utils/errors.d.ts +39 -0
  163. package/dist/utils/errors.js +67 -0
  164. package/dist/utils/errors.js.map +1 -0
  165. package/dist/utils/logger.d.ts +16 -0
  166. package/dist/utils/logger.js +51 -0
  167. package/dist/utils/logger.js.map +1 -0
  168. package/package.json +72 -0
@@ -0,0 +1,178 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { log } from "../utils/logger.js";
4
+ const execFileAsync = promisify(execFile);
5
+ export const DEFAULT_IMAGES = {
6
+ node: "node:22-alpine",
7
+ python: "python:3.12-slim",
8
+ default: "node:22-alpine",
9
+ };
10
+ let cachedEngine;
11
+ /**
12
+ * Detect an available container engine (docker preferred, then podman).
13
+ * Result is cached for the process lifetime. Returns null if none is usable.
14
+ */
15
+ export async function detectContainerEngine() {
16
+ if (cachedEngine !== undefined)
17
+ return cachedEngine;
18
+ for (const engine of ["docker", "podman"]) {
19
+ try {
20
+ await execFileAsync(engine, ["version", "--format", "{{.Client.Version}}"], {
21
+ timeout: 5_000,
22
+ });
23
+ log.info("Container engine detected", { engine });
24
+ cachedEngine = engine;
25
+ return engine;
26
+ }
27
+ catch {
28
+ // try next
29
+ }
30
+ }
31
+ log.warn("No container engine (docker/podman) available — L2 isolation unavailable");
32
+ cachedEngine = null;
33
+ return null;
34
+ }
35
+ /** Reset the cached engine (for tests). */
36
+ export function _resetEngineCache() {
37
+ cachedEngine = undefined;
38
+ }
39
+ /** Deterministic image name for a pre-baked server image. */
40
+ export function bakedImageName(slug) {
41
+ const safe = slug.replace(/[^a-z0-9_.-]/gi, "-").toLowerCase();
42
+ return `mcp-sandbox/${safe}:latest`;
43
+ }
44
+ /**
45
+ * Check whether a pre-baked image exists locally for this slug. A baked image
46
+ * has the server package installed at build time, so it runs offline under
47
+ * `network: none` without an npx/uvx fetch.
48
+ *
49
+ * Returns { image, bin } if present, else null.
50
+ */
51
+ export async function resolveBakedImage(engine, slug) {
52
+ const image = bakedImageName(slug);
53
+ try {
54
+ const { stdout } = await execFileAsync(engine, ["image", "inspect", "--format", '{{index .Config.Labels "mcp.sandbox.bin"}}', image], { timeout: 5_000 });
55
+ const bin = stdout.trim() || null;
56
+ log.debug("Using pre-baked server image", { slug, image, bin });
57
+ return { image, bin };
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ }
63
+ /**
64
+ * Choose a base image for the given command.
65
+ * - npx / npm / node / yarn / pnpm → node image
66
+ * - uvx / uv / python / python3 / pip → python image
67
+ * - anything else → default image
68
+ */
69
+ export function selectImage(command, images = DEFAULT_IMAGES) {
70
+ const c = command.toLowerCase().replace(/\.(exe|cmd)$/, "");
71
+ if (["npx", "npm", "node", "yarn", "pnpm"].includes(c))
72
+ return images.node;
73
+ if (["uvx", "uv", "python", "python3", "pip", "pip3"].includes(c))
74
+ return images.python;
75
+ return images.default;
76
+ }
77
+ /**
78
+ * Compute the run command for a pre-baked image. Runs the baked bin directly
79
+ * with any args that followed the package spec in the original invocation.
80
+ * Falls back to the original command if no bin was detected.
81
+ */
82
+ function bakedRunCommand(baked, command, args) {
83
+ if (!baked.bin) {
84
+ // No bin label — best effort: run original command (may fail offline).
85
+ return { cmd: command, cmdArgs: args };
86
+ }
87
+ // Pass through args that came AFTER the package token. The package token is
88
+ // the first non-flag arg (e.g. "@scope/pkg"); npx flags like -y precede it.
89
+ const pkgIdx = args.findIndex((a) => !a.startsWith("-"));
90
+ const passThrough = pkgIdx >= 0 ? args.slice(pkgIdx + 1) : [];
91
+ return { cmd: baked.bin, cmdArgs: passThrough };
92
+ }
93
+ export function buildContainerCommand(engine, manifest, command, args, opts = {}) {
94
+ const images = opts.images ?? DEFAULT_IMAGES;
95
+ const runArgs = ["run", "--rm", "-i"];
96
+ // ── Hardening (always applied) ──────────────────────────────────────────
97
+ runArgs.push("--cap-drop", "ALL");
98
+ runArgs.push("--security-opt", "no-new-privileges");
99
+ runArgs.push("--read-only");
100
+ // Writable scratch space the server can always use.
101
+ runArgs.push("--tmpfs", "/tmp:rw,exec,nosuid,size=128m");
102
+ // Run as a non-root uid:gid. 1000:1000 is the conventional first user.
103
+ runArgs.push("--user", "1000:1000");
104
+ // ── Network ─────────────────────────────────────────────────────────────
105
+ // NOTE: "allowlist" egress filtering requires a proxy/sidecar and is not yet
106
+ // hard-enforced — it currently maps to the default bridge with the intent
107
+ // declared in the manifest. "none" and "all" are enforced.
108
+ switch (manifest.network.mode) {
109
+ case "none":
110
+ runArgs.push("--network", "none");
111
+ break;
112
+ case "allowlist":
113
+ // Route egress through the gateway's allowlist proxy. The container uses
114
+ // the default bridge but only proxy-aware traffic to allowlisted hosts
115
+ // succeeds; the host.docker.internal mapping lets it reach the proxy.
116
+ if (opts.egressProxyUrl) {
117
+ runArgs.push("--add-host", "host.docker.internal:host-gateway");
118
+ runArgs.push("-e", `HTTPS_PROXY=${opts.egressProxyUrl}`);
119
+ runArgs.push("-e", `HTTP_PROXY=${opts.egressProxyUrl}`);
120
+ runArgs.push("-e", `https_proxy=${opts.egressProxyUrl}`);
121
+ runArgs.push("-e", `http_proxy=${opts.egressProxyUrl}`);
122
+ // Don't proxy localhost / the proxy host itself.
123
+ runArgs.push("-e", "NO_PROXY=localhost,127.0.0.1");
124
+ }
125
+ break;
126
+ case "all":
127
+ // default bridge network (egress allowed).
128
+ break;
129
+ }
130
+ // ── Resource limits ─────────────────────────────────────────────────────
131
+ if (manifest.limits?.maxMemoryMb) {
132
+ runArgs.push("--memory", `${manifest.limits.maxMemoryMb}m`);
133
+ }
134
+ if (manifest.limits?.maxCpuPercent) {
135
+ const cpus = Math.max(0.1, manifest.limits.maxCpuPercent / 100);
136
+ runArgs.push("--cpus", cpus.toFixed(2));
137
+ }
138
+ // ── Filesystem mounts ───────────────────────────────────────────────────
139
+ for (const p of manifest.filesystem.read) {
140
+ runArgs.push("-v", `${p}:${p}:ro`);
141
+ }
142
+ for (const p of manifest.filesystem.write) {
143
+ runArgs.push("-v", `${p}:${p}:rw`);
144
+ }
145
+ // ── Writable cache/home redirection ─────────────────────────────────────
146
+ // Under --read-only root, the default HOME (/home/node, /root) and package
147
+ // caches (~/.npm, ~/.cache) are not writable. Redirect them to the tmpfs so
148
+ // npx / uvx can install the server on first run inside the sandbox.
149
+ runArgs.push("-e", "HOME=/tmp");
150
+ runArgs.push("-e", "XDG_CACHE_HOME=/tmp/.cache");
151
+ const image = selectImage(command, images);
152
+ if (image === images.python) {
153
+ runArgs.push("-e", "UV_CACHE_DIR=/tmp/.uv");
154
+ runArgs.push("-e", "PIP_CACHE_DIR=/tmp/.pip");
155
+ }
156
+ else {
157
+ runArgs.push("-e", "npm_config_cache=/tmp/.npm");
158
+ }
159
+ // ── Environment forwarding (names only; values come from spawn env) ──────
160
+ const forwardedEnvNames = [];
161
+ for (const name of manifest.env.allow) {
162
+ runArgs.push("-e", name);
163
+ forwardedEnvNames.push(name);
164
+ }
165
+ // ── Image + the actual server command ───────────────────────────────────
166
+ // A pre-baked image already contains the package, so run its bin offline.
167
+ if (opts.bakedImage) {
168
+ const { cmd, cmdArgs } = bakedRunCommand(opts.bakedImage, command, args);
169
+ runArgs.push(opts.bakedImage.image);
170
+ runArgs.push(cmd, ...cmdArgs);
171
+ }
172
+ else {
173
+ runArgs.push(image);
174
+ runArgs.push(command, ...args);
175
+ }
176
+ return { command: engine, args: runArgs, forwardedEnvNames };
177
+ }
178
+ //# sourceMappingURL=container-runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"container-runtime.js","sourceRoot":"","sources":["../../src/sandbox/container-runtime.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Match a "host:port" target against an allowlist pattern.
3
+ *
4
+ * Supported patterns:
5
+ * - "api.github.com:443" exact host + port
6
+ * - "api.github.com" exact host, any port
7
+ * - "*.githubusercontent.com" wildcard subdomain, any port
8
+ * - "*.example.com:443" wildcard subdomain + port
9
+ */
10
+ export declare function matchesAllowlist(host: string, port: number, allowlist: string[]): boolean;
11
+ /**
12
+ * An in-process HTTP forward proxy that enforces a host allowlist via the
13
+ * CONNECT method (used by HTTPS clients). Connections to non-allowlisted
14
+ * hosts are refused with 403.
15
+ *
16
+ * Containerized servers are pointed at this proxy via HTTPS_PROXY/HTTP_PROXY
17
+ * and reach it through `host.docker.internal`. This gives `network: allowlist`
18
+ * real teeth without a container sidecar.
19
+ *
20
+ * Limitation: only proxy-aware clients (those honoring HTTP(S)_PROXY) are
21
+ * filtered. Clients that ignore proxy env vars bypass it — full enforcement
22
+ * still requires L2 network namespacing, but this covers the common case
23
+ * (node fetch/undici, axios, requests, etc. all honor the proxy env).
24
+ */
25
+ export declare class EgressProxy {
26
+ private readonly allowlist;
27
+ private readonly slug;
28
+ /** Optional safety audit log — records blocked/allowed egress for forensics. */
29
+ private readonly auditLog?;
30
+ private server;
31
+ private port;
32
+ private blockedCount;
33
+ constructor(allowlist: string[], slug: string,
34
+ /** Optional safety audit log — records blocked/allowed egress for forensics. */
35
+ auditLog?: {
36
+ record: (e: {
37
+ type: "egress_blocked" | "egress_allowed";
38
+ slug: string;
39
+ target?: string;
40
+ reason?: string;
41
+ }) => void;
42
+ } | undefined);
43
+ /** Start listening on a random loopback port. Resolves with the port. */
44
+ start(): Promise<number>;
45
+ private handleConnect;
46
+ /** The proxy URL the container should use (via host.docker.internal). */
47
+ get containerProxyUrl(): string;
48
+ get blocked(): number;
49
+ /** Stop the proxy. */
50
+ stop(): Promise<void>;
51
+ }
@@ -0,0 +1,146 @@
1
+ import { createServer } from "node:http";
2
+ import { connect as netConnect } from "node:net";
3
+ import { log } from "../utils/logger.js";
4
+ /**
5
+ * Match a "host:port" target against an allowlist pattern.
6
+ *
7
+ * Supported patterns:
8
+ * - "api.github.com:443" exact host + port
9
+ * - "api.github.com" exact host, any port
10
+ * - "*.githubusercontent.com" wildcard subdomain, any port
11
+ * - "*.example.com:443" wildcard subdomain + port
12
+ */
13
+ export function matchesAllowlist(host, port, allowlist) {
14
+ const h = host.toLowerCase();
15
+ for (const raw of allowlist) {
16
+ const pattern = raw.toLowerCase().trim();
17
+ if (!pattern)
18
+ continue;
19
+ const [patHost, patPort] = pattern.includes(":")
20
+ ? pattern.split(":")
21
+ : [pattern, undefined];
22
+ // Port check (if the pattern specified one)
23
+ if (patPort !== undefined && patPort !== String(port))
24
+ continue;
25
+ // Host check — exact or wildcard
26
+ if (patHost.startsWith("*.")) {
27
+ const suffix = patHost.slice(1); // ".githubusercontent.com"
28
+ if (h.endsWith(suffix) || h === patHost.slice(2))
29
+ return true;
30
+ }
31
+ else if (h === patHost) {
32
+ return true;
33
+ }
34
+ }
35
+ return false;
36
+ }
37
+ /**
38
+ * An in-process HTTP forward proxy that enforces a host allowlist via the
39
+ * CONNECT method (used by HTTPS clients). Connections to non-allowlisted
40
+ * hosts are refused with 403.
41
+ *
42
+ * Containerized servers are pointed at this proxy via HTTPS_PROXY/HTTP_PROXY
43
+ * and reach it through `host.docker.internal`. This gives `network: allowlist`
44
+ * real teeth without a container sidecar.
45
+ *
46
+ * Limitation: only proxy-aware clients (those honoring HTTP(S)_PROXY) are
47
+ * filtered. Clients that ignore proxy env vars bypass it — full enforcement
48
+ * still requires L2 network namespacing, but this covers the common case
49
+ * (node fetch/undici, axios, requests, etc. all honor the proxy env).
50
+ */
51
+ export class EgressProxy {
52
+ allowlist;
53
+ slug;
54
+ auditLog;
55
+ server = null;
56
+ port = 0;
57
+ blockedCount = 0;
58
+ constructor(allowlist, slug,
59
+ /** Optional safety audit log — records blocked/allowed egress for forensics. */
60
+ auditLog) {
61
+ this.allowlist = allowlist;
62
+ this.slug = slug;
63
+ this.auditLog = auditLog;
64
+ }
65
+ /** Start listening on a random loopback port. Resolves with the port. */
66
+ async start() {
67
+ return new Promise((resolve, reject) => {
68
+ const server = createServer((req, res) => {
69
+ // Plain HTTP proxying is not supported — only CONNECT (HTTPS tunnel).
70
+ // Reject non-CONNECT requests; most MCP servers use HTTPS APIs.
71
+ res.writeHead(405, { "Content-Type": "text/plain" });
72
+ res.end("Only CONNECT (HTTPS) is proxied by the gateway egress filter.\n");
73
+ });
74
+ server.on("connect", (req, clientSocket, head) => {
75
+ this.handleConnect(req, clientSocket, head);
76
+ });
77
+ server.on("error", (err) => reject(err));
78
+ // Bind to loopback only — the container reaches it via host.docker.internal,
79
+ // which Docker maps to the host loopback.
80
+ server.listen(0, "0.0.0.0", () => {
81
+ const addr = server.address();
82
+ this.port = typeof addr === "object" && addr ? addr.port : 0;
83
+ this.server = server;
84
+ log.info("Egress proxy started", {
85
+ slug: this.slug,
86
+ port: this.port,
87
+ allowlist: this.allowlist,
88
+ });
89
+ resolve(this.port);
90
+ });
91
+ });
92
+ }
93
+ handleConnect(req, clientSocket, head) {
94
+ // req.url for CONNECT is "host:port"
95
+ const target = req.url || "";
96
+ const [host, portStr] = target.split(":");
97
+ const port = parseInt(portStr || "443", 10);
98
+ if (!host || !matchesAllowlist(host, port, this.allowlist)) {
99
+ this.blockedCount++;
100
+ log.warn("Egress blocked by allowlist", {
101
+ slug: this.slug,
102
+ target,
103
+ blockedTotal: this.blockedCount,
104
+ });
105
+ this.auditLog?.record({
106
+ type: "egress_blocked",
107
+ slug: this.slug,
108
+ target,
109
+ reason: "not in network allowlist",
110
+ });
111
+ clientSocket.write("HTTP/1.1 403 Forbidden\r\n\r\nBlocked by MCP gateway egress allowlist\r\n");
112
+ clientSocket.end();
113
+ return;
114
+ }
115
+ this.auditLog?.record({ type: "egress_allowed", slug: this.slug, target });
116
+ // Allowed — open the upstream tunnel and pipe bytes both ways.
117
+ const upstream = netConnect(port, host, () => {
118
+ clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
119
+ if (head && head.length)
120
+ upstream.write(head);
121
+ upstream.pipe(clientSocket);
122
+ clientSocket.pipe(upstream);
123
+ });
124
+ upstream.on("error", (err) => {
125
+ log.debug("Egress upstream error", { slug: this.slug, target, error: String(err) });
126
+ clientSocket.end();
127
+ });
128
+ clientSocket.on("error", () => upstream.end());
129
+ }
130
+ /** The proxy URL the container should use (via host.docker.internal). */
131
+ get containerProxyUrl() {
132
+ return `http://host.docker.internal:${this.port}`;
133
+ }
134
+ get blocked() {
135
+ return this.blockedCount;
136
+ }
137
+ /** Stop the proxy. */
138
+ async stop() {
139
+ if (!this.server)
140
+ return;
141
+ await new Promise((resolve) => this.server.close(() => resolve()));
142
+ this.server = null;
143
+ log.debug("Egress proxy stopped", { slug: this.slug });
144
+ }
145
+ }
146
+ //# sourceMappingURL=egress-proxy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"egress-proxy.js","sourceRoot":"","sources":["../../src/sandbox/egress-proxy.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ import type { EnvPolicy } from "./types.js";
2
+ /**
3
+ * Build the scoped environment for a downstream stdio process.
4
+ *
5
+ * This is the first hard-enforced sandbox control (L1). Instead of passing the
6
+ * full inherited environment (which can leak API keys, cloud credentials, and
7
+ * tokens sitting in the parent process's env), we pass ONLY:
8
+ *
9
+ * 1. The SDK's safe default vars (PATH, HOME, etc.) — when `inheritDefaults`
10
+ * is true and the process needs them to start.
11
+ * 2. The explicitly allowlisted var NAMES from the manifest.
12
+ * 3. The user-supplied env values for this connection (always honored — the
13
+ * user clearly intends those to flow to this server).
14
+ *
15
+ * Everything else in `process.env` is withheld.
16
+ *
17
+ * @param policy Env policy from the resolved capability manifest
18
+ * @param userEnv Env values the user passed at connect time
19
+ * @param sourceEnv The environment to scope from (defaults to process.env)
20
+ * @returns The scoped environment object to hand to StdioClientTransport
21
+ */
22
+ export declare function buildScopedEnvironment(policy: EnvPolicy, userEnv: Record<string, string> | undefined, sourceEnv?: NodeJS.ProcessEnv): Record<string, string>;
23
+ /**
24
+ * Returns the names of environment variables that WOULD have been exposed
25
+ * under the old "inherit everything" behavior but are now withheld. Used for
26
+ * surfacing the security improvement to the user / logs without ever printing
27
+ * the values themselves.
28
+ */
29
+ export declare function withheldEnvNames(scoped: Record<string, string>, sourceEnv?: NodeJS.ProcessEnv): string[];
@@ -0,0 +1,60 @@
1
+ import { getDefaultEnvironment } from "@modelcontextprotocol/sdk/client/stdio.js";
2
+ import { log } from "../utils/logger.js";
3
+ /**
4
+ * Build the scoped environment for a downstream stdio process.
5
+ *
6
+ * This is the first hard-enforced sandbox control (L1). Instead of passing the
7
+ * full inherited environment (which can leak API keys, cloud credentials, and
8
+ * tokens sitting in the parent process's env), we pass ONLY:
9
+ *
10
+ * 1. The SDK's safe default vars (PATH, HOME, etc.) — when `inheritDefaults`
11
+ * is true and the process needs them to start.
12
+ * 2. The explicitly allowlisted var NAMES from the manifest.
13
+ * 3. The user-supplied env values for this connection (always honored — the
14
+ * user clearly intends those to flow to this server).
15
+ *
16
+ * Everything else in `process.env` is withheld.
17
+ *
18
+ * @param policy Env policy from the resolved capability manifest
19
+ * @param userEnv Env values the user passed at connect time
20
+ * @param sourceEnv The environment to scope from (defaults to process.env)
21
+ * @returns The scoped environment object to hand to StdioClientTransport
22
+ */
23
+ export function buildScopedEnvironment(policy, userEnv, sourceEnv = process.env) {
24
+ const scoped = {};
25
+ // 1. Safe defaults required for the process to launch (PATH, HOME, etc.)
26
+ if (policy.inheritDefaults) {
27
+ Object.assign(scoped, getDefaultEnvironment());
28
+ }
29
+ // 2. Allowlisted variable names pulled from the source environment.
30
+ let leakedBlocked = 0;
31
+ for (const name of policy.allow) {
32
+ const value = sourceEnv[name];
33
+ if (value !== undefined && !value.startsWith("()")) {
34
+ scoped[name] = value;
35
+ }
36
+ }
37
+ // 3. User-supplied env always flows (explicit intent for this connection).
38
+ if (userEnv) {
39
+ Object.assign(scoped, userEnv);
40
+ }
41
+ // Diagnostics: how many parent-env vars were withheld (not the values).
42
+ const withheld = Object.keys(sourceEnv).filter((k) => !(k in scoped));
43
+ leakedBlocked = withheld.length;
44
+ log.debug("Scoped downstream environment", {
45
+ allowed: Object.keys(scoped).length,
46
+ withheld: leakedBlocked,
47
+ inheritDefaults: policy.inheritDefaults,
48
+ });
49
+ return scoped;
50
+ }
51
+ /**
52
+ * Returns the names of environment variables that WOULD have been exposed
53
+ * under the old "inherit everything" behavior but are now withheld. Used for
54
+ * surfacing the security improvement to the user / logs without ever printing
55
+ * the values themselves.
56
+ */
57
+ export function withheldEnvNames(scoped, sourceEnv = process.env) {
58
+ return Object.keys(sourceEnv).filter((k) => !(k in scoped));
59
+ }
60
+ //# sourceMappingURL=env-scoper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env-scoper.js","sourceRoot":"","sources":["../../src/sandbox/env-scoper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,2CAA2C,CAAC;AAClF,OAAO,EAAE,GAAG,EAAE,MAAM,oBAAoB,CAAC;AAGzC;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,sBAAsB,CACpC,MAAiB,EACjB,OAA2C,EAC3C,YAA+B,OAAO,CAAC,GAAG;IAE1C,MAAM,MAAM,GAA2B,EAAE,CAAC;IAE1C,yEAAyE;IACzE,IAAI,MAAM,CAAC,eAAe,EAAE,CAAC;QAC3B,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,qBAAqB,EAAE,CAAC,CAAC;IACjD,CAAC;IAED,oEAAoE;IACpE,IAAI,aAAa,GAAG,CAAC,CAAC;IACtB,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QAChC,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;QAC9B,IAAI,KAAK,KAAK,SAAS,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YACnD,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;QACvB,CAAC;IACH,CAAC;IAED,2EAA2E;IAC3E,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,CAAC;IAED,wEAAwE;IACxE,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,CAC5C,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,CACtB,CAAC;IACF,aAAa,GAAG,QAAQ,CAAC,MAAM,CAAC;IAChC,GAAG,CAAC,KAAK,CAAC,+BAA+B,EAAE;QACzC,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM;QACnC,QAAQ,EAAE,aAAa;QACvB,eAAe,EAAE,MAAM,CAAC,eAAe;KACxC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAC9B,MAA8B,EAC9B,YAA+B,OAAO,CAAC,GAAG;IAE1C,OAAO,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC;AAC9D,CAAC"}
@@ -0,0 +1,40 @@
1
+ import type { TrustTier, RegistryServer } from "../registry/types.js";
2
+ import type { CapabilityManifest, ManifestSummary } from "./types.js";
3
+ /**
4
+ * Resolves and persists per-server capability manifests.
5
+ *
6
+ * A manifest is derived from (in priority order):
7
+ * 1. A user-saved override (persisted to disk)
8
+ * 2. Trust-tier defaults + the server's declared auth env vars
9
+ */
10
+ export declare class ManifestResolver {
11
+ private overrides;
12
+ private readonly filePath;
13
+ /**
14
+ * When true, untrusted tiers (community/unknown) default to l2-container
15
+ * enforcement instead of l1-process. Verified/trusted stay on L1.
16
+ */
17
+ private readonly containerIsolation;
18
+ constructor(filePath: string, containerIsolation?: boolean);
19
+ /**
20
+ * Resolve the effective manifest for a server. Uses a saved override if one
21
+ * exists; otherwise derives defaults from the trust tier and the server's
22
+ * declared auth env vars (so e.g. a GitHub server gets GITHUB_TOKEN allowed
23
+ * but nothing else).
24
+ */
25
+ resolve(slug: string, trustTier: TrustTier, registryServer?: RegistryServer | null,
26
+ /** Env var names the user explicitly supplied at connect time. */
27
+ userProvidedEnvKeys?: string[]): CapabilityManifest;
28
+ /** Get a saved override for a slug, if one exists. */
29
+ getOverride(slug: string): CapabilityManifest | undefined;
30
+ /** Persist a user-customized manifest. */
31
+ saveOverride(manifest: CapabilityManifest): void;
32
+ /** Remove a saved override, reverting to trust-tier defaults. */
33
+ clearOverride(slug: string): void;
34
+ /**
35
+ * Build a human-readable summary of a manifest for the HITL prompt.
36
+ */
37
+ static summarize(manifest: CapabilityManifest): ManifestSummary;
38
+ private loadFromDisk;
39
+ private saveToDisk;
40
+ }