@adtl/gateway 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,121 @@
1
+ import { DECISION_INFO_KEY, INVOKE_METHOD, LIST_METHOD, } from "@adtl/interceptors";
2
+ import { z } from "zod";
3
+ const AnyResult = z.object({}).passthrough();
4
+ /** Untyped request: interceptor methods are extension methods the SDK does not type. */
5
+ function request(client, method, params, timeout) {
6
+ const r = client.request;
7
+ return r.call(client, { method, params }, AnyResult, { timeout });
8
+ }
9
+ function priority(def, phase) {
10
+ const p = def.priorityHint;
11
+ if (p === undefined)
12
+ return 0;
13
+ if (typeof p === "number")
14
+ return p;
15
+ return p[phase] ?? 0;
16
+ }
17
+ function hooks(def, event, phase) {
18
+ return def.hooks.some((h) => h.phase === phase && (h.events.includes(event) || h.events.includes("*")));
19
+ }
20
+ export class InterceptorInvoker {
21
+ interceptors;
22
+ timeoutMs;
23
+ modeOverride;
24
+ constructor(interceptors, timeoutMs = 5000,
25
+ /** Overrides the mode every interceptor declares. */
26
+ modeOverride) {
27
+ this.interceptors = interceptors;
28
+ this.timeoutMs = timeoutMs;
29
+ this.modeOverride = modeOverride;
30
+ }
31
+ /** Discover the interceptors an interceptor server offers. */
32
+ static async discover(client) {
33
+ const res = await request(client, LIST_METHOD, {}, 10_000);
34
+ const defs = (res.interceptors ?? []);
35
+ return defs.map((definition) => ({ client, definition }));
36
+ }
37
+ get definitions() {
38
+ return this.interceptors.map((i) => i.definition);
39
+ }
40
+ async runRequest(opts) {
41
+ const out = { payload: opts.payload, results: [], warnings: [] };
42
+ if (!(await this.mutate("request", opts, out)))
43
+ return out;
44
+ await this.validate("request", opts, out);
45
+ return out;
46
+ }
47
+ async runResponse(opts) {
48
+ const out = { payload: opts.payload, results: [], warnings: [] };
49
+ if (!(await this.validate("response", opts, out)))
50
+ return out;
51
+ await this.mutate("response", opts, out);
52
+ return out;
53
+ }
54
+ isAudit(i) {
55
+ return (this.modeOverride ?? i.definition.mode) === "audit";
56
+ }
57
+ select(type, event, phase) {
58
+ return this.interceptors.filter((i) => i.definition.type === type && hooks(i.definition, event, phase));
59
+ }
60
+ async invoke(i, phase, opts, payload) {
61
+ const res = await request(i.client, INVOKE_METHOD, { name: i.definition.name, event: opts.event, phase, payload, config: opts.config, timeoutMs: this.timeoutMs, context: opts.context }, this.timeoutMs);
62
+ return res;
63
+ }
64
+ /** Handle an interceptor failure. Returns false if the chain must stop. */
65
+ failed(i, err, out) {
66
+ const msg = `${i.definition.name} failed: ${err instanceof Error ? err.message : String(err)}`;
67
+ if (this.isAudit(i) || i.definition.failOpen === true) {
68
+ out.warnings.push(msg);
69
+ return true;
70
+ }
71
+ out.blocked = { interceptor: i.definition.name, decision: "block", reason: msg };
72
+ return false;
73
+ }
74
+ /** Sequential mutations, atomic: on failure the original payload is kept. */
75
+ async mutate(phase, opts, out) {
76
+ const chain = this.select("mutation", opts.event, phase).sort((a, b) => priority(a.definition, phase) - priority(b.definition, phase) || a.definition.name.localeCompare(b.definition.name));
77
+ let payload = out.payload;
78
+ for (const i of chain) {
79
+ let r;
80
+ try {
81
+ r = (await this.invoke(i, phase, opts, payload));
82
+ }
83
+ catch (err) {
84
+ if (!this.failed(i, err, out))
85
+ return false;
86
+ continue;
87
+ }
88
+ out.results.push(r);
89
+ if (!this.isAudit(i) && r.modified)
90
+ payload = r.payload;
91
+ }
92
+ out.payload = payload;
93
+ return true;
94
+ }
95
+ /** Parallel validations. Only severity "error" blocks. */
96
+ async validate(phase, opts, out) {
97
+ const chain = this.select("validation", opts.event, phase);
98
+ const settled = await Promise.allSettled(chain.map((i) => this.invoke(i, phase, opts, out.payload)));
99
+ let ok = true;
100
+ settled.forEach((s, idx) => {
101
+ const i = chain[idx];
102
+ if (s.status === "rejected") {
103
+ if (!this.failed(i, s.reason, out))
104
+ ok = false;
105
+ return;
106
+ }
107
+ const r = s.value;
108
+ out.results.push(r);
109
+ if (r.valid || r.severity !== "error" || this.isAudit(i))
110
+ return;
111
+ if (!out.blocked) {
112
+ const decision = r.info?.[DECISION_INFO_KEY] ?? "block";
113
+ const reason = r.messages?.map((m) => m.message).join("; ") || `${i.definition.name} rejected the call`;
114
+ out.blocked = { interceptor: i.definition.name, decision, reason };
115
+ }
116
+ ok = false;
117
+ });
118
+ return ok;
119
+ }
120
+ }
121
+ //# sourceMappingURL=invoker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invoker.js","sourceRoot":"","sources":["../src/invoker.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import type { AdtlConfig } from "@adtl/labels";
2
+ import { type GatewayConfig } from "./config.js";
3
+ import { AdtlGateway, type GatewayLogger } from "./gateway.js";
4
+ export declare class GatewayConfigError extends Error {
5
+ constructor(message: string);
6
+ }
7
+ export interface LoadedConfig {
8
+ path: string;
9
+ labels: AdtlConfig;
10
+ gateway: GatewayConfig;
11
+ }
12
+ export declare function loadGatewayConfig(path: string): Promise<LoadedConfig>;
13
+ /** Path of the bundled adtl-interceptors CLI. */
14
+ export declare function bundledInterceptorsCli(): string;
15
+ export interface StartedGateway {
16
+ gateway: AdtlGateway;
17
+ close(): Promise<void>;
18
+ }
19
+ export declare function startGateway(loaded: LoadedConfig, opts?: {
20
+ env?: NodeJS.ProcessEnv;
21
+ logger?: GatewayLogger;
22
+ }): Promise<StartedGateway>;
23
+ //# sourceMappingURL=startup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"startup.d.ts","sourceRoot":"","sources":["../src/startup.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAI/C,OAAO,EAIL,KAAK,aAAa,EAGnB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,WAAW,EAAmB,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AAGhF,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,OAAO,EAAE,MAAM;CAI5B;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,UAAU,CAAC;IACnB,OAAO,EAAE,aAAa,CAAC;CACxB;AAED,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CA2B3E;AAED,iDAAiD;AACjD,wBAAgB,sBAAsB,IAAI,MAAM,CAI/C;AAsBD,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,WAAW,CAAC;IACrB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,wBAAsB,YAAY,CAChC,MAAM,EAAE,YAAY,EACpB,IAAI,GAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IAAC,MAAM,CAAC,EAAE,aAAa,CAAA;CAAO,GAC7D,OAAO,CAAC,cAAc,CAAC,CAyDzB"}
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Build a running gateway from an adtl.yaml file: start the interceptor
3
+ * server, connect every upstream, and check that ADTL's interceptors are there.
4
+ */
5
+ import { readFile } from "node:fs/promises";
6
+ import { dirname, isAbsolute, resolve } from "node:path";
7
+ import { createRequire } from "node:module";
8
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
9
+ import { StdioClientTransport, getDefaultEnvironment } from "@modelcontextprotocol/sdk/client/stdio.js";
10
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
11
+ import { INTERCEPTORS_EXTENSION, LABEL_INTERCEPTOR, POLICY_INTERCEPTOR, parseConfigText } from "@adtl/interceptors";
12
+ import { parse as parseYaml } from "yaml";
13
+ import { ZodError } from "zod";
14
+ import { JsonlAuditLog } from "./audit.js";
15
+ import { GatewayConfigSchema, expandUpstream, isHttpUpstream, } from "./config.js";
16
+ import { AdtlGateway, GATEWAY_VERSION } from "./gateway.js";
17
+ import { InterceptorInvoker } from "./invoker.js";
18
+ export class GatewayConfigError extends Error {
19
+ constructor(message) {
20
+ super(message);
21
+ this.name = "GatewayConfigError";
22
+ }
23
+ }
24
+ export async function loadGatewayConfig(path) {
25
+ const abs = resolve(path);
26
+ let text;
27
+ try {
28
+ text = await readFile(abs, "utf8");
29
+ }
30
+ catch (err) {
31
+ throw new GatewayConfigError(`Cannot read config file ${abs}: ${err.message}`);
32
+ }
33
+ let labels;
34
+ try {
35
+ labels = parseConfigText(text, abs);
36
+ }
37
+ catch (err) {
38
+ throw new GatewayConfigError(err.message);
39
+ }
40
+ const raw = (parseYaml(text) ?? {});
41
+ if (raw.gateway === undefined) {
42
+ throw new GatewayConfigError(`${abs}: missing "gateway:" section (at least one upstream under gateway.upstreams)`);
43
+ }
44
+ try {
45
+ return { path: abs, labels, gateway: GatewayConfigSchema.parse(raw.gateway) };
46
+ }
47
+ catch (err) {
48
+ if (err instanceof ZodError) {
49
+ const problems = err.issues.map((i) => ` - gateway.${i.path.join(".")}: ${i.message}`).join("\n");
50
+ throw new GatewayConfigError(`${abs}: invalid gateway config\n${problems}`);
51
+ }
52
+ throw err;
53
+ }
54
+ }
55
+ /** Path of the bundled adtl-interceptors CLI. */
56
+ export function bundledInterceptorsCli() {
57
+ // Resolve via package.json: works in plain Node and under tools that lack import.meta.resolve.
58
+ const pkg = createRequire(import.meta.url).resolve("@adtl/interceptors/package.json");
59
+ return resolve(dirname(pkg), "dist", "cli.js");
60
+ }
61
+ function stdioTransport(u, baseDir) {
62
+ return new StdioClientTransport({
63
+ command: u.command,
64
+ args: u.args,
65
+ // The SDK passes only `env` when given, which would drop PATH; start from its safe defaults.
66
+ env: { ...getDefaultEnvironment(), ...u.env },
67
+ ...(u.cwd !== undefined ? { cwd: isAbsolute(u.cwd) ? u.cwd : resolve(baseDir, u.cwd) } : {}),
68
+ stderr: "inherit",
69
+ });
70
+ }
71
+ async function connect(id, u, baseDir) {
72
+ const client = new Client({ name: `adtl-gateway→${id}`, version: GATEWAY_VERSION });
73
+ const transport = isHttpUpstream(u)
74
+ ? new StreamableHTTPClientTransport(new URL(u.url), { requestInit: { headers: u.headers } })
75
+ : stdioTransport(u, baseDir);
76
+ await client.connect(transport);
77
+ return client;
78
+ }
79
+ export async function startGateway(loaded, opts = {}) {
80
+ const env = opts.env ?? process.env;
81
+ const baseDir = dirname(loaded.path);
82
+ const log = opts.logger ?? { info: (m) => console.error(`[adtl-gateway] ${m}`), warn: (m) => console.error(`[adtl-gateway] warning: ${m}`) };
83
+ const clients = [];
84
+ const closeAll = () => Promise.allSettled(clients.map((c) => c.close())).then(() => undefined);
85
+ try {
86
+ // 1. Interceptor server (out of process, per the Interceptors WG).
87
+ const icfg = loaded.gateway.interceptors
88
+ ? expandUpstream("interceptors", loaded.gateway.interceptors, env)
89
+ : { command: process.execPath, args: [bundledInterceptorsCli(), "--config", loaded.path], env: {} };
90
+ const interceptors = await connect("interceptors", icfg, baseDir);
91
+ clients.push(interceptors);
92
+ if (!interceptors.getServerCapabilities()?.extensions?.[INTERCEPTORS_EXTENSION]) {
93
+ throw new GatewayConfigError(`The interceptor server does not declare the ${INTERCEPTORS_EXTENSION} extension`);
94
+ }
95
+ const bound = await InterceptorInvoker.discover(interceptors);
96
+ const names = bound.map((b) => b.definition.name);
97
+ for (const required of [POLICY_INTERCEPTOR, LABEL_INTERCEPTOR]) {
98
+ if (!names.includes(required)) {
99
+ throw new GatewayConfigError(`The interceptor server does not offer ${required}; refusing to start without it`);
100
+ }
101
+ }
102
+ const audit = loaded.gateway.mode === "audit";
103
+ const invoker = new InterceptorInvoker(bound, loaded.gateway.interceptorTimeoutMs, audit ? "audit" : undefined);
104
+ log.info(`interceptors: ${names.join(", ")}`);
105
+ if (audit)
106
+ log.warn("audit mode: calls are logged but never stopped or changed");
107
+ // 2. Upstream servers. One failing upstream does not stop the others.
108
+ const upstreams = new Map();
109
+ const ids = Object.keys(loaded.gateway.upstreams);
110
+ const settled = await Promise.allSettled(ids.map((id) => connect(id, expandUpstream(id, loaded.gateway.upstreams[id], env), baseDir)));
111
+ settled.forEach((s, i) => {
112
+ const id = ids[i];
113
+ if (s.status === "fulfilled") {
114
+ clients.push(s.value);
115
+ upstreams.set(id, s.value);
116
+ if (!loaded.labels.servers[id]) {
117
+ log.warn(`upstream "${id}" has no entry under servers: in the label config; its results get the default label`);
118
+ }
119
+ }
120
+ else {
121
+ log.warn(`could not connect to upstream "${id}": ${s.reason.message}`);
122
+ }
123
+ });
124
+ if (upstreams.size === 0)
125
+ throw new GatewayConfigError("No upstream server could be connected");
126
+ log.info(`upstreams: ${[...upstreams.keys()].join(", ")}`);
127
+ const auditLog = loaded.gateway.audit ? new JsonlAuditLog(resolve(baseDir, loaded.gateway.audit)) : undefined;
128
+ const gateway = new AdtlGateway({ upstreams, invoker, ...(auditLog ? { audit: auditLog } : {}), logger: log });
129
+ return { gateway, close: closeAll };
130
+ }
131
+ catch (err) {
132
+ await closeAll();
133
+ throw err;
134
+ }
135
+ }
136
+ //# sourceMappingURL=startup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"startup.js","sourceRoot":"","sources":["../src/startup.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,61 @@
1
1
  {
2
2
  "name": "@adtl/gateway",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Experimental: MCP gateway that labels every tool result and blocks risky tool calls using ADTL trust labels",
5
+ "keywords": [
6
+ "mcp",
7
+ "model-context-protocol",
8
+ "mcp-gateway",
9
+ "mcp-proxy",
10
+ "ai-agents",
11
+ "ai-security",
12
+ "prompt-injection",
13
+ "trust-labels"
14
+ ],
15
+ "author": "Murthy G",
16
+ "license": "Apache-2.0",
17
+ "type": "module",
18
+ "main": "./dist/index.js",
19
+ "types": "./dist/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js"
24
+ }
25
+ },
26
+ "bin": {
27
+ "adtl-gateway": "./dist/cli.js"
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "src",
32
+ "README.md",
33
+ "LICENSE"
34
+ ],
35
+ "scripts": {
36
+ "build": "tsc -b",
37
+ "test": "vitest run"
38
+ },
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/probita-ai/adtl.git",
42
+ "directory": "packages/gateway"
43
+ },
44
+ "homepage": "https://github.com/probita-ai/adtl/tree/main/packages/gateway#readme",
45
+ "bugs": {
46
+ "url": "https://github.com/probita-ai/adtl/issues"
47
+ },
48
+ "dependencies": {
49
+ "@adtl/interceptors": "0.1.1",
50
+ "@adtl/labels": "0.1.1",
51
+ "@modelcontextprotocol/sdk": "1.31.0",
52
+ "yaml": "2.9.1",
53
+ "zod": "3.25.76"
54
+ },
55
+ "engines": {
56
+ "node": ">=20"
57
+ },
58
+ "publishConfig": {
59
+ "access": "public"
60
+ }
61
+ }
package/src/audit.ts ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Append-only JSONL audit log. One line per tool call.
3
+ *
4
+ * Arguments and results are never written; the call is identified by its
5
+ * hash, so the log can be shared without leaking the data it describes.
6
+ */
7
+ import { appendFileSync, mkdirSync } from "node:fs";
8
+ import { dirname } from "node:path";
9
+
10
+ export interface AuditEntry {
11
+ ts: string;
12
+ sessionId: string;
13
+ event: "tools/call";
14
+ server: string;
15
+ tool: string;
16
+ /** Final outcome of the call. */
17
+ outcome: "forwarded" | "blocked" | "upstream_error";
18
+ /** The policy's decision. In audit mode a call can be forwarded with decision "ask" or "block". */
19
+ decision?: string;
20
+ /** Present when the gateway ran in audit mode: nothing was stopped or changed. */
21
+ mode?: "audit";
22
+ reason?: string;
23
+ matched?: unknown;
24
+ callHash?: unknown;
25
+ label?: unknown;
26
+ sessionLabel?: unknown;
27
+ warnings?: string[];
28
+ durationMs: number;
29
+ }
30
+
31
+ export interface AuditSink {
32
+ write(entry: AuditEntry): void;
33
+ }
34
+
35
+ export class JsonlAuditLog implements AuditSink {
36
+ constructor(readonly path: string) {
37
+ mkdirSync(dirname(path), { recursive: true });
38
+ }
39
+
40
+ write(entry: AuditEntry): void {
41
+ appendFileSync(this.path, `${JSON.stringify(entry)}\n`, "utf8");
42
+ }
43
+ }
44
+
45
+ export class MemoryAuditLog implements AuditSink {
46
+ readonly entries: AuditEntry[] = [];
47
+ write(entry: AuditEntry): void {
48
+ this.entries.push(entry);
49
+ }
50
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * adtl-gateway: an MCP server for your agent that proxies to your real MCP
4
+ * servers through ADTL's interceptors.
5
+ *
6
+ * adtl-gateway --config adtl.yaml [--audit]
7
+ *
8
+ * Point your MCP client (e.g. Claude Code's .mcp.json) at this command.
9
+ * Logs go to stderr; stdout carries MCP JSON-RPC only.
10
+ */
11
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
+ import { EnvExpansionError } from "./config.js";
13
+ import { GATEWAY_VERSION } from "./gateway.js";
14
+ import { GatewayConfigError, loadGatewayConfig, startGateway } from "./startup.js";
15
+
16
+ const USAGE = `Usage: adtl-gateway --config <adtl.yaml> [--audit]
17
+
18
+ Runs an MCP server over stdio that forwards tool calls to the upstream
19
+ servers in the config, checking and labeling each call with ADTL.
20
+
21
+ Options:
22
+ -c, --config <path> ADTL config file with a gateway: section. Required.
23
+ --audit Log what would be stopped, but stop nothing
24
+ (same as gateway.mode: audit).
25
+ -v, --version Print the version and exit.
26
+ -h, --help Show this help.`;
27
+
28
+ interface Args {
29
+ config?: string;
30
+ audit?: boolean;
31
+ help?: boolean;
32
+ version?: boolean;
33
+ }
34
+
35
+ function parseArgs(argv: string[]): Args {
36
+ const out: Args = {};
37
+ for (let i = 0; i < argv.length; i++) {
38
+ const a = argv[i];
39
+ if (a === "-h" || a === "--help") out.help = true;
40
+ else if (a === "-v" || a === "--version") out.version = true;
41
+ else if (a === "-c" || a === "--config") out.config = argv[++i];
42
+ else if (a?.startsWith("--config=")) out.config = a.slice("--config=".length);
43
+ else if (a === "--audit") out.audit = true;
44
+ else throw new GatewayConfigError(`Unknown argument: ${a}\n\n${USAGE}`);
45
+ }
46
+ return out;
47
+ }
48
+
49
+ async function main(): Promise<void> {
50
+ const args = parseArgs(process.argv.slice(2));
51
+ // --help and --version print to stdout and exit; MCP is never served in these modes.
52
+ if (args.help) return void console.log(USAGE);
53
+ if (args.version) return void console.log(GATEWAY_VERSION);
54
+ if (!args.config) throw new GatewayConfigError(`Missing --config\n\n${USAGE}`);
55
+
56
+ const loaded = await loadGatewayConfig(args.config);
57
+ if (args.audit) loaded.gateway.mode = "audit";
58
+ const started = await startGateway(loaded);
59
+ const server = started.gateway.createServer();
60
+
61
+ let closing = false;
62
+ const shutdown = async () => {
63
+ if (closing) return;
64
+ closing = true;
65
+ await server.close().catch(() => undefined);
66
+ await started.close();
67
+ process.exit(0);
68
+ };
69
+ server.onclose = () => void shutdown();
70
+ // The agent closing our stdin means the session is over; don't wait for a signal.
71
+ process.stdin.on("end", () => void shutdown());
72
+ process.stdin.on("close", () => void shutdown());
73
+ process.on("SIGINT", () => void shutdown());
74
+ process.on("SIGTERM", () => void shutdown());
75
+
76
+ await server.connect(new StdioServerTransport());
77
+ console.error(`[adtl-gateway] ${GATEWAY_VERSION} ready (session ${started.gateway.sessionId})`);
78
+ }
79
+
80
+ main().catch((err: unknown) => {
81
+ const known = err instanceof GatewayConfigError || err instanceof EnvExpansionError;
82
+ console.error(known ? `[adtl-gateway] ${(err as Error).message}` : err);
83
+ process.exit(1);
84
+ });
package/src/config.ts ADDED
@@ -0,0 +1,106 @@
1
+ /**
2
+ * The `gateway:` section of adtl.yaml.
3
+ *
4
+ * gateway:
5
+ * audit: ./adtl-audit.jsonl # optional JSONL audit log
6
+ * mode: audit # optional: log what would be stopped, stop nothing
7
+ * upstreams:
8
+ * github: # id must match `servers.github` in the label config
9
+ * command: npx
10
+ * args: ["-y", "@modelcontextprotocol/server-github"]
11
+ * env: { GITHUB_PERSONAL_ACCESS_TOKEN: "${GITHUB_TOKEN}" }
12
+ * docs:
13
+ * url: https://example.com/mcp
14
+ * headers: { Authorization: "Bearer ${DOCS_TOKEN}" }
15
+ *
16
+ * `${VAR}` in command, args, env, cwd, url and headers is replaced from the
17
+ * gateway's environment. A missing variable is a config error.
18
+ */
19
+ import { z } from "zod";
20
+
21
+ const UPSTREAM_ID = /^[A-Za-z0-9-]+$/;
22
+
23
+ export const StdioUpstreamSchema = z
24
+ .object({
25
+ command: z.string().min(1),
26
+ args: z.array(z.string()).default([]),
27
+ env: z.record(z.string()).default({}),
28
+ cwd: z.string().optional(),
29
+ })
30
+ .strict();
31
+
32
+ export const HttpUpstreamSchema = z
33
+ .object({
34
+ url: z.string().min(1),
35
+ headers: z.record(z.string()).default({}),
36
+ })
37
+ .strict();
38
+
39
+ export const UpstreamSchema = z.union([StdioUpstreamSchema, HttpUpstreamSchema]);
40
+ export type StdioUpstream = z.infer<typeof StdioUpstreamSchema>;
41
+ export type HttpUpstream = z.infer<typeof HttpUpstreamSchema>;
42
+ export type Upstream = z.infer<typeof UpstreamSchema>;
43
+
44
+ export const GatewayConfigSchema = z
45
+ .object({
46
+ upstreams: z
47
+ .record(UpstreamSchema)
48
+ .refine((u) => Object.keys(u).length > 0, "At least one upstream server is required")
49
+ .refine((u) => Object.keys(u).every((id) => UPSTREAM_ID.test(id)), {
50
+ message: "Upstream ids may only contain letters, digits and '-' (they prefix tool names as <id>__<tool>)",
51
+ }),
52
+ /** How to start the interceptor server. Default: the bundled adtl-interceptors with this same config file. */
53
+ interceptors: StdioUpstreamSchema.optional(),
54
+ /** Path of a JSONL audit log. Relative paths are resolved against the config file's directory. */
55
+ audit: z.string().min(1).optional(),
56
+ /**
57
+ * `active` (default) enforces the policy. `audit` stops and changes nothing, and
58
+ * logs what would have been stopped: for trying a policy out safely.
59
+ */
60
+ mode: z.enum(["active", "audit"]).default("active"),
61
+ /** Timeout for each interceptor invocation, in milliseconds. */
62
+ interceptorTimeoutMs: z.number().int().positive().default(5000),
63
+ })
64
+ .strict();
65
+ export type GatewayConfig = z.infer<typeof GatewayConfigSchema>;
66
+
67
+ export const TOOL_SEPARATOR = "__";
68
+
69
+ export class EnvExpansionError extends Error {
70
+ constructor(readonly variable: string, where: string) {
71
+ super(`Environment variable ${variable} is not set (used in ${where})`);
72
+ this.name = "EnvExpansionError";
73
+ }
74
+ }
75
+
76
+ /** Replace `${VAR}` with values from `env`. `$${VAR}` keeps a literal `${VAR}`. */
77
+ export function expandEnv(value: string, env: NodeJS.ProcessEnv, where: string): string {
78
+ return value.replace(/(\$?)\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (match, escape: string, name: string) => {
79
+ if (escape) return match.slice(1);
80
+ const v = env[name];
81
+ if (v === undefined) throw new EnvExpansionError(name, where);
82
+ return v;
83
+ });
84
+ }
85
+
86
+ function expandRecord(rec: Record<string, string>, env: NodeJS.ProcessEnv, where: string): Record<string, string> {
87
+ return Object.fromEntries(Object.entries(rec).map(([k, v]) => [k, expandEnv(v, env, `${where}.${k}`)]));
88
+ }
89
+
90
+ export function expandUpstream<T extends Upstream>(id: string, u: T, env: NodeJS.ProcessEnv): T {
91
+ const where = `gateway.upstreams.${id}`;
92
+ if ("url" in u) {
93
+ return { ...u, url: expandEnv(u.url, env, `${where}.url`), headers: expandRecord(u.headers, env, `${where}.headers`) };
94
+ }
95
+ return {
96
+ ...u,
97
+ command: expandEnv(u.command, env, `${where}.command`),
98
+ args: u.args.map((a, i) => expandEnv(a, env, `${where}.args[${i}]`)),
99
+ env: expandRecord(u.env, env, `${where}.env`),
100
+ ...(u.cwd !== undefined ? { cwd: expandEnv(u.cwd, env, `${where}.cwd`) } : {}),
101
+ };
102
+ }
103
+
104
+ export function isHttpUpstream(u: Upstream): u is HttpUpstream {
105
+ return "url" in u;
106
+ }