diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ed28e9d..72502f9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,7 +7,7 @@ Thanks for your interest in contributing! This guide explains how to get involve 1. Fork the repository and clone it locally 2. Install dependencies with `npm install` 3. Run `npm run dev` to start both client and server in development mode -4. Use the web UI at http://127.0.0.1:5173 to interact with the inspector +4. Use the web UI at http://127.0.0.1:6274 to interact with the inspector ## Development Process & Pull Requests diff --git a/README.md b/README.md index 50db8ac..fff4dae 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ The MCP inspector is a developer tool for testing and debugging MCP servers. -![MCP Inspector Screenshot](mcp-inspector.png) +![MCP Inspector Screenshot](https://raw.githubusercontent.com/modelcontextprotocol/inspector/main/mcp-inspector.png) ## Running the Inspector @@ -30,7 +30,7 @@ npx @modelcontextprotocol/inspector -e key=value -e key2=$VALUE2 node build/inde npx @modelcontextprotocol/inspector -e key=$VALUE -- node 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: +The inspector runs both an MCP Inspector (MCPI) client UI (default port 6274) and an MCP Proxy (MCPP) server (default port 6277). Open the MCPI client UI in your browser to use the inspector. (These ports are derived from the T9 dialpad mapping of MCPI and MCPP respectively, as a mnemonic). You can customize the ports if needed: ```bash CLIENT_PORT=8080 SERVER_PORT=9000 npx @modelcontextprotocol/inspector node build/index.js @@ -50,9 +50,14 @@ The MCP Inspector includes a proxy server that can run and communicate with loca The MCP Inspector supports the following configuration settings. To change them, click on the `Configuration` button in the MCP Inspector UI: -| Name | Purpose | Default Value | -| -------------------------- | ----------------------------------------------------------------------------------------- | ------------- | -| MCP_SERVER_REQUEST_TIMEOUT | Maximum time in milliseconds to wait for a response from the MCP server before timing out | 10000 | +| Setting | Description | Default | +| --------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ------- | +| `MCP_SERVER_REQUEST_TIMEOUT` | Timeout for requests to the MCP server (ms) | 10000 | +| `MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS` | Reset timeout on progress notifications | true | +| `MCP_REQUEST_MAX_TOTAL_TIMEOUT` | Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications) | 60000 | +| `MCP_PROXY_FULL_ADDRESS` | Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577 | "" | + +These settings can be adjusted in real-time through the UI and will persist across sessions. The inspector also supports configuration files to store settings for different MCP servers. This is useful when working with multiple servers or complex configurations: diff --git a/bin/cli.js b/bin/cli.js index 3aa9d6c..169d23c 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -19,7 +19,7 @@ function handleError(error) { process.exit(1); } function delay(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); + return new Promise((resolve) => setTimeout(resolve, ms, true)); } async function runWebClient(args) { const inspectorServerPath = resolve( diff --git a/client/bin/cli.js b/client/bin/cli.js index 7dc93ea..d30cc70 100755 --- a/client/bin/cli.js +++ b/client/bin/cli.js @@ -15,5 +15,19 @@ const server = http.createServer((request, response) => { }); }); -const port = process.env.PORT || 5173; -server.listen(port, () => {}); +const port = process.env.PORT || 6274; +server.on("listening", () => { + console.log( + `🔍 MCP Inspector is up and running at http://127.0.0.1:${port} 🚀`, + ); +}); +server.on("error", (err) => { + if (err.message.includes(`EADDRINUSE`)) { + console.error( + `❌ MCP Inspector PORT IS IN USE at http://127.0.0.1:${port} ❌ `, + ); + } else { + throw err; + } +}); +server.listen(port); diff --git a/client/package.json b/client/package.json index 9eff88c..0734ea9 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-client", - "version": "0.7.0", + "version": "0.8.2", "description": "Client-side application for the Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", @@ -18,12 +18,12 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", - "preview": "vite preview", + "preview": "vite preview --port 6274", "test": "jest --config jest.config.cjs", "test:watch": "jest --config jest.config.cjs --watch" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.6.1", + "@modelcontextprotocol/sdk": "^1.9.0", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.3", "@radix-ui/react-icons": "^1.3.0", @@ -32,7 +32,8 @@ "@radix-ui/react-select": "^2.1.2", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tabs": "^1.1.1", - "@types/prismjs": "^1.26.5", + "@radix-ui/react-toast": "^1.2.6", + "@radix-ui/react-tooltip": "^1.1.8", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cmdk": "^1.0.4", @@ -42,7 +43,6 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-simple-code-editor": "^0.14.1", - "react-toastify": "^10.0.6", "serve-handler": "^6.1.6", "tailwind-merge": "^2.5.3", "tailwindcss-animate": "^1.0.7", @@ -54,6 +54,7 @@ "@testing-library/react": "^16.2.0", "@types/jest": "^29.5.14", "@types/node": "^22.7.5", + "@types/prismjs": "^1.26.5", "@types/react": "^18.3.10", "@types/react-dom": "^18.3.0", "@types/serve-handler": "^6.1.4", diff --git a/client/src/App.tsx b/client/src/App.tsx index c29ef71..4f99ffd 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -20,7 +20,6 @@ import { import React, { Suspense, useEffect, useRef, useState } from "react"; import { useConnection } from "./lib/hooks/useConnection"; import { useDraggablePane } from "./lib/hooks/useDraggablePane"; - import { StdErrNotification } from "./lib/notificationTypes"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; @@ -33,7 +32,6 @@ import { MessageSquare, } from "lucide-react"; -import { toast } from "react-toastify"; import { z } from "zod"; import "./App.css"; import ConsoleTab from "./components/ConsoleTab"; @@ -47,13 +45,14 @@ import Sidebar from "./components/Sidebar"; import ToolsTab from "./components/ToolsTab"; import { DEFAULT_INSPECTOR_CONFIG } from "./lib/constants"; import { InspectorConfig } from "./lib/configurationTypes"; +import { getMCPProxyAddress } from "./utils/configUtils"; +import { useToast } from "@/hooks/use-toast"; const params = new URLSearchParams(window.location.search); -const PROXY_PORT = params.get("proxyPort") ?? "3000"; -const PROXY_SERVER_URL = `http://${window.location.hostname}:${PROXY_PORT}`; const CONFIG_LOCAL_STORAGE_KEY = "inspectorConfig_v1"; const App = () => { + const { toast } = useToast(); // Handle OAuth callback route const [resources, setResources] = useState([]); const [resourceTemplates, setResourceTemplates] = useState< @@ -95,7 +94,24 @@ const App = () => { const [config, setConfig] = useState(() => { const savedConfig = localStorage.getItem(CONFIG_LOCAL_STORAGE_KEY); - return savedConfig ? JSON.parse(savedConfig) : DEFAULT_INSPECTOR_CONFIG; + if (savedConfig) { + // merge default config with saved config + const mergedConfig = { + ...DEFAULT_INSPECTOR_CONFIG, + ...JSON.parse(savedConfig), + } as InspectorConfig; + + // update description of keys to match the new description (in case of any updates to the default config description) + Object.entries(mergedConfig).forEach(([key, value]) => { + mergedConfig[key as keyof InspectorConfig] = { + ...value, + label: DEFAULT_INSPECTOR_CONFIG[key as keyof InspectorConfig].label, + }; + }); + + return mergedConfig; + } + return DEFAULT_INSPECTOR_CONFIG; }); const [bearerToken, setBearerToken] = useState(() => { return localStorage.getItem("lastBearerToken") || ""; @@ -140,11 +156,12 @@ const App = () => { serverCapabilities, mcpClient, requestHistory, - makeRequest: makeConnectionRequest, + makeRequest, sendNotification, handleCompletion, completionsSupported, connect: connectMcpServer, + disconnect: disconnectMcpServer, } = useConnection({ transportType, command, @@ -152,8 +169,7 @@ const App = () => { sseUrl, env, bearerToken, - proxyServerUrl: PROXY_SERVER_URL, - requestTimeout: config.MCP_SERVER_REQUEST_TIMEOUT.value as number, + config, onNotification: (notification) => { setNotifications((prev) => [...prev, notification as ServerNotification]); }, @@ -196,8 +212,13 @@ const App = () => { localStorage.setItem(CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(config)); }, [config]); + const hasProcessedRef = useRef(false); // Auto-connect if serverUrl is provided in URL params (e.g. after OAuth callback) useEffect(() => { + if (hasProcessedRef.current) { + // Only try to connect once + return; + } const serverUrl = params.get("serverUrl"); if (serverUrl) { setSseUrl(serverUrl); @@ -207,14 +228,18 @@ const App = () => { newUrl.searchParams.delete("serverUrl"); window.history.replaceState({}, "", newUrl.toString()); // Show success toast for OAuth - toast.success("Successfully authenticated with OAuth"); + toast({ + title: "Success", + description: "Successfully authenticated with OAuth", + }); + hasProcessedRef.current = true; // Connect to the server connectMcpServer(); } - }, [connectMcpServer]); + }, [connectMcpServer, toast]); useEffect(() => { - fetch(`${PROXY_SERVER_URL}/config`) + fetch(`${getMCPProxyAddress(config)}/config`) .then((response) => response.json()) .then((data) => { setEnv(data.defaultEnvironment); @@ -228,6 +253,7 @@ const App = () => { .catch((error) => console.error("Error fetching default environment:", error), ); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { @@ -260,13 +286,13 @@ const App = () => { setErrors((prev) => ({ ...prev, [tabKey]: null })); }; - const makeRequest = async ( + const sendMCPRequest = async ( request: ClientRequest, schema: T, tabKey?: keyof typeof errors, ) => { try { - const response = await makeConnectionRequest(request, schema); + const response = await makeRequest(request, schema); if (tabKey !== undefined) { clearError(tabKey); } @@ -284,7 +310,7 @@ const App = () => { }; const listResources = async () => { - const response = await makeRequest( + const response = await sendMCPRequest( { method: "resources/list" as const, params: nextResourceCursor ? { cursor: nextResourceCursor } : {}, @@ -297,7 +323,7 @@ const App = () => { }; const listResourceTemplates = async () => { - const response = await makeRequest( + const response = await sendMCPRequest( { method: "resources/templates/list" as const, params: nextResourceTemplateCursor @@ -314,7 +340,7 @@ const App = () => { }; const readResource = async (uri: string) => { - const response = await makeRequest( + const response = await sendMCPRequest( { method: "resources/read" as const, params: { uri }, @@ -327,7 +353,7 @@ const App = () => { const subscribeToResource = async (uri: string) => { if (!resourceSubscriptions.has(uri)) { - await makeRequest( + await sendMCPRequest( { method: "resources/subscribe" as const, params: { uri }, @@ -343,7 +369,7 @@ const App = () => { const unsubscribeFromResource = async (uri: string) => { if (resourceSubscriptions.has(uri)) { - await makeRequest( + await sendMCPRequest( { method: "resources/unsubscribe" as const, params: { uri }, @@ -358,7 +384,7 @@ const App = () => { }; const listPrompts = async () => { - const response = await makeRequest( + const response = await sendMCPRequest( { method: "prompts/list" as const, params: nextPromptCursor ? { cursor: nextPromptCursor } : {}, @@ -371,7 +397,7 @@ const App = () => { }; const getPrompt = async (name: string, args: Record = {}) => { - const response = await makeRequest( + const response = await sendMCPRequest( { method: "prompts/get" as const, params: { name, arguments: args }, @@ -383,7 +409,7 @@ const App = () => { }; const listTools = async () => { - const response = await makeRequest( + const response = await sendMCPRequest( { method: "tools/list" as const, params: nextToolCursor ? { cursor: nextToolCursor } : {}, @@ -396,21 +422,34 @@ const App = () => { }; const callTool = async (name: string, params: Record) => { - const response = await makeRequest( - { - method: "tools/call" as const, - params: { - name, - arguments: params, - _meta: { - progressToken: progressTokenRef.current++, + try { + const response = await sendMCPRequest( + { + method: "tools/call" as const, + params: { + name, + arguments: params, + _meta: { + progressToken: progressTokenRef.current++, + }, }, }, - }, - CompatibilityCallToolResultSchema, - "tools", - ); - setToolResult(response); + CompatibilityCallToolResultSchema, + "tools", + ); + setToolResult(response); + } catch (e) { + const toolResult: CompatibilityCallToolResult = { + content: [ + { + type: "text", + text: (e as Error).message ?? String(e), + }, + ], + isError: true, + }; + setToolResult(toolResult); + } }; const handleRootsChange = async () => { @@ -418,7 +457,7 @@ const App = () => { }; const sendLogLevelRequest = async (level: LoggingLevel) => { - await makeRequest( + await sendMCPRequest( { method: "logging/setLevel" as const, params: { level }, @@ -458,6 +497,7 @@ const App = () => { bearerToken={bearerToken} setBearerToken={setBearerToken} onConnect={connectMcpServer} + onDisconnect={disconnectMcpServer} stdErrNotifications={stdErrNotifications} logLevel={logLevel} sendLogLevelRequest={sendLogLevelRequest} @@ -617,9 +657,10 @@ const App = () => { setTools([]); setNextToolCursor(undefined); }} - callTool={(name, params) => { + callTool={async (name, params) => { clearError("tools"); - callTool(name, params); + setToolResult(null); + await callTool(name, params); }} selectedTool={selectedTool} setSelectedTool={(tool) => { @@ -634,7 +675,7 @@ const App = () => { { - void makeRequest( + void sendMCPRequest( { method: "ping" as const, }, diff --git a/client/src/components/DynamicJsonForm.tsx b/client/src/components/DynamicJsonForm.tsx index f5b0d63..bd77639 100644 --- a/client/src/components/DynamicJsonForm.tsx +++ b/client/src/components/DynamicJsonForm.tsx @@ -3,33 +3,9 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import JsonEditor from "./JsonEditor"; -import { updateValueAtPath, JsonObject } from "@/utils/jsonPathUtils"; +import { updateValueAtPath } from "@/utils/jsonUtils"; import { generateDefaultValue, formatFieldLabel } from "@/utils/schemaUtils"; - -export type JsonValue = - | string - | number - | boolean - | null - | undefined - | JsonValue[] - | { [key: string]: JsonValue }; - -export type JsonSchemaType = { - type: - | "string" - | "number" - | "integer" - | "boolean" - | "array" - | "object" - | "null"; - description?: string; - required?: boolean; - default?: JsonValue; - properties?: Record; - items?: JsonSchemaType; -}; +import type { JsonValue, JsonSchemaType, JsonObject } from "@/utils/jsonUtils"; interface DynamicJsonFormProps { schema: JsonSchemaType; diff --git a/client/src/components/History.tsx b/client/src/components/History.tsx index b03d1f4..0b05b55 100644 --- a/client/src/components/History.tsx +++ b/client/src/components/History.tsx @@ -1,5 +1,4 @@ import { ServerNotification } from "@modelcontextprotocol/sdk/types.js"; -import { Copy } from "lucide-react"; import { useState } from "react"; import JsonView from "./JsonView"; @@ -25,10 +24,6 @@ const HistoryAndNotifications = ({ setExpandedNotifications((prev) => ({ ...prev, [index]: !prev[index] })); }; - const copyToClipboard = (text: string) => { - navigator.clipboard.writeText(text); - }; - return (
@@ -68,16 +63,12 @@ const HistoryAndNotifications = ({ Request: - -
-
-
+ +
{request.response && (
@@ -85,16 +76,11 @@ const HistoryAndNotifications = ({ Response: - -
-
-
+ )} @@ -134,20 +120,11 @@ const HistoryAndNotifications = ({ Details: - - -
-
+ )} diff --git a/client/src/components/JsonView.tsx b/client/src/components/JsonView.tsx index e2922f0..3fcbf8e 100644 --- a/client/src/components/JsonView.tsx +++ b/client/src/components/JsonView.tsx @@ -1,45 +1,96 @@ -import { useState, memo } from "react"; -import { JsonValue } from "./DynamicJsonForm"; +import { useState, memo, useMemo, useCallback, useEffect } from "react"; +import type { JsonValue } from "@/utils/jsonUtils"; import clsx from "clsx"; +import { Copy, CheckCheck } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { useToast } from "@/hooks/use-toast"; +import { getDataType, tryParseJson } from "@/utils/jsonUtils"; interface JsonViewProps { data: unknown; name?: string; initialExpandDepth?: number; -} - -function tryParseJson(str: string): { success: boolean; data: JsonValue } { - const trimmed = str.trim(); - if ( - !(trimmed.startsWith("{") && trimmed.endsWith("}")) && - !(trimmed.startsWith("[") && trimmed.endsWith("]")) - ) { - return { success: false, data: str }; - } - try { - return { success: true, data: JSON.parse(str) }; - } catch { - return { success: false, data: str }; - } + className?: string; + withCopyButton?: boolean; + isError?: boolean; } const JsonView = memo( - ({ data, name, initialExpandDepth = 3 }: JsonViewProps) => { - const normalizedData = - typeof data === "string" + ({ + data, + name, + initialExpandDepth = 3, + className, + withCopyButton = true, + isError = false, + }: JsonViewProps) => { + const { toast } = useToast(); + const [copied, setCopied] = useState(false); + + useEffect(() => { + let timeoutId: NodeJS.Timeout; + if (copied) { + timeoutId = setTimeout(() => { + setCopied(false); + }, 500); + } + return () => { + if (timeoutId) { + clearTimeout(timeoutId); + } + }; + }, [copied]); + + const normalizedData = useMemo(() => { + return typeof data === "string" ? tryParseJson(data).success ? tryParseJson(data).data : data : data; + }, [data]); + + const handleCopy = useCallback(() => { + try { + navigator.clipboard.writeText( + typeof normalizedData === "string" + ? normalizedData + : JSON.stringify(normalizedData, null, 2), + ); + setCopied(true); + } catch (error) { + toast({ + title: "Error", + description: `There was an error coping result into the clipboard: ${error instanceof Error ? error.message : String(error)}`, + variant: "destructive", + }); + } + }, [toast, normalizedData]); return ( -
- +
+ {withCopyButton && ( + + )} +
+ +
); }, @@ -52,28 +103,28 @@ interface JsonNodeProps { name?: string; depth: number; initialExpandDepth: number; + isError?: boolean; } const JsonNode = memo( - ({ data, name, depth = 0, initialExpandDepth }: JsonNodeProps) => { + ({ + data, + name, + depth = 0, + initialExpandDepth, + isError = false, + }: JsonNodeProps) => { const [isExpanded, setIsExpanded] = useState(depth < initialExpandDepth); - - const getDataType = (value: JsonValue): string => { - if (Array.isArray(value)) return "array"; - if (value === null) return "null"; - return typeof value; - }; - - const dataType = getDataType(data); - - const typeStyleMap: Record = { + const [typeStyleMap] = useState>({ number: "text-blue-600", boolean: "text-amber-600", null: "text-purple-600", undefined: "text-gray-600", - string: "text-green-600 break-all whitespace-pre-wrap", + string: "text-green-600 group-hover:text-green-500", + error: "text-red-600 group-hover:text-red-500", default: "text-gray-700", - }; + }); + const dataType = getDataType(data); const renderCollapsible = (isArray: boolean) => { const items = isArray @@ -174,7 +225,14 @@ const JsonNode = memo( {name}: )} -
"{value}"
+
+              "{value}"
+            
); } @@ -188,8 +246,8 @@ const JsonNode = memo( )}
 setIsExpanded(!isExpanded)}
             title={isExpanded ? "Click to collapse" : "Click to expand"}
diff --git a/client/src/components/ListPane.tsx b/client/src/components/ListPane.tsx
index 90693dd..6f9a8cf 100644
--- a/client/src/components/ListPane.tsx
+++ b/client/src/components/ListPane.tsx
@@ -22,7 +22,7 @@ const ListPane = ({
   isButtonDisabled,
 }: ListPaneProps) => (
   
-
+

{title}

diff --git a/client/src/components/PingTab.tsx b/client/src/components/PingTab.tsx index 287356c..6546901 100644 --- a/client/src/components/PingTab.tsx +++ b/client/src/components/PingTab.tsx @@ -3,14 +3,16 @@ import { Button } from "@/components/ui/button"; const PingTab = ({ onPingClick }: { onPingClick: () => void }) => { return ( - -
- + +
+
+ +
); diff --git a/client/src/components/PromptsTab.tsx b/client/src/components/PromptsTab.tsx index b42cf77..c697d52 100644 --- a/client/src/components/PromptsTab.tsx +++ b/client/src/components/PromptsTab.tsx @@ -84,86 +84,88 @@ const PromptsTab = ({ }; return ( - - { - setSelectedPrompt(prompt); - setPromptArgs({}); - }} - renderItem={(prompt) => ( - <> - {prompt.name} - {prompt.description} - - )} - title="Prompts" - buttonText={nextCursor ? "List More Prompts" : "List Prompts"} - isButtonDisabled={!nextCursor && prompts.length > 0} - /> - -
-
-

- {selectedPrompt ? selectedPrompt.name : "Select a prompt"} -

-
-
- {error ? ( - - - Error - {error} - - ) : selectedPrompt ? ( -
- {selectedPrompt.description && ( -

- {selectedPrompt.description} -

- )} - {selectedPrompt.arguments?.map((arg) => ( -
- - handleInputChange(arg.name, value)} - onInputChange={(value) => - handleInputChange(arg.name, value) - } - options={completions[arg.name] || []} - /> - - {arg.description && ( -

- {arg.description} - {arg.required && ( - (Required) - )} -

- )} -
- ))} - - {promptContent && ( -
- -
- )} -
- ) : ( - - - Select a prompt from the list to view and use it - - + +
+ { + setSelectedPrompt(prompt); + setPromptArgs({}); + }} + renderItem={(prompt) => ( + <> + {prompt.name} + + {prompt.description} + + )} + title="Prompts" + buttonText={nextCursor ? "List More Prompts" : "List Prompts"} + isButtonDisabled={!nextCursor && prompts.length > 0} + /> + +
+
+

+ {selectedPrompt ? selectedPrompt.name : "Select a prompt"} +

+
+
+ {error ? ( + + + Error + {error} + + ) : selectedPrompt ? ( +
+ {selectedPrompt.description && ( +

+ {selectedPrompt.description} +

+ )} + {selectedPrompt.arguments?.map((arg) => ( +
+ + handleInputChange(arg.name, value)} + onInputChange={(value) => + handleInputChange(arg.name, value) + } + options={completions[arg.name] || []} + /> + + {arg.description && ( +

+ {arg.description} + {arg.required && ( + (Required) + )} +

+ )} +
+ ))} + + {promptContent && ( + + )} +
+ ) : ( + + + Select a prompt from the list to view and use it + + + )} +
diff --git a/client/src/components/ResourcesTab.tsx b/client/src/components/ResourcesTab.tsx index 443a902..ac9a824 100644 --- a/client/src/components/ResourcesTab.tsx +++ b/client/src/components/ResourcesTab.tsx @@ -111,154 +111,158 @@ const ResourcesTab = ({ }; return ( - - { - setSelectedResource(resource); - readResource(resource.uri); - setSelectedTemplate(null); - }} - renderItem={(resource) => ( -
- - - {resource.name} - - -
- )} - title="Resources" - buttonText={nextCursor ? "List More Resources" : "List Resources"} - isButtonDisabled={!nextCursor && resources.length > 0} - /> - - { - setSelectedTemplate(template); - setSelectedResource(null); - setTemplateValues({}); - }} - renderItem={(template) => ( -
- - - {template.name} - - -
- )} - title="Resource Templates" - buttonText={ - nextTemplateCursor ? "List More Templates" : "List Templates" - } - isButtonDisabled={!nextTemplateCursor && resourceTemplates.length > 0} - /> - -
-
-

- {selectedResource - ? selectedResource.name - : selectedTemplate - ? selectedTemplate.name - : "Select a resource or template"} -

- {selectedResource && ( -
- {resourceSubscriptionsSupported && - !resourceSubscriptions.has(selectedResource.uri) && ( - - )} - {resourceSubscriptionsSupported && - resourceSubscriptions.has(selectedResource.uri) && ( - - )} - + +
+ { + setSelectedResource(resource); + readResource(resource.uri); + setSelectedTemplate(null); + }} + renderItem={(resource) => ( +
+ + + {resource.name} + +
)} -
-
- {error ? ( - - - Error - {error} - - ) : selectedResource ? ( -
- + title="Resources" + buttonText={nextCursor ? "List More Resources" : "List Resources"} + isButtonDisabled={!nextCursor && resources.length > 0} + /> + + { + setSelectedTemplate(template); + setSelectedResource(null); + setTemplateValues({}); + }} + renderItem={(template) => ( +
+ + + {template.name} + +
- ) : selectedTemplate ? ( -
-

- {selectedTemplate.description} -

- {selectedTemplate.uriTemplate - .match(/{([^}]+)}/g) - ?.map((param) => { - const key = param.slice(1, -1); - return ( -
- - - handleTemplateValueChange(key, value) - } - onInputChange={(value) => - handleTemplateValueChange(key, value) - } - options={completions[key] || []} - /> -
- ); - })} - -
- ) : ( - - - Select a resource or template from the list to view its contents - - )} + title="Resource Templates" + buttonText={ + nextTemplateCursor ? "List More Templates" : "List Templates" + } + isButtonDisabled={!nextTemplateCursor && resourceTemplates.length > 0} + /> + +
+
+

+ {selectedResource + ? selectedResource.name + : selectedTemplate + ? selectedTemplate.name + : "Select a resource or template"} +

+ {selectedResource && ( +
+ {resourceSubscriptionsSupported && + !resourceSubscriptions.has(selectedResource.uri) && ( + + )} + {resourceSubscriptionsSupported && + resourceSubscriptions.has(selectedResource.uri) && ( + + )} + +
+ )} +
+
+ {error ? ( + + + Error + {error} + + ) : selectedResource ? ( + + ) : selectedTemplate ? ( +
+

+ {selectedTemplate.description} +

+ {selectedTemplate.uriTemplate + .match(/{([^}]+)}/g) + ?.map((param) => { + const key = param.slice(1, -1); + return ( +
+ + + handleTemplateValueChange(key, value) + } + onInputChange={(value) => + handleTemplateValueChange(key, value) + } + options={completions[key] || []} + /> +
+ ); + })} + +
+ ) : ( + + + Select a resource or template from the list to view its + contents + + + )} +
diff --git a/client/src/components/RootsTab.tsx b/client/src/components/RootsTab.tsx index 33f60d5..308b88b 100644 --- a/client/src/components/RootsTab.tsx +++ b/client/src/components/RootsTab.tsx @@ -35,40 +35,42 @@ const RootsTab = ({ }; return ( - - - - Configure the root directories that the server can access - - + +
+ + + Configure the root directories that the server can access + + - {roots.map((root, index) => ( -
- updateRoot(index, "uri", e.target.value)} - className="flex-1" - /> - +
+ ))} + +
+ +
- ))} - -
- -
); diff --git a/client/src/components/SamplingTab.tsx b/client/src/components/SamplingTab.tsx index 21fc7dd..d7d0212 100644 --- a/client/src/components/SamplingTab.tsx +++ b/client/src/components/SamplingTab.tsx @@ -33,31 +33,37 @@ const SamplingTab = ({ pendingRequests, onApprove, onReject }: Props) => { }; return ( - - - - When the server requests LLM sampling, requests will appear here for - approval. - - -
-

Recent Requests

- {pendingRequests.map((request) => ( -
-
- + +
+ + + When the server requests LLM sampling, requests will appear here for + approval. + + +
+

Recent Requests

+ {pendingRequests.map((request) => ( +
+ + +
+ + +
-
- - -
-
- ))} - {pendingRequests.length === 0 && ( -

No pending requests

- )} + ))} + {pendingRequests.length === 0 && ( +

No pending requests

+ )} +
); diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx index ca6d5fe..2ddb18d 100644 --- a/client/src/components/Sidebar.tsx +++ b/client/src/components/Sidebar.tsx @@ -8,7 +8,10 @@ import { Github, Eye, EyeOff, + RotateCcw, Settings, + HelpCircle, + RefreshCwOff, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -25,12 +28,17 @@ import { LoggingLevelSchema, } from "@modelcontextprotocol/sdk/types.js"; import { InspectorConfig } from "@/lib/configurationTypes"; - +import { ConnectionStatus } from "@/lib/constants"; import useTheme from "../lib/useTheme"; import { version } from "../../../package.json"; +import { + Tooltip, + TooltipTrigger, + TooltipContent, +} from "@/components/ui/tooltip"; interface SidebarProps { - connectionStatus: "disconnected" | "connected" | "error"; + connectionStatus: ConnectionStatus; transportType: "stdio" | "sse"; setTransportType: (type: "stdio" | "sse") => void; command: string; @@ -44,6 +52,7 @@ interface SidebarProps { bearerToken: string; setBearerToken: (token: string) => void; onConnect: () => void; + onDisconnect: () => void; stdErrNotifications: StdErrNotification[]; logLevel: LoggingLevel; sendLogLevelRequest: (level: LoggingLevel) => void; @@ -67,6 +76,7 @@ const Sidebar = ({ bearerToken, setBearerToken, onConnect, + onDisconnect, stdErrNotifications, logLevel, sendLogLevelRequest, @@ -82,7 +92,7 @@ const Sidebar = ({ return (
-
+

MCP Inspector v{version} @@ -93,14 +103,19 @@ const Sidebar = ({
- + setCommand(e.target.value)} @@ -122,8 +140,14 @@ const Sidebar = ({ />
- + setArgs(e.target.value)} @@ -134,8 +158,11 @@ const Sidebar = ({ ) : ( <>
- + setSseUrl(e.target.value)} @@ -147,6 +174,7 @@ const Sidebar = ({ variant="outline" onClick={() => setShowBearerToken(!showBearerToken)} className="flex items-center w-full" + aria-expanded={showBearerToken} > {showBearerToken ? ( @@ -157,8 +185,14 @@ const Sidebar = ({ {showBearerToken && (
- + setBearerToken(e.target.value)} @@ -176,6 +210,8 @@ const Sidebar = ({ variant="outline" onClick={() => setShowEnvVars(!showEnvVars)} className="flex items-center w-full" + data-testid="env-vars-button" + aria-expanded={showEnvVars} > {showEnvVars ? ( @@ -190,6 +226,7 @@ const Sidebar = ({
{ @@ -232,6 +269,7 @@ const Sidebar = ({
setShowConfig(!showConfig)} className="flex items-center w-full" + data-testid="config-button" + aria-expanded={showConfig} > {showConfig ? ( @@ -312,11 +352,25 @@ const Sidebar = ({ const configKey = key as keyof InspectorConfig; return (
- +
+ + + + + + + {configItem.description} + + +
{typeof configItem.value === "number" ? ( - + @@ -353,6 +407,7 @@ const Sidebar = ({ ) : ( { @@ -374,40 +429,77 @@ const Sidebar = ({
- + {connectionStatus === "connected" && ( +
+ + +
+ )} + {connectionStatus !== "connected" && ( + + )}
{ + switch (connectionStatus) { + case "connected": + return "bg-green-500"; + case "error": + return "bg-red-500"; + case "error-connecting-to-proxy": + return "bg-red-500"; + default: + return "bg-gray-500"; + } + })()}`} /> - {connectionStatus === "connected" - ? "Connected" - : connectionStatus === "error" - ? "Connection Error" - : "Disconnected"} + {(() => { + switch (connectionStatus) { + case "connected": + return "Connected"; + case "error": + return "Connection Error, is your MCP server running?"; + case "error-connecting-to-proxy": + return "Error Connecting to MCP Inspector Proxy - Check Console logs"; + default: + return "Disconnected"; + } + })()}
{loggingSupported && connectionStatus === "connected" && (
- +