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 1e6a37c2..285e0f5b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,44 +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 - -## Tool Input Validation Guidelines - -When handling tool input parameters and form fields: - -- **Optional fields with empty values should be omitted entirely** - Do not send empty strings or null values for optional parameters, UNLESS the field has an explicit default value in the schema that matches the current value -- **Fields with explicit defaults should preserve their default values** - If a field has an explicit default in its schema (e.g., `default: null`), and the current value matches that default, include it in the request. This is a meaningful value the tool expects -- **Required fields should preserve their values even when empty** - This allows the server to properly validate and return appropriate error messages -- **Deeper validation should be handled by the server** - Inspector should focus on basic field presence, while the MCP server handles parameter validation according to its schema - -These guidelines ensure clean parameter passing and proper separation of concerns between the Inspector client and MCP servers. +@./AGENTS.md diff --git a/client/src/App.tsx b/client/src/App.tsx index b7fc607b..5937d738 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -141,6 +141,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"); @@ -262,6 +266,7 @@ const App = () => { env, customHeaders, oauthClientId, + oauthClientSecret, oauthScope, config, connectionType, @@ -394,6 +399,10 @@ const App = () => { localStorage.setItem("lastOauthScope", oauthScope); }, [oauthScope]); + useEffect(() => { + localStorage.setItem("lastOauthClientSecret", oauthClientSecret); + }, [oauthClientSecret]); + useEffect(() => { saveInspectorConfig(CONFIG_LOCAL_STORAGE_KEY, config); }, [config]); @@ -900,6 +909,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/PromptsTab.tsx b/client/src/components/PromptsTab.tsx index 0df36bb8..424845d0 100644 --- a/client/src/components/PromptsTab.tsx +++ b/client/src/components/PromptsTab.tsx @@ -131,7 +131,9 @@ const PromptsTab = ({ Error - {error} + + {error} + ) : selectedPrompt ? (
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 0123296a..50edaf51 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 { @@ -114,7 +121,7 @@ const ToolsTab = ({ renderItem={(tool) => (
{tool.name} - + {tool.description}
@@ -137,10 +144,12 @@ const ToolsTab = ({ Error - {error} + + {error} + )} -

+

{selectedTool.description}

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