Skip to content

Commit 98e6f0e

Browse files
authored
Merge pull request #124 from modelcontextprotocol/ashwin/envvar
allow passing env vars to server from command line
2 parents 5a58732 + ec150eb commit 98e6f0e

File tree

4 files changed

+114
-26
lines changed

4 files changed

+114
-26
lines changed

README.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,20 @@ To inspect an MCP server implementation, there's no need to clone this repo. Ins
1414
npx @modelcontextprotocol/inspector build/index.js
1515
```
1616

17-
You can also pass arguments along which will get passed as arguments to your MCP server:
17+
You can pass both arguments and environment variables to your MCP server. Arguments are passed directly to your server, while environment variables can be set using the `-e` flag:
1818

19-
```
20-
npx @modelcontextprotocol/inspector build/index.js arg1 arg2 ...
19+
```bash
20+
# Pass arguments only
21+
npx @modelcontextprotocol/inspector build/index.js arg1 arg2
22+
23+
# Pass environment variables only
24+
npx @modelcontextprotocol/inspector -e KEY=value -e KEY2=$VALUE2 build/index.js
25+
26+
# Pass both environment variables and arguments
27+
npx @modelcontextprotocol/inspector -e KEY=value -e KEY2=$VALUE2 build/index.js arg1 arg2
28+
29+
# Use -- to separate inspector flags from server arguments
30+
npx @modelcontextprotocol/inspector -e KEY=$VALUE -- build/index.js -e server-flag
2131
```
2232

2333
The inspector runs both a client UI (default port 5173) and an MCP proxy server (default port 3000). Open the client UI in your browser to use the inspector. You can customize the ports if needed:

bin/cli.js

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,32 @@ function delay(ms) {
1111
}
1212

1313
async function main() {
14-
// Get command line arguments
15-
const [, , command, ...mcpServerArgs] = process.argv;
14+
// Parse command line arguments
15+
const args = process.argv.slice(2);
16+
const envVars = {};
17+
const mcpServerArgs = [];
18+
let command = null;
19+
let parsingFlags = true;
20+
21+
for (let i = 0; i < args.length; i++) {
22+
const arg = args[i];
23+
24+
if (parsingFlags && arg === "--") {
25+
parsingFlags = false;
26+
continue;
27+
}
28+
29+
if (parsingFlags && arg === "-e" && i + 1 < args.length) {
30+
const [key, value] = args[++i].split("=");
31+
if (key && value) {
32+
envVars[key] = value;
33+
}
34+
} else if (!command) {
35+
command = arg;
36+
} else {
37+
mcpServerArgs.push(arg);
38+
}
39+
}
1640

1741
const inspectorServerPath = resolve(
1842
__dirname,
@@ -52,7 +76,11 @@ async function main() {
5276
...(mcpServerArgs ? [`--args=${mcpServerArgs.join(" ")}`] : []),
5377
],
5478
{
55-
env: { ...process.env, PORT: SERVER_PORT },
79+
env: {
80+
...process.env,
81+
PORT: SERVER_PORT,
82+
MCP_ENV_VARS: JSON.stringify(envVars),
83+
},
5684
signal: abort.signal,
5785
echoOutput: true,
5886
},

client/src/components/Sidebar.tsx

Lines changed: 62 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
CircleHelp,
77
Bug,
88
Github,
9+
Eye,
10+
EyeOff,
911
} from "lucide-react";
1012
import { Button } from "@/components/ui/button";
1113
import { Input } from "@/components/ui/input";
@@ -54,6 +56,7 @@ const Sidebar = ({
5456
}: SidebarProps) => {
5557
const [theme, setTheme] = useTheme();
5658
const [showEnvVars, setShowEnvVars] = useState(false);
59+
const [shownEnvVars, setShownEnvVars] = useState<Set<string>>(new Set());
5760

5861
return (
5962
<div className="w-80 bg-card border-r border-border flex flex-col h-full">
@@ -134,20 +137,44 @@ const Sidebar = ({
134137
{showEnvVars && (
135138
<div className="space-y-2">
136139
{Object.entries(env).map(([key, value], idx) => (
137-
<div key={idx} className="grid grid-cols-[1fr,auto] gap-2">
138-
<div className="space-y-1">
140+
<div key={idx} className="space-y-2 pb-4">
141+
<div className="flex gap-2">
139142
<Input
140143
placeholder="Key"
141144
value={key}
142145
onChange={(e) => {
146+
const newKey = e.target.value;
143147
const newEnv = { ...env };
144148
delete newEnv[key];
145-
newEnv[e.target.value] = value;
149+
newEnv[newKey] = value;
146150
setEnv(newEnv);
151+
setShownEnvVars((prev) => {
152+
const next = new Set(prev);
153+
if (next.has(key)) {
154+
next.delete(key);
155+
next.add(newKey);
156+
}
157+
return next;
158+
});
147159
}}
148160
className="font-mono"
149161
/>
162+
<Button
163+
variant="destructive"
164+
size="icon"
165+
className="h-9 w-9 p-0 shrink-0"
166+
onClick={() => {
167+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
168+
const { [key]: _removed, ...rest } = env;
169+
setEnv(rest);
170+
}}
171+
>
172+
×
173+
</Button>
174+
</div>
175+
<div className="flex gap-2">
150176
<Input
177+
type={shownEnvVars.has(key) ? "text" : "password"}
151178
placeholder="Value"
152179
value={value}
153180
onChange={(e) => {
@@ -157,24 +184,45 @@ const Sidebar = ({
157184
}}
158185
className="font-mono"
159186
/>
187+
<Button
188+
variant="outline"
189+
size="icon"
190+
className="h-9 w-9 p-0 shrink-0"
191+
onClick={() => {
192+
setShownEnvVars((prev) => {
193+
const next = new Set(prev);
194+
if (next.has(key)) {
195+
next.delete(key);
196+
} else {
197+
next.add(key);
198+
}
199+
return next;
200+
});
201+
}}
202+
aria-label={
203+
shownEnvVars.has(key) ? "Hide value" : "Show value"
204+
}
205+
aria-pressed={shownEnvVars.has(key)}
206+
title={
207+
shownEnvVars.has(key) ? "Hide value" : "Show value"
208+
}
209+
>
210+
{shownEnvVars.has(key) ? (
211+
<Eye className="h-4 w-4" aria-hidden="true" />
212+
) : (
213+
<EyeOff className="h-4 w-4" aria-hidden="true" />
214+
)}
215+
</Button>
160216
</div>
161-
<Button
162-
variant="destructive"
163-
onClick={() => {
164-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
165-
const { [key]: removed, ...rest } = env;
166-
setEnv(rest);
167-
}}
168-
>
169-
Remove
170-
</Button>
171217
</div>
172218
))}
173219
<Button
174220
variant="outline"
221+
className="w-full mt-2"
175222
onClick={() => {
223+
const key = "";
176224
const newEnv = { ...env };
177-
newEnv[""] = "";
225+
newEnv[key] = "";
178226
setEnv(newEnv);
179227
}}
180228
>

server/src/index.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ import express from "express";
1515
import mcpProxy from "./mcpProxy.js";
1616
import { findActualExecutable } from "spawn-rx";
1717

18+
const defaultEnvironment = {
19+
...getDefaultEnvironment(),
20+
...(process.env.MCP_ENV_VARS ? JSON.parse(process.env.MCP_ENV_VARS) : {}),
21+
};
22+
1823
// Polyfill EventSource for an SSE client in Node.js
1924
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2025
(global as any).EventSource = EventSource;
@@ -40,13 +45,12 @@ const createTransport = async (query: express.Request["query"]) => {
4045
if (transportType === "stdio") {
4146
const command = query.command as string;
4247
const origArgs = shellParseArgs(query.args as string) as string[];
43-
const env = query.env ? JSON.parse(query.env as string) : undefined;
48+
const queryEnv = query.env ? JSON.parse(query.env as string) : {};
49+
const env = { ...process.env, ...defaultEnvironment, ...queryEnv };
4450

4551
const { cmd, args } = findActualExecutable(command, origArgs);
4652

47-
console.log(
48-
`Stdio transport: command=${cmd}, args=${args}, env=${JSON.stringify(env)}`,
49-
);
53+
console.log(`Stdio transport: command=${cmd}, args=${args}`);
5054

5155
const transport = new StdioClientTransport({
5256
command: cmd,
@@ -136,8 +140,6 @@ app.post("/message", async (req, res) => {
136140

137141
app.get("/config", (req, res) => {
138142
try {
139-
const defaultEnvironment = getDefaultEnvironment();
140-
141143
res.json({
142144
defaultEnvironment,
143145
defaultCommand: values.env,

0 commit comments

Comments
 (0)