Merge pull request #243 from pulkitsharma07/add_proxy_config
Add MCP proxy address config support, better error messages, redesigned Config panel
This commit is contained in:
@@ -53,6 +53,7 @@ The MCP Inspector supports the following configuration settings. To change them
|
|||||||
| Name | Purpose | Default Value |
|
| 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_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` |
|
||||||
|
|
||||||
### From this repository
|
### From this repository
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"@radix-ui/react-select": "^2.1.2",
|
"@radix-ui/react-select": "^2.1.2",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
"@radix-ui/react-tabs": "^1.1.1",
|
"@radix-ui/react-tabs": "^1.1.1",
|
||||||
|
"@radix-ui/react-tooltip": "^1.1.8",
|
||||||
"@radix-ui/react-toast": "^1.2.6",
|
"@radix-ui/react-toast": "^1.2.6",
|
||||||
"@types/prismjs": "^1.26.5",
|
"@types/prismjs": "^1.26.5",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import {
|
|||||||
import React, { Suspense, useEffect, useRef, useState } from "react";
|
import React, { Suspense, useEffect, useRef, useState } from "react";
|
||||||
import { useConnection } from "./lib/hooks/useConnection";
|
import { useConnection } from "./lib/hooks/useConnection";
|
||||||
import { useDraggablePane } from "./lib/hooks/useDraggablePane";
|
import { useDraggablePane } from "./lib/hooks/useDraggablePane";
|
||||||
|
|
||||||
import { StdErrNotification } from "./lib/notificationTypes";
|
import { StdErrNotification } from "./lib/notificationTypes";
|
||||||
|
|
||||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
@@ -46,10 +45,12 @@ import Sidebar from "./components/Sidebar";
|
|||||||
import ToolsTab from "./components/ToolsTab";
|
import ToolsTab from "./components/ToolsTab";
|
||||||
import { DEFAULT_INSPECTOR_CONFIG } from "./lib/constants";
|
import { DEFAULT_INSPECTOR_CONFIG } from "./lib/constants";
|
||||||
import { InspectorConfig } from "./lib/configurationTypes";
|
import { InspectorConfig } from "./lib/configurationTypes";
|
||||||
|
import {
|
||||||
|
getMCPProxyAddress,
|
||||||
|
getMCPServerRequestTimeout,
|
||||||
|
} from "./utils/configUtils";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
const PROXY_PORT = params.get("proxyPort") ?? "6277";
|
|
||||||
const PROXY_SERVER_URL = `http://${window.location.hostname}:${PROXY_PORT}`;
|
|
||||||
const CONFIG_LOCAL_STORAGE_KEY = "inspectorConfig_v1";
|
const CONFIG_LOCAL_STORAGE_KEY = "inspectorConfig_v1";
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
@@ -95,7 +96,13 @@ const App = () => {
|
|||||||
|
|
||||||
const [config, setConfig] = useState<InspectorConfig>(() => {
|
const [config, setConfig] = useState<InspectorConfig>(() => {
|
||||||
const savedConfig = localStorage.getItem(CONFIG_LOCAL_STORAGE_KEY);
|
const savedConfig = localStorage.getItem(CONFIG_LOCAL_STORAGE_KEY);
|
||||||
return savedConfig ? JSON.parse(savedConfig) : DEFAULT_INSPECTOR_CONFIG;
|
if (savedConfig) {
|
||||||
|
return {
|
||||||
|
...DEFAULT_INSPECTOR_CONFIG,
|
||||||
|
...JSON.parse(savedConfig),
|
||||||
|
} as InspectorConfig;
|
||||||
|
}
|
||||||
|
return DEFAULT_INSPECTOR_CONFIG;
|
||||||
});
|
});
|
||||||
const [bearerToken, setBearerToken] = useState<string>(() => {
|
const [bearerToken, setBearerToken] = useState<string>(() => {
|
||||||
return localStorage.getItem("lastBearerToken") || "";
|
return localStorage.getItem("lastBearerToken") || "";
|
||||||
@@ -153,8 +160,8 @@ const App = () => {
|
|||||||
sseUrl,
|
sseUrl,
|
||||||
env,
|
env,
|
||||||
bearerToken,
|
bearerToken,
|
||||||
proxyServerUrl: PROXY_SERVER_URL,
|
proxyServerUrl: getMCPProxyAddress(config),
|
||||||
requestTimeout: config.MCP_SERVER_REQUEST_TIMEOUT.value as number,
|
requestTimeout: getMCPServerRequestTimeout(config),
|
||||||
onNotification: (notification) => {
|
onNotification: (notification) => {
|
||||||
setNotifications((prev) => [...prev, notification as ServerNotification]);
|
setNotifications((prev) => [...prev, notification as ServerNotification]);
|
||||||
},
|
},
|
||||||
@@ -218,7 +225,7 @@ const App = () => {
|
|||||||
}, [connectMcpServer, toast]);
|
}, [connectMcpServer, toast]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch(`${PROXY_SERVER_URL}/config`)
|
fetch(`${getMCPProxyAddress(config)}/config`)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
setEnv(data.defaultEnvironment);
|
setEnv(data.defaultEnvironment);
|
||||||
@@ -232,6 +239,7 @@ const App = () => {
|
|||||||
.catch((error) =>
|
.catch((error) =>
|
||||||
console.error("Error fetching default environment:", error),
|
console.error("Error fetching default environment:", error),
|
||||||
);
|
);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
EyeOff,
|
EyeOff,
|
||||||
RotateCcw,
|
RotateCcw,
|
||||||
Settings,
|
Settings,
|
||||||
|
HelpCircle,
|
||||||
RefreshCwOff,
|
RefreshCwOff,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -27,12 +28,17 @@ import {
|
|||||||
LoggingLevelSchema,
|
LoggingLevelSchema,
|
||||||
} from "@modelcontextprotocol/sdk/types.js";
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
import { InspectorConfig } from "@/lib/configurationTypes";
|
import { InspectorConfig } from "@/lib/configurationTypes";
|
||||||
|
import { ConnectionStatus } from "@/lib/constants";
|
||||||
import useTheme from "../lib/useTheme";
|
import useTheme from "../lib/useTheme";
|
||||||
import { version } from "../../../package.json";
|
import { version } from "../../../package.json";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipTrigger,
|
||||||
|
TooltipContent,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
connectionStatus: "disconnected" | "connected" | "error";
|
connectionStatus: ConnectionStatus;
|
||||||
transportType: "stdio" | "sse";
|
transportType: "stdio" | "sse";
|
||||||
setTransportType: (type: "stdio" | "sse") => void;
|
setTransportType: (type: "stdio" | "sse") => void;
|
||||||
command: string;
|
command: string;
|
||||||
@@ -180,6 +186,7 @@ const Sidebar = ({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setShowEnvVars(!showEnvVars)}
|
onClick={() => setShowEnvVars(!showEnvVars)}
|
||||||
className="flex items-center w-full"
|
className="flex items-center w-full"
|
||||||
|
data-testid="env-vars-button"
|
||||||
>
|
>
|
||||||
{showEnvVars ? (
|
{showEnvVars ? (
|
||||||
<ChevronDown className="w-4 h-4 mr-2" />
|
<ChevronDown className="w-4 h-4 mr-2" />
|
||||||
@@ -301,6 +308,7 @@ const Sidebar = ({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setShowConfig(!showConfig)}
|
onClick={() => setShowConfig(!showConfig)}
|
||||||
className="flex items-center w-full"
|
className="flex items-center w-full"
|
||||||
|
data-testid="config-button"
|
||||||
>
|
>
|
||||||
{showConfig ? (
|
{showConfig ? (
|
||||||
<ChevronDown className="w-4 h-4 mr-2" />
|
<ChevronDown className="w-4 h-4 mr-2" />
|
||||||
@@ -316,9 +324,19 @@ const Sidebar = ({
|
|||||||
const configKey = key as keyof InspectorConfig;
|
const configKey = key as keyof InspectorConfig;
|
||||||
return (
|
return (
|
||||||
<div key={key} className="space-y-2">
|
<div key={key} className="space-y-2">
|
||||||
<label className="text-sm font-medium">
|
<div className="flex items-center gap-1">
|
||||||
{configItem.description}
|
<label className="text-sm font-medium text-green-600">
|
||||||
</label>
|
{configKey}
|
||||||
|
</label>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
{configItem.description}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
{typeof configItem.value === "number" ? (
|
{typeof configItem.value === "number" ? (
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
@@ -380,7 +398,7 @@ const Sidebar = ({
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{connectionStatus === "connected" && (
|
{connectionStatus === "connected" && (
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<Button onClick={onConnect}>
|
<Button data-testid="connect-button" onClick={onConnect}>
|
||||||
<RotateCcw className="w-4 h-4 mr-2" />
|
<RotateCcw className="w-4 h-4 mr-2" />
|
||||||
{transportType === "stdio" ? "Restart" : "Reconnect"}
|
{transportType === "stdio" ? "Restart" : "Reconnect"}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -399,20 +417,32 @@ const Sidebar = ({
|
|||||||
|
|
||||||
<div className="flex items-center justify-center space-x-2 mb-4">
|
<div className="flex items-center justify-center space-x-2 mb-4">
|
||||||
<div
|
<div
|
||||||
className={`w-2 h-2 rounded-full ${
|
className={`w-2 h-2 rounded-full ${(() => {
|
||||||
connectionStatus === "connected"
|
switch (connectionStatus) {
|
||||||
? "bg-green-500"
|
case "connected":
|
||||||
: connectionStatus === "error"
|
return "bg-green-500";
|
||||||
? "bg-red-500"
|
case "error":
|
||||||
: "bg-gray-500"
|
return "bg-red-500";
|
||||||
}`}
|
case "error-connecting-to-proxy":
|
||||||
|
return "bg-red-500";
|
||||||
|
default:
|
||||||
|
return "bg-gray-500";
|
||||||
|
}
|
||||||
|
})()}`}
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-gray-600">
|
<span className="text-sm text-gray-600">
|
||||||
{connectionStatus === "connected"
|
{(() => {
|
||||||
? "Connected"
|
switch (connectionStatus) {
|
||||||
: connectionStatus === "error"
|
case "connected":
|
||||||
? "Connection Error"
|
return "Connected";
|
||||||
: "Disconnected"}
|
case "error":
|
||||||
|
return "Connection Error, is your MCP server running?";
|
||||||
|
case "error-connecting-to-proxy":
|
||||||
|
return "Error Connecting to MCP Inspector Proxy - Check Console logs";
|
||||||
|
default:
|
||||||
|
return "Disconnected";
|
||||||
|
}
|
||||||
|
})()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { render, screen, fireEvent } from "@testing-library/react";
|
import { render, screen, fireEvent } from "@testing-library/react";
|
||||||
import { describe, it, beforeEach, jest } from "@jest/globals";
|
import { describe, it, beforeEach, jest } from "@jest/globals";
|
||||||
import Sidebar from "../Sidebar";
|
import Sidebar from "../Sidebar";
|
||||||
import { DEFAULT_INSPECTOR_CONFIG } from "../../lib/constants";
|
import { DEFAULT_INSPECTOR_CONFIG } from "@/lib/constants";
|
||||||
import { InspectorConfig } from "../../lib/configurationTypes";
|
import { InspectorConfig } from "@/lib/configurationTypes";
|
||||||
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
|
|
||||||
// Mock theme hook
|
// Mock theme hook
|
||||||
jest.mock("../../lib/useTheme", () => ({
|
jest.mock("../../lib/useTheme", () => ({
|
||||||
@@ -36,11 +37,15 @@ describe("Sidebar Environment Variables", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderSidebar = (props = {}) => {
|
const renderSidebar = (props = {}) => {
|
||||||
return render(<Sidebar {...defaultProps} {...props} />);
|
return render(
|
||||||
|
<TooltipProvider>
|
||||||
|
<Sidebar {...defaultProps} {...props} />
|
||||||
|
</TooltipProvider>,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const openEnvVarsSection = () => {
|
const openEnvVarsSection = () => {
|
||||||
const button = screen.getByText("Environment Variables");
|
const button = screen.getByTestId("env-vars-button");
|
||||||
fireEvent.click(button);
|
fireEvent.click(button);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -216,7 +221,11 @@ describe("Sidebar Environment Variables", () => {
|
|||||||
const updatedEnv = setEnv.mock.calls[0][0] as Record<string, string>;
|
const updatedEnv = setEnv.mock.calls[0][0] as Record<string, string>;
|
||||||
|
|
||||||
// Rerender with the updated env
|
// Rerender with the updated env
|
||||||
rerender(<Sidebar {...defaultProps} env={updatedEnv} setEnv={setEnv} />);
|
rerender(
|
||||||
|
<TooltipProvider>
|
||||||
|
<Sidebar {...defaultProps} env={updatedEnv} setEnv={setEnv} />
|
||||||
|
</TooltipProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
// Second key edit
|
// Second key edit
|
||||||
const secondKeyInput = screen.getByDisplayValue("SECOND_KEY");
|
const secondKeyInput = screen.getByDisplayValue("SECOND_KEY");
|
||||||
@@ -247,7 +256,11 @@ describe("Sidebar Environment Variables", () => {
|
|||||||
fireEvent.change(keyInput, { target: { value: "NEW_KEY" } });
|
fireEvent.change(keyInput, { target: { value: "NEW_KEY" } });
|
||||||
|
|
||||||
// Rerender with updated env
|
// Rerender with updated env
|
||||||
rerender(<Sidebar {...defaultProps} env={{ NEW_KEY: "test_value" }} />);
|
rerender(
|
||||||
|
<TooltipProvider>
|
||||||
|
<Sidebar {...defaultProps} env={{ NEW_KEY: "test_value" }} />
|
||||||
|
</TooltipProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
// Value should still be visible
|
// Value should still be visible
|
||||||
const updatedValueInput = screen.getByDisplayValue("test_value");
|
const updatedValueInput = screen.getByDisplayValue("test_value");
|
||||||
@@ -312,7 +325,7 @@ describe("Sidebar Environment Variables", () => {
|
|||||||
|
|
||||||
describe("Configuration Operations", () => {
|
describe("Configuration Operations", () => {
|
||||||
const openConfigSection = () => {
|
const openConfigSection = () => {
|
||||||
const button = screen.getByText("Configuration");
|
const button = screen.getByTestId("config-button");
|
||||||
fireEvent.click(button);
|
fireEvent.click(button);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -327,12 +340,14 @@ describe("Sidebar Environment Variables", () => {
|
|||||||
);
|
);
|
||||||
fireEvent.change(timeoutInput, { target: { value: "5000" } });
|
fireEvent.change(timeoutInput, { target: { value: "5000" } });
|
||||||
|
|
||||||
expect(setConfig).toHaveBeenCalledWith({
|
expect(setConfig).toHaveBeenCalledWith(
|
||||||
MCP_SERVER_REQUEST_TIMEOUT: {
|
expect.objectContaining({
|
||||||
description: "Timeout for requests to the MCP server (ms)",
|
MCP_SERVER_REQUEST_TIMEOUT: {
|
||||||
value: 5000,
|
description: "Timeout for requests to the MCP server (ms)",
|
||||||
},
|
value: 5000,
|
||||||
});
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle invalid timeout values entered by user", () => {
|
it("should handle invalid timeout values entered by user", () => {
|
||||||
@@ -346,12 +361,14 @@ describe("Sidebar Environment Variables", () => {
|
|||||||
);
|
);
|
||||||
fireEvent.change(timeoutInput, { target: { value: "abc1" } });
|
fireEvent.change(timeoutInput, { target: { value: "abc1" } });
|
||||||
|
|
||||||
expect(setConfig).toHaveBeenCalledWith({
|
expect(setConfig).toHaveBeenCalledWith(
|
||||||
MCP_SERVER_REQUEST_TIMEOUT: {
|
expect.objectContaining({
|
||||||
description: "Timeout for requests to the MCP server (ms)",
|
MCP_SERVER_REQUEST_TIMEOUT: {
|
||||||
value: 0,
|
description: "Timeout for requests to the MCP server (ms)",
|
||||||
},
|
value: 0,
|
||||||
});
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should maintain configuration state after multiple updates", () => {
|
it("should maintain configuration state after multiple updates", () => {
|
||||||
@@ -362,7 +379,6 @@ describe("Sidebar Environment Variables", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
openConfigSection();
|
openConfigSection();
|
||||||
|
|
||||||
// First update
|
// First update
|
||||||
const timeoutInput = screen.getByTestId(
|
const timeoutInput = screen.getByTestId(
|
||||||
"MCP_SERVER_REQUEST_TIMEOUT-input",
|
"MCP_SERVER_REQUEST_TIMEOUT-input",
|
||||||
@@ -374,11 +390,13 @@ describe("Sidebar Environment Variables", () => {
|
|||||||
|
|
||||||
// Rerender with the updated config
|
// Rerender with the updated config
|
||||||
rerender(
|
rerender(
|
||||||
<Sidebar
|
<TooltipProvider>
|
||||||
{...defaultProps}
|
<Sidebar
|
||||||
config={updatedConfig}
|
{...defaultProps}
|
||||||
setConfig={setConfig}
|
config={updatedConfig}
|
||||||
/>,
|
setConfig={setConfig}
|
||||||
|
/>
|
||||||
|
</TooltipProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Second update
|
// Second update
|
||||||
@@ -388,12 +406,14 @@ describe("Sidebar Environment Variables", () => {
|
|||||||
fireEvent.change(updatedTimeoutInput, { target: { value: "3000" } });
|
fireEvent.change(updatedTimeoutInput, { target: { value: "3000" } });
|
||||||
|
|
||||||
// Verify the final state matches what we expect
|
// Verify the final state matches what we expect
|
||||||
expect(setConfig).toHaveBeenLastCalledWith({
|
expect(setConfig).toHaveBeenLastCalledWith(
|
||||||
MCP_SERVER_REQUEST_TIMEOUT: {
|
expect.objectContaining({
|
||||||
description: "Timeout for requests to the MCP server (ms)",
|
MCP_SERVER_REQUEST_TIMEOUT: {
|
||||||
value: 3000,
|
description: "Timeout for requests to the MCP server (ms)",
|
||||||
},
|
value: 3000,
|
||||||
});
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
30
client/src/components/ui/tooltip.tsx
Normal file
30
client/src/components/ui/tooltip.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const TooltipProvider = TooltipPrimitive.Provider;
|
||||||
|
|
||||||
|
const Tooltip = TooltipPrimitive.Root;
|
||||||
|
|
||||||
|
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||||
|
|
||||||
|
const TooltipContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||||
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||||
|
<TooltipPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||||
@@ -15,4 +15,5 @@ export type InspectorConfig = {
|
|||||||
* Maximum time in milliseconds to wait for a response from the MCP server before timing out.
|
* Maximum time in milliseconds to wait for a response from the MCP server before timing out.
|
||||||
*/
|
*/
|
||||||
MCP_SERVER_REQUEST_TIMEOUT: ConfigItem;
|
MCP_SERVER_REQUEST_TIMEOUT: ConfigItem;
|
||||||
|
MCP_PROXY_FULL_ADDRESS: ConfigItem;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,9 +8,26 @@ export const SESSION_KEYS = {
|
|||||||
CLIENT_INFORMATION: "mcp_client_information",
|
CLIENT_INFORMATION: "mcp_client_information",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export type ConnectionStatus =
|
||||||
|
| "disconnected"
|
||||||
|
| "connected"
|
||||||
|
| "error"
|
||||||
|
| "error-connecting-to-proxy";
|
||||||
|
|
||||||
|
export const DEFAULT_MCP_PROXY_LISTEN_PORT = "6277";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default configuration for the MCP Inspector, Currently persisted in local_storage in the Browser.
|
||||||
|
* Future plans: Provide json config file + Browser local_storage to override default values
|
||||||
|
**/
|
||||||
export const DEFAULT_INSPECTOR_CONFIG: InspectorConfig = {
|
export const DEFAULT_INSPECTOR_CONFIG: InspectorConfig = {
|
||||||
MCP_SERVER_REQUEST_TIMEOUT: {
|
MCP_SERVER_REQUEST_TIMEOUT: {
|
||||||
description: "Timeout for requests to the MCP server (ms)",
|
description: "Timeout for requests to the MCP server (ms)",
|
||||||
value: 10000,
|
value: 10000,
|
||||||
},
|
},
|
||||||
|
MCP_PROXY_FULL_ADDRESS: {
|
||||||
|
description:
|
||||||
|
"Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577",
|
||||||
|
value: "",
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import {
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { SESSION_KEYS } from "../constants";
|
import { ConnectionStatus, SESSION_KEYS } from "../constants";
|
||||||
import { Notification, StdErrNotificationSchema } from "../notificationTypes";
|
import { Notification, StdErrNotificationSchema } from "../notificationTypes";
|
||||||
import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
|
import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
|
||||||
import { authProvider } from "../auth";
|
import { authProvider } from "../auth";
|
||||||
@@ -70,10 +70,9 @@ export function useConnection({
|
|||||||
onPendingRequest,
|
onPendingRequest,
|
||||||
getRoots,
|
getRoots,
|
||||||
}: UseConnectionOptions) {
|
}: UseConnectionOptions) {
|
||||||
|
const [connectionStatus, setConnectionStatus] =
|
||||||
|
useState<ConnectionStatus>("disconnected");
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [connectionStatus, setConnectionStatus] = useState<
|
|
||||||
"disconnected" | "connected" | "error"
|
|
||||||
>("disconnected");
|
|
||||||
const [serverCapabilities, setServerCapabilities] =
|
const [serverCapabilities, setServerCapabilities] =
|
||||||
useState<ServerCapabilities | null>(null);
|
useState<ServerCapabilities | null>(null);
|
||||||
const [mcpClient, setMcpClient] = useState<Client | null>(null);
|
const [mcpClient, setMcpClient] = useState<Client | null>(null);
|
||||||
@@ -210,6 +209,20 @@ export function useConnection({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const checkProxyHealth = async () => {
|
||||||
|
try {
|
||||||
|
const proxyHealthUrl = new URL(`${proxyServerUrl}/health`);
|
||||||
|
const proxyHealthResponse = await fetch(proxyHealthUrl);
|
||||||
|
const proxyHealth = await proxyHealthResponse.json();
|
||||||
|
if (proxyHealth?.status !== "ok") {
|
||||||
|
throw new Error("MCP Proxy Server is not healthy");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Couldn't connect to MCP Proxy Server", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleAuthError = async (error: unknown) => {
|
const handleAuthError = async (error: unknown) => {
|
||||||
if (error instanceof SseError && error.code === 401) {
|
if (error instanceof SseError && error.code === 401) {
|
||||||
sessionStorage.setItem(SESSION_KEYS.SERVER_URL, sseUrl);
|
sessionStorage.setItem(SESSION_KEYS.SERVER_URL, sseUrl);
|
||||||
@@ -222,33 +235,38 @@ export function useConnection({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const connect = async (_e?: unknown, retryCount: number = 0) => {
|
const connect = async (_e?: unknown, retryCount: number = 0) => {
|
||||||
try {
|
const client = new Client<Request, Notification, Result>(
|
||||||
const client = new Client<Request, Notification, Result>(
|
{
|
||||||
{
|
name: "mcp-inspector",
|
||||||
name: "mcp-inspector",
|
version: packageJson.version,
|
||||||
version: packageJson.version,
|
},
|
||||||
},
|
{
|
||||||
{
|
capabilities: {
|
||||||
capabilities: {
|
sampling: {},
|
||||||
sampling: {},
|
roots: {
|
||||||
roots: {
|
listChanged: true,
|
||||||
listChanged: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const backendUrl = new URL(`${proxyServerUrl}/sse`);
|
try {
|
||||||
|
await checkProxyHealth();
|
||||||
backendUrl.searchParams.append("transportType", transportType);
|
} catch {
|
||||||
if (transportType === "stdio") {
|
setConnectionStatus("error-connecting-to-proxy");
|
||||||
backendUrl.searchParams.append("command", command);
|
return;
|
||||||
backendUrl.searchParams.append("args", args);
|
}
|
||||||
backendUrl.searchParams.append("env", JSON.stringify(env));
|
const mcpProxyServerUrl = new URL(`${proxyServerUrl}/sse`);
|
||||||
} else {
|
mcpProxyServerUrl.searchParams.append("transportType", transportType);
|
||||||
backendUrl.searchParams.append("url", sseUrl);
|
if (transportType === "stdio") {
|
||||||
}
|
mcpProxyServerUrl.searchParams.append("command", command);
|
||||||
|
mcpProxyServerUrl.searchParams.append("args", args);
|
||||||
|
mcpProxyServerUrl.searchParams.append("env", JSON.stringify(env));
|
||||||
|
} else {
|
||||||
|
mcpProxyServerUrl.searchParams.append("url", sseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
// Inject auth manually instead of using SSEClientTransport, because we're
|
// Inject auth manually instead of using SSEClientTransport, because we're
|
||||||
// proxying through the inspector server first.
|
// proxying through the inspector server first.
|
||||||
const headers: HeadersInit = {};
|
const headers: HeadersInit = {};
|
||||||
@@ -259,7 +277,7 @@ export function useConnection({
|
|||||||
headers["Authorization"] = `Bearer ${token}`;
|
headers["Authorization"] = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const clientTransport = new SSEClientTransport(backendUrl, {
|
const clientTransport = new SSEClientTransport(mcpProxyServerUrl, {
|
||||||
eventSourceInit: {
|
eventSourceInit: {
|
||||||
fetch: (url, init) => fetch(url, { ...init, headers }),
|
fetch: (url, init) => fetch(url, { ...init, headers }),
|
||||||
},
|
},
|
||||||
@@ -299,7 +317,10 @@ export function useConnection({
|
|||||||
try {
|
try {
|
||||||
await client.connect(clientTransport);
|
await client.connect(clientTransport);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to connect to MCP server:", error);
|
console.error(
|
||||||
|
`Failed to connect to MCP Server via the MCP Inspector Proxy: ${mcpProxyServerUrl}:`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
const shouldRetry = await handleAuthError(error);
|
const shouldRetry = await handleAuthError(error);
|
||||||
if (shouldRetry) {
|
if (shouldRetry) {
|
||||||
return connect(undefined, retryCount + 1);
|
return connect(undefined, retryCount + 1);
|
||||||
|
|||||||
@@ -3,10 +3,13 @@ import { createRoot } from "react-dom/client";
|
|||||||
import { Toaster } from "@/components/ui/toaster.tsx";
|
import { Toaster } from "@/components/ui/toaster.tsx";
|
||||||
import App from "./App.tsx";
|
import App from "./App.tsx";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
import { TooltipProvider } from "./components/ui/tooltip.tsx";
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<TooltipProvider>
|
||||||
|
<App />
|
||||||
|
</TooltipProvider>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
);
|
);
|
||||||
|
|||||||
14
client/src/utils/configUtils.ts
Normal file
14
client/src/utils/configUtils.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { InspectorConfig } from "@/lib/configurationTypes";
|
||||||
|
import { DEFAULT_MCP_PROXY_LISTEN_PORT } from "@/lib/constants";
|
||||||
|
|
||||||
|
export const getMCPProxyAddress = (config: InspectorConfig): string => {
|
||||||
|
const proxyFullAddress = config.MCP_PROXY_FULL_ADDRESS.value as string;
|
||||||
|
if (proxyFullAddress) {
|
||||||
|
return proxyFullAddress;
|
||||||
|
}
|
||||||
|
return `http://${window.location.hostname}:${DEFAULT_MCP_PROXY_LISTEN_PORT}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMCPServerRequestTimeout = (config: InspectorConfig): number => {
|
||||||
|
return config.MCP_SERVER_REQUEST_TIMEOUT.value as number;
|
||||||
|
};
|
||||||
2795
package-lock.json
generated
2795
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -38,7 +38,7 @@ app.use(cors());
|
|||||||
|
|
||||||
let webAppTransports: SSEServerTransport[] = [];
|
let webAppTransports: SSEServerTransport[] = [];
|
||||||
|
|
||||||
const createTransport = async (req: express.Request) => {
|
const createTransport = async (req: express.Request): Promise<Transport> => {
|
||||||
const query = req.query;
|
const query = req.query;
|
||||||
console.log("Query parameters:", query);
|
console.log("Query parameters:", query);
|
||||||
|
|
||||||
@@ -70,6 +70,7 @@ const createTransport = async (req: express.Request) => {
|
|||||||
const headers: HeadersInit = {
|
const headers: HeadersInit = {
|
||||||
Accept: "text/event-stream",
|
Accept: "text/event-stream",
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const key of SSE_HEADERS_PASSTHROUGH) {
|
for (const key of SSE_HEADERS_PASSTHROUGH) {
|
||||||
if (req.headers[key] === undefined) {
|
if (req.headers[key] === undefined) {
|
||||||
continue;
|
continue;
|
||||||
@@ -172,6 +173,12 @@ app.post("/message", async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get("/health", (req, res) => {
|
||||||
|
res.json({
|
||||||
|
status: "ok",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
app.get("/config", (req, res) => {
|
app.get("/config", (req, res) => {
|
||||||
try {
|
try {
|
||||||
res.json({
|
res.json({
|
||||||
|
|||||||
Reference in New Issue
Block a user