diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ed28e9d6..72502f96 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 50db8ace..fff4dae0 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 3aa9d6c4..169d23ca 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 7dc93eab..d30cc702 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 9eff88c5..0734ea94 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 c29ef71b..4f99ffd4 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 f5b0d630..bd77639c 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 b03d1f44..0b05b55d 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 e2922f0e..3fcbf8e2 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 90693dd2..6f9a8cf7 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 287356c0..65469016 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 b42cf77e..c697d522 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 443a902c..ac9a8244 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 33f60d5d..308b88bb 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 21fc7dd8..d7d02126 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 ca6d5fe3..2ddb18d7 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" && (
- +