Compare commits

...

9 Commits

Author SHA1 Message Date
Devin AI
b7e1e9ac06 chore: update express version in package.json to match package-lock.json
Co-Authored-By: ashwin@anthropic.com <ashwin@anthropic.com>
2025-01-16 20:46:45 +00:00
Devin AI
f398d39f4b fix: update express and path-to-regexp to fix npm audit vulnerabilities
Co-Authored-By: ashwin@anthropic.com <ashwin@anthropic.com>
2025-01-16 20:40:51 +00:00
Ashwin Bhat
98e6f0e5ec Merge pull request #124 from modelcontextprotocol/ashwin/envvar
allow passing env vars to server from command line
2025-01-13 12:11:25 -08:00
Ashwin Bhat
ec150eb8b4 prettier 2025-01-10 07:53:55 -08:00
Ashwin Bhat
052de8690d respond to PR feedback 2025-01-10 07:51:55 -08:00
Ashwin Bhat
a976aefb39 allow passing env vars to server from command line 2025-01-10 07:51:54 -08:00
Ashwin Bhat
5a5873277c Merge pull request #123 from modelcontextprotocol/ashwin/prettier
enforce prettier formatting
2025-01-10 07:28:29 -08:00
Ashwin Bhat
715936d747 run prettier 2025-01-09 11:01:35 -08:00
Ashwin Bhat
d973f58bef run prettier check in CI 2025-01-09 11:01:28 -08:00
14 changed files with 249 additions and 95 deletions

View File

@@ -14,6 +14,9 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Check formatting
run: npx prettier --check .
- uses: actions/setup-node@v4
with:
node-version: 18

View File

@@ -1,2 +1,4 @@
packages
server/build
CODE_OF_CONDUCT.md
SECURITY.md

View File

@@ -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:

View File

@@ -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,
},

View File

@@ -16,7 +16,7 @@ import {
ResourceTemplate,
Root,
ServerNotification,
Tool
Tool,
} from "@modelcontextprotocol/sdk/types.js";
import { useEffect, useRef, useState } from "react";
@@ -124,10 +124,7 @@ const App = () => {
const [nextToolCursor, setNextToolCursor] = useState<string | undefined>();
const progressTokenRef = useRef(0);
const {
height: historyPaneHeight,
handleDragStart
} = useDraggablePane(300);
const { height: historyPaneHeight, handleDragStart } = useDraggablePane(300);
const {
connectionStatus,
@@ -136,7 +133,7 @@ const App = () => {
requestHistory,
makeRequest: makeConnectionRequest,
sendNotification,
connect: connectMcpServer
connect: connectMcpServer,
} = useConnection({
transportType,
command,
@@ -145,18 +142,21 @@ const App = () => {
env,
proxyServerUrl: PROXY_SERVER_URL,
onNotification: (notification) => {
setNotifications(prev => [...prev, notification as ServerNotification]);
setNotifications((prev) => [...prev, notification as ServerNotification]);
},
onStdErrNotification: (notification) => {
setStdErrNotifications(prev => [...prev, notification as StdErrNotification]);
},
onPendingRequest: (request, resolve, reject) => {
setPendingSampleRequests(prev => [
setStdErrNotifications((prev) => [
...prev,
{ id: nextRequestId.current++, request, resolve, reject }
notification as StdErrNotification,
]);
},
getRoots: () => rootsRef.current
onPendingRequest: (request, resolve, reject) => {
setPendingSampleRequests((prev) => [
...prev,
{ id: nextRequestId.current++, request, resolve, reject },
]);
},
getRoots: () => rootsRef.current,
});
const makeRequest = async <T extends z.ZodType>(
@@ -345,26 +345,40 @@ const App = () => {
{mcpClient ? (
<Tabs
defaultValue={
Object.keys(serverCapabilities ?? {}).includes(window.location.hash.slice(1)) ?
window.location.hash.slice(1) :
serverCapabilities?.resources ? "resources" :
serverCapabilities?.prompts ? "prompts" :
serverCapabilities?.tools ? "tools" :
"ping"
Object.keys(serverCapabilities ?? {}).includes(
window.location.hash.slice(1),
)
? window.location.hash.slice(1)
: serverCapabilities?.resources
? "resources"
: serverCapabilities?.prompts
? "prompts"
: serverCapabilities?.tools
? "tools"
: "ping"
}
className="w-full p-4"
onValueChange={(value) => (window.location.hash = value)}
>
<TabsList className="mb-4 p-0">
<TabsTrigger value="resources" disabled={!serverCapabilities?.resources}>
<TabsTrigger
value="resources"
disabled={!serverCapabilities?.resources}
>
<Files className="w-4 h-4 mr-2" />
Resources
</TabsTrigger>
<TabsTrigger value="prompts" disabled={!serverCapabilities?.prompts}>
<TabsTrigger
value="prompts"
disabled={!serverCapabilities?.prompts}
>
<MessageSquare className="w-4 h-4 mr-2" />
Prompts
</TabsTrigger>
<TabsTrigger value="tools" disabled={!serverCapabilities?.tools}>
<TabsTrigger
value="tools"
disabled={!serverCapabilities?.tools}
>
<Hammer className="w-4 h-4 mr-2" />
Tools
</TabsTrigger>
@@ -388,7 +402,9 @@ const App = () => {
</TabsList>
<div className="w-full">
{!serverCapabilities?.resources && !serverCapabilities?.prompts && !serverCapabilities?.tools ? (
{!serverCapabilities?.resources &&
!serverCapabilities?.prompts &&
!serverCapabilities?.tools ? (
<div className="flex items-center justify-center p-4">
<p className="text-lg text-gray-500">
The connected server does not support any MCP capabilities

View File

@@ -1,5 +1,14 @@
import { useState } from "react";
import { Play, ChevronDown, ChevronRight, CircleHelp, Bug, Github } from "lucide-react";
import {
Play,
ChevronDown,
ChevronRight,
CircleHelp,
Bug,
Github,
Eye,
EyeOff,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
@@ -47,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">
@@ -127,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) => {
@@ -150,24 +184,45 @@ const Sidebar = ({
}}
className="font-mono"
/>
<Button
variant="outline"
size="icon"
className="h-9 w-9 p-0 shrink-0"
onClick={() => {
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"
}
>
{shownEnvVars.has(key) ? (
<Eye className="h-4 w-4" aria-hidden="true" />
) : (
<EyeOff className="h-4 w-4" aria-hidden="true" />
)}
</Button>
</div>
<Button
variant="destructive"
onClick={() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { [key]: removed, ...rest } = env;
setEnv(rest);
}}
>
Remove
</Button>
</div>
))}
<Button
variant="outline"
className="w-full mt-2"
onClick={() => {
const key = "";
const newEnv = { ...env };
newEnv[""] = "";
newEnv[key] = "";
setEnv(newEnv);
}}
>
@@ -243,18 +298,33 @@ const Sidebar = ({
</Select>
<div className="flex items-center space-x-2">
<a href="https://modelcontextprotocol.io/docs/tools/inspector" target="_blank" rel="noopener noreferrer">
<a
href="https://modelcontextprotocol.io/docs/tools/inspector"
target="_blank"
rel="noopener noreferrer"
>
<Button variant="ghost" title="Inspector Documentation">
<CircleHelp className="w-4 h-4 text-gray-800" />
</Button>
</a>
<a href="https://modelcontextprotocol.io/docs/tools/debugging" target="_blank" rel="noopener noreferrer">
<a
href="https://modelcontextprotocol.io/docs/tools/debugging"
target="_blank"
rel="noopener noreferrer"
>
<Button variant="ghost" title="Debugging Guide">
<Bug className="w-4 h-4 text-gray-800" />
</Button>
</a>
<a href="https://github.com/modelcontextprotocol/inspector" target="_blank" rel="noopener noreferrer">
<Button variant="ghost" title="Report bugs or contribute on GitHub">
<a
href="https://github.com/modelcontextprotocol/inspector"
target="_blank"
rel="noopener noreferrer"
>
<Button
variant="ghost"
title="Report bugs or contribute on GitHub"
>
<Github className="w-4 h-4 text-gray-800" />
</Button>
</a>

View File

@@ -174,8 +174,7 @@ const ToolsTab = ({
}
className="mt-1"
/>
) :
/* @ts-expect-error value type is currently unknown */
) : /* @ts-expect-error value type is currently unknown */
value.type === "object" ? (
<Textarea
id={key}

View File

@@ -44,10 +44,15 @@ export function useConnection({
onPendingRequest,
getRoots,
}: UseConnectionOptions) {
const [connectionStatus, setConnectionStatus] = useState<"disconnected" | "connected" | "error">("disconnected");
const [serverCapabilities, setServerCapabilities] = useState<ServerCapabilities | null>(null);
const [connectionStatus, setConnectionStatus] = useState<
"disconnected" | "connected" | "error"
>("disconnected");
const [serverCapabilities, setServerCapabilities] =
useState<ServerCapabilities | null>(null);
const [mcpClient, setMcpClient] = useState<Client | null>(null);
const [requestHistory, setRequestHistory] = useState<{ request: string; response?: string }[]>([]);
const [requestHistory, setRequestHistory] = useState<
{ request: string; response?: string }[]
>([]);
const pushHistory = (request: object, response?: object) => {
setRequestHistory((prev) => [
@@ -61,7 +66,7 @@ export function useConnection({
const makeRequest = async <T extends z.ZodType>(
request: ClientRequest,
schema: T
schema: T,
) => {
if (!mcpClient) {
throw new Error("MCP client not connected");
@@ -80,14 +85,14 @@ export function useConnection({
});
pushHistory(request, response);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const errorMessage =
error instanceof Error ? error.message : String(error);
pushHistory(request, { error: errorMessage });
throw error;
} finally {
clearTimeout(timeoutId);
}
return response;
} catch (e: unknown) {
const errorString = (e as Error).message ?? String(e);
@@ -140,13 +145,19 @@ export function useConnection({
}
const clientTransport = new SSEClientTransport(backendUrl);
if (onNotification) {
client.setNotificationHandler(ProgressNotificationSchema, onNotification);
client.setNotificationHandler(
ProgressNotificationSchema,
onNotification,
);
}
if (onStdErrNotification) {
client.setNotificationHandler(StdErrNotificationSchema, onStdErrNotification);
client.setNotificationHandler(
StdErrNotificationSchema,
onStdErrNotification,
);
}
await client.connect(clientTransport);
@@ -183,6 +194,6 @@ export function useConnection({
requestHistory,
makeRequest,
sendNotification,
connect
connect,
};
}
}

View File

@@ -6,19 +6,28 @@ export function useDraggablePane(initialHeight: number) {
const dragStartY = useRef<number>(0);
const dragStartHeight = useRef<number>(0);
const handleDragStart = useCallback((e: React.MouseEvent) => {
setIsDragging(true);
dragStartY.current = e.clientY;
dragStartHeight.current = height;
document.body.style.userSelect = "none";
}, [height]);
const handleDragStart = useCallback(
(e: React.MouseEvent) => {
setIsDragging(true);
dragStartY.current = e.clientY;
dragStartHeight.current = height;
document.body.style.userSelect = "none";
},
[height],
);
const handleDragMove = useCallback((e: MouseEvent) => {
if (!isDragging) return;
const deltaY = dragStartY.current - e.clientY;
const newHeight = Math.max(100, Math.min(800, dragStartHeight.current + deltaY));
setHeight(newHeight);
}, [isDragging]);
const handleDragMove = useCallback(
(e: MouseEvent) => {
if (!isDragging) return;
const deltaY = dragStartY.current - e.clientY;
const newHeight = Math.max(
100,
Math.min(800, dragStartHeight.current + deltaY),
);
setHeight(newHeight);
},
[isDragging],
);
const handleDragEnd = useCallback(() => {
setIsDragging(false);
@@ -39,6 +48,6 @@ export function useDraggablePane(initialHeight: number) {
return {
height,
isDragging,
handleDragStart
handleDragStart,
};
}
}

View File

@@ -1,7 +1,7 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import App from "./App.tsx";
import "./index.css";

View File

@@ -14,8 +14,8 @@ export default defineConfig({
minify: false,
rollupOptions: {
output: {
manualChunks: undefined
}
}
}
manualChunks: undefined,
},
},
},
});

18
package-lock.json generated
View File

@@ -3698,9 +3698,9 @@
}
},
"node_modules/express": {
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
@@ -3722,7 +3722,7 @@
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.10",
"path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
@@ -3737,6 +3737,10 @@
},
"engines": {
"node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/express/node_modules/debug": {
@@ -4894,9 +4898,9 @@
"license": "ISC"
},
"node_modules/path-to-regexp": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==",
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
},
"node_modules/picocolors": {

View File

@@ -30,7 +30,7 @@
"@modelcontextprotocol/sdk": "^1.0.3",
"cors": "^2.8.5",
"eventsource": "^2.0.2",
"express": "^4.21.0",
"express": "^4.21.2",
"ws": "^8.18.0",
"zod": "^3.23.8"
}

View File

@@ -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,