diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..ec826b1f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,33 @@ +# MCP Inspector Development Guide + +## Build Commands + +- Build all: `npm run build` +- Build client: `npm run build-client` +- Build server: `npm run build-server` +- Development mode: `npm run dev` (use `npm run dev:windows` on Windows) +- Format code: `npm run prettier-fix` +- Client lint: `cd client && npm run lint` + +## Code Style Guidelines + +- Use TypeScript with proper type annotations +- Follow React functional component patterns with hooks +- Use ES modules (import/export) not CommonJS +- Use Prettier for formatting (auto-formatted on commit) +- Follow existing naming conventions: + - camelCase for variables and functions + - PascalCase for component names and types + - kebab-case for file names +- Use async/await for asynchronous operations +- Implement proper error handling with try/catch blocks +- Use Tailwind CSS for styling in the client +- Keep components small and focused on a single responsibility + +## Project Organization + +The project is organized as a monorepo with workspaces: + +- `client/`: React frontend with Vite, TypeScript and Tailwind +- `server/`: Express backend with TypeScript +- `cli/`: Command-line interface for testing and invoking MCP server methods directly diff --git a/CLAUDE.md b/CLAUDE.md index ec826b1f..285e0f5b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,33 +1 @@ -# MCP Inspector Development Guide - -## Build Commands - -- Build all: `npm run build` -- Build client: `npm run build-client` -- Build server: `npm run build-server` -- Development mode: `npm run dev` (use `npm run dev:windows` on Windows) -- Format code: `npm run prettier-fix` -- Client lint: `cd client && npm run lint` - -## Code Style Guidelines - -- Use TypeScript with proper type annotations -- Follow React functional component patterns with hooks -- Use ES modules (import/export) not CommonJS -- Use Prettier for formatting (auto-formatted on commit) -- Follow existing naming conventions: - - camelCase for variables and functions - - PascalCase for component names and types - - kebab-case for file names -- Use async/await for asynchronous operations -- Implement proper error handling with try/catch blocks -- Use Tailwind CSS for styling in the client -- Keep components small and focused on a single responsibility - -## Project Organization - -The project is organized as a monorepo with workspaces: - -- `client/`: React frontend with Vite, TypeScript and Tailwind -- `server/`: Express backend with TypeScript -- `cli/`: Command-line interface for testing and invoking MCP server methods directly +@./AGENTS.md diff --git a/README.md b/README.md index 29037550..4f07df86 100644 --- a/README.md +++ b/README.md @@ -236,13 +236,15 @@ ALLOWED_ORIGINS=http://localhost:6274,http://localhost:8000 npm start The MCP Inspector supports the following configuration settings. To change them, click on the `Configuration` button in the MCP Inspector UI: -| 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 | "" | -| `MCP_AUTO_OPEN_ENABLED` | Enable automatic browser opening when inspector starts (works with authentication enabled). Only as environment var, not configurable in browser. | true | +| Setting | Description | Default | +| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | +| `MCP_SERVER_REQUEST_TIMEOUT` | Client-side timeout (ms) - Inspector will cancel the request if no response is received within this time. Note: servers may have their own timeouts | 300000 | +| `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 | "" | +| `MCP_AUTO_OPEN_ENABLED` | Enable automatic browser opening when inspector starts (works with authentication enabled). Only as environment var, not configurable in browser. | true | + +**Note on Timeouts:** The timeout settings above control when the Inspector (as an MCP client) will cancel requests. These are independent of any server-side timeouts. For example, if a server tool has a 10-minute timeout but the Inspector's timeout is set to 30 seconds, the Inspector will cancel the request after 30 seconds. Conversely, if the Inspector's timeout is 10 minutes but the server times out after 30 seconds, you'll receive the server's timeout error. For tools that require user interaction (like elicitation) or long-running operations, ensure the Inspector's timeout is set appropriately. These settings can be adjusted in real-time through the UI and will persist across sessions. @@ -361,7 +363,7 @@ http://localhost:6274/?transport=stdio&serverCommand=npx&serverArgs=arg1%20arg2 You can also set initial config settings via query params, for example: ``` -http://localhost:6274/?MCP_SERVER_REQUEST_TIMEOUT=10000&MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS=false&MCP_PROXY_FULL_ADDRESS=http://10.1.1.22:5577 +http://localhost:6274/?MCP_SERVER_REQUEST_TIMEOUT=60000&MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS=false&MCP_PROXY_FULL_ADDRESS=http://10.1.1.22:5577 ``` Note that if both the query param and the corresponding localStorage item are set, the query param will take precedence. @@ -456,6 +458,17 @@ npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --me | **Automation** | N/A | Ideal for CI/CD pipelines, batch processing, and integration with coding assistants | | **Learning MCP** | Rich visual interface helps new users understand server capabilities | Simplified commands for focused learning of specific endpoints | +## Tool Input Validation Guidelines + +When implementing or modifying tool input parameter handling in the Inspector: + +- **Omit optional fields with empty values** - When processing form inputs, omit empty strings or null values for optional parameters, UNLESS the field has an explicit default value in the schema that matches the current value +- **Preserve explicit default values** - If a field schema contains an explicit default (e.g., `default: null`), and the current value matches that default, include it in the request. This is a meaningful value the tool expects +- **Always include required fields** - Preserve required field values even when empty, allowing the MCP server to validate and return appropriate error messages +- **Defer deep validation to the server** - Implement basic field presence checking in the Inspector client, but rely on the MCP server for parameter validation according to its schema + +These guidelines maintain clean parameter passing and proper separation of concerns between the Inspector client and MCP servers. + ## License This project is licensed under the MIT License—see the [LICENSE](LICENSE) file for details. diff --git a/cli/package.json b/cli/package.json index ddeb3b93..85431073 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-cli", - "version": "0.17.0", + "version": "0.17.2", "description": "CLI for the Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", @@ -24,7 +24,7 @@ }, "devDependencies": {}, "dependencies": { - "@modelcontextprotocol/sdk": "^1.18.0", + "@modelcontextprotocol/sdk": "^1.20.1", "commander": "^13.1.0", "spawn-rx": "^5.1.2" } diff --git a/cli/src/index.ts b/cli/src/index.ts index ab655ff1..45a71a05 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -1,5 +1,6 @@ #!/usr/bin/env node +import * as fs from "fs"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { Command } from "commander"; import { @@ -31,8 +32,6 @@ type JsonValue = | JsonValue[] | { [key: string]: JsonValue }; -import packageJson from "../package.json" with { type: "json" }; - type Args = { target: string[]; method?: string; @@ -103,6 +102,15 @@ function createTransportOptions( } async function callMethod(args: Args): Promise { + // Read package.json to get name and version for client identity + const pathA = "../package.json"; // We're in package @modelcontextprotocol/inspector-cli + const pathB = "../../package.json"; // We're in package @modelcontextprotocol/inspector + let packageJson: { name: string; version: string }; + let packageJsonData = await import(fs.existsSync(pathA) ? pathA : pathB, { + with: { type: "json" }, + }); + packageJson = packageJsonData.default; + const transportOptions = createTransportOptions( args.target, args.transport, diff --git a/client/package.json b/client/package.json index 6881c1f7..56317c40 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-client", - "version": "0.17.0", + "version": "0.17.2", "description": "Client-side application for the Model Context Protocol inspector", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", @@ -25,7 +25,7 @@ "cleanup:e2e": "node e2e/global-teardown.js" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.18.0", + "@modelcontextprotocol/sdk": "^1.20.1", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.3", "@radix-ui/react-icons": "^1.3.0", @@ -61,7 +61,7 @@ "@types/react": "^18.3.23", "@types/react-dom": "^18.3.0", "@types/serve-handler": "^6.1.4", - "@vitejs/plugin-react": "^4.7.0", + "@vitejs/plugin-react": "^5.0.4", "autoprefixer": "^10.4.20", "co": "^4.6.0", "eslint": "^9.11.1", @@ -77,6 +77,6 @@ "ts-jest": "^29.4.0", "typescript": "^5.5.3", "typescript-eslint": "^8.38.0", - "vite": "^6.3.5" + "vite": "^7.1.11" } } diff --git a/client/src/App.tsx b/client/src/App.tsx index 14f4c339..802c6b08 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -143,6 +143,10 @@ const App = () => { return localStorage.getItem("lastOauthScope") || ""; }); + const [oauthClientSecret, setOauthClientSecret] = useState(() => { + return localStorage.getItem("lastOauthClientSecret") || ""; + }); + // Custom headers state with migration from legacy auth const [customHeaders, setCustomHeaders] = useState(() => { const savedHeaders = localStorage.getItem("lastCustomHeaders"); @@ -282,6 +286,7 @@ const App = () => { env, customHeaders, oauthClientId, + oauthClientSecret, oauthScope, config, connectionType, @@ -415,6 +420,10 @@ const App = () => { localStorage.setItem("lastOauthScope", oauthScope); }, [oauthScope]); + useEffect(() => { + localStorage.setItem("lastOauthClientSecret", oauthClientSecret); + }, [oauthClientSecret]); + useEffect(() => { saveInspectorConfig(CONFIG_LOCAL_STORAGE_KEY, config); }, [config]); @@ -931,6 +940,8 @@ const App = () => { setCustomHeaders={setCustomHeaders} oauthClientId={oauthClientId} setOauthClientId={setOauthClientId} + oauthClientSecret={oauthClientSecret} + setOauthClientSecret={setOauthClientSecret} oauthScope={oauthScope} setOauthScope={setOauthScope} onConnect={connectMcpServer} diff --git a/client/src/components/JsonView.tsx b/client/src/components/JsonView.tsx index 1febff6a..8c520ebe 100644 --- a/client/src/components/JsonView.tsx +++ b/client/src/components/JsonView.tsx @@ -1,4 +1,5 @@ -import { useState, memo, useMemo, useCallback } from "react"; +import { useState, memo, useMemo, useCallback, useEffect } from "react"; +import type React from "react"; import type { JsonValue } from "@/utils/jsonUtils"; import clsx from "clsx"; import { Copy, CheckCheck } from "lucide-react"; @@ -51,7 +52,7 @@ const JsonView = memo( variant: "destructive", }); } - }, [toast, normalizedData]); + }, [toast, normalizedData, setCopied]); return (
@@ -101,6 +102,7 @@ const JsonNode = memo( initialExpandDepth, isError = false, }: JsonNodeProps) => { + const { toast } = useToast(); const [isExpanded, setIsExpanded] = useState(depth < initialExpandDepth); const [typeStyleMap] = useState>({ number: "text-blue-600", @@ -113,6 +115,52 @@ const JsonNode = memo( }); const dataType = getDataType(data); + const [copied, setCopied] = useState(false); + useEffect(() => { + let timeoutId: NodeJS.Timeout; + if (copied) { + timeoutId = setTimeout(() => setCopied(false), 500); + } + return () => { + if (timeoutId) clearTimeout(timeoutId); + }; + }, [copied]); + + const handleCopyValue = useCallback( + (value: JsonValue) => { + try { + let text: string; + const valueType = getDataType(value); + switch (valueType) { + case "string": + text = value as unknown as string; + break; + case "number": + case "boolean": + text = String(value); + break; + case "null": + text = "null"; + break; + case "undefined": + text = "undefined"; + break; + default: + text = JSON.stringify(value); + } + navigator.clipboard.writeText(text); + 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], + ); + const renderCollapsible = (isArray: boolean) => { const items = isArray ? (data as JsonValue[]) @@ -206,7 +254,7 @@ const JsonNode = memo( if (!isTooLong) { return ( -
+
{name && ( {name}: @@ -220,12 +268,28 @@ const JsonNode = memo( > "{value}" +
); } return ( -
+
{name && ( {name}: @@ -241,6 +305,22 @@ const JsonNode = memo( > {isExpanded ? `"${value}"` : `"${value.slice(0, maxLength)}..."`} +
); }; @@ -253,7 +333,7 @@ const JsonNode = memo( return renderString(data as string); default: return ( -
+
{name && ( {name}: @@ -262,6 +342,22 @@ const JsonNode = memo( {data === null ? "null" : String(data)} +
); } diff --git a/client/src/components/OAuthFlowProgress.tsx b/client/src/components/OAuthFlowProgress.tsx index 5f44a4f5..6e0fd695 100644 --- a/client/src/components/OAuthFlowProgress.tsx +++ b/client/src/components/OAuthFlowProgress.tsx @@ -181,8 +181,8 @@ export const OAuthFlowProgress = ({

Resource metadata was added in the{" "} - - 2025-DRAFT-v2 specification update + + 2025-06-18 specification update
{authState.resourceMetadataError.message} diff --git a/client/src/components/PromptsTab.tsx b/client/src/components/PromptsTab.tsx index fa590558..424845d0 100644 --- a/client/src/components/PromptsTab.tsx +++ b/client/src/components/PromptsTab.tsx @@ -63,9 +63,7 @@ const PromptsTab = ({ clearCompletions(); }, [clearCompletions, selectedPrompt]); - const handleInputChange = async (argName: string, value: string) => { - setPromptArgs((prev) => ({ ...prev, [argName]: value })); - + const triggerCompletions = (argName: string, value: string) => { if (selectedPrompt) { requestCompletions( { @@ -79,6 +77,16 @@ const PromptsTab = ({ } }; + const handleInputChange = async (argName: string, value: string) => { + setPromptArgs((prev) => ({ ...prev, [argName]: value })); + triggerCompletions(argName, value); + }; + + const handleFocus = async (argName: string) => { + const currentValue = promptArgs[argName] || ""; + triggerCompletions(argName, currentValue); + }; + const handleGetPrompt = () => { if (selectedPrompt) { getPrompt(selectedPrompt.name, promptArgs); @@ -123,7 +131,9 @@ const PromptsTab = ({ Error - {error} + + {error} + ) : selectedPrompt ? (

@@ -143,6 +153,7 @@ const PromptsTab = ({ onInputChange={(value) => handleInputChange(arg.name, value) } + onFocus={() => handleFocus(arg.name)} options={completions[arg.name] || []} /> diff --git a/client/src/components/ResourcesTab.tsx b/client/src/components/ResourcesTab.tsx index 874e0ff7..f44a8402 100644 --- a/client/src/components/ResourcesTab.tsx +++ b/client/src/components/ResourcesTab.tsx @@ -225,7 +225,9 @@ const ResourcesTab = ({ Error - {error} + + {error} + ) : selectedResource ? ( void; oauthClientId: string; setOauthClientId: (id: string) => void; + oauthClientSecret: string; + setOauthClientSecret: (secret: string) => void; oauthScope: string; setOauthScope: (scope: string) => void; onConnect: () => void; @@ -87,6 +89,8 @@ const Sidebar = ({ setCustomHeaders, oauthClientId, setOauthClientId, + oauthClientSecret, + setOauthClientSecret, oauthScope, setOauthScope, onConnect, @@ -104,6 +108,7 @@ const Sidebar = ({ const [showAuthConfig, setShowAuthConfig] = useState(false); const [showConfig, setShowConfig] = useState(false); const [shownEnvVars, setShownEnvVars] = useState>(new Set()); + const [showClientSecret, setShowClientSecret] = useState(false); const [copiedServerEntry, setCopiedServerEntry] = useState(false); const [copiedServerFile, setCopiedServerFile] = useState(false); const { toast } = useToast(); @@ -555,6 +560,38 @@ const Sidebar = ({ data-testid="oauth-client-id-input" className="font-mono" /> + +
+ setOauthClientSecret(e.target.value)} + value={oauthClientSecret} + data-testid="oauth-client-secret-input" + className="font-mono" + /> + +
diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index 780fa208..39366b2b 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -5,6 +5,13 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { TabsContent } from "@/components/ui/tabs"; import { Textarea } from "@/components/ui/textarea"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import DynamicJsonForm, { DynamicJsonFormRef } from "./DynamicJsonForm"; import type { JsonValue, JsonSchemaType } from "@/utils/jsonUtils"; import { @@ -121,7 +128,7 @@ const ToolsTab = ({ renderItem={(tool) => (
{tool.name} - + {tool.description}
@@ -144,10 +151,12 @@ const ToolsTab = ({ Error - {error} + + {error} + )} -

+

{selectedTool.description}

{Object.entries(selectedTool.inputSchema.properties ?? []).map( @@ -158,148 +167,225 @@ const ToolsTab = ({ const required = isPropertyRequired(key, inputSchema); return (
- - {prop.type === "boolean" ? ( -
- - setParams({ - ...params, - [key]: checked, - }) - } - /> - -
- ) : prop.type === "string" ? ( -