@eznix/mcp-gateway 1.3.4 → 1.4.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.
package/README.md CHANGED
@@ -124,8 +124,29 @@ Each entry specifies:
124
124
  - `command` (local only): Array with command and arguments to spawn the upstream server
125
125
  - `url` (remote only): Full URL of the remote MCP server
126
126
  - `transport` (optional, remote only): Override transport detection (`"streamable_http"` or `"websocket"`). Usually auto-detected from URL protocol.
127
+ - `environment` (local only): Environment variables to pass to the spawned process, with `{env:VAR_NAME}` substitution support
127
128
  - `enabled`: Set to false to skip connecting to this server
128
129
 
130
+ #### Environment Variables with Substitution
131
+
132
+ Local MCP servers support environment variable substitution using `{env:VAR_NAME}` syntax:
133
+
134
+ ```json
135
+ {
136
+ "jupyter-lab": {
137
+ "type": "local",
138
+ "command": ["uvx", "jupyter-mcp-server@latest"],
139
+ "environment": {
140
+ "JUPYTER_URL": "http://localhost:{env:JUPYTER_PORT}/",
141
+ "JUPYTER_TOKEN": "{env:JUPYTER_TOKEN}",
142
+ "DEBUG": "true"
143
+ }
144
+ }
145
+ }
146
+ ```
147
+
148
+ The `{env:VAR_NAME}` placeholders are resolved from the current process environment. If an environment variable is not set, it's replaced with an empty string.
149
+
129
150
  ### Docker
130
151
 
131
152
  Run MCP Gateway in Docker with HTTP transport:
package/dist/index.js CHANGED
@@ -24784,6 +24784,20 @@ class WebSocketClientTransport {
24784
24784
  }
24785
24785
 
24786
24786
  // src/connections.ts
24787
+ function parseEnvironmentVariables(env) {
24788
+ if (!env)
24789
+ return;
24790
+ const parsed = {};
24791
+ const processEnv = process.env;
24792
+ for (const [key, value] of Object.entries(env)) {
24793
+ const substitutedValue = value.replace(/\{env:(\w+)\}/g, (_, envVarName) => {
24794
+ return processEnv[envVarName] || "";
24795
+ });
24796
+ parsed[key] = substitutedValue;
24797
+ }
24798
+ return parsed;
24799
+ }
24800
+
24787
24801
  class ConnectionManager {
24788
24802
  searchEngine;
24789
24803
  jobManager;
@@ -24803,7 +24817,12 @@ class ConnectionManager {
24803
24817
  const [cmd, ...args] = config2.command || [];
24804
24818
  if (!cmd)
24805
24819
  throw new Error(`Missing command for ${serverKey}`);
24806
- const transport = new StdioClientTransport({ command: cmd, args });
24820
+ const env = parseEnvironmentVariables(config2.environment);
24821
+ const transport = new StdioClientTransport({
24822
+ command: cmd,
24823
+ args,
24824
+ env
24825
+ });
24807
24826
  await this.connectTransport(serverKey, config2, transport);
24808
24827
  }
24809
24828
  async connectRemote(serverKey, config2) {
@@ -24882,7 +24901,7 @@ class ConnectionManager {
24882
24901
  // package.json
24883
24902
  var package_default = {
24884
24903
  name: "@eznix/mcp-gateway",
24885
- version: "1.3.4",
24904
+ version: "1.4.0",
24886
24905
  description: "MCP Gateway - Aggregate multiple MCP servers into a single gateway",
24887
24906
  type: "module",
24888
24907
  bin: {
@@ -24894,7 +24913,10 @@ var package_default = {
24894
24913
  build: "bun build src/index.ts --outdir dist --target node",
24895
24914
  "build:docker": "bun build src/docker.ts --target bun --outfile=gateway",
24896
24915
  "docker:build": "docker build -t mcp-gateway .",
24897
- "docker:run": "docker run -p 3000:3000 -v ./examples/config.json:/home/gateway/.config/mcp-gateway/config.json:ro mcp-gateway"
24916
+ "docker:run": "docker run -p 3000:3000 -v ./examples/config.json:/home/gateway/.config/mcp-gateway/config.json:ro mcp-gateway",
24917
+ preversion: 'bun run build && git add dist && git diff --cached --quiet || git commit -m "chore: update build"',
24918
+ prepublishOnly: "bun run build",
24919
+ postversion: "git push && git push --tag"
24898
24920
  },
24899
24921
  devDependencies: {
24900
24922
  "@types/bun": "latest",
@@ -6,5 +6,24 @@
6
6
  "playwright": {
7
7
  "type": "local",
8
8
  "command": ["bunx", "@playwright/mcp@latest"]
9
+ },
10
+ "JupyterLab": {
11
+ "type": "local",
12
+ "command": ["uvx", "jupyter-mcp-server@latest"],
13
+ "environment": {
14
+ "JUPYTER_URL": "http://localhost:{env:JUPYTER_PORT}/",
15
+ "JUPYTER_TOKEN": "{env:JUPYTER_TOKEN}",
16
+ "ALLOW_IMG_OUTPUT": "true"
17
+ },
18
+ "enabled": true
19
+ },
20
+ "custom-server": {
21
+ "type": "local",
22
+ "command": ["python", "scripts/server.py"],
23
+ "environment": {
24
+ "API_KEY": "{env:API_KEY}",
25
+ "DEBUG": "true",
26
+ "DATABASE_URL": "postgresql://{env:DB_USER}:{env:DB_PASS}@localhost:5432/mydb"
27
+ }
9
28
  }
10
29
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eznix/mcp-gateway",
3
- "version": "1.3.4",
3
+ "version": "1.4.0",
4
4
  "description": "MCP Gateway - Aggregate multiple MCP servers into a single gateway",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,7 +12,10 @@
12
12
  "build": "bun build src/index.ts --outdir dist --target node",
13
13
  "build:docker": "bun build src/docker.ts --target bun --outfile=gateway",
14
14
  "docker:build": "docker build -t mcp-gateway .",
15
- "docker:run": "docker run -p 3000:3000 -v ./examples/config.json:/home/gateway/.config/mcp-gateway/config.json:ro mcp-gateway"
15
+ "docker:run": "docker run -p 3000:3000 -v ./examples/config.json:/home/gateway/.config/mcp-gateway/config.json:ro mcp-gateway",
16
+ "preversion": "bun run build && git add dist && git diff --cached --quiet || git commit -m \"chore: update build\"",
17
+ "prepublishOnly": "bun run build",
18
+ "postversion": "git push && git push --tag"
16
19
  },
17
20
  "devDependencies": {
18
21
  "@types/bun": "latest",
@@ -6,6 +6,28 @@ import type { UpstreamConfig, ToolCatalogEntry } from "./types.js";
6
6
  import { SearchEngine } from "./search.js";
7
7
  import { JobManager } from "./jobs.js";
8
8
 
9
+ /**
10
+ * Parse environment variables with {env:VAR_NAME} substitution.
11
+ * Replaces {env:VAR} with the corresponding value from process.env.
12
+ * If the environment variable is not set, the placeholder is replaced with an empty string.
13
+ */
14
+ export function parseEnvironmentVariables(env?: Record<string, string>): Record<string, string> | undefined {
15
+ if (!env) return undefined;
16
+
17
+ const parsed: Record<string, string> = {};
18
+ const processEnv = process.env as Record<string, string>;
19
+
20
+ for (const [key, value] of Object.entries(env)) {
21
+ // Replace all {env:VAR_NAME} patterns with actual environment variable values
22
+ const substitutedValue = value.replace(/\{env:(\w+)\}/g, (_, envVarName: string) => {
23
+ return processEnv[envVarName] || "";
24
+ });
25
+ parsed[key] = substitutedValue;
26
+ }
27
+
28
+ return parsed;
29
+ }
30
+
9
31
  export class ConnectionManager {
10
32
  private upstreams = new Map<string, Client>();
11
33
 
@@ -26,7 +48,14 @@ export class ConnectionManager {
26
48
  const [cmd, ...args] = config.command || [];
27
49
  if (!cmd) throw new Error(`Missing command for ${serverKey}`);
28
50
 
29
- const transport = new StdioClientTransport({ command: cmd, args });
51
+ // Parse environment variables with {env:VAR_NAME} substitution
52
+ const env = parseEnvironmentVariables(config.environment);
53
+
54
+ const transport = new StdioClientTransport({
55
+ command: cmd,
56
+ args,
57
+ env
58
+ });
30
59
  await this.connectTransport(serverKey, config, transport);
31
60
  }
32
61
 
package/src/types.ts CHANGED
@@ -4,9 +4,10 @@ export interface UpstreamConfig {
4
4
  url?: string;
5
5
  transport?: "streamable_http" | "websocket";
6
6
  endpoint?: string;
7
+ environment?: Record<string, string>; // Environment variables with support for {env:VAR_NAME} substitution
7
8
  enabled?: boolean;
8
- lazy?: boolean; // if true, only connect on first request
9
- idleTimeout?: number; // milliseconds before sleeping (default: 2hrs)
9
+ lazy?: boolean; // if true, only connect on first request
10
+ idleTimeout?: number; // milliseconds before sleeping (default: 2hrs)
10
11
  }
11
12
 
12
13
  export interface GatewayConfig {
@@ -0,0 +1,192 @@
1
+ import { test, expect, beforeAll, afterAll } from "bun:test";
2
+ import { parseEnvironmentVariables } from "../src/connections.js";
3
+
4
+ // Store original environment variables
5
+ const originalEnv: Record<string, string | undefined> = {};
6
+
7
+ beforeAll(() => {
8
+ // Save original environment variables
9
+ for (const key of ["TEST_VAR_1", "TEST_VAR_2", "MISSING_VAR", "JUPYTER_TOKEN", "JUPYTER_PORT"]) {
10
+ originalEnv[key] = process.env[key];
11
+ }
12
+
13
+ // Set test environment variables
14
+ process.env.TEST_VAR_1 = "test-value-1";
15
+ process.env.TEST_VAR_2 = "test-value-2";
16
+ process.env.JUPYTER_TOKEN = "my-secret-token";
17
+ process.env.JUPYTER_PORT = "8888";
18
+
19
+ // Ensure MISSING_VAR is not set
20
+ delete process.env.MISSING_VAR;
21
+ });
22
+
23
+ afterAll(() => {
24
+ // Restore original environment variables
25
+ for (const key of ["TEST_VAR_1", "TEST_VAR_2", "MISSING_VAR", "JUPYTER_TOKEN", "JUPYTER_PORT"]) {
26
+ if (originalEnv[key] !== undefined) {
27
+ process.env[key] = originalEnv[key];
28
+ } else {
29
+ delete process.env[key];
30
+ }
31
+ }
32
+ });
33
+
34
+ test("parseEnvironmentVariables returns undefined when env is undefined", () => {
35
+ const result = parseEnvironmentVariables(undefined);
36
+ expect(result).toBeUndefined();
37
+ });
38
+
39
+ test("parseEnvironmentVariables returns undefined when env is empty object", () => {
40
+ const result = parseEnvironmentVariables({});
41
+ expect(result).toEqual({});
42
+ });
43
+
44
+ test("parseEnvironmentVariables preserves values without substitution", () => {
45
+ const input = {
46
+ STATIC_VALUE: "hello world",
47
+ NUMBER_VALUE: "12345",
48
+ URL_VALUE: "https://example.com/api"
49
+ };
50
+
51
+ const result = parseEnvironmentVariables(input);
52
+ expect(result).toEqual(input);
53
+ });
54
+
55
+ test("parseEnvironmentVariables substitutes existing environment variables", () => {
56
+ const input = {
57
+ STATIC: "static",
58
+ SUBSTITUTED: "{env:TEST_VAR_1}",
59
+ ANOTHER_SUB: "{env:TEST_VAR_2}"
60
+ };
61
+
62
+ const expected = {
63
+ STATIC: "static",
64
+ SUBSTITUTED: "test-value-1",
65
+ ANOTHER_SUB: "test-value-2"
66
+ };
67
+
68
+ const result = parseEnvironmentVariables(input);
69
+ expect(result).toEqual(expected);
70
+ });
71
+
72
+ test("parseEnvironmentVariables substitutes multiple variables in same value", () => {
73
+ const input = {
74
+ COMBINED: "prefix-{env:TEST_VAR_1}-middle-{env:TEST_VAR_2}-suffix"
75
+ };
76
+
77
+ const expected = {
78
+ COMBINED: "prefix-test-value-1-middle-test-value-2-suffix"
79
+ };
80
+
81
+ const result = parseEnvironmentVariables(input);
82
+ expect(result).toEqual(expected);
83
+ });
84
+
85
+ test("parseEnvironmentVariables replaces missing variables with empty string", () => {
86
+ const input = {
87
+ PRESENT: "{env:TEST_VAR_1}",
88
+ MISSING: "{env:MISSING_VAR}",
89
+ AFTER_MISSING: "value-after-{env:MISSING_VAR}"
90
+ };
91
+
92
+ const expected = {
93
+ PRESENT: "test-value-1",
94
+ MISSING: "",
95
+ AFTER_MISSING: "value-after-"
96
+ };
97
+
98
+ const result = parseEnvironmentVariables(input);
99
+ expect(result).toEqual(expected);
100
+ });
101
+
102
+ test("parseEnvironmentVariables works with JupyterLab example from README", () => {
103
+ const input = {
104
+ JUPYTER_URL: "http://localhost:{env:JUPYTER_PORT}/",
105
+ JUPYTER_TOKEN: "{env:JUPYTER_TOKEN}",
106
+ ALLOW_IMG_OUTPUT: "true"
107
+ };
108
+
109
+ const expected = {
110
+ JUPYTER_URL: "http://localhost:8888/",
111
+ JUPYTER_TOKEN: "my-secret-token",
112
+ ALLOW_IMG_OUTPUT: "true"
113
+ };
114
+
115
+ const result = parseEnvironmentVariables(input);
116
+ expect(result).toEqual(expected);
117
+ });
118
+
119
+ test("parseEnvironmentVariables handles database URL with multiple substitutions", () => {
120
+ const input = {
121
+ DATABASE_URL: "postgresql://{env:DB_USER}:{env:DB_PASS}@localhost:5432/mydb"
122
+ };
123
+
124
+ // Set up the environment variables for this test
125
+ const originalDbUser = process.env.DB_USER;
126
+ const originalDbPass = process.env.DB_PASS;
127
+
128
+ try {
129
+ process.env.DB_USER = "admin";
130
+ process.env.DB_PASS = "secret123";
131
+
132
+ const expected = {
133
+ DATABASE_URL: "postgresql://admin:secret123@localhost:5432/mydb"
134
+ };
135
+
136
+ const result = parseEnvironmentVariables(input);
137
+ expect(result).toEqual(expected);
138
+ } finally {
139
+ // Restore
140
+ if (originalDbUser !== undefined) {
141
+ process.env.DB_USER = originalDbUser;
142
+ } else {
143
+ delete process.env.DB_USER;
144
+ }
145
+
146
+ if (originalDbPass !== undefined) {
147
+ process.env.DB_PASS = originalDbPass;
148
+ } else {
149
+ delete process.env.DB_PASS;
150
+ }
151
+ }
152
+ });
153
+
154
+ test("parseEnvironmentVariables handles empty values", () => {
155
+ const input = {
156
+ EMPTY_STRING: "",
157
+ SPACES: " "
158
+ };
159
+
160
+ const result = parseEnvironmentVariables(input);
161
+ expect(result).toEqual(input);
162
+ });
163
+
164
+ test("parseEnvironmentVariables is case-sensitive for variable names", () => {
165
+ const input = {
166
+ LOWERCASE: "{env:test_var_1}", // Note: different case than TEST_VAR_1
167
+ };
168
+
169
+ const expected = {
170
+ LOWERCASE: "" // Should be empty because test_var_1 is not set
171
+ };
172
+
173
+ const result = parseEnvironmentVariables(input);
174
+ expect(result).toEqual(expected);
175
+ });
176
+
177
+ test("parseEnvironmentVariables handles complex patterns", () => {
178
+ const input = {
179
+ NESTED: "prefix-{env:TEST_VAR_1}-middle-{env:TEST_VAR_2}-suffix-{env:JUPYTER_TOKEN}",
180
+ WITH_NUMBERS: "server-{env:JUPYTER_PORT}.example.com",
181
+ MIXED: "{env:TEST_VAR_1}-{env:MISSING}-{env:TEST_VAR_2}"
182
+ };
183
+
184
+ const expected = {
185
+ NESTED: "prefix-test-value-1-middle-test-value-2-suffix-my-secret-token",
186
+ WITH_NUMBERS: "server-8888.example.com",
187
+ MIXED: "test-value-1--test-value-2"
188
+ };
189
+
190
+ const result = parseEnvironmentVariables(input);
191
+ expect(result).toEqual(expected);
192
+ });