import { ClientRequest, CompatibilityCallToolResult, CompatibilityCallToolResultSchema, CreateMessageResult, EmptyResultSchema, GetPromptResultSchema, ListPromptsResultSchema, ListResourcesResultSchema, ListResourceTemplatesResultSchema, ListToolsResultSchema, ReadResourceResultSchema, Resource, ResourceTemplate, Root, ServerNotification, Tool, LoggingLevel, Task, GetTaskResultSchema, } from "@modelcontextprotocol/sdk/types.js"; import { OAuthTokensSchema } from "@modelcontextprotocol/sdk/shared/auth.js"; import type { AnySchema, SchemaOutput, } from "@modelcontextprotocol/sdk/server/zod-compat.js"; import { SESSION_KEYS, getServerSpecificKey } from "./lib/constants"; import { hasValidMetaName, hasValidMetaPrefix, isReservedMetaKey, } from "@/utils/metaUtils"; import { getToolUiResourceUri } from "@modelcontextprotocol/ext-apps/app-bridge"; import { AuthDebuggerState, EMPTY_DEBUGGER_STATE } from "./lib/auth-types"; import { OAuthStateMachine } from "./lib/oauth-state-machine"; import { cacheToolOutputSchemas } from "./utils/schemaUtils"; import { cleanParams } from "./utils/paramUtils"; import type { JsonSchemaType } from "./utils/jsonUtils"; import React, { Suspense, useCallback, useEffect, useRef, useState, } from "react"; import { useConnection } from "./lib/hooks/useConnection"; import { useDraggablePane, useDraggableSidebar, } from "./lib/hooks/useDraggablePane"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import { AppWindow, Bell, Files, FolderTree, Hammer, Hash, Key, ListTodo, MessageSquare, Settings, } from "lucide-react"; import { z } from "zod"; import "./App.css"; import AuthDebugger from "./components/AuthDebugger"; import ConsoleTab from "./components/ConsoleTab"; import HistoryAndNotifications from "./components/HistoryAndNotifications"; import PingTab from "./components/PingTab"; import PromptsTab, { Prompt } from "./components/PromptsTab"; import ResourcesTab from "./components/ResourcesTab"; import RootsTab from "./components/RootsTab"; 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, getMCPProxyAuthToken, getInitialSseUrl, getInitialTransportType, getInitialCommand, getInitialArgs, initializeInspectorConfig, saveInspectorConfig, getMCPTaskTtl, } from "./utils/configUtils"; import ElicitationTab, { PendingElicitationRequest, ElicitationResponse, } from "./components/ElicitationTab"; import { CustomHeaders, migrateFromLegacyAuth, } from "./lib/types/customHeaders"; import MetadataTab from "./components/MetadataTab"; const CONFIG_LOCAL_STORAGE_KEY = "inspectorConfig_v1"; type PrefilledAppsToolCall = { id: number; toolName: string; params: Record; result: CompatibilityCallToolResult; }; const hasAppResourceUri = (tool: Tool): boolean => { return Boolean(getToolUiResourceUri(tool)); }; const cloneToolParams = ( source: Record, ): Record => { try { return structuredClone(source); } catch { return { ...source }; } }; const filterReservedMetadata = ( metadata: Record, ): Record => { return Object.entries(metadata).reduce>( (acc, [key, value]) => { if ( !isReservedMetaKey(key) && hasValidMetaPrefix(key) && hasValidMetaName(key) ) { acc[key] = value; } return acc; }, {}, ); }; const App = () => { const [resources, setResources] = useState([]); const [resourceTemplates, setResourceTemplates] = useState< ResourceTemplate[] >([]); const [resourceContent, setResourceContent] = useState(""); const [resourceContentMap, setResourceContentMap] = useState< Record >({}); const [fetchingResources, setFetchingResources] = useState>( new Set(), ); const [prompts, setPrompts] = useState([]); const [promptContent, setPromptContent] = useState(""); const [tools, setTools] = useState([]); const [tasks, setTasks] = useState([]); const [toolResult, setToolResult] = useState(null); const [prefilledAppsToolCall, setPrefilledAppsToolCall] = useState(null); const [errors, setErrors] = useState>({ resources: null, prompts: null, tools: null, tasks: null, }); const [command, setCommand] = useState(getInitialCommand); const [args, setArgs] = useState(getInitialArgs); const [sseUrl, setSseUrl] = useState(getInitialSseUrl); const [transportType, setTransportType] = useState< "stdio" | "sse" | "streamable-http" >(getInitialTransportType); const [connectionType, setConnectionType] = useState<"direct" | "proxy">( () => { return ( (localStorage.getItem("lastConnectionType") as "direct" | "proxy") || "proxy" ); }, ); const [logLevel, setLogLevel] = useState("debug"); const [notifications, setNotifications] = useState([]); const [roots, setRoots] = useState([]); const [env, setEnv] = useState>({}); const [config, setConfig] = useState(() => initializeInspectorConfig(CONFIG_LOCAL_STORAGE_KEY), ); const [bearerToken, setBearerToken] = useState(() => { return localStorage.getItem("lastBearerToken") || ""; }); const [headerName, setHeaderName] = useState(() => { return localStorage.getItem("lastHeaderName") || ""; }); const [oauthClientId, setOauthClientId] = useState(() => { return localStorage.getItem("lastOauthClientId") || ""; }); const [oauthScope, setOauthScope] = useState(() => { 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"); if (savedHeaders) { try { return JSON.parse(savedHeaders); } catch (error) { console.warn( `Failed to parse custom headers: "${savedHeaders}", will try legacy migration`, error, ); // Fall back to migration if JSON parsing fails } } // Migrate from legacy auth if available const legacyToken = localStorage.getItem("lastBearerToken") || ""; const legacyHeaderName = localStorage.getItem("lastHeaderName") || ""; if (legacyToken) { return migrateFromLegacyAuth(legacyToken, legacyHeaderName); } // Default to empty array return [ { name: "Authorization", value: "Bearer ", enabled: false, }, ]; }); const [pendingSampleRequests, setPendingSampleRequests] = useState< Array< PendingRequest & { resolve: (result: CreateMessageResult) => void; reject: (error: Error) => void; } > >([]); const [pendingElicitationRequests, setPendingElicitationRequests] = useState< Array< PendingElicitationRequest & { resolve: (response: ElicitationResponse) => void; decline: (error: Error) => void; } > >([]); const [isAuthDebuggerVisible, setIsAuthDebuggerVisible] = useState(false); const [authState, setAuthState] = useState(EMPTY_DEBUGGER_STATE); // Metadata state - persisted in localStorage const [metadata, setMetadata] = useState>(() => { const savedMetadata = localStorage.getItem("lastMetadata"); if (savedMetadata) { try { const parsed = JSON.parse(savedMetadata); if (parsed && typeof parsed === "object") { return filterReservedMetadata(parsed); } } catch (error) { console.warn("Failed to parse saved metadata:", error); } } return {}; }); const updateAuthState = (updates: Partial) => { setAuthState((prev) => ({ ...prev, ...updates })); }; const handleMetadataChange = (newMetadata: Record) => { const sanitizedMetadata = filterReservedMetadata(newMetadata); setMetadata(sanitizedMetadata); localStorage.setItem("lastMetadata", JSON.stringify(sanitizedMetadata)); }; const nextRequestId = useRef(0); const rootsRef = useRef([]); const [selectedResource, setSelectedResource] = useState( null, ); const [resourceSubscriptions, setResourceSubscriptions] = useState< Set >(new Set()); const [selectedPrompt, setSelectedPrompt] = useState(null); const [selectedTool, setSelectedTool] = useState(null); const [selectedTask, setSelectedTask] = useState(null); const [isPollingTask, setIsPollingTask] = useState(false); const [nextResourceCursor, setNextResourceCursor] = useState< string | undefined >(); const [nextResourceTemplateCursor, setNextResourceTemplateCursor] = useState< string | undefined >(); const [nextPromptCursor, setNextPromptCursor] = useState< string | undefined >(); const [nextToolCursor, setNextToolCursor] = useState(); const [nextTaskCursor, setNextTaskCursor] = useState(); const progressTokenRef = useRef(0); const prefilledAppsToolCallIdRef = useRef(0); const [activeTab, setActiveTab] = useState(() => { const hash = window.location.hash.slice(1); const initialTab = hash || "resources"; return initialTab; }); const currentTabRef = useRef(activeTab); const lastToolCallOriginTabRef = useRef(activeTab); useEffect(() => { currentTabRef.current = activeTab; }, [activeTab]); const navigateToOriginatingTab = (originatingTab?: string) => { if (!originatingTab) return; const validTabs = [ ...(serverCapabilities?.resources ? ["resources"] : []), ...(serverCapabilities?.prompts ? ["prompts"] : []), ...(serverCapabilities?.tools ? ["tools"] : []), ...(serverCapabilities?.tasks ? ["tasks"] : []), "apps", "ping", "sampling", "elicitations", "roots", "auth", "metadata", ]; if (!validTabs.includes(originatingTab)) return; setActiveTab(originatingTab); window.location.hash = originatingTab; setTimeout(() => { setActiveTab(originatingTab); window.location.hash = originatingTab; }, 100); }; const { height: historyPaneHeight, handleDragStart } = useDraggablePane(300); const { width: sidebarWidth, isDragging: isSidebarDragging, handleDragStart: handleSidebarDragStart, } = useDraggableSidebar(320); const selectedTaskRef = useRef(null); useEffect(() => { selectedTaskRef.current = selectedTask; }, [selectedTask]); const { connectionStatus, serverCapabilities, serverImplementation, mcpClient, requestHistory, clearRequestHistory, makeRequest, cancelTask: cancelMcpTask, listTasks: listMcpTasks, sendNotification, handleCompletion, completionsSupported, connect: connectMcpServer, disconnect: disconnectMcpServer, } = useConnection({ transportType, command, args, sseUrl, env, customHeaders, oauthClientId, oauthClientSecret, oauthScope, config, connectionType, onNotification: (notification) => { setNotifications((prev) => [...prev, notification as ServerNotification]); if (notification.method === "notifications/tasks/list_changed") { void listTasks(); } if (notification.method === "notifications/tasks/status") { const task = notification.params as unknown as Task; setTasks((prev) => { const exists = prev.some((t) => t.taskId === task.taskId); if (exists) { return prev.map((t) => (t.taskId === task.taskId ? task : t)); } else { return [task, ...prev]; } }); if (selectedTaskRef.current?.taskId === task.taskId) { setSelectedTask(task); } } }, onPendingRequest: (request, resolve, reject) => { const currentTab = lastToolCallOriginTabRef.current; setPendingSampleRequests((prev) => [ ...prev, { id: nextRequestId.current++, request, originatingTab: currentTab, resolve, reject, }, ]); setActiveTab("sampling"); window.location.hash = "sampling"; }, onElicitationRequest: (request, resolve) => { const currentTab = lastToolCallOriginTabRef.current; setPendingElicitationRequests((prev) => [ ...prev, { id: nextRequestId.current++, request: { id: nextRequestId.current, message: request.params.message, requestedSchema: request.params.requestedSchema, }, originatingTab: currentTab, resolve, decline: (error: Error) => { console.error("Elicitation request rejected:", error); }, }, ]); setActiveTab("elicitations"); window.location.hash = "elicitations"; }, getRoots: () => rootsRef.current, defaultLoggingLevel: logLevel, metadata, }); useEffect(() => { if (serverCapabilities) { const hash = window.location.hash.slice(1); const validTabs = [ ...(serverCapabilities?.resources ? ["resources"] : []), ...(serverCapabilities?.prompts ? ["prompts"] : []), ...(serverCapabilities?.tools ? ["tools"] : []), ...(serverCapabilities?.tasks ? ["tasks"] : []), "apps", "ping", "sampling", "elicitations", "roots", "auth", "metadata", ]; const isValidTab = validTabs.includes(hash); if (!isValidTab) { const defaultTab = serverCapabilities?.resources ? "resources" : serverCapabilities?.prompts ? "prompts" : serverCapabilities?.tools ? "tools" : serverCapabilities?.tasks ? "tasks" : "ping"; setActiveTab(defaultTab); window.location.hash = defaultTab; } } }, [serverCapabilities]); useEffect(() => { if (mcpClient && activeTab === "tasks") { void listTasks(); } // 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]); useEffect(() => { localStorage.setItem("lastArgs", args); }, [args]); useEffect(() => { localStorage.setItem("lastSseUrl", sseUrl); }, [sseUrl]); useEffect(() => { localStorage.setItem("lastTransportType", transportType); }, [transportType]); useEffect(() => { localStorage.setItem("lastConnectionType", connectionType); }, [connectionType]); useEffect(() => { if (bearerToken) { localStorage.setItem("lastBearerToken", bearerToken); } else { localStorage.removeItem("lastBearerToken"); } }, [bearerToken]); useEffect(() => { if (headerName) { localStorage.setItem("lastHeaderName", headerName); } else { localStorage.removeItem("lastHeaderName"); } }, [headerName]); useEffect(() => { localStorage.setItem("lastCustomHeaders", JSON.stringify(customHeaders)); }, [customHeaders]); // Auto-migrate from legacy auth when custom headers are empty but legacy auth exists useEffect(() => { if (customHeaders.length === 0 && (bearerToken || headerName)) { const migratedHeaders = migrateFromLegacyAuth(bearerToken, headerName); if (migratedHeaders.length > 0) { setCustomHeaders(migratedHeaders); // Clear legacy auth after migration setBearerToken(""); setHeaderName(""); } } }, [bearerToken, headerName, customHeaders, setCustomHeaders]); useEffect(() => { localStorage.setItem("lastOauthClientId", oauthClientId); }, [oauthClientId]); useEffect(() => { localStorage.setItem("lastOauthScope", oauthScope); }, [oauthScope]); useEffect(() => { localStorage.setItem("lastOauthClientSecret", oauthClientSecret); }, [oauthClientSecret]); useEffect(() => { saveInspectorConfig(CONFIG_LOCAL_STORAGE_KEY, config); }, [config]); const onOAuthConnect = useCallback( (serverUrl: string) => { setSseUrl(serverUrl); setIsAuthDebuggerVisible(false); void connectMcpServer(); }, [connectMcpServer], ); const onOAuthDebugConnect = useCallback( async ({ authorizationCode, errorMsg, restoredState, }: { authorizationCode?: string; errorMsg?: string; restoredState?: AuthDebuggerState; }) => { setIsAuthDebuggerVisible(true); if (errorMsg) { updateAuthState({ latestError: new Error(errorMsg), }); return; } if (restoredState && authorizationCode) { let currentState: AuthDebuggerState = { ...restoredState, authorizationCode, oauthStep: "token_request", isInitiatingAuth: true, statusMessage: null, latestError: null, }; try { const stateMachine = new OAuthStateMachine(sseUrl, (updates) => { currentState = { ...currentState, ...updates }; }); while ( currentState.oauthStep !== "complete" && currentState.oauthStep !== "authorization_code" ) { await stateMachine.executeStep(currentState); } if (currentState.oauthStep === "complete") { updateAuthState({ ...currentState, statusMessage: { type: "success", message: "Authentication completed successfully", }, isInitiatingAuth: false, }); } } catch (error) { console.error("OAuth continuation error:", error); updateAuthState({ latestError: error instanceof Error ? error : new Error(String(error)), statusMessage: { type: "error", message: `Failed to complete OAuth flow: ${error instanceof Error ? error.message : String(error)}`, }, isInitiatingAuth: false, }); } } else if (authorizationCode) { updateAuthState({ authorizationCode, oauthStep: "token_request", }); } }, [sseUrl], ); useEffect(() => { const loadOAuthTokens = async () => { try { if (sseUrl) { const key = getServerSpecificKey(SESSION_KEYS.TOKENS, sseUrl); const tokens = sessionStorage.getItem(key); if (tokens) { const parsedTokens = await OAuthTokensSchema.parseAsync( JSON.parse(tokens), ); updateAuthState({ oauthTokens: parsedTokens, oauthStep: "complete", }); } } } catch (error) { console.error("Error loading OAuth tokens:", error); } }; loadOAuthTokens(); }, [sseUrl]); useEffect(() => { const headers: HeadersInit = {}; const { token: proxyAuthToken, header: proxyAuthTokenHeader } = getMCPProxyAuthToken(config); if (proxyAuthToken) { headers[proxyAuthTokenHeader] = `Bearer ${proxyAuthToken}`; } fetch(`${getMCPProxyAddress(config)}/config`, { headers }) .then((response) => response.json()) .then((data) => { setEnv(data.defaultEnvironment); if (data.defaultCommand) { setCommand(data.defaultCommand); } if (data.defaultArgs) { setArgs(data.defaultArgs); } if (data.defaultTransport) { setTransportType( data.defaultTransport as "stdio" | "sse" | "streamable-http", ); } if (data.defaultServerUrl) { setSseUrl(data.defaultServerUrl); } }) .catch((error) => console.error("Error fetching default environment:", error), ); }, [config]); useEffect(() => { rootsRef.current = roots; }, [roots]); useEffect(() => { if (mcpClient && !window.location.hash) { const defaultTab = serverCapabilities?.resources ? "resources" : serverCapabilities?.prompts ? "prompts" : serverCapabilities?.tools ? "tools" : serverCapabilities?.tasks ? "tasks" : "ping"; window.location.hash = defaultTab; } else if (!mcpClient && window.location.hash) { // Clear hash when disconnected - completely remove the fragment window.history.replaceState( null, "", window.location.pathname + window.location.search, ); } }, [mcpClient, serverCapabilities]); useEffect(() => { const handleHashChange = () => { const hash = window.location.hash.slice(1); if (hash && hash !== activeTab) { setActiveTab(hash); } }; window.addEventListener("hashchange", handleHashChange); return () => window.removeEventListener("hashchange", handleHashChange); }, [activeTab]); const handleApproveSampling = (id: number, result: CreateMessageResult) => { setPendingSampleRequests((prev) => { const request = prev.find((r) => r.id === id); request?.resolve(result); navigateToOriginatingTab(request?.originatingTab); return prev.filter((r) => r.id !== id); }); }; const handleRejectSampling = (id: number) => { setPendingSampleRequests((prev) => { const request = prev.find((r) => r.id === id); request?.reject(new Error("Sampling request rejected")); navigateToOriginatingTab(request?.originatingTab); return prev.filter((r) => r.id !== id); }); }; const handleResolveElicitation = ( id: number, response: ElicitationResponse, ) => { setPendingElicitationRequests((prev) => { const request = prev.find((r) => r.id === id); if (request) { request.resolve(response); if (request.originatingTab) { const originatingTab = request.originatingTab; const validTabs = [ ...(serverCapabilities?.resources ? ["resources"] : []), ...(serverCapabilities?.prompts ? ["prompts"] : []), ...(serverCapabilities?.tools ? ["tools"] : []), ...(serverCapabilities?.tasks ? ["tasks"] : []), "apps", "ping", "sampling", "elicitations", "roots", "auth", "metadata", ]; if (validTabs.includes(originatingTab)) { setActiveTab(originatingTab); window.location.hash = originatingTab; setTimeout(() => { setActiveTab(originatingTab); window.location.hash = originatingTab; }, 100); } } } return prev.filter((r) => r.id !== id); }); }; const clearError = (tabKey: keyof typeof errors) => { setErrors((prev) => ({ ...prev, [tabKey]: null })); }; const sendMCPRequest = async ( request: ClientRequest, schema: T, tabKey?: keyof typeof errors, ): Promise> => { try { const response = await makeRequest(request, schema); if (tabKey !== undefined) { clearError(tabKey); } return response; } catch (e) { const errorString = (e as Error).message ?? String(e); if (tabKey !== undefined) { setErrors((prev) => ({ ...prev, [tabKey]: errorString, })); } throw e; } }; const listResources = async () => { const response = await sendMCPRequest( { method: "resources/list" as const, params: nextResourceCursor ? { cursor: nextResourceCursor } : {}, }, ListResourcesResultSchema, "resources", ); setResources(resources.concat(response.resources ?? [])); setNextResourceCursor(response.nextCursor); }; const listResourceTemplates = async () => { const response = await sendMCPRequest( { method: "resources/templates/list" as const, params: nextResourceTemplateCursor ? { cursor: nextResourceTemplateCursor } : {}, }, ListResourceTemplatesResultSchema, "resources", ); setResourceTemplates( resourceTemplates.concat(response.resourceTemplates ?? []), ); setNextResourceTemplateCursor(response.nextCursor); }; const getPrompt = async (name: string, args: Record = {}) => { lastToolCallOriginTabRef.current = currentTabRef.current; const response = await sendMCPRequest( { method: "prompts/get" as const, params: { name, arguments: args }, }, GetPromptResultSchema, "prompts", ); setPromptContent(JSON.stringify(response, null, 2)); }; 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; 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) => { if (!resourceSubscriptions.has(uri)) { await sendMCPRequest( { method: "resources/subscribe" as const, params: { uri }, }, z.object({}), "resources", ); const clone = new Set(resourceSubscriptions); clone.add(uri); setResourceSubscriptions(clone); } }; const unsubscribeFromResource = async (uri: string) => { if (resourceSubscriptions.has(uri)) { await sendMCPRequest( { method: "resources/unsubscribe" as const, params: { uri }, }, z.object({}), "resources", ); const clone = new Set(resourceSubscriptions); clone.delete(uri); setResourceSubscriptions(clone); } }; const listPrompts = async () => { const response = await sendMCPRequest( { method: "prompts/list" as const, params: nextPromptCursor ? { cursor: nextPromptCursor } : {}, }, ListPromptsResultSchema, "prompts", ); setPrompts(response.prompts); setNextPromptCursor(response.nextCursor); }; const listTools = async () => { const response = await sendMCPRequest( { method: "tools/list" as const, params: nextToolCursor ? { cursor: nextToolCursor } : {}, }, ListToolsResultSchema, "tools", ); setTools(response.tools); setNextToolCursor(response.nextCursor); cacheToolOutputSchemas(response.tools); }; const callTool = async ( name: string, params: Record, toolMetadata?: Record, runAsTask?: boolean, ): Promise => { lastToolCallOriginTabRef.current = currentTabRef.current; try { // Find the tool schema to clean parameters properly const tool = tools.find((t) => t.name === name); const cleanedParams = tool?.inputSchema ? cleanParams(params, tool.inputSchema as JsonSchemaType) : params; // Merge general metadata with tool-specific metadata // Tool-specific metadata takes precedence over general metadata const mergedMetadata = { ...metadata, // General metadata progressToken: progressTokenRef.current++, ...toolMetadata, // Tool-specific metadata }; const request: ClientRequest = { method: "tools/call" as const, params: { name, arguments: cleanedParams, _meta: mergedMetadata, }, }; if (runAsTask) { request.params = { ...request.params, task: { ttl: getMCPTaskTtl(config), }, }; } const response = await sendMCPRequest( request, CompatibilityCallToolResultSchema, "tools", ); // Check if this was a task-augmented request that returned a task reference // The server returns { task: { taskId, status, ... } } when a task is created const isTaskResult = ( res: unknown, ): res is { task: { taskId: string; status: string; pollInterval: number }; } => !!res && typeof res === "object" && "task" in res && !!res.task && typeof res.task === "object" && "taskId" in res.task; if (runAsTask && isTaskResult(response)) { const taskId = response.task.taskId; const pollInterval = response.task.pollInterval; // Set polling state BEFORE setting tool result for proper UI update setIsPollingTask(true); // Safely extract any _meta from the original response (if present) const initialResponseMeta = response && typeof response === "object" && "_meta" in (response as Record) ? ((response as { _meta?: Record })._meta ?? {}) : undefined; let latestToolResult: CompatibilityCallToolResult = { content: [ { type: "text", text: `Task created: ${taskId}. Polling for status...`, }, ], _meta: { ...(initialResponseMeta || {}), "io.modelcontextprotocol/related-task": { taskId }, }, }; setToolResult(latestToolResult); // Polling loop let taskCompleted = false; while (!taskCompleted) { try { // Wait for 1 second before polling await new Promise((resolve) => setTimeout(resolve, pollInterval)); const taskStatus = await sendMCPRequest( { method: "tasks/get", params: { taskId }, }, GetTaskResultSchema, ); if ( taskStatus.status === "completed" || taskStatus.status === "failed" || taskStatus.status === "cancelled" ) { taskCompleted = true; console.log( `Polling complete for task ${taskId}: ${taskStatus.status}`, ); if (taskStatus.status === "completed") { console.log(`Fetching result for task ${taskId}`); const result = await sendMCPRequest( { method: "tasks/result", params: { taskId }, }, CompatibilityCallToolResultSchema, ); console.log(`Result received for task ${taskId}:`, result); latestToolResult = result as CompatibilityCallToolResult; setToolResult(latestToolResult); // Refresh tasks list to show completed state void listTasks(); } else { latestToolResult = { content: [ { type: "text", text: `Task ${taskStatus.status}: ${taskStatus.statusMessage || "No additional information"}`, }, ], isError: true, }; setToolResult(latestToolResult); // Refresh tasks list to show failed/cancelled state void listTasks(); } } else { // Update status message while polling // Safely extract any _meta from the original response (if present) const pollingResponseMeta = response && typeof response === "object" && "_meta" in (response as Record) ? ((response as { _meta?: Record })._meta ?? {}) : undefined; latestToolResult = { content: [ { type: "text", text: `Task status: ${taskStatus.status}${taskStatus.statusMessage ? ` - ${taskStatus.statusMessage}` : ""}. Polling...`, }, ], _meta: { ...(pollingResponseMeta || {}), "io.modelcontextprotocol/related-task": { taskId }, }, }; setToolResult(latestToolResult); // Refresh tasks list to show progress void listTasks(); } } catch (pollingError) { console.error("Error polling task status:", pollingError); latestToolResult = { content: [ { type: "text", text: `Error polling task status: ${pollingError instanceof Error ? pollingError.message : String(pollingError)}`, }, ], isError: true, }; setToolResult(latestToolResult); taskCompleted = true; } } setIsPollingTask(false); // Clear any validation errors since tool execution completed setErrors((prev) => ({ ...prev, tools: null })); return latestToolResult; } else { const directResult = response as CompatibilityCallToolResult; setToolResult(directResult); // Clear any validation errors since tool execution completed setErrors((prev) => ({ ...prev, tools: null })); return directResult; } } catch (e) { const toolResult: CompatibilityCallToolResult = { content: [ { type: "text", text: (e as Error).message ?? String(e), }, ], isError: true, }; setToolResult(toolResult); // Clear validation errors - tool execution errors are shown in ToolResults setErrors((prev) => ({ ...prev, tools: null })); return toolResult; } }; const listTasks = useCallback(async () => { try { const response = await listMcpTasks(nextTaskCursor); setTasks(response.tasks); setNextTaskCursor(response.nextCursor); // Inline error clear to avoid extra dependency on clearError setErrors((prev) => ({ ...prev, tasks: null })); } catch (e) { setErrors((prev) => ({ ...prev, tasks: (e as Error).message ?? String(e), })); } }, [listMcpTasks, nextTaskCursor]); const cancelTask = async (taskId: string) => { try { const response = await cancelMcpTask(taskId); setTasks((prev) => prev.map((t) => (t.taskId === taskId ? response : t))); if (selectedTask?.taskId === taskId) { setSelectedTask(response); } clearError("tasks"); } catch (e) { setErrors((prev) => ({ ...prev, tasks: (e as Error).message ?? String(e), })); } }; const handleRootsChange = async () => { await sendNotification({ method: "notifications/roots/list_changed" }); }; const handleClearNotifications = () => { setNotifications([]); }; const sendLogLevelRequest = async (level: LoggingLevel) => { await sendMCPRequest( { method: "logging/setLevel" as const, params: { level }, }, z.object({}), ); setLogLevel(level); }; const AuthDebuggerWrapper = () => ( setIsAuthDebuggerVisible(false)} authState={authState} updateAuthState={updateAuthState} /> ); if (window.location.pathname === "/oauth/callback") { const OAuthCallback = React.lazy( () => import("./components/OAuthCallback"), ); return ( Loading...}> ); } if (window.location.pathname === "/oauth/callback/debug") { const OAuthDebugCallback = React.lazy( () => import("./components/OAuthDebugCallback"), ); return ( Loading...}> ); } return (
{mcpClient ? ( { setActiveTab(value); window.location.hash = value; }} > Resources Prompts Tools Tasks Apps Ping Sampling {pendingSampleRequests.length > 0 && ( {pendingSampleRequests.length} )} Elicitations {pendingElicitationRequests.length > 0 && ( {pendingElicitationRequests.length} )} Roots Auth Metadata
{!serverCapabilities?.resources && !serverCapabilities?.prompts && !serverCapabilities?.tools ? ( <>

The connected server does not support any MCP capabilities

{ void sendMCPRequest( { method: "ping" as const, }, EmptyResultSchema, ); }} /> ) : ( <> { clearError("resources"); listResources(); }} clearResources={() => { setResources([]); setNextResourceCursor(undefined); }} listResourceTemplates={() => { clearError("resources"); listResourceTemplates(); }} clearResourceTemplates={() => { setResourceTemplates([]); setNextResourceTemplateCursor(undefined); }} readResource={(uri) => { clearError("resources"); readResource(uri); }} selectedResource={selectedResource} setSelectedResource={(resource) => { clearError("resources"); setSelectedResource(resource); }} resourceSubscriptionsSupported={ serverCapabilities?.resources?.subscribe || false } resourceSubscriptions={resourceSubscriptions} subscribeToResource={(uri) => { clearError("resources"); subscribeToResource(uri); }} unsubscribeFromResource={(uri) => { clearError("resources"); unsubscribeFromResource(uri); }} handleCompletion={handleCompletion} completionsSupported={completionsSupported} resourceContent={resourceContent} nextCursor={nextResourceCursor} nextTemplateCursor={nextResourceTemplateCursor} error={errors.resources} /> { clearError("prompts"); listPrompts(); }} clearPrompts={() => { setPrompts([]); setNextPromptCursor(undefined); }} getPrompt={(name, args) => { clearError("prompts"); getPrompt(name, args); }} selectedPrompt={selectedPrompt} setSelectedPrompt={(prompt) => { clearError("prompts"); setSelectedPrompt(prompt); setPromptContent(""); }} handleCompletion={handleCompletion} completionsSupported={completionsSupported} promptContent={promptContent} nextCursor={nextPromptCursor} error={errors.prompts} /> { clearError("tools"); listTools(); }} clearTools={() => { setTools([]); setNextToolCursor(undefined); cacheToolOutputSchemas([]); }} callTool={async ( name: string, params: Record, metadata?: Record, runAsTask?: boolean, ) => { clearError("tools"); setToolResult(null); const result = await callTool( name, params, metadata, runAsTask, ); const calledTool = tools.find( (tool) => tool.name === name, ); if (calledTool && hasAppResourceUri(calledTool)) { setPrefilledAppsToolCall({ id: ++prefilledAppsToolCallIdRef.current, toolName: name, params: cloneToolParams(params), result, }); } else { setPrefilledAppsToolCall(null); } return result; }} selectedTool={selectedTool} setSelectedTool={(tool) => { clearError("tools"); setSelectedTool(tool); setToolResult(null); }} toolResult={toolResult} isPollingTask={isPollingTask} nextCursor={nextToolCursor} error={errors.tools} resourceContent={resourceContentMap} onReadResource={(uri: string) => { clearError("resources"); readResource(uri); }} /> { clearError("tasks"); listTasks(); }} clearTasks={() => { setTasks([]); setNextTaskCursor(undefined); }} cancelTask={cancelTask} selectedTask={selectedTask} setSelectedTask={(task) => { clearError("tasks"); setSelectedTask(task); }} error={errors.tasks} nextCursor={nextTaskCursor} /> { clearError("tools"); listTools(); }} callTool={async ( name: string, params: Record, metadata?: Record, runAsTask?: boolean, ) => { clearError("tools"); setToolResult(null); return callTool(name, params, metadata, runAsTask); }} prefilledToolCall={prefilledAppsToolCall} onPrefilledToolCallConsumed={(callId) => { setPrefilledAppsToolCall((prev) => prev?.id === callId ? null : prev, ); }} error={errors.tools} mcpClient={mcpClient} onNotification={(notification) => { setNotifications((prev) => [...prev, notification]); }} /> { void sendMCPRequest( { method: "ping" as const, }, EmptyResultSchema, ); }} /> )}
) : isAuthDebuggerVisible ? ( (window.location.hash = value)} > ) : (

Connect to an MCP server to start inspecting

Need to configure authentication?

)}
); }; export default App;