diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2665e9d..572e79e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -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 diff --git a/.prettierignore b/.prettierignore index b7c83c0..c8824c9 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,4 @@ packages server/build +CODE_OF_CONDUCT.md +SECURITY.md diff --git a/README.md b/README.md index c5e6b2e..98b5704 100644 --- a/README.md +++ b/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: @@ -26,7 +36,7 @@ The inspector runs both a client UI (default port 5173) and an MCP proxy server CLIENT_PORT=8080 SERVER_PORT=9000 npx @modelcontextprotocol/inspector build/index.js ``` -For more details on ways to use the inspector, see the [Inspector section of the MCP docs site](https://modelcontextprotocol.io/docs/tools/inspector). +For more details on ways to use the inspector, see the [Inspector section of the MCP docs site](https://modelcontextprotocol.io/docs/tools/inspector). For help with debugging, see the [Debugging guide](https://modelcontextprotocol.io/docs/tools/debugging). ### From this repository diff --git a/bin/cli.js b/bin/cli.js index 2dcc613..94348fb 100755 --- a/bin/cli.js +++ b/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, }, diff --git a/client/src/App.tsx b/client/src/App.tsx index 2c84377..f3791b2 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,36 +1,26 @@ -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; +import { useDraggablePane } from "./lib/hooks/useDraggablePane"; +import { useConnection } from "./lib/hooks/useConnection"; import { - ClientNotification, ClientRequest, CompatibilityCallToolResult, CompatibilityCallToolResultSchema, - CreateMessageRequestSchema, CreateMessageResult, EmptyResultSchema, GetPromptResultSchema, ListPromptsResultSchema, ListResourcesResultSchema, ListResourceTemplatesResultSchema, - ListRootsRequestSchema, - ListToolsResultSchema, - ProgressNotificationSchema, ReadResourceResultSchema, - Request, + ListToolsResultSchema, Resource, ResourceTemplate, - Result, Root, ServerNotification, Tool, } from "@modelcontextprotocol/sdk/types.js"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; -import { - Notification, - StdErrNotification, - StdErrNotificationSchema, -} from "./lib/notificationTypes"; +import { StdErrNotification } from "./lib/notificationTypes"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { @@ -42,8 +32,7 @@ import { MessageSquare, } from "lucide-react"; -import { toast } from "react-toastify"; -import { ZodType } from "zod"; +import { z } from "zod"; import "./App.css"; import ConsoleTab from "./components/ConsoleTab"; import HistoryAndNotifications from "./components/History"; @@ -55,17 +44,11 @@ import SamplingTab, { PendingRequest } from "./components/SamplingTab"; import Sidebar from "./components/Sidebar"; import ToolsTab from "./components/ToolsTab"; -const DEFAULT_REQUEST_TIMEOUT_MSEC = 10000; - const params = new URLSearchParams(window.location.search); const PROXY_PORT = params.get("proxyPort") ?? "3000"; -const REQUEST_TIMEOUT = parseInt(params.get("timeout") ?? "") || DEFAULT_REQUEST_TIMEOUT_MSEC; const PROXY_SERVER_URL = `http://localhost:${PROXY_PORT}`; const App = () => { - const [connectionStatus, setConnectionStatus] = useState< - "disconnected" | "connected" | "error" - >("disconnected"); const [resources, setResources] = useState([]); const [resourceTemplates, setResourceTemplates] = useState< ResourceTemplate[] @@ -90,10 +73,6 @@ const App = () => { const [sseUrl, setSseUrl] = useState("http://localhost:3001/sse"); const [transportType, setTransportType] = useState<"stdio" | "sse">("stdio"); - const [requestHistory, setRequestHistory] = useState< - { request: string; response?: string }[] - >([]); - const [mcpClient, setMcpClient] = useState(null); const [notifications, setNotifications] = useState([]); const [stdErrNotifications, setStdErrNotifications] = useState< StdErrNotification[] @@ -144,49 +123,64 @@ const App = () => { >(); const [nextToolCursor, setNextToolCursor] = useState(); const progressTokenRef = useRef(0); - const [historyPaneHeight, setHistoryPaneHeight] = useState(300); - const [isDragging, setIsDragging] = useState(false); - const dragStartY = useRef(0); - const dragStartHeight = useRef(0); - const handleDragStart = useCallback( - (e: React.MouseEvent) => { - setIsDragging(true); - dragStartY.current = e.clientY; - dragStartHeight.current = historyPaneHeight; - document.body.style.userSelect = "none"; + const { height: historyPaneHeight, handleDragStart } = useDraggablePane(300); + + const { + connectionStatus, + serverCapabilities, + mcpClient, + requestHistory, + makeRequest: makeConnectionRequest, + sendNotification, + connect: connectMcpServer, + } = useConnection({ + transportType, + command, + args, + sseUrl, + env, + proxyServerUrl: PROXY_SERVER_URL, + onNotification: (notification) => { + setNotifications((prev) => [...prev, notification as ServerNotification]); }, - [historyPaneHeight], - ); - - 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), - ); - setHistoryPaneHeight(newHeight); + onStdErrNotification: (notification) => { + setStdErrNotifications((prev) => [ + ...prev, + notification as StdErrNotification, + ]); }, - [isDragging], - ); + onPendingRequest: (request, resolve, reject) => { + setPendingSampleRequests((prev) => [ + ...prev, + { id: nextRequestId.current++, request, resolve, reject }, + ]); + }, + getRoots: () => rootsRef.current, + }); - const handleDragEnd = useCallback(() => { - setIsDragging(false); - document.body.style.userSelect = ""; - }, []); - - useEffect(() => { - if (isDragging) { - window.addEventListener("mousemove", handleDragMove); - window.addEventListener("mouseup", handleDragEnd); - return () => { - window.removeEventListener("mousemove", handleDragMove); - window.removeEventListener("mouseup", handleDragEnd); - }; + const makeRequest = async ( + request: ClientRequest, + schema: T, + tabKey?: keyof typeof errors, + ) => { + try { + const response = await makeConnectionRequest(request, schema); + if (tabKey !== undefined) { + clearError(tabKey); + } + return response; + } catch (e) { + const errorString = (e as Error).message ?? String(e); + if (tabKey !== undefined) { + setErrors((prev) => ({ + ...prev, + [tabKey]: errorString, + })); + } + throw e; } - }, [isDragging, handleDragMove, handleDragEnd]); + }; useEffect(() => { localStorage.setItem("lastCommand", command); @@ -217,79 +211,16 @@ const App = () => { rootsRef.current = roots; }, [roots]); - const pushHistory = (request: object, response?: object) => { - setRequestHistory((prev) => [ - ...prev, - { - request: JSON.stringify(request), - response: response !== undefined ? JSON.stringify(response) : undefined, - }, - ]); - }; + useEffect(() => { + if (!window.location.hash) { + window.location.hash = "resources"; + } + }, []); const clearError = (tabKey: keyof typeof errors) => { setErrors((prev) => ({ ...prev, [tabKey]: null })); }; - const makeRequest = async >( - request: ClientRequest, - schema: T, - tabKey?: keyof typeof errors, - ) => { - if (!mcpClient) { - throw new Error("MCP client not connected"); - } - - try { - const abortController = new AbortController(); - const timeoutId = setTimeout(() => { - abortController.abort("Request timed out"); - }, REQUEST_TIMEOUT); - - let response; - try { - response = await mcpClient.request(request, schema, { - signal: abortController.signal, - }); - } finally { - clearTimeout(timeoutId); - } - pushHistory(request, response); - - if (tabKey !== undefined) { - clearError(tabKey); - } - - return response; - } catch (e: unknown) { - const errorString = (e as Error).message ?? String(e); - if (tabKey === undefined) { - toast.error(errorString); - } else { - setErrors((prev) => ({ - ...prev, - [tabKey]: errorString, - })); - } - - throw e; - } - }; - - const sendNotification = async (notification: ClientNotification) => { - if (!mcpClient) { - throw new Error("MCP client not connected"); - } - - try { - await mcpClient.notification(notification); - pushHistory(notification); - } catch (e: unknown) { - toast.error((e as Error).message ?? String(e)); - throw e; - } - }; - const listResources = async () => { const response = await makeRequest( { @@ -392,79 +323,6 @@ const App = () => { await sendNotification({ method: "notifications/roots/list_changed" }); }; - const connectMcpServer = async () => { - try { - const client = new Client( - { - name: "mcp-inspector", - version: "0.0.1", - }, - { - capabilities: { - // Support all client capabilities since we're an inspector tool - sampling: {}, - roots: { - listChanged: true, - }, - }, - }, - ); - - const backendUrl = new URL(`${PROXY_SERVER_URL}/sse`); - - backendUrl.searchParams.append("transportType", transportType); - if (transportType === "stdio") { - backendUrl.searchParams.append("command", command); - backendUrl.searchParams.append("args", args); - backendUrl.searchParams.append("env", JSON.stringify(env)); - } else { - backendUrl.searchParams.append("url", sseUrl); - } - - const clientTransport = new SSEClientTransport(backendUrl); - client.setNotificationHandler( - ProgressNotificationSchema, - (notification) => { - setNotifications((prevNotifications) => [ - ...prevNotifications, - notification, - ]); - }, - ); - - client.setNotificationHandler( - StdErrNotificationSchema, - (notification) => { - setStdErrNotifications((prevErrorNotifications) => [ - ...prevErrorNotifications, - notification, - ]); - }, - ); - - await client.connect(clientTransport); - - client.setRequestHandler(CreateMessageRequestSchema, (request) => { - return new Promise((resolve, reject) => { - setPendingSampleRequests((prev) => [ - ...prev, - { id: nextRequestId.current++, request, resolve, reject }, - ]); - }); - }); - - client.setRequestHandler(ListRootsRequestSchema, async () => { - return { roots: rootsRef.current }; - }); - - setMcpClient(client); - setConnectionStatus("connected"); - } catch (e) { - console.error(e); - setConnectionStatus("error"); - } - }; - return (
{
{mcpClient ? ( - + (window.location.hash = value)} + > - + Resources - + Prompts - + Tools @@ -519,107 +402,119 @@ const App = () => {
- { - clearError("resources"); - listResources(); - }} - clearResources={() => { - setResources([]); - setNextResourceCursor(undefined); - }} - listResourceTemplates={() => { - clearError("resources"); - listResourceTemplates(); - }} - clearResourceTemplates={() => { - setResourceTemplates([]); - setNextResourceTemplateCursor(undefined); - }} - readResource={(uri) => { - clearError("resources"); - readResource(uri); - }} - selectedResource={selectedResource} - setSelectedResource={(resource) => { - clearError("resources"); - setSelectedResource(resource); - }} - resourceContent={resourceContent} - nextCursor={nextResourceCursor} - nextTemplateCursor={nextResourceTemplateCursor} - error={errors.resources} - /> - { - clearError("prompts"); - listPrompts(); - }} - clearPrompts={() => { - setPrompts([]); - setNextPromptCursor(undefined); - }} - getPrompt={(name, args) => { - clearError("prompts"); - getPrompt(name, args); - }} - selectedPrompt={selectedPrompt} - setSelectedPrompt={(prompt) => { - clearError("prompts"); - setSelectedPrompt(prompt); - }} - promptContent={promptContent} - nextCursor={nextPromptCursor} - error={errors.prompts} - /> - { - clearError("tools"); - listTools(); - }} - clearTools={() => { - setTools([]); - setNextToolCursor(undefined); - }} - callTool={(name, params) => { - clearError("tools"); - callTool(name, params); - }} - selectedTool={selectedTool} - setSelectedTool={(tool) => { - clearError("tools"); - setSelectedTool(tool); - setToolResult(null); - }} - toolResult={toolResult} - nextCursor={nextToolCursor} - error={errors.tools} - /> - - { - void makeRequest( - { - method: "ping" as const, - }, - EmptyResultSchema, - ); - }} - /> - - + {!serverCapabilities?.resources && + !serverCapabilities?.prompts && + !serverCapabilities?.tools ? ( +
+

+ The connected server does not support any MCP capabilities +

+
+ ) : ( + <> + { + clearError("resources"); + listResources(); + }} + clearResources={() => { + setResources([]); + setNextResourceCursor(undefined); + }} + listResourceTemplates={() => { + clearError("resources"); + listResourceTemplates(); + }} + clearResourceTemplates={() => { + setResourceTemplates([]); + setNextResourceTemplateCursor(undefined); + }} + readResource={(uri) => { + clearError("resources"); + readResource(uri); + }} + selectedResource={selectedResource} + setSelectedResource={(resource) => { + clearError("resources"); + setSelectedResource(resource); + }} + resourceContent={resourceContent} + nextCursor={nextResourceCursor} + nextTemplateCursor={nextResourceTemplateCursor} + error={errors.resources} + /> + { + clearError("prompts"); + listPrompts(); + }} + clearPrompts={() => { + setPrompts([]); + setNextPromptCursor(undefined); + }} + getPrompt={(name, args) => { + clearError("prompts"); + getPrompt(name, args); + }} + selectedPrompt={selectedPrompt} + setSelectedPrompt={(prompt) => { + clearError("prompts"); + setSelectedPrompt(prompt); + }} + promptContent={promptContent} + nextCursor={nextPromptCursor} + error={errors.prompts} + /> + { + clearError("tools"); + listTools(); + }} + clearTools={() => { + setTools([]); + setNextToolCursor(undefined); + }} + callTool={(name, params) => { + clearError("tools"); + callTool(name, params); + }} + selectedTool={selectedTool} + setSelectedTool={(tool) => { + clearError("tools"); + setSelectedTool(tool); + setToolResult(null); + }} + toolResult={toolResult} + nextCursor={nextToolCursor} + error={errors.tools} + /> + + { + void makeRequest( + { + method: "ping" as const, + }, + EmptyResultSchema, + ); + }} + /> + + + + )}
) : ( diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx index 266f98e..c95f621 100644 --- a/client/src/components/Sidebar.tsx +++ b/client/src/components/Sidebar.tsx @@ -1,5 +1,14 @@ import { useState } from "react"; -import { Play, ChevronDown, ChevronRight } 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>(new Set()); return (
@@ -86,6 +96,7 @@ const Sidebar = ({ placeholder="Command" value={command} onChange={(e) => setCommand(e.target.value)} + className="font-mono" />
@@ -94,6 +105,7 @@ const Sidebar = ({ placeholder="Arguments (space-separated)" value={args} onChange={(e) => setArgs(e.target.value)} + className="font-mono" />
@@ -104,6 +116,7 @@ const Sidebar = ({ placeholder="URL" value={sseUrl} onChange={(e) => setSseUrl(e.target.value)} + className="font-mono" />
)} @@ -124,19 +137,44 @@ const Sidebar = ({ {showEnvVars && (
{Object.entries(env).map(([key, value], idx) => ( -
-
+
+
{ + 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" /> + +
+
{ @@ -144,25 +182,47 @@ const Sidebar = ({ newEnv[key] = e.target.value; setEnv(newEnv); }} + className="font-mono" /> +
-
))}
-
+
+ +
diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index c170ba3..ebdd131 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -10,7 +10,7 @@ import { CallToolResultSchema, } from "@modelcontextprotocol/sdk/types.js"; import { AlertCircle, Send } from "lucide-react"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import ListPane from "./ListPane"; import { CompatibilityCallToolResult } from "@modelcontextprotocol/sdk/types.js"; @@ -31,12 +31,15 @@ const ToolsTab = ({ clearTools: () => void; callTool: (name: string, params: Record) => void; selectedTool: Tool | null; - setSelectedTool: (tool: Tool) => void; + setSelectedTool: (tool: Tool | null) => void; toolResult: CompatibilityCallToolResult | null; nextCursor: ListToolsResult["nextCursor"]; error: string | null; }) => { const [params, setParams] = useState>({}); + useEffect(() => { + setParams({}); + }, [selectedTool]); const renderToolResult = () => { if (!toolResult) return null; @@ -107,7 +110,7 @@ const ToolsTab = ({ return ( <>

Tool Result (Legacy):

-
+          
             {JSON.stringify(toolResult.toolResult, null, 2)}
           
@@ -120,7 +123,10 @@ const ToolsTab = ({ { + clearTools(); + setSelectedTool(null); + }} setSelectedItem={setSelectedTool} renderItem={(tool) => ( <> @@ -178,6 +184,30 @@ const ToolsTab = ({ } className="mt-1" /> + ) : /* @ts-expect-error value type is currently unknown */ + value.type === "object" ? ( +