diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..5ace4600 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 23a66f86..52ca5ce4 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -33,7 +33,7 @@ jobs: github.event_name == 'pull_request_review_comment' || github.event_name == 'pull_request_review' id: pr - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | let prNumber; diff --git a/.github/workflows/cli_tests.yml b/.github/workflows/cli_tests.yml index ede7643e..3b46b431 100644 --- a/.github/workflows/cli_tests.yml +++ b/.github/workflows/cli_tests.yml @@ -18,7 +18,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version-file: package.json cache: npm diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml index 378905b4..4d0c4cec 100644 --- a/.github/workflows/e2e_tests.yml +++ b/.github/workflows/e2e_tests.yml @@ -20,7 +20,7 @@ jobs: - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 id: setup_node with: node-version-file: package.json @@ -29,7 +29,7 @@ jobs: # Cache Playwright browsers - name: Cache Playwright browsers id: cache-playwright - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.cache/ms-playwright # The default Playwright cache path key: ${{ runner.os }}-playwright-${{ hashFiles('package-lock.json') }} # Cache key based on OS and package-lock.json @@ -51,7 +51,7 @@ jobs: run: npm run test:e2e - name: Upload Playwright Report and Screenshots - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 if: steps.playwright-tests.conclusion != 'skipped' with: name: playwright-report diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d01f7175..96262fda 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,7 +17,7 @@ jobs: - name: Check formatting run: npx prettier --check . - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version-file: package.json cache: npm @@ -51,7 +51,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version-file: package.json cache: npm @@ -109,7 +109,7 @@ jobs: labels: ${{ steps.meta.outputs.labels }} - name: Generate artifact attestation - uses: actions/attest-build-provenance@v2 + uses: actions/attest-build-provenance@v3 with: subject-name: ghcr.io/${{ github.repository }} subject-digest: ${{ steps.push.outputs.digest }} diff --git a/cli/__tests__/helpers/assertions.ts b/cli/__tests__/helpers/assertions.ts index e3ed9d02..3631c00b 100644 --- a/cli/__tests__/helpers/assertions.ts +++ b/cli/__tests__/helpers/assertions.ts @@ -1,18 +1,30 @@ import { expect } from "vitest"; import type { CliResult } from "./cli-runner.js"; +function formatCliOutput(result: CliResult): string { + const out = result.stdout?.trim() || "(empty)"; + const err = result.stderr?.trim() || "(empty)"; + return `stdout: ${out}\nstderr: ${err}`; +} + /** * Assert that CLI command succeeded (exit code 0) */ export function expectCliSuccess(result: CliResult) { - expect(result.exitCode).toBe(0); + expect( + result.exitCode, + `CLI exited with code ${result.exitCode}. ${formatCliOutput(result)}`, + ).toBe(0); } /** * Assert that CLI command failed (non-zero exit code) */ export function expectCliFailure(result: CliResult) { - expect(result.exitCode).not.toBe(0); + expect( + result.exitCode, + `CLI unexpectedly exited with code ${result.exitCode}. ${formatCliOutput(result)}`, + ).not.toBe(0); } /** diff --git a/cli/__tests__/helpers/fixtures.ts b/cli/__tests__/helpers/fixtures.ts index 5914f485..c7f10fc7 100644 --- a/cli/__tests__/helpers/fixtures.ts +++ b/cli/__tests__/helpers/fixtures.ts @@ -1,7 +1,7 @@ -import fs from "fs"; -import path from "path"; -import os from "os"; -import crypto from "crypto"; +import * as fs from "fs"; +import * as path from "path"; +import * as os from "os"; +import * as crypto from "crypto"; import { getTestMcpServerCommand } from "./test-server-stdio.js"; /** diff --git a/cli/__tests__/helpers/test-server-http.ts b/cli/__tests__/helpers/test-server-http.ts index 4626ef51..d5eadc3f 100644 --- a/cli/__tests__/helpers/test-server-http.ts +++ b/cli/__tests__/helpers/test-server-http.ts @@ -6,6 +6,7 @@ import type { Request, Response } from "express"; import express from "express"; import { createServer as createHttpServer, Server as HttpServer } from "http"; import { createServer as createNetServer } from "net"; +import { randomUUID } from "crypto"; import * as z from "zod/v4"; import type { ServerConfig } from "./test-fixtures.js"; @@ -187,22 +188,24 @@ export class TestServerHttp { } /** - * Start the server with the specified transport + * Start the server with the specified transport. + * When requestedPort is omitted, uses port 0 so the OS assigns a unique port (avoids EADDRINUSE when tests run in parallel). */ async start( transport: "http" | "sse", requestedPort?: number, ): Promise { - const port = requestedPort - ? await findAvailablePort(requestedPort) - : await findAvailablePort(transport === "http" ? 3001 : 3000); - - this.url = `http://localhost:${port}`; + const port = + requestedPort !== undefined ? await findAvailablePort(requestedPort) : 0; if (transport === "http") { - return this.startHttp(port); + const actualPort = await this.startHttp(port); + this.url = `http://localhost:${actualPort}`; + return actualPort; } else { - return this.startSse(port); + const actualPort = await this.startSse(port); + this.url = `http://localhost:${actualPort}`; + return actualPort; } } @@ -213,8 +216,10 @@ export class TestServerHttp { // Create HTTP server this.httpServer = createHttpServer(app); - // Create StreamableHTTP transport - this.transport = new StreamableHTTPServerTransport({}); + // Create StreamableHTTP transport (stateful so it can handle multiple requests per session) + this.transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + }); // Set up Express route to handle MCP requests app.post("/mcp", async (req: Request, res: Response) => { @@ -292,10 +297,12 @@ export class TestServerHttp { // Connect transport to server await this.mcpServer.connect(this.transport); - // Start listening + // Start listening (port 0 = OS assigns a unique port) return new Promise((resolve, reject) => { this.httpServer!.listen(port, () => { - resolve(port); + const assignedPort = (this.httpServer!.address() as { port: number }) + ?.port; + resolve(assignedPort ?? port); }); this.httpServer!.on("error", reject); }); @@ -371,10 +378,12 @@ export class TestServerHttp { // Note: SSE transport is created per request, so we don't store a single instance this.transport = undefined; - // Start listening + // Start listening (port 0 = OS assigns a unique port) return new Promise((resolve, reject) => { this.httpServer!.listen(port, () => { - resolve(port); + const assignedPort = (this.httpServer!.address() as { port: number }) + ?.port; + resolve(assignedPort ?? port); }); this.httpServer!.on("error", reject); }); diff --git a/cli/package.json b/cli/package.json index ce521bee..94a59d84 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-cli", - "version": "0.19.0", + "version": "0.20.0", "description": "CLI for the Model Context Protocol inspector", "license": "SEE LICENSE IN LICENSE", "author": "Model Context Protocol a Series of LF Projects, LLC.", @@ -25,7 +25,7 @@ "test:cli-metadata": "vitest run metadata.test.ts" }, "devDependencies": { - "@types/express": "^5.0.6", + "@types/express": "^5.0.0", "tsx": "^4.7.0", "vitest": "^4.0.17" }, diff --git a/client/bin/start.js b/client/bin/start.js index f67301de..7e7e013a 100755 --- a/client/bin/start.js +++ b/client/bin/start.js @@ -56,9 +56,11 @@ async function startDevServer(serverOptions) { echoOutput: true, }; - // For Windows, we need to use stdin: 'ignore' to simulate < NUL + // For Windows, we need to ignore stdin to simulate < NUL + // spawn-rx's 'stdin' option expects an Observable, not 'ignore' + // Use Node's stdio option instead if (isWindows) { - spawnOptions.stdin = "ignore"; + spawnOptions.stdio = ["ignore", "pipe", "pipe"]; } const server = spawn(serverCommand, serverArgs, spawnOptions); @@ -140,13 +142,21 @@ async function startDevClient(clientOptions) { const clientCommand = "npx"; const host = process.env.HOST || "localhost"; const clientArgs = ["vite", "--port", CLIENT_PORT, "--host", host]; + const isWindows = process.platform === "win32"; - const client = spawn(clientCommand, clientArgs, { + const spawnOptions = { cwd: resolve(__dirname, ".."), env: { ...process.env, CLIENT_PORT }, signal: abort.signal, echoOutput: true, - }); + }; + + // For Windows, we need to ignore stdin to prevent hanging + if (isWindows) { + spawnOptions.stdio = ["ignore", "pipe", "pipe"]; + } + + const client = spawn(clientCommand, clientArgs, spawnOptions); const url = getClientUrl( CLIENT_PORT, diff --git a/client/jest.config.cjs b/client/jest.config.cjs index 9b793eae..70485296 100644 --- a/client/jest.config.cjs +++ b/client/jest.config.cjs @@ -13,8 +13,15 @@ module.exports = { tsconfig: "tsconfig.jest.json", }, ], + "^.+\\.m?js$": [ + "ts-jest", + { + tsconfig: "tsconfig.jest.json", + }, + ], }, extensionsToTreatAsEsm: [".ts", ".tsx"], + transformIgnorePatterns: ["node_modules/(?!(@modelcontextprotocol)/)"], testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", // Exclude directories and files that don't need to be tested testPathIgnorePatterns: [ diff --git a/client/package.json b/client/package.json index 1feec212..4586168f 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/inspector-client", - "version": "0.19.0", + "version": "0.20.0", "description": "Client-side application for the Model Context Protocol inspector", "license": "SEE LICENSE IN LICENSE", "author": "Model Context Protocol a Series of LF Projects, LLC.", @@ -25,6 +25,8 @@ "cleanup:e2e": "node e2e/global-teardown.js" }, "dependencies": { + "@mcp-ui/client": "^6.0.0", + "@modelcontextprotocol/ext-apps": "^1.0.0", "@modelcontextprotocol/sdk": "^1.25.2", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.3", diff --git a/client/src/App.tsx b/client/src/App.tsx index 39fc2812..5a69495b 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -51,6 +51,7 @@ import { import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import { + AppWindow, Bell, Files, FolderTree, @@ -75,6 +76,7 @@ import SamplingTab, { PendingRequest } from "./components/SamplingTab"; import Sidebar from "./components/Sidebar"; import ToolsTab from "./components/ToolsTab"; import TasksTab from "./components/TasksTab"; +import AppsTab from "./components/AppsTab"; import { InspectorConfig } from "./lib/configurationTypes"; import { getMCPProxyAddress, @@ -126,6 +128,9 @@ const App = () => { const [resourceContentMap, setResourceContentMap] = useState< Record >({}); + const [fetchingResources, setFetchingResources] = useState>( + new Set(), + ); const [prompts, setPrompts] = useState([]); const [promptContent, setPromptContent] = useState(""); const [tools, setTools] = useState([]); @@ -308,11 +313,13 @@ const App = () => { ...(serverCapabilities?.prompts ? ["prompts"] : []), ...(serverCapabilities?.tools ? ["tools"] : []), ...(serverCapabilities?.tasks ? ["tasks"] : []), + "apps", "ping", "sampling", "elicitations", "roots", "auth", + "metadata", ]; if (!validTabs.includes(originatingTab)) return; @@ -440,11 +447,13 @@ const App = () => { ...(serverCapabilities?.prompts ? ["prompts"] : []), ...(serverCapabilities?.tools ? ["tools"] : []), ...(serverCapabilities?.tasks ? ["tasks"] : []), + "apps", "ping", "sampling", "elicitations", "roots", "auth", + "metadata", ]; const isValidTab = validTabs.includes(hash); @@ -473,6 +482,13 @@ const App = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [mcpClient, activeTab]); + useEffect(() => { + if (mcpClient && activeTab === "apps" && serverCapabilities?.tools) { + void listTools(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [mcpClient, activeTab, serverCapabilities?.tools]); + useEffect(() => { localStorage.setItem("lastCommand", command); }, [command]); @@ -757,11 +773,13 @@ const App = () => { ...(serverCapabilities?.prompts ? ["prompts"] : []), ...(serverCapabilities?.tools ? ["tools"] : []), ...(serverCapabilities?.tasks ? ["tasks"] : []), + "apps", "ping", "sampling", "elicitations", "roots", "auth", + "metadata", ]; if (validTabs.includes(originatingTab)) { @@ -851,22 +869,48 @@ const App = () => { }; const readResource = async (uri: string) => { + if (fetchingResources.has(uri) || resourceContentMap[uri]) { + return; + } + + console.log("[App] Reading resource:", uri); + setFetchingResources((prev) => new Set(prev).add(uri)); lastToolCallOriginTabRef.current = currentTabRef.current; - const response = await sendMCPRequest( - { - method: "resources/read" as const, - params: { uri }, - }, - ReadResourceResultSchema, - "resources", - ); - const content = JSON.stringify(response, null, 2); - setResourceContent(content); - setResourceContentMap((prev) => ({ - ...prev, - [uri]: content, - })); + try { + const response = await sendMCPRequest( + { + method: "resources/read" as const, + params: { uri }, + }, + ReadResourceResultSchema, + "resources", + ); + console.log("[App] Resource read response:", { + uri, + responseLength: JSON.stringify(response).length, + hasContents: !!(response as { contents?: unknown[] }).contents, + }); + const content = JSON.stringify(response, null, 2); + setResourceContent(content); + setResourceContentMap((prev) => ({ + ...prev, + [uri]: content, + })); + } catch (error) { + console.error(`[App] Failed to read resource ${uri}:`, error); + const errorString = (error as Error).message ?? String(error); + setResourceContentMap((prev) => ({ + ...prev, + [uri]: JSON.stringify({ error: errorString }), + })); + } finally { + setFetchingResources((prev) => { + const next = new Set(prev); + next.delete(uri); + return next; + }); + } }; const subscribeToResource = async (uri: string) => { @@ -1308,6 +1352,10 @@ const App = () => { Tasks + + + Apps + Ping @@ -1497,6 +1545,19 @@ const App = () => { error={errors.tasks} nextCursor={nextTaskCursor} /> + { + clearError("tools"); + listTools(); + }} + error={errors.tools} + mcpClient={mcpClient} + onNotification={(notification) => { + setNotifications((prev) => [...prev, notification]); + }} + /> { diff --git a/client/src/components/AppRenderer.tsx b/client/src/components/AppRenderer.tsx new file mode 100644 index 00000000..e0c25980 --- /dev/null +++ b/client/src/components/AppRenderer.tsx @@ -0,0 +1,128 @@ +import { useMemo, useState } from "react"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { + Tool, + ContentBlock, + ServerNotification, + LoggingMessageNotificationParams, +} from "@modelcontextprotocol/sdk/types.js"; +import { + AppRenderer as McpUiAppRenderer, + type McpUiHostContext, + type RequestHandlerExtra, +} from "@mcp-ui/client"; +import { + type McpUiMessageRequest, + type McpUiMessageResult, +} from "@modelcontextprotocol/ext-apps/app-bridge"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { AlertCircle } from "lucide-react"; +import { useToast } from "@/lib/hooks/useToast"; + +interface AppRendererProps { + sandboxPath: string; + tool: Tool; + mcpClient: Client | null; + toolInput?: Record; + onNotification?: (notification: ServerNotification) => void; +} + +const AppRenderer = ({ + sandboxPath, + tool, + mcpClient, + toolInput, + onNotification, +}: AppRendererProps) => { + const [error, setError] = useState(null); + const { toast } = useToast(); + + const hostContext: McpUiHostContext = useMemo( + () => ({ + theme: document.documentElement.classList.contains("dark") + ? "dark" + : "light", + }), + [], + ); + + const handleOpenLink = async ({ url }: { url: string }) => { + let isError = true; + if (url.startsWith("https://") || url.startsWith("http://")) { + window.open(url, "_blank"); + isError = false; + } + return { isError }; + }; + + const handleMessage = async ( + params: McpUiMessageRequest["params"], + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _extra: RequestHandlerExtra, + ): Promise => { + const message = params.content + .filter((block): block is ContentBlock & { type: "text" } => + Boolean(block.type === "text"), + ) + .map((block) => block.text) + .join("\n"); + + if (message) { + toast({ + description: message, + }); + } + + return {}; + }; + + const handleLoggingMessage = (params: LoggingMessageNotificationParams) => { + if (onNotification) { + onNotification({ + method: "notifications/message", + params, + } as ServerNotification); + } + }; + + if (!mcpClient) { + return ( + + + Waiting for MCP client... + + ); + } + + return ( +
+ {error && ( + + + {error} + + )} + +
+ setError(err.message)} + /> +
+
+ ); +}; + +export default AppRenderer; diff --git a/client/src/components/AppsTab.tsx b/client/src/components/AppsTab.tsx new file mode 100644 index 00000000..1943c439 --- /dev/null +++ b/client/src/components/AppsTab.tsx @@ -0,0 +1,530 @@ +import { useEffect, useState, useCallback, useRef } from "react"; +import { TabsContent } from "@/components/ui/tabs"; +import { Button } from "@/components/ui/button"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { + AlertCircle, + X, + Play, + ChevronRight, + Maximize2, + Minimize2, +} from "lucide-react"; +import { Tool, ServerNotification } from "@modelcontextprotocol/sdk/types.js"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { getToolUiResourceUri } from "@modelcontextprotocol/ext-apps/app-bridge"; +import AppRenderer from "./AppRenderer"; +import ListPane from "./ListPane"; +import IconDisplay, { WithIcons } from "./IconDisplay"; +import { Label } from "@/components/ui/label"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; +import { Input } from "@/components/ui/input"; +import DynamicJsonForm, { DynamicJsonFormRef } from "./DynamicJsonForm"; +import { JsonSchemaType, JsonValue } from "@/utils/jsonUtils"; +import { + generateDefaultValue, + isPropertyRequired, + normalizeUnionType, + resolveRef, +} from "@/utils/schemaUtils"; + +interface AppsTabProps { + sandboxPath: string; + tools: Tool[]; + listTools: () => void; + error: string | null; + mcpClient: Client | null; + onNotification?: (notification: ServerNotification) => void; +} + +// Type guard to check if a tool has UI metadata +const hasUIMetadata = (tool: Tool): boolean => { + return !!getToolUiResourceUri(tool); +}; + +const AppsTab = ({ + sandboxPath, + tools, + listTools, + error, + mcpClient, + onNotification, +}: AppsTabProps) => { + const [appTools, setAppTools] = useState([]); + const [selectedTool, setSelectedTool] = useState(null); + const [params, setParams] = useState>({}); + const [isAppOpen, setIsAppOpen] = useState(false); + const [isMaximized, setIsMaximized] = useState(false); + const [hasValidationErrors, setHasValidationErrors] = useState(false); + const formRefs = useRef>({}); + + // Function to check if any form has validation errors + const checkValidationErrors = useCallback(() => { + const errors = Object.values(formRefs.current).some( + (ref) => ref && !ref.validateJson().isValid, + ); + setHasValidationErrors(errors); + return errors; + }, []); + + // Filter tools that have UI metadata + useEffect(() => { + const filtered = tools.filter(hasUIMetadata); + console.log("[AppsTab] Filtered app tools:", { + totalTools: tools.length, + appTools: filtered.length, + appToolNames: filtered.map((t) => t.name), + }); + setAppTools(filtered); + + // If current selected tool is no longer available, reset selection + if (selectedTool && !filtered.find((t) => t.name === selectedTool.name)) { + setSelectedTool(null); + setIsAppOpen(false); + } + }, [tools, selectedTool]); + + useEffect(() => { + if (selectedTool) { + const initialParams = Object.entries( + selectedTool.inputSchema.properties ?? [], + ).map(([key, value]) => { + // First resolve any $ref references + const resolvedValue = resolveRef( + value as JsonSchemaType, + selectedTool.inputSchema as JsonSchemaType, + ); + return [ + key, + generateDefaultValue( + resolvedValue, + key, + selectedTool.inputSchema as JsonSchemaType, + ), + ]; + }); + setParams(Object.fromEntries(initialParams)); + setHasValidationErrors(false); + formRefs.current = {}; + } else { + setParams({}); + setIsAppOpen(false); + } + }, [selectedTool]); + + const handleRefresh = useCallback(() => { + listTools(); + }, [listTools]); + + const handleCloseApp = useCallback(() => { + setIsAppOpen(false); + }, []); + + const handleOpenApp = useCallback(() => { + if (!checkValidationErrors()) { + setIsAppOpen(true); + } + }, [checkValidationErrors]); + + const handleSelectTool = useCallback((tool: Tool) => { + setSelectedTool(tool); + const hasFields = + tool.inputSchema.properties && + Object.keys(tool.inputSchema.properties).length > 0; + setIsAppOpen(!hasFields); + }, []); + + const handleDeselectTool = useCallback(() => { + setSelectedTool(null); + setIsAppOpen(false); + setIsMaximized(false); + }, []); + + return ( + +
+ {!isMaximized && ( + { + return ( +
+
+ +
+
+ {tool.name} + {tool.description && ( + + {tool.description} + + )} +
+ +
+ ); + }} + title="MCP Apps" + buttonText="Refresh Apps" + /> + )} + +
+
+
+
+ {selectedTool && ( + + )} +

+ {selectedTool ? selectedTool.name : "Select an app"} +

+
+
+ {selectedTool && isAppOpen && ( + + )} + {selectedTool && ( + + )} +
+
+
+ +
+ {error && ( + + + {error} + + )} + + {selectedTool ? ( + (() => { + const hasFields = + selectedTool.inputSchema.properties && + Object.keys(selectedTool.inputSchema.properties).length > 0; + + return ( +
+ {!isAppOpen ? ( +
+ {selectedTool.description && ( +

+ {selectedTool.description} +

+ )} + +
+

App Input

+ {Object.entries( + selectedTool.inputSchema.properties ?? [], + ).map(([key, value]) => { + // First resolve any $ref references + const resolvedValue = resolveRef( + value as JsonSchemaType, + selectedTool.inputSchema as JsonSchemaType, + ); + const prop = normalizeUnionType(resolvedValue); + const inputSchema = + selectedTool.inputSchema as JsonSchemaType; + const required = isPropertyRequired( + key, + inputSchema, + ); + + return ( +
+
+ + {prop.nullable ? ( +
+ + setParams({ + ...params, + [key]: checked + ? null + : prop.type === "array" + ? undefined + : prop.default !== null + ? prop.default + : prop.type === "boolean" + ? false + : prop.type === "string" + ? "" + : undefined, + }) + } + /> + +
+ ) : null} +
+ +
+ {prop.type === "boolean" ? ( +
+ + setParams({ + ...params, + [key]: checked, + }) + } + /> + +
+ ) : prop.type === "string" && prop.enum ? ( + + ) : prop.type === "string" ? ( +