diff --git a/README.md b/README.md index d307b01..0db92bf 100644 --- a/README.md +++ b/README.md @@ -48,12 +48,16 @@ The MCP Inspector includes a proxy server that can run and communicate with loca ### Configuration -The MCP Inspector supports the following configuration settings. To change them click on the `Configuration` button in the MCP Inspector UI : +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 | -| MCP_PROXY_FULL_ADDRESS | The full URL of the MCP Inspector proxy server (e.g. `http://10.2.1.14:2277`) | `null` | +| 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. ### From this repository diff --git a/client/src/App.tsx b/client/src/App.tsx index 7564544..4f99ffd 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -45,10 +45,7 @@ import Sidebar from "./components/Sidebar"; import ToolsTab from "./components/ToolsTab"; import { DEFAULT_INSPECTOR_CONFIG } from "./lib/constants"; import { InspectorConfig } from "./lib/configurationTypes"; -import { - getMCPProxyAddress, - getMCPServerRequestTimeout, -} from "./utils/configUtils"; +import { getMCPProxyAddress } from "./utils/configUtils"; import { useToast } from "@/hooks/use-toast"; const params = new URLSearchParams(window.location.search); @@ -98,10 +95,21 @@ const App = () => { const [config, setConfig] = useState(() => { const savedConfig = localStorage.getItem(CONFIG_LOCAL_STORAGE_KEY); if (savedConfig) { - return { + // 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; }); @@ -148,7 +156,7 @@ const App = () => { serverCapabilities, mcpClient, requestHistory, - makeRequest: makeConnectionRequest, + makeRequest, sendNotification, handleCompletion, completionsSupported, @@ -161,8 +169,7 @@ const App = () => { sseUrl, env, bearerToken, - proxyServerUrl: getMCPProxyAddress(config), - requestTimeout: getMCPServerRequestTimeout(config), + config, onNotification: (notification) => { setNotifications((prev) => [...prev, notification as ServerNotification]); }, @@ -279,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); } @@ -303,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 } : {}, @@ -316,7 +323,7 @@ const App = () => { }; const listResourceTemplates = async () => { - const response = await makeRequest( + const response = await sendMCPRequest( { method: "resources/templates/list" as const, params: nextResourceTemplateCursor @@ -333,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 }, @@ -346,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 }, @@ -362,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 }, @@ -377,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 } : {}, @@ -390,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 }, @@ -402,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 } : {}, @@ -415,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 () => { @@ -437,7 +457,7 @@ const App = () => { }; const sendLogLevelRequest = async (level: LoggingLevel) => { - await makeRequest( + await sendMCPRequest( { method: "logging/setLevel" as const, params: { level }, @@ -637,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) => { @@ -654,7 +675,7 @@ const App = () => { { - void makeRequest( + void sendMCPRequest( { method: "ping" as const, }, diff --git a/client/src/components/PingTab.tsx b/client/src/components/PingTab.tsx index 287356c..6546901 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 48c847d..80e5fe6 100644 --- a/client/src/components/PromptsTab.tsx +++ b/client/src/components/PromptsTab.tsx @@ -84,84 +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 2a10824..23cfbe7 100644 --- a/client/src/components/ResourcesTab.tsx +++ b/client/src/components/ResourcesTab.tsx @@ -111,155 +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 ? ( - - ) : 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] || []} - /> -
- ); - })} - + title="Resources" + buttonText={nextCursor ? "List More Resources" : "List Resources"} + isButtonDisabled={!nextCursor && resources.length > 0} + /> + + { + setSelectedTemplate(template); + setSelectedResource(null); + setTemplateValues({}); + }} + renderItem={(template) => ( +
+ + + {template.name} + +
- ) : ( - - - 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 33f60d5..308b88b 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 a72ea7d..d7d0212 100644 --- a/client/src/components/SamplingTab.tsx +++ b/client/src/components/SamplingTab.tsx @@ -33,33 +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 19f8020..4633d64 100644 --- a/client/src/components/Sidebar.tsx +++ b/client/src/components/Sidebar.tsx @@ -325,8 +325,8 @@ const Sidebar = ({ return (
-