Merge pull request #124 from modelcontextprotocol/ashwin/envvar
allow passing env vars to server from command line
This commit is contained in:
16
README.md
16
README.md
@@ -14,10 +14,20 @@ To inspect an MCP server implementation, there's no need to clone this repo. Ins
|
||||
npx @modelcontextprotocol/inspector build/index.js
|
||||
```
|
||||
|
||||
You can also pass arguments along which will get passed as arguments to your MCP server:
|
||||
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:
|
||||
|
||||
```
|
||||
npx @modelcontextprotocol/inspector build/index.js arg1 arg2 ...
|
||||
```bash
|
||||
# Pass arguments only
|
||||
npx @modelcontextprotocol/inspector build/index.js arg1 arg2
|
||||
|
||||
# Pass environment variables only
|
||||
npx @modelcontextprotocol/inspector -e KEY=value -e KEY2=$VALUE2 build/index.js
|
||||
|
||||
# Pass both environment variables and arguments
|
||||
npx @modelcontextprotocol/inspector -e KEY=value -e KEY2=$VALUE2 build/index.js arg1 arg2
|
||||
|
||||
# Use -- to separate inspector flags from server arguments
|
||||
npx @modelcontextprotocol/inspector -e KEY=$VALUE -- build/index.js -e server-flag
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
34
bin/cli.js
34
bin/cli.js
@@ -11,8 +11,32 @@ function delay(ms) {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Get command line arguments
|
||||
const [, , command, ...mcpServerArgs] = process.argv;
|
||||
// Parse command line arguments
|
||||
const args = process.argv.slice(2);
|
||||
const envVars = {};
|
||||
const mcpServerArgs = [];
|
||||
let command = null;
|
||||
let parsingFlags = true;
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
|
||||
if (parsingFlags && arg === "--") {
|
||||
parsingFlags = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parsingFlags && arg === "-e" && i + 1 < args.length) {
|
||||
const [key, value] = args[++i].split("=");
|
||||
if (key && value) {
|
||||
envVars[key] = value;
|
||||
}
|
||||
} else if (!command) {
|
||||
command = arg;
|
||||
} else {
|
||||
mcpServerArgs.push(arg);
|
||||
}
|
||||
}
|
||||
|
||||
const inspectorServerPath = resolve(
|
||||
__dirname,
|
||||
@@ -52,7 +76,11 @@ async function main() {
|
||||
...(mcpServerArgs ? [`--args=${mcpServerArgs.join(" ")}`] : []),
|
||||
],
|
||||
{
|
||||
env: { ...process.env, PORT: SERVER_PORT },
|
||||
env: {
|
||||
...process.env,
|
||||
PORT: SERVER_PORT,
|
||||
MCP_ENV_VARS: JSON.stringify(envVars),
|
||||
},
|
||||
signal: abort.signal,
|
||||
echoOutput: true,
|
||||
},
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
CircleHelp,
|
||||
Bug,
|
||||
Github,
|
||||
Eye,
|
||||
EyeOff,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -54,6 +56,7 @@ const Sidebar = ({
|
||||
}: SidebarProps) => {
|
||||
const [theme, setTheme] = useTheme();
|
||||
const [showEnvVars, setShowEnvVars] = useState(false);
|
||||
const [shownEnvVars, setShownEnvVars] = useState<Set<string>>(new Set());
|
||||
|
||||
return (
|
||||
<div className="w-80 bg-card border-r border-border flex flex-col h-full">
|
||||
@@ -134,20 +137,44 @@ const Sidebar = ({
|
||||
{showEnvVars && (
|
||||
<div className="space-y-2">
|
||||
{Object.entries(env).map(([key, value], idx) => (
|
||||
<div key={idx} className="grid grid-cols-[1fr,auto] gap-2">
|
||||
<div className="space-y-1">
|
||||
<div key={idx} className="space-y-2 pb-4">
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="Key"
|
||||
value={key}
|
||||
onChange={(e) => {
|
||||
const newKey = e.target.value;
|
||||
const newEnv = { ...env };
|
||||
delete newEnv[key];
|
||||
newEnv[e.target.value] = value;
|
||||
newEnv[newKey] = value;
|
||||
setEnv(newEnv);
|
||||
setShownEnvVars((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(key)) {
|
||||
next.delete(key);
|
||||
next.add(newKey);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
className="font-mono"
|
||||
/>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
className="h-9 w-9 p-0 shrink-0"
|
||||
onClick={() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { [key]: _removed, ...rest } = env;
|
||||
setEnv(rest);
|
||||
}}
|
||||
>
|
||||
×
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type={shownEnvVars.has(key) ? "text" : "password"}
|
||||
placeholder="Value"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
@@ -157,24 +184,45 @@ const Sidebar = ({
|
||||
}}
|
||||
className="font-mono"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="destructive"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-9 w-9 p-0 shrink-0"
|
||||
onClick={() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { [key]: removed, ...rest } = env;
|
||||
setEnv(rest);
|
||||
setShownEnvVars((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(key)) {
|
||||
next.delete(key);
|
||||
} else {
|
||||
next.add(key);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
aria-label={
|
||||
shownEnvVars.has(key) ? "Hide value" : "Show value"
|
||||
}
|
||||
aria-pressed={shownEnvVars.has(key)}
|
||||
title={
|
||||
shownEnvVars.has(key) ? "Hide value" : "Show value"
|
||||
}
|
||||
>
|
||||
Remove
|
||||
{shownEnvVars.has(key) ? (
|
||||
<Eye className="h-4 w-4" aria-hidden="true" />
|
||||
) : (
|
||||
<EyeOff className="h-4 w-4" aria-hidden="true" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full mt-2"
|
||||
onClick={() => {
|
||||
const key = "";
|
||||
const newEnv = { ...env };
|
||||
newEnv[""] = "";
|
||||
newEnv[key] = "";
|
||||
setEnv(newEnv);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -15,6 +15,11 @@ import express from "express";
|
||||
import mcpProxy from "./mcpProxy.js";
|
||||
import { findActualExecutable } from "spawn-rx";
|
||||
|
||||
const defaultEnvironment = {
|
||||
...getDefaultEnvironment(),
|
||||
...(process.env.MCP_ENV_VARS ? JSON.parse(process.env.MCP_ENV_VARS) : {}),
|
||||
};
|
||||
|
||||
// Polyfill EventSource for an SSE client in Node.js
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(global as any).EventSource = EventSource;
|
||||
@@ -40,13 +45,12 @@ const createTransport = async (query: express.Request["query"]) => {
|
||||
if (transportType === "stdio") {
|
||||
const command = query.command as string;
|
||||
const origArgs = shellParseArgs(query.args as string) as string[];
|
||||
const env = query.env ? JSON.parse(query.env as string) : undefined;
|
||||
const queryEnv = query.env ? JSON.parse(query.env as string) : {};
|
||||
const env = { ...process.env, ...defaultEnvironment, ...queryEnv };
|
||||
|
||||
const { cmd, args } = findActualExecutable(command, origArgs);
|
||||
|
||||
console.log(
|
||||
`Stdio transport: command=${cmd}, args=${args}, env=${JSON.stringify(env)}`,
|
||||
);
|
||||
console.log(`Stdio transport: command=${cmd}, args=${args}`);
|
||||
|
||||
const transport = new StdioClientTransport({
|
||||
command: cmd,
|
||||
@@ -136,8 +140,6 @@ app.post("/message", async (req, res) => {
|
||||
|
||||
app.get("/config", (req, res) => {
|
||||
try {
|
||||
const defaultEnvironment = getDefaultEnvironment();
|
||||
|
||||
res.json({
|
||||
defaultEnvironment,
|
||||
defaultCommand: values.env,
|
||||
|
||||
Reference in New Issue
Block a user