Merge branch 'main' into set-header
This commit is contained in:
16
README.md
16
README.md
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
The MCP inspector is a developer tool for testing and debugging MCP servers.
|
The MCP inspector is a developer tool for testing and debugging MCP servers.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Running the Inspector
|
## Running the Inspector
|
||||||
|
|
||||||
@@ -48,12 +48,16 @@ The MCP Inspector includes a proxy server that can run and communicate with loca
|
|||||||
|
|
||||||
### Configuration
|
### 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 |
|
| Setting | Description | Default |
|
||||||
| -------------------------- | ----------------------------------------------------------------------------------------- | ------------- |
|
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ------- |
|
||||||
| 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` | Timeout for requests to the MCP server (ms) | 10000 |
|
||||||
| MCP_PROXY_FULL_ADDRESS | The full URL of the MCP Inspector proxy server (e.g. `http://10.2.1.14:2277`) | `null` |
|
| `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
|
### From this repository
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@modelcontextprotocol/inspector-client",
|
"name": "@modelcontextprotocol/inspector-client",
|
||||||
"version": "0.8.1",
|
"version": "0.8.2",
|
||||||
"description": "Client-side application for the Model Context Protocol inspector",
|
"description": "Client-side application for the Model Context Protocol inspector",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
"test:watch": "jest --config jest.config.cjs --watch"
|
"test:watch": "jest --config jest.config.cjs --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.8.0",
|
"@modelcontextprotocol/sdk": "^1.9.0",
|
||||||
"@radix-ui/react-checkbox": "^1.1.4",
|
"@radix-ui/react-checkbox": "^1.1.4",
|
||||||
"@radix-ui/react-dialog": "^1.1.3",
|
"@radix-ui/react-dialog": "^1.1.3",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
@@ -32,9 +32,8 @@
|
|||||||
"@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",
|
"@radix-ui/react-tooltip": "^1.1.8",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.0.4",
|
"cmdk": "^1.0.4",
|
||||||
@@ -55,6 +54,7 @@
|
|||||||
"@testing-library/react": "^16.2.0",
|
"@testing-library/react": "^16.2.0",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^22.7.5",
|
"@types/node": "^22.7.5",
|
||||||
|
"@types/prismjs": "^1.26.5",
|
||||||
"@types/react": "^18.3.10",
|
"@types/react": "^18.3.10",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@types/serve-handler": "^6.1.4",
|
"@types/serve-handler": "^6.1.4",
|
||||||
|
|||||||
@@ -45,10 +45,7 @@ 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 {
|
import { getMCPProxyAddress } from "./utils/configUtils";
|
||||||
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);
|
||||||
@@ -98,10 +95,21 @@ 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);
|
||||||
if (savedConfig) {
|
if (savedConfig) {
|
||||||
return {
|
// merge default config with saved config
|
||||||
|
const mergedConfig = {
|
||||||
...DEFAULT_INSPECTOR_CONFIG,
|
...DEFAULT_INSPECTOR_CONFIG,
|
||||||
...JSON.parse(savedConfig),
|
...JSON.parse(savedConfig),
|
||||||
} as InspectorConfig;
|
} 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;
|
return DEFAULT_INSPECTOR_CONFIG;
|
||||||
});
|
});
|
||||||
@@ -152,7 +160,7 @@ const App = () => {
|
|||||||
serverCapabilities,
|
serverCapabilities,
|
||||||
mcpClient,
|
mcpClient,
|
||||||
requestHistory,
|
requestHistory,
|
||||||
makeRequest: makeConnectionRequest,
|
makeRequest,
|
||||||
sendNotification,
|
sendNotification,
|
||||||
handleCompletion,
|
handleCompletion,
|
||||||
completionsSupported,
|
completionsSupported,
|
||||||
@@ -168,6 +176,7 @@ const App = () => {
|
|||||||
headerName,
|
headerName,
|
||||||
proxyServerUrl: getMCPProxyAddress(config),
|
proxyServerUrl: getMCPProxyAddress(config),
|
||||||
requestTimeout: getMCPServerRequestTimeout(config),
|
requestTimeout: getMCPServerRequestTimeout(config),
|
||||||
|
config,
|
||||||
onNotification: (notification) => {
|
onNotification: (notification) => {
|
||||||
setNotifications((prev) => [...prev, notification as ServerNotification]);
|
setNotifications((prev) => [...prev, notification as ServerNotification]);
|
||||||
},
|
},
|
||||||
@@ -288,13 +297,13 @@ const App = () => {
|
|||||||
setErrors((prev) => ({ ...prev, [tabKey]: null }));
|
setErrors((prev) => ({ ...prev, [tabKey]: null }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const makeRequest = async <T extends z.ZodType>(
|
const sendMCPRequest = async <T extends z.ZodType>(
|
||||||
request: ClientRequest,
|
request: ClientRequest,
|
||||||
schema: T,
|
schema: T,
|
||||||
tabKey?: keyof typeof errors,
|
tabKey?: keyof typeof errors,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const response = await makeConnectionRequest(request, schema);
|
const response = await makeRequest(request, schema);
|
||||||
if (tabKey !== undefined) {
|
if (tabKey !== undefined) {
|
||||||
clearError(tabKey);
|
clearError(tabKey);
|
||||||
}
|
}
|
||||||
@@ -312,7 +321,7 @@ const App = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const listResources = async () => {
|
const listResources = async () => {
|
||||||
const response = await makeRequest(
|
const response = await sendMCPRequest(
|
||||||
{
|
{
|
||||||
method: "resources/list" as const,
|
method: "resources/list" as const,
|
||||||
params: nextResourceCursor ? { cursor: nextResourceCursor } : {},
|
params: nextResourceCursor ? { cursor: nextResourceCursor } : {},
|
||||||
@@ -325,7 +334,7 @@ const App = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const listResourceTemplates = async () => {
|
const listResourceTemplates = async () => {
|
||||||
const response = await makeRequest(
|
const response = await sendMCPRequest(
|
||||||
{
|
{
|
||||||
method: "resources/templates/list" as const,
|
method: "resources/templates/list" as const,
|
||||||
params: nextResourceTemplateCursor
|
params: nextResourceTemplateCursor
|
||||||
@@ -342,7 +351,7 @@ const App = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const readResource = async (uri: string) => {
|
const readResource = async (uri: string) => {
|
||||||
const response = await makeRequest(
|
const response = await sendMCPRequest(
|
||||||
{
|
{
|
||||||
method: "resources/read" as const,
|
method: "resources/read" as const,
|
||||||
params: { uri },
|
params: { uri },
|
||||||
@@ -355,7 +364,7 @@ const App = () => {
|
|||||||
|
|
||||||
const subscribeToResource = async (uri: string) => {
|
const subscribeToResource = async (uri: string) => {
|
||||||
if (!resourceSubscriptions.has(uri)) {
|
if (!resourceSubscriptions.has(uri)) {
|
||||||
await makeRequest(
|
await sendMCPRequest(
|
||||||
{
|
{
|
||||||
method: "resources/subscribe" as const,
|
method: "resources/subscribe" as const,
|
||||||
params: { uri },
|
params: { uri },
|
||||||
@@ -371,7 +380,7 @@ const App = () => {
|
|||||||
|
|
||||||
const unsubscribeFromResource = async (uri: string) => {
|
const unsubscribeFromResource = async (uri: string) => {
|
||||||
if (resourceSubscriptions.has(uri)) {
|
if (resourceSubscriptions.has(uri)) {
|
||||||
await makeRequest(
|
await sendMCPRequest(
|
||||||
{
|
{
|
||||||
method: "resources/unsubscribe" as const,
|
method: "resources/unsubscribe" as const,
|
||||||
params: { uri },
|
params: { uri },
|
||||||
@@ -386,7 +395,7 @@ const App = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const listPrompts = async () => {
|
const listPrompts = async () => {
|
||||||
const response = await makeRequest(
|
const response = await sendMCPRequest(
|
||||||
{
|
{
|
||||||
method: "prompts/list" as const,
|
method: "prompts/list" as const,
|
||||||
params: nextPromptCursor ? { cursor: nextPromptCursor } : {},
|
params: nextPromptCursor ? { cursor: nextPromptCursor } : {},
|
||||||
@@ -399,7 +408,7 @@ const App = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getPrompt = async (name: string, args: Record<string, string> = {}) => {
|
const getPrompt = async (name: string, args: Record<string, string> = {}) => {
|
||||||
const response = await makeRequest(
|
const response = await sendMCPRequest(
|
||||||
{
|
{
|
||||||
method: "prompts/get" as const,
|
method: "prompts/get" as const,
|
||||||
params: { name, arguments: args },
|
params: { name, arguments: args },
|
||||||
@@ -411,7 +420,7 @@ const App = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const listTools = async () => {
|
const listTools = async () => {
|
||||||
const response = await makeRequest(
|
const response = await sendMCPRequest(
|
||||||
{
|
{
|
||||||
method: "tools/list" as const,
|
method: "tools/list" as const,
|
||||||
params: nextToolCursor ? { cursor: nextToolCursor } : {},
|
params: nextToolCursor ? { cursor: nextToolCursor } : {},
|
||||||
@@ -424,21 +433,34 @@ const App = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const callTool = async (name: string, params: Record<string, unknown>) => {
|
const callTool = async (name: string, params: Record<string, unknown>) => {
|
||||||
const response = await makeRequest(
|
try {
|
||||||
{
|
const response = await sendMCPRequest(
|
||||||
method: "tools/call" as const,
|
{
|
||||||
params: {
|
method: "tools/call" as const,
|
||||||
name,
|
params: {
|
||||||
arguments: params,
|
name,
|
||||||
_meta: {
|
arguments: params,
|
||||||
progressToken: progressTokenRef.current++,
|
_meta: {
|
||||||
|
progressToken: progressTokenRef.current++,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
CompatibilityCallToolResultSchema,
|
||||||
CompatibilityCallToolResultSchema,
|
"tools",
|
||||||
"tools",
|
);
|
||||||
);
|
setToolResult(response);
|
||||||
setToolResult(response);
|
} catch (e) {
|
||||||
|
const toolResult: CompatibilityCallToolResult = {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: (e as Error).message ?? String(e),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
setToolResult(toolResult);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRootsChange = async () => {
|
const handleRootsChange = async () => {
|
||||||
@@ -446,7 +468,7 @@ const App = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const sendLogLevelRequest = async (level: LoggingLevel) => {
|
const sendLogLevelRequest = async (level: LoggingLevel) => {
|
||||||
await makeRequest(
|
await sendMCPRequest(
|
||||||
{
|
{
|
||||||
method: "logging/setLevel" as const,
|
method: "logging/setLevel" as const,
|
||||||
params: { level },
|
params: { level },
|
||||||
@@ -648,9 +670,10 @@ const App = () => {
|
|||||||
setTools([]);
|
setTools([]);
|
||||||
setNextToolCursor(undefined);
|
setNextToolCursor(undefined);
|
||||||
}}
|
}}
|
||||||
callTool={(name, params) => {
|
callTool={async (name, params) => {
|
||||||
clearError("tools");
|
clearError("tools");
|
||||||
callTool(name, params);
|
setToolResult(null);
|
||||||
|
await callTool(name, params);
|
||||||
}}
|
}}
|
||||||
selectedTool={selectedTool}
|
selectedTool={selectedTool}
|
||||||
setSelectedTool={(tool) => {
|
setSelectedTool={(tool) => {
|
||||||
@@ -665,7 +688,7 @@ const App = () => {
|
|||||||
<ConsoleTab />
|
<ConsoleTab />
|
||||||
<PingTab
|
<PingTab
|
||||||
onPingClick={() => {
|
onPingClick={() => {
|
||||||
void makeRequest(
|
void sendMCPRequest(
|
||||||
{
|
{
|
||||||
method: "ping" as const,
|
method: "ping" as const,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,33 +3,9 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import JsonEditor from "./JsonEditor";
|
import JsonEditor from "./JsonEditor";
|
||||||
import { updateValueAtPath, JsonObject } from "@/utils/jsonPathUtils";
|
import { updateValueAtPath } from "@/utils/jsonUtils";
|
||||||
import { generateDefaultValue, formatFieldLabel } from "@/utils/schemaUtils";
|
import { generateDefaultValue, formatFieldLabel } from "@/utils/schemaUtils";
|
||||||
|
import type { JsonValue, JsonSchemaType, JsonObject } from "@/utils/jsonUtils";
|
||||||
export type JsonValue =
|
|
||||||
| string
|
|
||||||
| number
|
|
||||||
| boolean
|
|
||||||
| null
|
|
||||||
| undefined
|
|
||||||
| JsonValue[]
|
|
||||||
| { [key: string]: JsonValue };
|
|
||||||
|
|
||||||
export type JsonSchemaType = {
|
|
||||||
type:
|
|
||||||
| "string"
|
|
||||||
| "number"
|
|
||||||
| "integer"
|
|
||||||
| "boolean"
|
|
||||||
| "array"
|
|
||||||
| "object"
|
|
||||||
| "null";
|
|
||||||
description?: string;
|
|
||||||
required?: boolean;
|
|
||||||
default?: JsonValue;
|
|
||||||
properties?: Record<string, JsonSchemaType>;
|
|
||||||
items?: JsonSchemaType;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface DynamicJsonFormProps {
|
interface DynamicJsonFormProps {
|
||||||
schema: JsonSchemaType;
|
schema: JsonSchemaType;
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { useState, memo, useMemo, useCallback, useEffect } from "react";
|
import { useState, memo, useMemo, useCallback, useEffect } from "react";
|
||||||
import { JsonValue } from "./DynamicJsonForm";
|
import type { JsonValue } from "@/utils/jsonUtils";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { Copy, CheckCheck } from "lucide-react";
|
import { Copy, CheckCheck } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { getDataType, tryParseJson } from "@/utils/jsonUtils";
|
||||||
|
|
||||||
interface JsonViewProps {
|
interface JsonViewProps {
|
||||||
data: unknown;
|
data: unknown;
|
||||||
@@ -11,21 +12,7 @@ interface JsonViewProps {
|
|||||||
initialExpandDepth?: number;
|
initialExpandDepth?: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
withCopyButton?: boolean;
|
withCopyButton?: boolean;
|
||||||
}
|
isError?: boolean;
|
||||||
|
|
||||||
function tryParseJson(str: string): { success: boolean; data: JsonValue } {
|
|
||||||
const trimmed = str.trim();
|
|
||||||
if (
|
|
||||||
!(trimmed.startsWith("{") && trimmed.endsWith("}")) &&
|
|
||||||
!(trimmed.startsWith("[") && trimmed.endsWith("]"))
|
|
||||||
) {
|
|
||||||
return { success: false, data: str };
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return { success: true, data: JSON.parse(str) };
|
|
||||||
} catch {
|
|
||||||
return { success: false, data: str };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const JsonView = memo(
|
const JsonView = memo(
|
||||||
@@ -35,6 +22,7 @@ const JsonView = memo(
|
|||||||
initialExpandDepth = 3,
|
initialExpandDepth = 3,
|
||||||
className,
|
className,
|
||||||
withCopyButton = true,
|
withCopyButton = true,
|
||||||
|
isError = false,
|
||||||
}: JsonViewProps) => {
|
}: JsonViewProps) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
@@ -100,6 +88,7 @@ const JsonView = memo(
|
|||||||
name={name}
|
name={name}
|
||||||
depth={0}
|
depth={0}
|
||||||
initialExpandDepth={initialExpandDepth}
|
initialExpandDepth={initialExpandDepth}
|
||||||
|
isError={isError}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -114,28 +103,28 @@ interface JsonNodeProps {
|
|||||||
name?: string;
|
name?: string;
|
||||||
depth: number;
|
depth: number;
|
||||||
initialExpandDepth: number;
|
initialExpandDepth: number;
|
||||||
|
isError?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const JsonNode = memo(
|
const JsonNode = memo(
|
||||||
({ data, name, depth = 0, initialExpandDepth }: JsonNodeProps) => {
|
({
|
||||||
|
data,
|
||||||
|
name,
|
||||||
|
depth = 0,
|
||||||
|
initialExpandDepth,
|
||||||
|
isError = false,
|
||||||
|
}: JsonNodeProps) => {
|
||||||
const [isExpanded, setIsExpanded] = useState(depth < initialExpandDepth);
|
const [isExpanded, setIsExpanded] = useState(depth < initialExpandDepth);
|
||||||
|
const [typeStyleMap] = useState<Record<string, string>>({
|
||||||
const getDataType = (value: JsonValue): string => {
|
|
||||||
if (Array.isArray(value)) return "array";
|
|
||||||
if (value === null) return "null";
|
|
||||||
return typeof value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const dataType = getDataType(data);
|
|
||||||
|
|
||||||
const typeStyleMap: Record<string, string> = {
|
|
||||||
number: "text-blue-600",
|
number: "text-blue-600",
|
||||||
boolean: "text-amber-600",
|
boolean: "text-amber-600",
|
||||||
null: "text-purple-600",
|
null: "text-purple-600",
|
||||||
undefined: "text-gray-600",
|
undefined: "text-gray-600",
|
||||||
string: "text-green-600 break-all whitespace-pre-wrap",
|
string: "text-green-600 group-hover:text-green-500",
|
||||||
|
error: "text-red-600 group-hover:text-red-500",
|
||||||
default: "text-gray-700",
|
default: "text-gray-700",
|
||||||
};
|
});
|
||||||
|
const dataType = getDataType(data);
|
||||||
|
|
||||||
const renderCollapsible = (isArray: boolean) => {
|
const renderCollapsible = (isArray: boolean) => {
|
||||||
const items = isArray
|
const items = isArray
|
||||||
@@ -236,7 +225,14 @@ const JsonNode = memo(
|
|||||||
{name}:
|
{name}:
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<pre className={typeStyleMap.string}>"{value}"</pre>
|
<pre
|
||||||
|
className={clsx(
|
||||||
|
typeStyleMap.string,
|
||||||
|
"break-all whitespace-pre-wrap",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
"{value}"
|
||||||
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -250,8 +246,8 @@ const JsonNode = memo(
|
|||||||
)}
|
)}
|
||||||
<pre
|
<pre
|
||||||
className={clsx(
|
className={clsx(
|
||||||
typeStyleMap.string,
|
isError ? typeStyleMap.error : typeStyleMap.string,
|
||||||
"cursor-pointer group-hover:text-green-500",
|
"cursor-pointer break-all whitespace-pre-wrap",
|
||||||
)}
|
)}
|
||||||
onClick={() => setIsExpanded(!isExpanded)}
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
title={isExpanded ? "Click to collapse" : "Click to expand"}
|
title={isExpanded ? "Click to collapse" : "Click to expand"}
|
||||||
|
|||||||
@@ -3,14 +3,16 @@ import { Button } from "@/components/ui/button";
|
|||||||
|
|
||||||
const PingTab = ({ onPingClick }: { onPingClick: () => void }) => {
|
const PingTab = ({ onPingClick }: { onPingClick: () => void }) => {
|
||||||
return (
|
return (
|
||||||
<TabsContent value="ping" className="grid grid-cols-2 gap-4">
|
<TabsContent value="ping">
|
||||||
<div className="col-span-2 flex justify-center items-center">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<Button
|
<div className="col-span-2 flex justify-center items-center">
|
||||||
onClick={onPingClick}
|
<Button
|
||||||
className="font-bold py-6 px-12 rounded-full"
|
onClick={onPingClick}
|
||||||
>
|
className="font-bold py-6 px-12 rounded-full"
|
||||||
Ping Server
|
>
|
||||||
</Button>
|
Ping Server
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -84,84 +84,88 @@ const PromptsTab = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabsContent value="prompts" className="grid grid-cols-2 gap-4">
|
<TabsContent value="prompts">
|
||||||
<ListPane
|
<div className="grid grid-cols-2 gap-4">
|
||||||
items={prompts}
|
<ListPane
|
||||||
listItems={listPrompts}
|
items={prompts}
|
||||||
clearItems={clearPrompts}
|
listItems={listPrompts}
|
||||||
setSelectedItem={(prompt) => {
|
clearItems={clearPrompts}
|
||||||
setSelectedPrompt(prompt);
|
setSelectedItem={(prompt) => {
|
||||||
setPromptArgs({});
|
setSelectedPrompt(prompt);
|
||||||
}}
|
setPromptArgs({});
|
||||||
renderItem={(prompt) => (
|
}}
|
||||||
<>
|
renderItem={(prompt) => (
|
||||||
<span className="flex-1">{prompt.name}</span>
|
<>
|
||||||
<span className="text-sm text-gray-500">{prompt.description}</span>
|
<span className="flex-1">{prompt.name}</span>
|
||||||
</>
|
<span className="text-sm text-gray-500">
|
||||||
)}
|
{prompt.description}
|
||||||
title="Prompts"
|
</span>
|
||||||
buttonText={nextCursor ? "List More Prompts" : "List Prompts"}
|
</>
|
||||||
isButtonDisabled={!nextCursor && prompts.length > 0}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="bg-card rounded-lg shadow">
|
|
||||||
<div className="p-4 border-b border-gray-200">
|
|
||||||
<h3 className="font-semibold">
|
|
||||||
{selectedPrompt ? selectedPrompt.name : "Select a prompt"}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div className="p-4">
|
|
||||||
{error ? (
|
|
||||||
<Alert variant="destructive">
|
|
||||||
<AlertCircle className="h-4 w-4" />
|
|
||||||
<AlertTitle>Error</AlertTitle>
|
|
||||||
<AlertDescription>{error}</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
) : selectedPrompt ? (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{selectedPrompt.description && (
|
|
||||||
<p className="text-sm text-gray-600">
|
|
||||||
{selectedPrompt.description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{selectedPrompt.arguments?.map((arg) => (
|
|
||||||
<div key={arg.name}>
|
|
||||||
<Label htmlFor={arg.name}>{arg.name}</Label>
|
|
||||||
<Combobox
|
|
||||||
id={arg.name}
|
|
||||||
placeholder={`Enter ${arg.name}`}
|
|
||||||
value={promptArgs[arg.name] || ""}
|
|
||||||
onChange={(value) => handleInputChange(arg.name, value)}
|
|
||||||
onInputChange={(value) =>
|
|
||||||
handleInputChange(arg.name, value)
|
|
||||||
}
|
|
||||||
options={completions[arg.name] || []}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{arg.description && (
|
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
|
||||||
{arg.description}
|
|
||||||
{arg.required && (
|
|
||||||
<span className="text-xs mt-1 ml-1">(Required)</span>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<Button onClick={handleGetPrompt} className="w-full">
|
|
||||||
Get Prompt
|
|
||||||
</Button>
|
|
||||||
{promptContent && (
|
|
||||||
<JsonView data={promptContent} withCopyButton={false} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Alert>
|
|
||||||
<AlertDescription>
|
|
||||||
Select a prompt from the list to view and use it
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
)}
|
||||||
|
title="Prompts"
|
||||||
|
buttonText={nextCursor ? "List More Prompts" : "List Prompts"}
|
||||||
|
isButtonDisabled={!nextCursor && prompts.length > 0}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="bg-card rounded-lg shadow">
|
||||||
|
<div className="p-4 border-b border-gray-200">
|
||||||
|
<h3 className="font-semibold">
|
||||||
|
{selectedPrompt ? selectedPrompt.name : "Select a prompt"}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="p-4">
|
||||||
|
{error ? (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
<AlertTitle>Error</AlertTitle>
|
||||||
|
<AlertDescription>{error}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
) : selectedPrompt ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{selectedPrompt.description && (
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
{selectedPrompt.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{selectedPrompt.arguments?.map((arg) => (
|
||||||
|
<div key={arg.name}>
|
||||||
|
<Label htmlFor={arg.name}>{arg.name}</Label>
|
||||||
|
<Combobox
|
||||||
|
id={arg.name}
|
||||||
|
placeholder={`Enter ${arg.name}`}
|
||||||
|
value={promptArgs[arg.name] || ""}
|
||||||
|
onChange={(value) => handleInputChange(arg.name, value)}
|
||||||
|
onInputChange={(value) =>
|
||||||
|
handleInputChange(arg.name, value)
|
||||||
|
}
|
||||||
|
options={completions[arg.name] || []}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{arg.description && (
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
{arg.description}
|
||||||
|
{arg.required && (
|
||||||
|
<span className="text-xs mt-1 ml-1">(Required)</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button onClick={handleGetPrompt} className="w-full">
|
||||||
|
Get Prompt
|
||||||
|
</Button>
|
||||||
|
{promptContent && (
|
||||||
|
<JsonView data={promptContent} withCopyButton={false} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Alert>
|
||||||
|
<AlertDescription>
|
||||||
|
Select a prompt from the list to view and use it
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
@@ -111,155 +111,158 @@ const ResourcesTab = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabsContent value="resources" className="grid grid-cols-3 gap-4">
|
<TabsContent value="resources">
|
||||||
<ListPane
|
<div className="grid grid-cols-3 gap-4">
|
||||||
items={resources}
|
<ListPane
|
||||||
listItems={listResources}
|
items={resources}
|
||||||
clearItems={clearResources}
|
listItems={listResources}
|
||||||
setSelectedItem={(resource) => {
|
clearItems={clearResources}
|
||||||
setSelectedResource(resource);
|
setSelectedItem={(resource) => {
|
||||||
readResource(resource.uri);
|
setSelectedResource(resource);
|
||||||
setSelectedTemplate(null);
|
readResource(resource.uri);
|
||||||
}}
|
setSelectedTemplate(null);
|
||||||
renderItem={(resource) => (
|
}}
|
||||||
<div className="flex items-center w-full">
|
renderItem={(resource) => (
|
||||||
<FileText className="w-4 h-4 mr-2 flex-shrink-0 text-gray-500" />
|
<div className="flex items-center w-full">
|
||||||
<span className="flex-1 truncate" title={resource.uri.toString()}>
|
<FileText className="w-4 h-4 mr-2 flex-shrink-0 text-gray-500" />
|
||||||
{resource.name}
|
<span className="flex-1 truncate" title={resource.uri.toString()}>
|
||||||
</span>
|
{resource.name}
|
||||||
<ChevronRight className="w-4 h-4 flex-shrink-0 text-gray-400" />
|
</span>
|
||||||
</div>
|
<ChevronRight className="w-4 h-4 flex-shrink-0 text-gray-400" />
|
||||||
)}
|
|
||||||
title="Resources"
|
|
||||||
buttonText={nextCursor ? "List More Resources" : "List Resources"}
|
|
||||||
isButtonDisabled={!nextCursor && resources.length > 0}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ListPane
|
|
||||||
items={resourceTemplates}
|
|
||||||
listItems={listResourceTemplates}
|
|
||||||
clearItems={clearResourceTemplates}
|
|
||||||
setSelectedItem={(template) => {
|
|
||||||
setSelectedTemplate(template);
|
|
||||||
setSelectedResource(null);
|
|
||||||
setTemplateValues({});
|
|
||||||
}}
|
|
||||||
renderItem={(template) => (
|
|
||||||
<div className="flex items-center w-full">
|
|
||||||
<FileText className="w-4 h-4 mr-2 flex-shrink-0 text-gray-500" />
|
|
||||||
<span className="flex-1 truncate" title={template.uriTemplate}>
|
|
||||||
{template.name}
|
|
||||||
</span>
|
|
||||||
<ChevronRight className="w-4 h-4 flex-shrink-0 text-gray-400" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
title="Resource Templates"
|
|
||||||
buttonText={
|
|
||||||
nextTemplateCursor ? "List More Templates" : "List Templates"
|
|
||||||
}
|
|
||||||
isButtonDisabled={!nextTemplateCursor && resourceTemplates.length > 0}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="bg-card rounded-lg shadow">
|
|
||||||
<div className="p-4 border-b border-gray-200 flex justify-between items-center">
|
|
||||||
<h3
|
|
||||||
className="font-semibold truncate"
|
|
||||||
title={selectedResource?.name || selectedTemplate?.name}
|
|
||||||
>
|
|
||||||
{selectedResource
|
|
||||||
? selectedResource.name
|
|
||||||
: selectedTemplate
|
|
||||||
? selectedTemplate.name
|
|
||||||
: "Select a resource or template"}
|
|
||||||
</h3>
|
|
||||||
{selectedResource && (
|
|
||||||
<div className="flex row-auto gap-1 justify-end w-2/5">
|
|
||||||
{resourceSubscriptionsSupported &&
|
|
||||||
!resourceSubscriptions.has(selectedResource.uri) && (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => subscribeToResource(selectedResource.uri)}
|
|
||||||
>
|
|
||||||
Subscribe
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{resourceSubscriptionsSupported &&
|
|
||||||
resourceSubscriptions.has(selectedResource.uri) && (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() =>
|
|
||||||
unsubscribeFromResource(selectedResource.uri)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Unsubscribe
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => readResource(selectedResource.uri)}
|
|
||||||
>
|
|
||||||
<RefreshCw className="w-4 h-4 mr-2" />
|
|
||||||
Refresh
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
title="Resources"
|
||||||
<div className="p-4">
|
buttonText={nextCursor ? "List More Resources" : "List Resources"}
|
||||||
{error ? (
|
isButtonDisabled={!nextCursor && resources.length > 0}
|
||||||
<Alert variant="destructive">
|
/>
|
||||||
<AlertCircle className="h-4 w-4" />
|
|
||||||
<AlertTitle>Error</AlertTitle>
|
<ListPane
|
||||||
<AlertDescription>{error}</AlertDescription>
|
items={resourceTemplates}
|
||||||
</Alert>
|
listItems={listResourceTemplates}
|
||||||
) : selectedResource ? (
|
clearItems={clearResourceTemplates}
|
||||||
<JsonView
|
setSelectedItem={(template) => {
|
||||||
data={resourceContent}
|
setSelectedTemplate(template);
|
||||||
className="bg-gray-50 dark:bg-gray-800 p-4 rounded text-sm overflow-auto max-h-96 text-gray-900 dark:text-gray-100"
|
setSelectedResource(null);
|
||||||
/>
|
setTemplateValues({});
|
||||||
) : selectedTemplate ? (
|
}}
|
||||||
<div className="space-y-4">
|
renderItem={(template) => (
|
||||||
<p className="text-sm text-gray-600">
|
<div className="flex items-center w-full">
|
||||||
{selectedTemplate.description}
|
<FileText className="w-4 h-4 mr-2 flex-shrink-0 text-gray-500" />
|
||||||
</p>
|
<span className="flex-1 truncate" title={template.uriTemplate}>
|
||||||
{selectedTemplate.uriTemplate
|
{template.name}
|
||||||
.match(/{([^}]+)}/g)
|
</span>
|
||||||
?.map((param) => {
|
<ChevronRight className="w-4 h-4 flex-shrink-0 text-gray-400" />
|
||||||
const key = param.slice(1, -1);
|
|
||||||
return (
|
|
||||||
<div key={key}>
|
|
||||||
<Label htmlFor={key}>{key}</Label>
|
|
||||||
<Combobox
|
|
||||||
id={key}
|
|
||||||
placeholder={`Enter ${key}`}
|
|
||||||
value={templateValues[key] || ""}
|
|
||||||
onChange={(value) =>
|
|
||||||
handleTemplateValueChange(key, value)
|
|
||||||
}
|
|
||||||
onInputChange={(value) =>
|
|
||||||
handleTemplateValueChange(key, value)
|
|
||||||
}
|
|
||||||
options={completions[key] || []}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<Button
|
|
||||||
onClick={handleReadTemplateResource}
|
|
||||||
disabled={Object.keys(templateValues).length === 0}
|
|
||||||
>
|
|
||||||
Read Resource
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<Alert>
|
|
||||||
<AlertDescription>
|
|
||||||
Select a resource or template from the list to view its contents
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
)}
|
||||||
|
title="Resource Templates"
|
||||||
|
buttonText={
|
||||||
|
nextTemplateCursor ? "List More Templates" : "List Templates"
|
||||||
|
}
|
||||||
|
isButtonDisabled={!nextTemplateCursor && resourceTemplates.length > 0}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="bg-card rounded-lg shadow">
|
||||||
|
<div className="p-4 border-b border-gray-200 flex justify-between items-center">
|
||||||
|
<h3
|
||||||
|
className="font-semibold truncate"
|
||||||
|
title={selectedResource?.name || selectedTemplate?.name}
|
||||||
|
>
|
||||||
|
{selectedResource
|
||||||
|
? selectedResource.name
|
||||||
|
: selectedTemplate
|
||||||
|
? selectedTemplate.name
|
||||||
|
: "Select a resource or template"}
|
||||||
|
</h3>
|
||||||
|
{selectedResource && (
|
||||||
|
<div className="flex row-auto gap-1 justify-end w-2/5">
|
||||||
|
{resourceSubscriptionsSupported &&
|
||||||
|
!resourceSubscriptions.has(selectedResource.uri) && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => subscribeToResource(selectedResource.uri)}
|
||||||
|
>
|
||||||
|
Subscribe
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{resourceSubscriptionsSupported &&
|
||||||
|
resourceSubscriptions.has(selectedResource.uri) && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
unsubscribeFromResource(selectedResource.uri)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Unsubscribe
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => readResource(selectedResource.uri)}
|
||||||
|
>
|
||||||
|
<RefreshCw className="w-4 h-4 mr-2" />
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="p-4">
|
||||||
|
{error ? (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
<AlertTitle>Error</AlertTitle>
|
||||||
|
<AlertDescription>{error}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
) : selectedResource ? (
|
||||||
|
<JsonView
|
||||||
|
data={resourceContent}
|
||||||
|
className="bg-gray-50 dark:bg-gray-800 p-4 rounded text-sm overflow-auto max-h-96 text-gray-900 dark:text-gray-100"
|
||||||
|
/>
|
||||||
|
) : selectedTemplate ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
{selectedTemplate.description}
|
||||||
|
</p>
|
||||||
|
{selectedTemplate.uriTemplate
|
||||||
|
.match(/{([^}]+)}/g)
|
||||||
|
?.map((param) => {
|
||||||
|
const key = param.slice(1, -1);
|
||||||
|
return (
|
||||||
|
<div key={key}>
|
||||||
|
<Label htmlFor={key}>{key}</Label>
|
||||||
|
<Combobox
|
||||||
|
id={key}
|
||||||
|
placeholder={`Enter ${key}`}
|
||||||
|
value={templateValues[key] || ""}
|
||||||
|
onChange={(value) =>
|
||||||
|
handleTemplateValueChange(key, value)
|
||||||
|
}
|
||||||
|
onInputChange={(value) =>
|
||||||
|
handleTemplateValueChange(key, value)
|
||||||
|
}
|
||||||
|
options={completions[key] || []}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<Button
|
||||||
|
onClick={handleReadTemplateResource}
|
||||||
|
disabled={Object.keys(templateValues).length === 0}
|
||||||
|
>
|
||||||
|
Read Resource
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Alert>
|
||||||
|
<AlertDescription>
|
||||||
|
Select a resource or template from the list to view its
|
||||||
|
contents
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
@@ -35,40 +35,42 @@ const RootsTab = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabsContent value="roots" className="space-y-4">
|
<TabsContent value="roots">
|
||||||
<Alert>
|
<div className="space-y-4">
|
||||||
<AlertDescription>
|
<Alert>
|
||||||
Configure the root directories that the server can access
|
<AlertDescription>
|
||||||
</AlertDescription>
|
Configure the root directories that the server can access
|
||||||
</Alert>
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
{roots.map((root, index) => (
|
{roots.map((root, index) => (
|
||||||
<div key={index} className="flex gap-2 items-center">
|
<div key={index} className="flex gap-2 items-center">
|
||||||
<Input
|
<Input
|
||||||
placeholder="file:// URI"
|
placeholder="file:// URI"
|
||||||
value={root.uri}
|
value={root.uri}
|
||||||
onChange={(e) => updateRoot(index, "uri", e.target.value)}
|
onChange={(e) => updateRoot(index, "uri", e.target.value)}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => removeRoot(index)}
|
onClick={() => removeRoot(index)}
|
||||||
>
|
>
|
||||||
<Minus className="h-4 w-4" />
|
<Minus className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button variant="outline" onClick={addRoot}>
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
Add Root
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSave}>
|
||||||
|
<Save className="h-4 w-4 mr-2" />
|
||||||
|
Save Changes
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button variant="outline" onClick={addRoot}>
|
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
|
||||||
Add Root
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleSave}>
|
|
||||||
<Save className="h-4 w-4 mr-2" />
|
|
||||||
Save Changes
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -33,33 +33,37 @@ const SamplingTab = ({ pendingRequests, onApprove, onReject }: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabsContent value="sampling" className="h-96">
|
<TabsContent value="sampling">
|
||||||
<Alert>
|
<div className="h-96">
|
||||||
<AlertDescription>
|
<Alert>
|
||||||
When the server requests LLM sampling, requests will appear here for
|
<AlertDescription>
|
||||||
approval.
|
When the server requests LLM sampling, requests will appear here for
|
||||||
</AlertDescription>
|
approval.
|
||||||
</Alert>
|
</AlertDescription>
|
||||||
<div className="mt-4 space-y-4">
|
</Alert>
|
||||||
<h3 className="text-lg font-semibold">Recent Requests</h3>
|
<div className="mt-4 space-y-4">
|
||||||
{pendingRequests.map((request) => (
|
<h3 className="text-lg font-semibold">Recent Requests</h3>
|
||||||
<div key={request.id} className="p-4 border rounded-lg space-y-4">
|
{pendingRequests.map((request) => (
|
||||||
<JsonView
|
<div key={request.id} className="p-4 border rounded-lg space-y-4">
|
||||||
className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 rounded"
|
<JsonView
|
||||||
data={JSON.stringify(request.request)}
|
className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 rounded"
|
||||||
/>
|
data={JSON.stringify(request.request)}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
<Button onClick={() => handleApprove(request.id)}>Approve</Button>
|
<Button onClick={() => handleApprove(request.id)}>
|
||||||
<Button variant="outline" onClick={() => onReject(request.id)}>
|
Approve
|
||||||
Reject
|
</Button>
|
||||||
</Button>
|
<Button variant="outline" onClick={() => onReject(request.id)}>
|
||||||
|
Reject
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))}
|
||||||
))}
|
{pendingRequests.length === 0 && (
|
||||||
{pendingRequests.length === 0 && (
|
<p className="text-gray-500">No pending requests</p>
|
||||||
<p className="text-gray-500">No pending requests</p>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -107,14 +107,19 @@ const Sidebar = ({
|
|||||||
<div className="p-4 flex-1 overflow-auto">
|
<div className="p-4 flex-1 overflow-auto">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium">Transport Type</label>
|
<label
|
||||||
|
className="text-sm font-medium"
|
||||||
|
htmlFor="transport-type-select"
|
||||||
|
>
|
||||||
|
Transport Type
|
||||||
|
</label>
|
||||||
<Select
|
<Select
|
||||||
value={transportType}
|
value={transportType}
|
||||||
onValueChange={(value: "stdio" | "sse") =>
|
onValueChange={(value: "stdio" | "sse") =>
|
||||||
setTransportType(value)
|
setTransportType(value)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger id="transport-type-select">
|
||||||
<SelectValue placeholder="Select transport type" />
|
<SelectValue placeholder="Select transport type" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -127,8 +132,11 @@ const Sidebar = ({
|
|||||||
{transportType === "stdio" ? (
|
{transportType === "stdio" ? (
|
||||||
<>
|
<>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium">Command</label>
|
<label className="text-sm font-medium" htmlFor="command-input">
|
||||||
|
Command
|
||||||
|
</label>
|
||||||
<Input
|
<Input
|
||||||
|
id="command-input"
|
||||||
placeholder="Command"
|
placeholder="Command"
|
||||||
value={command}
|
value={command}
|
||||||
onChange={(e) => setCommand(e.target.value)}
|
onChange={(e) => setCommand(e.target.value)}
|
||||||
@@ -136,8 +144,14 @@ const Sidebar = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium">Arguments</label>
|
<label
|
||||||
|
className="text-sm font-medium"
|
||||||
|
htmlFor="arguments-input"
|
||||||
|
>
|
||||||
|
Arguments
|
||||||
|
</label>
|
||||||
<Input
|
<Input
|
||||||
|
id="arguments-input"
|
||||||
placeholder="Arguments (space-separated)"
|
placeholder="Arguments (space-separated)"
|
||||||
value={args}
|
value={args}
|
||||||
onChange={(e) => setArgs(e.target.value)}
|
onChange={(e) => setArgs(e.target.value)}
|
||||||
@@ -148,8 +162,11 @@ const Sidebar = ({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium">URL</label>
|
<label className="text-sm font-medium" htmlFor="sse-url-input">
|
||||||
|
URL
|
||||||
|
</label>
|
||||||
<Input
|
<Input
|
||||||
|
id="sse-url-input"
|
||||||
placeholder="URL"
|
placeholder="URL"
|
||||||
value={sseUrl}
|
value={sseUrl}
|
||||||
onChange={(e) => setSseUrl(e.target.value)}
|
onChange={(e) => setSseUrl(e.target.value)}
|
||||||
@@ -162,6 +179,7 @@ const Sidebar = ({
|
|||||||
onClick={() => setShowBearerToken(!showBearerToken)}
|
onClick={() => setShowBearerToken(!showBearerToken)}
|
||||||
className="flex items-center w-full"
|
className="flex items-center w-full"
|
||||||
data-testid="auth-button"
|
data-testid="auth-button"
|
||||||
|
aria-expanded={showBearerToken}
|
||||||
>
|
>
|
||||||
{showBearerToken ? (
|
{showBearerToken ? (
|
||||||
<ChevronDown className="w-4 h-4 mr-2" />
|
<ChevronDown className="w-4 h-4 mr-2" />
|
||||||
@@ -182,8 +200,14 @@ const Sidebar = ({
|
|||||||
className="font-mono"
|
className="font-mono"
|
||||||
value={headerName}
|
value={headerName}
|
||||||
/>
|
/>
|
||||||
<label className="text-sm font-medium">Bearer Token</label>
|
<label
|
||||||
|
className="text-sm font-medium"
|
||||||
|
htmlFor="bearer-token-input"
|
||||||
|
>
|
||||||
|
Bearer Token
|
||||||
|
</label>
|
||||||
<Input
|
<Input
|
||||||
|
id="bearer-token-input"
|
||||||
placeholder="Bearer Token"
|
placeholder="Bearer Token"
|
||||||
value={bearerToken}
|
value={bearerToken}
|
||||||
onChange={(e) => setBearerToken(e.target.value)}
|
onChange={(e) => setBearerToken(e.target.value)}
|
||||||
@@ -203,6 +227,7 @@ const Sidebar = ({
|
|||||||
onClick={() => setShowEnvVars(!showEnvVars)}
|
onClick={() => setShowEnvVars(!showEnvVars)}
|
||||||
className="flex items-center w-full"
|
className="flex items-center w-full"
|
||||||
data-testid="env-vars-button"
|
data-testid="env-vars-button"
|
||||||
|
aria-expanded={showEnvVars}
|
||||||
>
|
>
|
||||||
{showEnvVars ? (
|
{showEnvVars ? (
|
||||||
<ChevronDown className="w-4 h-4 mr-2" />
|
<ChevronDown className="w-4 h-4 mr-2" />
|
||||||
@@ -217,6 +242,7 @@ const Sidebar = ({
|
|||||||
<div key={idx} className="space-y-2 pb-4">
|
<div key={idx} className="space-y-2 pb-4">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
|
aria-label={`Environment variable key ${idx + 1}`}
|
||||||
placeholder="Key"
|
placeholder="Key"
|
||||||
value={key}
|
value={key}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -259,6 +285,7 @@ const Sidebar = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
|
aria-label={`Environment variable value ${idx + 1}`}
|
||||||
type={shownEnvVars.has(key) ? "text" : "password"}
|
type={shownEnvVars.has(key) ? "text" : "password"}
|
||||||
placeholder="Value"
|
placeholder="Value"
|
||||||
value={value}
|
value={value}
|
||||||
@@ -325,6 +352,7 @@ const Sidebar = ({
|
|||||||
onClick={() => setShowConfig(!showConfig)}
|
onClick={() => setShowConfig(!showConfig)}
|
||||||
className="flex items-center w-full"
|
className="flex items-center w-full"
|
||||||
data-testid="config-button"
|
data-testid="config-button"
|
||||||
|
aria-expanded={showConfig}
|
||||||
>
|
>
|
||||||
{showConfig ? (
|
{showConfig ? (
|
||||||
<ChevronDown className="w-4 h-4 mr-2" />
|
<ChevronDown className="w-4 h-4 mr-2" />
|
||||||
@@ -341,8 +369,11 @@ const Sidebar = ({
|
|||||||
return (
|
return (
|
||||||
<div key={key} className="space-y-2">
|
<div key={key} className="space-y-2">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<label className="text-sm font-medium text-green-600">
|
<label
|
||||||
{configKey}
|
className="text-sm font-medium text-green-600 break-all"
|
||||||
|
htmlFor={`${configKey}-input`}
|
||||||
|
>
|
||||||
|
{configItem.label}
|
||||||
</label>
|
</label>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
@@ -355,6 +386,7 @@ const Sidebar = ({
|
|||||||
</div>
|
</div>
|
||||||
{typeof configItem.value === "number" ? (
|
{typeof configItem.value === "number" ? (
|
||||||
<Input
|
<Input
|
||||||
|
id={`${configKey}-input`}
|
||||||
type="number"
|
type="number"
|
||||||
data-testid={`${configKey}-input`}
|
data-testid={`${configKey}-input`}
|
||||||
value={configItem.value}
|
value={configItem.value}
|
||||||
@@ -381,7 +413,7 @@ const Sidebar = ({
|
|||||||
setConfig(newConfig);
|
setConfig(newConfig);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger id={`${configKey}-input`}>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -391,6 +423,7 @@ const Sidebar = ({
|
|||||||
</Select>
|
</Select>
|
||||||
) : (
|
) : (
|
||||||
<Input
|
<Input
|
||||||
|
id={`${configKey}-input`}
|
||||||
data-testid={`${configKey}-input`}
|
data-testid={`${configKey}-input`}
|
||||||
value={configItem.value}
|
value={configItem.value}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -464,14 +497,19 @@ const Sidebar = ({
|
|||||||
|
|
||||||
{loggingSupported && connectionStatus === "connected" && (
|
{loggingSupported && connectionStatus === "connected" && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium">Logging Level</label>
|
<label
|
||||||
|
className="text-sm font-medium"
|
||||||
|
htmlFor="logging-level-select"
|
||||||
|
>
|
||||||
|
Logging Level
|
||||||
|
</label>
|
||||||
<Select
|
<Select
|
||||||
value={logLevel}
|
value={logLevel}
|
||||||
onValueChange={(value: LoggingLevel) =>
|
onValueChange={(value: LoggingLevel) =>
|
||||||
sendLogLevelRequest(value)
|
sendLogLevelRequest(value)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger id="logging-level-select">
|
||||||
<SelectValue placeholder="Select logging level" />
|
<SelectValue placeholder="Select logging level" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { TabsContent } from "@/components/ui/tabs";
|
import { TabsContent } from "@/components/ui/tabs";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import DynamicJsonForm, { JsonSchemaType, JsonValue } from "./DynamicJsonForm";
|
import DynamicJsonForm from "./DynamicJsonForm";
|
||||||
|
import type { JsonValue, JsonSchemaType } from "@/utils/jsonUtils";
|
||||||
import { generateDefaultValue } from "@/utils/schemaUtils";
|
import { generateDefaultValue } from "@/utils/schemaUtils";
|
||||||
import {
|
import {
|
||||||
CallToolResultSchema,
|
CallToolResultSchema,
|
||||||
@@ -13,7 +14,7 @@ import {
|
|||||||
ListToolsResult,
|
ListToolsResult,
|
||||||
Tool,
|
Tool,
|
||||||
} from "@modelcontextprotocol/sdk/types.js";
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
import { Send } from "lucide-react";
|
import { Loader2, Send } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import ListPane from "./ListPane";
|
import ListPane from "./ListPane";
|
||||||
import JsonView from "./JsonView";
|
import JsonView from "./JsonView";
|
||||||
@@ -31,7 +32,7 @@ const ToolsTab = ({
|
|||||||
tools: Tool[];
|
tools: Tool[];
|
||||||
listTools: () => void;
|
listTools: () => void;
|
||||||
clearTools: () => void;
|
clearTools: () => void;
|
||||||
callTool: (name: string, params: Record<string, unknown>) => void;
|
callTool: (name: string, params: Record<string, unknown>) => Promise<void>;
|
||||||
selectedTool: Tool | null;
|
selectedTool: Tool | null;
|
||||||
setSelectedTool: (tool: Tool | null) => void;
|
setSelectedTool: (tool: Tool | null) => void;
|
||||||
toolResult: CompatibilityCallToolResult | null;
|
toolResult: CompatibilityCallToolResult | null;
|
||||||
@@ -39,6 +40,8 @@ const ToolsTab = ({
|
|||||||
error: string | null;
|
error: string | null;
|
||||||
}) => {
|
}) => {
|
||||||
const [params, setParams] = useState<Record<string, unknown>>({});
|
const [params, setParams] = useState<Record<string, unknown>>({});
|
||||||
|
const [isToolRunning, setIsToolRunning] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setParams({});
|
setParams({});
|
||||||
}, [selectedTool]);
|
}, [selectedTool]);
|
||||||
@@ -66,11 +69,18 @@ const ToolsTab = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h4 className="font-semibold mb-2">
|
<h4 className="font-semibold mb-2">
|
||||||
Tool Result: {isError ? "Error" : "Success"}
|
Tool Result:{" "}
|
||||||
|
{isError ? (
|
||||||
|
<span className="text-red-600 font-semibold">Error</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-green-600 font-semibold">Success</span>
|
||||||
|
)}
|
||||||
</h4>
|
</h4>
|
||||||
{structuredResult.content.map((item, index) => (
|
{structuredResult.content.map((item, index) => (
|
||||||
<div key={index} className="mb-2">
|
<div key={index} className="mb-2">
|
||||||
{item.type === "text" && <JsonView data={item.text} />}
|
{item.type === "text" && (
|
||||||
|
<JsonView data={item.text} isError={isError} />
|
||||||
|
)}
|
||||||
{item.type === "image" && (
|
{item.type === "image" && (
|
||||||
<img
|
<img
|
||||||
src={`data:${item.mimeType};base64,${item.data}`}
|
src={`data:${item.mimeType};base64,${item.data}`}
|
||||||
@@ -106,147 +116,168 @@ const ToolsTab = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabsContent value="tools" className="grid grid-cols-2 gap-4">
|
<TabsContent value="tools">
|
||||||
<ListPane
|
<div className="grid grid-cols-2 gap-4">
|
||||||
items={tools}
|
<ListPane
|
||||||
listItems={listTools}
|
items={tools}
|
||||||
clearItems={() => {
|
listItems={listTools}
|
||||||
clearTools();
|
clearItems={() => {
|
||||||
setSelectedTool(null);
|
clearTools();
|
||||||
}}
|
setSelectedTool(null);
|
||||||
setSelectedItem={setSelectedTool}
|
}}
|
||||||
renderItem={(tool) => (
|
setSelectedItem={setSelectedTool}
|
||||||
<>
|
renderItem={(tool) => (
|
||||||
<span className="flex-1">{tool.name}</span>
|
<>
|
||||||
<span className="text-sm text-gray-500 text-right">
|
<span className="flex-1">{tool.name}</span>
|
||||||
{tool.description}
|
<span className="text-sm text-gray-500 text-right">
|
||||||
</span>
|
{tool.description}
|
||||||
</>
|
</span>
|
||||||
)}
|
</>
|
||||||
title="Tools"
|
)}
|
||||||
buttonText={nextCursor ? "List More Tools" : "List Tools"}
|
title="Tools"
|
||||||
isButtonDisabled={!nextCursor && tools.length > 0}
|
buttonText={nextCursor ? "List More Tools" : "List Tools"}
|
||||||
/>
|
isButtonDisabled={!nextCursor && tools.length > 0}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="bg-card rounded-lg shadow">
|
<div className="bg-card rounded-lg shadow">
|
||||||
<div className="p-4 border-b border-gray-200">
|
<div className="p-4 border-b border-gray-200">
|
||||||
<h3 className="font-semibold">
|
<h3 className="font-semibold">
|
||||||
{selectedTool ? selectedTool.name : "Select a tool"}
|
{selectedTool ? selectedTool.name : "Select a tool"}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
{selectedTool ? (
|
{selectedTool ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
{selectedTool.description}
|
{selectedTool.description}
|
||||||
</p>
|
</p>
|
||||||
{Object.entries(selectedTool.inputSchema.properties ?? []).map(
|
{Object.entries(selectedTool.inputSchema.properties ?? []).map(
|
||||||
([key, value]) => {
|
([key, value]) => {
|
||||||
const prop = value as JsonSchemaType;
|
const prop = value as JsonSchemaType;
|
||||||
return (
|
return (
|
||||||
<div key={key}>
|
<div key={key}>
|
||||||
<Label
|
<Label
|
||||||
htmlFor={key}
|
htmlFor={key}
|
||||||
className="block text-sm font-medium text-gray-700"
|
className="block text-sm font-medium text-gray-700"
|
||||||
>
|
>
|
||||||
{key}
|
{key}
|
||||||
</Label>
|
</Label>
|
||||||
{prop.type === "boolean" ? (
|
{prop.type === "boolean" ? (
|
||||||
<div className="flex items-center space-x-2 mt-2">
|
<div className="flex items-center space-x-2 mt-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
id={key}
|
||||||
|
name={key}
|
||||||
|
checked={!!params[key]}
|
||||||
|
onCheckedChange={(checked: boolean) =>
|
||||||
|
setParams({
|
||||||
|
...params,
|
||||||
|
[key]: checked,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor={key}
|
||||||
|
className="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
{prop.description || "Toggle this option"}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
) : prop.type === "string" ? (
|
||||||
|
<Textarea
|
||||||
id={key}
|
id={key}
|
||||||
name={key}
|
name={key}
|
||||||
checked={!!params[key]}
|
placeholder={prop.description}
|
||||||
onCheckedChange={(checked: boolean) =>
|
value={(params[key] as string) ?? ""}
|
||||||
|
onChange={(e) =>
|
||||||
setParams({
|
setParams({
|
||||||
...params,
|
...params,
|
||||||
[key]: checked,
|
[key]: e.target.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
className="mt-1"
|
||||||
/>
|
/>
|
||||||
<label
|
) : prop.type === "object" || prop.type === "array" ? (
|
||||||
htmlFor={key}
|
<div className="mt-1">
|
||||||
className="text-sm font-medium text-gray-700 dark:text-gray-300"
|
<DynamicJsonForm
|
||||||
>
|
schema={{
|
||||||
{prop.description || "Toggle this option"}
|
type: prop.type,
|
||||||
</label>
|
properties: prop.properties,
|
||||||
</div>
|
description: prop.description,
|
||||||
) : prop.type === "string" ? (
|
items: prop.items,
|
||||||
<Textarea
|
}}
|
||||||
id={key}
|
value={
|
||||||
name={key}
|
(params[key] as JsonValue) ??
|
||||||
placeholder={prop.description}
|
generateDefaultValue(prop)
|
||||||
value={(params[key] as string) ?? ""}
|
}
|
||||||
onChange={(e) =>
|
onChange={(newValue: JsonValue) => {
|
||||||
setParams({
|
setParams({
|
||||||
...params,
|
...params,
|
||||||
[key]: e.target.value,
|
[key]: newValue,
|
||||||
})
|
});
|
||||||
}
|
}}
|
||||||
className="mt-1"
|
/>
|
||||||
/>
|
</div>
|
||||||
) : prop.type === "object" || prop.type === "array" ? (
|
) : (
|
||||||
<div className="mt-1">
|
<Input
|
||||||
<DynamicJsonForm
|
type={
|
||||||
schema={{
|
prop.type === "number" || prop.type === "integer"
|
||||||
type: prop.type,
|
? "number"
|
||||||
properties: prop.properties,
|
: "text"
|
||||||
description: prop.description,
|
|
||||||
items: prop.items,
|
|
||||||
}}
|
|
||||||
value={
|
|
||||||
(params[key] as JsonValue) ??
|
|
||||||
generateDefaultValue(prop)
|
|
||||||
}
|
}
|
||||||
onChange={(newValue: JsonValue) => {
|
id={key}
|
||||||
|
name={key}
|
||||||
|
placeholder={prop.description}
|
||||||
|
value={(params[key] as string) ?? ""}
|
||||||
|
onChange={(e) =>
|
||||||
setParams({
|
setParams({
|
||||||
...params,
|
...params,
|
||||||
[key]: newValue,
|
[key]:
|
||||||
});
|
prop.type === "number" ||
|
||||||
}}
|
prop.type === "integer"
|
||||||
|
? Number(e.target.value)
|
||||||
|
: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="mt-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
) : (
|
</div>
|
||||||
<Input
|
);
|
||||||
type={
|
},
|
||||||
prop.type === "number" || prop.type === "integer"
|
)}
|
||||||
? "number"
|
<Button
|
||||||
: "text"
|
onClick={async () => {
|
||||||
}
|
try {
|
||||||
id={key}
|
setIsToolRunning(true);
|
||||||
name={key}
|
await callTool(selectedTool.name, params);
|
||||||
placeholder={prop.description}
|
} finally {
|
||||||
value={(params[key] as string) ?? ""}
|
setIsToolRunning(false);
|
||||||
onChange={(e) =>
|
}
|
||||||
setParams({
|
}}
|
||||||
...params,
|
disabled={isToolRunning}
|
||||||
[key]:
|
>
|
||||||
prop.type === "number" ||
|
{isToolRunning ? (
|
||||||
prop.type === "integer"
|
<>
|
||||||
? Number(e.target.value)
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
: e.target.value,
|
Running...
|
||||||
})
|
</>
|
||||||
}
|
) : (
|
||||||
className="mt-1"
|
<>
|
||||||
/>
|
<Send className="w-4 h-4 mr-2" />
|
||||||
)}
|
Run Tool
|
||||||
</div>
|
</>
|
||||||
);
|
)}
|
||||||
},
|
</Button>
|
||||||
)}
|
{toolResult && renderToolResult()}
|
||||||
<Button onClick={() => callTool(selectedTool.name, params)}>
|
</div>
|
||||||
<Send className="w-4 h-4 mr-2" />
|
) : (
|
||||||
Run Tool
|
<Alert>
|
||||||
</Button>
|
<AlertDescription>
|
||||||
{toolResult && renderToolResult()}
|
Select a tool from the list to view its details and run it
|
||||||
</div>
|
</AlertDescription>
|
||||||
) : (
|
</Alert>
|
||||||
<Alert>
|
)}
|
||||||
<AlertDescription>
|
</div>
|
||||||
Select a tool from the list to view its details and run it
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { render, screen, fireEvent } from "@testing-library/react";
|
import { render, screen, fireEvent } from "@testing-library/react";
|
||||||
import { describe, it, expect, jest } from "@jest/globals";
|
import { describe, it, expect, jest } from "@jest/globals";
|
||||||
import DynamicJsonForm from "../DynamicJsonForm";
|
import DynamicJsonForm from "../DynamicJsonForm";
|
||||||
import type { JsonSchemaType } from "../DynamicJsonForm";
|
import type { JsonSchemaType } from "@/utils/jsonUtils";
|
||||||
|
|
||||||
describe("DynamicJsonForm String Fields", () => {
|
describe("DynamicJsonForm String Fields", () => {
|
||||||
const renderForm = (props = {}) => {
|
const renderForm = (props = {}) => {
|
||||||
|
|||||||
@@ -495,6 +495,7 @@ describe("Sidebar Environment Variables", () => {
|
|||||||
expect(setConfig).toHaveBeenCalledWith(
|
expect(setConfig).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
MCP_SERVER_REQUEST_TIMEOUT: {
|
MCP_SERVER_REQUEST_TIMEOUT: {
|
||||||
|
label: "Request Timeout",
|
||||||
description: "Timeout for requests to the MCP server (ms)",
|
description: "Timeout for requests to the MCP server (ms)",
|
||||||
value: 5000,
|
value: 5000,
|
||||||
},
|
},
|
||||||
@@ -502,6 +503,56 @@ describe("Sidebar Environment Variables", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should update MCP server proxy address", () => {
|
||||||
|
const setConfig = jest.fn();
|
||||||
|
renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig });
|
||||||
|
|
||||||
|
openConfigSection();
|
||||||
|
|
||||||
|
const proxyAddressInput = screen.getByTestId(
|
||||||
|
"MCP_PROXY_FULL_ADDRESS-input",
|
||||||
|
);
|
||||||
|
fireEvent.change(proxyAddressInput, {
|
||||||
|
target: { value: "http://localhost:8080" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(setConfig).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
MCP_PROXY_FULL_ADDRESS: {
|
||||||
|
label: "Inspector Proxy 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: "http://localhost:8080",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update max total timeout", () => {
|
||||||
|
const setConfig = jest.fn();
|
||||||
|
renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig });
|
||||||
|
|
||||||
|
openConfigSection();
|
||||||
|
|
||||||
|
const maxTotalTimeoutInput = screen.getByTestId(
|
||||||
|
"MCP_REQUEST_MAX_TOTAL_TIMEOUT-input",
|
||||||
|
);
|
||||||
|
fireEvent.change(maxTotalTimeoutInput, {
|
||||||
|
target: { value: "10000" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(setConfig).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
MCP_REQUEST_MAX_TOTAL_TIMEOUT: {
|
||||||
|
label: "Maximum Total Timeout",
|
||||||
|
description:
|
||||||
|
"Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications)",
|
||||||
|
value: 10000,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("should handle invalid timeout values entered by user", () => {
|
it("should handle invalid timeout values entered by user", () => {
|
||||||
const setConfig = jest.fn();
|
const setConfig = jest.fn();
|
||||||
renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig });
|
renderSidebar({ config: DEFAULT_INSPECTOR_CONFIG, setConfig });
|
||||||
@@ -516,6 +567,7 @@ describe("Sidebar Environment Variables", () => {
|
|||||||
expect(setConfig).toHaveBeenCalledWith(
|
expect(setConfig).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
MCP_SERVER_REQUEST_TIMEOUT: {
|
MCP_SERVER_REQUEST_TIMEOUT: {
|
||||||
|
label: "Request Timeout",
|
||||||
description: "Timeout for requests to the MCP server (ms)",
|
description: "Timeout for requests to the MCP server (ms)",
|
||||||
value: 0,
|
value: 0,
|
||||||
},
|
},
|
||||||
@@ -561,6 +613,7 @@ describe("Sidebar Environment Variables", () => {
|
|||||||
expect(setConfig).toHaveBeenLastCalledWith(
|
expect(setConfig).toHaveBeenLastCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
MCP_SERVER_REQUEST_TIMEOUT: {
|
MCP_SERVER_REQUEST_TIMEOUT: {
|
||||||
|
label: "Request Timeout",
|
||||||
description: "Timeout for requests to the MCP server (ms)",
|
description: "Timeout for requests to the MCP server (ms)",
|
||||||
value: 3000,
|
value: 3000,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { render, screen, fireEvent } from "@testing-library/react";
|
import { render, screen, fireEvent, act } from "@testing-library/react";
|
||||||
import { describe, it, expect, jest } from "@jest/globals";
|
import { describe, it, expect, jest } from "@jest/globals";
|
||||||
import "@testing-library/jest-dom";
|
import "@testing-library/jest-dom";
|
||||||
import ToolsTab from "../ToolsTab";
|
import ToolsTab from "../ToolsTab";
|
||||||
@@ -43,7 +43,7 @@ describe("ToolsTab", () => {
|
|||||||
tools: mockTools,
|
tools: mockTools,
|
||||||
listTools: jest.fn(),
|
listTools: jest.fn(),
|
||||||
clearTools: jest.fn(),
|
clearTools: jest.fn(),
|
||||||
callTool: jest.fn(),
|
callTool: jest.fn(async () => {}),
|
||||||
selectedTool: null,
|
selectedTool: null,
|
||||||
setSelectedTool: jest.fn(),
|
setSelectedTool: jest.fn(),
|
||||||
toolResult: null,
|
toolResult: null,
|
||||||
@@ -59,14 +59,16 @@ describe("ToolsTab", () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
it("should reset input values when switching tools", () => {
|
it("should reset input values when switching tools", async () => {
|
||||||
const { rerender } = renderToolsTab({
|
const { rerender } = renderToolsTab({
|
||||||
selectedTool: mockTools[0],
|
selectedTool: mockTools[0],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Enter a value in the first tool's input
|
// Enter a value in the first tool's input
|
||||||
const input = screen.getByRole("spinbutton") as HTMLInputElement;
|
const input = screen.getByRole("spinbutton") as HTMLInputElement;
|
||||||
fireEvent.change(input, { target: { value: "42" } });
|
await act(async () => {
|
||||||
|
fireEvent.change(input, { target: { value: "42" } });
|
||||||
|
});
|
||||||
expect(input.value).toBe("42");
|
expect(input.value).toBe("42");
|
||||||
|
|
||||||
// Switch to second tool
|
// Switch to second tool
|
||||||
@@ -80,7 +82,8 @@ describe("ToolsTab", () => {
|
|||||||
const newInput = screen.getByRole("spinbutton") as HTMLInputElement;
|
const newInput = screen.getByRole("spinbutton") as HTMLInputElement;
|
||||||
expect(newInput.value).toBe("");
|
expect(newInput.value).toBe("");
|
||||||
});
|
});
|
||||||
it("should handle integer type inputs", () => {
|
|
||||||
|
it("should handle integer type inputs", async () => {
|
||||||
renderToolsTab({
|
renderToolsTab({
|
||||||
selectedTool: mockTools[1], // Use the tool with integer type
|
selectedTool: mockTools[1], // Use the tool with integer type
|
||||||
});
|
});
|
||||||
@@ -93,10 +96,49 @@ describe("ToolsTab", () => {
|
|||||||
expect(input.value).toBe("42");
|
expect(input.value).toBe("42");
|
||||||
|
|
||||||
const submitButton = screen.getByRole("button", { name: /run tool/i });
|
const submitButton = screen.getByRole("button", { name: /run tool/i });
|
||||||
fireEvent.click(submitButton);
|
await act(async () => {
|
||||||
|
fireEvent.click(submitButton);
|
||||||
|
});
|
||||||
|
|
||||||
expect(defaultProps.callTool).toHaveBeenCalledWith(mockTools[1].name, {
|
expect(defaultProps.callTool).toHaveBeenCalledWith(mockTools[1].name, {
|
||||||
count: 42,
|
count: 42,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should disable button and change text while tool is running", async () => {
|
||||||
|
// Create a promise that we can resolve later
|
||||||
|
let resolvePromise: ((value: unknown) => void) | undefined;
|
||||||
|
const mockPromise = new Promise((resolve) => {
|
||||||
|
resolvePromise = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock callTool to return our promise
|
||||||
|
const mockCallTool = jest.fn().mockReturnValue(mockPromise);
|
||||||
|
|
||||||
|
renderToolsTab({
|
||||||
|
selectedTool: mockTools[0],
|
||||||
|
callTool: mockCallTool,
|
||||||
|
});
|
||||||
|
|
||||||
|
const submitButton = screen.getByRole("button", { name: /run tool/i });
|
||||||
|
expect(submitButton.getAttribute("disabled")).toBeNull();
|
||||||
|
|
||||||
|
// Click the button and verify immediate state changes
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.click(submitButton);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify button is disabled and text changed
|
||||||
|
expect(submitButton.getAttribute("disabled")).not.toBeNull();
|
||||||
|
expect(submitButton.textContent).toBe("Running...");
|
||||||
|
|
||||||
|
// Resolve the promise to simulate tool completion
|
||||||
|
await act(async () => {
|
||||||
|
if (resolvePromise) {
|
||||||
|
await resolvePromise({});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(submitButton.getAttribute("disabled")).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export type ConfigItem = {
|
export type ConfigItem = {
|
||||||
|
label: string;
|
||||||
description: string;
|
description: string;
|
||||||
value: string | number | boolean;
|
value: string | number | boolean;
|
||||||
};
|
};
|
||||||
@@ -15,5 +16,21 @@ 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to reset the timeout on progress notifications. Useful for long-running operations that send periodic progress updates.
|
||||||
|
* Refer: https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/utilities/progress/#progress-flow
|
||||||
|
*/
|
||||||
|
MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS: ConfigItem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum total time in milliseconds to wait for a response from the MCP server before timing out. Used in conjunction with MCP_SERVER_REQUEST_TIMEOUT_RESET_ON_PROGRESS.
|
||||||
|
* Refer: https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/utilities/progress/#progress-flow
|
||||||
|
*/
|
||||||
|
MCP_REQUEST_MAX_TOTAL_TIMEOUT: ConfigItem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The full address of the MCP Proxy Server, in case it is running on a non-default address. Example: http://10.1.1.22:5577
|
||||||
|
*/
|
||||||
MCP_PROXY_FULL_ADDRESS: ConfigItem;
|
MCP_PROXY_FULL_ADDRESS: ConfigItem;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,10 +22,23 @@ export const DEFAULT_MCP_PROXY_LISTEN_PORT = "6277";
|
|||||||
**/
|
**/
|
||||||
export const DEFAULT_INSPECTOR_CONFIG: InspectorConfig = {
|
export const DEFAULT_INSPECTOR_CONFIG: InspectorConfig = {
|
||||||
MCP_SERVER_REQUEST_TIMEOUT: {
|
MCP_SERVER_REQUEST_TIMEOUT: {
|
||||||
|
label: "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_REQUEST_TIMEOUT_RESET_ON_PROGRESS: {
|
||||||
|
label: "Reset Timeout on Progress",
|
||||||
|
description: "Reset timeout on progress notifications",
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
MCP_REQUEST_MAX_TOTAL_TIMEOUT: {
|
||||||
|
label: "Maximum Total Timeout",
|
||||||
|
description:
|
||||||
|
"Maximum total timeout for requests sent to the MCP server (ms) (Use with progress notifications)",
|
||||||
|
value: 60000,
|
||||||
|
},
|
||||||
MCP_PROXY_FULL_ADDRESS: {
|
MCP_PROXY_FULL_ADDRESS: {
|
||||||
|
label: "Inspector Proxy Address",
|
||||||
description:
|
description:
|
||||||
"Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577",
|
"Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577",
|
||||||
value: "",
|
value: "",
|
||||||
|
|||||||
164
client/src/lib/hooks/__tests__/useConnection.test.tsx
Normal file
164
client/src/lib/hooks/__tests__/useConnection.test.tsx
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import { renderHook, act } from "@testing-library/react";
|
||||||
|
import { useConnection } from "../useConnection";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { ClientRequest } from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
import { DEFAULT_INSPECTOR_CONFIG } from "../../constants";
|
||||||
|
|
||||||
|
// Mock fetch
|
||||||
|
global.fetch = jest.fn().mockResolvedValue({
|
||||||
|
json: () => Promise.resolve({ status: "ok" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock the SDK dependencies
|
||||||
|
const mockRequest = jest.fn().mockResolvedValue({ test: "response" });
|
||||||
|
const mockClient = {
|
||||||
|
request: mockRequest,
|
||||||
|
notification: jest.fn(),
|
||||||
|
connect: jest.fn().mockResolvedValue(undefined),
|
||||||
|
close: jest.fn(),
|
||||||
|
getServerCapabilities: jest.fn(),
|
||||||
|
setNotificationHandler: jest.fn(),
|
||||||
|
setRequestHandler: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.mock("@modelcontextprotocol/sdk/client/index.js", () => ({
|
||||||
|
Client: jest.fn().mockImplementation(() => mockClient),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("@modelcontextprotocol/sdk/client/sse.js", () => ({
|
||||||
|
SSEClientTransport: jest.fn(),
|
||||||
|
SseError: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({
|
||||||
|
auth: jest.fn().mockResolvedValue("AUTHORIZED"),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock the toast hook
|
||||||
|
jest.mock("@/hooks/use-toast", () => ({
|
||||||
|
useToast: () => ({
|
||||||
|
toast: jest.fn(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock the auth provider
|
||||||
|
jest.mock("../../auth", () => ({
|
||||||
|
authProvider: {
|
||||||
|
tokens: jest.fn().mockResolvedValue({ access_token: "mock-token" }),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("useConnection", () => {
|
||||||
|
const defaultProps = {
|
||||||
|
transportType: "sse" as const,
|
||||||
|
command: "",
|
||||||
|
args: "",
|
||||||
|
sseUrl: "http://localhost:8080",
|
||||||
|
env: {},
|
||||||
|
config: DEFAULT_INSPECTOR_CONFIG,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("Request Configuration", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("uses the default config values in makeRequest", async () => {
|
||||||
|
const { result } = renderHook(() => useConnection(defaultProps));
|
||||||
|
|
||||||
|
// Connect the client
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.connect();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for state update
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockRequest: ClientRequest = {
|
||||||
|
method: "ping",
|
||||||
|
params: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockSchema = z.object({
|
||||||
|
test: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.makeRequest(mockRequest, mockSchema);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockClient.request).toHaveBeenCalledWith(
|
||||||
|
mockRequest,
|
||||||
|
mockSchema,
|
||||||
|
expect.objectContaining({
|
||||||
|
timeout: DEFAULT_INSPECTOR_CONFIG.MCP_SERVER_REQUEST_TIMEOUT.value,
|
||||||
|
maxTotalTimeout:
|
||||||
|
DEFAULT_INSPECTOR_CONFIG.MCP_REQUEST_MAX_TOTAL_TIMEOUT.value,
|
||||||
|
resetTimeoutOnProgress:
|
||||||
|
DEFAULT_INSPECTOR_CONFIG.MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS
|
||||||
|
.value,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("overrides the default config values when passed in options in makeRequest", async () => {
|
||||||
|
const { result } = renderHook(() => useConnection(defaultProps));
|
||||||
|
|
||||||
|
// Connect the client
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.connect();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for state update
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockRequest: ClientRequest = {
|
||||||
|
method: "ping",
|
||||||
|
params: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockSchema = z.object({
|
||||||
|
test: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.makeRequest(mockRequest, mockSchema, {
|
||||||
|
timeout: 1000,
|
||||||
|
maxTotalTimeout: 2000,
|
||||||
|
resetTimeoutOnProgress: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockClient.request).toHaveBeenCalledWith(
|
||||||
|
mockRequest,
|
||||||
|
mockSchema,
|
||||||
|
expect.objectContaining({
|
||||||
|
timeout: 1000,
|
||||||
|
maxTotalTimeout: 2000,
|
||||||
|
resetTimeoutOnProgress: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws error when mcpClient is not connected", async () => {
|
||||||
|
const { result } = renderHook(() => useConnection(defaultProps));
|
||||||
|
|
||||||
|
const mockRequest: ClientRequest = {
|
||||||
|
method: "ping",
|
||||||
|
params: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockSchema = z.object({
|
||||||
|
test: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
result.current.makeRequest(mockRequest, mockSchema),
|
||||||
|
).rejects.toThrow("MCP client not connected");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
ClientRequest,
|
ClientRequest,
|
||||||
CreateMessageRequestSchema,
|
CreateMessageRequestSchema,
|
||||||
ListRootsRequestSchema,
|
ListRootsRequestSchema,
|
||||||
ProgressNotificationSchema,
|
|
||||||
ResourceUpdatedNotificationSchema,
|
ResourceUpdatedNotificationSchema,
|
||||||
LoggingMessageNotificationSchema,
|
LoggingMessageNotificationSchema,
|
||||||
Request,
|
Request,
|
||||||
@@ -23,7 +22,9 @@ import {
|
|||||||
ResourceListChangedNotificationSchema,
|
ResourceListChangedNotificationSchema,
|
||||||
ToolListChangedNotificationSchema,
|
ToolListChangedNotificationSchema,
|
||||||
PromptListChangedNotificationSchema,
|
PromptListChangedNotificationSchema,
|
||||||
|
Progress,
|
||||||
} from "@modelcontextprotocol/sdk/types.js";
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
import { RequestOptions } from "@modelcontextprotocol/sdk/shared/protocol.js";
|
||||||
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";
|
||||||
@@ -32,6 +33,13 @@ 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";
|
||||||
import packageJson from "../../../package.json";
|
import packageJson from "../../../package.json";
|
||||||
|
import {
|
||||||
|
getMCPProxyAddress,
|
||||||
|
getMCPServerRequestMaxTotalTimeout,
|
||||||
|
resetRequestTimeoutOnProgress,
|
||||||
|
} from "@/utils/configUtils";
|
||||||
|
import { getMCPServerRequestTimeout } from "@/utils/configUtils";
|
||||||
|
import { InspectorConfig } from "../configurationTypes";
|
||||||
|
|
||||||
interface UseConnectionOptions {
|
interface UseConnectionOptions {
|
||||||
transportType: "stdio" | "sse";
|
transportType: "stdio" | "sse";
|
||||||
@@ -39,10 +47,10 @@ interface UseConnectionOptions {
|
|||||||
args: string;
|
args: string;
|
||||||
sseUrl: string;
|
sseUrl: string;
|
||||||
env: Record<string, string>;
|
env: Record<string, string>;
|
||||||
proxyServerUrl: string;
|
|
||||||
bearerToken?: string;
|
bearerToken?: string;
|
||||||
headerName?: string;
|
headerName?: string;
|
||||||
requestTimeout?: number;
|
requestTimeout?: number;
|
||||||
|
config: InspectorConfig;
|
||||||
onNotification?: (notification: Notification) => void;
|
onNotification?: (notification: Notification) => void;
|
||||||
onStdErrNotification?: (notification: Notification) => void;
|
onStdErrNotification?: (notification: Notification) => void;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@@ -51,22 +59,16 @@ interface UseConnectionOptions {
|
|||||||
getRoots?: () => any[];
|
getRoots?: () => any[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RequestOptions {
|
|
||||||
signal?: AbortSignal;
|
|
||||||
timeout?: number;
|
|
||||||
suppressToast?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useConnection({
|
export function useConnection({
|
||||||
transportType,
|
transportType,
|
||||||
command,
|
command,
|
||||||
args,
|
args,
|
||||||
sseUrl,
|
sseUrl,
|
||||||
env,
|
env,
|
||||||
proxyServerUrl,
|
|
||||||
bearerToken,
|
bearerToken,
|
||||||
headerName,
|
headerName,
|
||||||
requestTimeout,
|
requestTimeout,
|
||||||
|
config,
|
||||||
onNotification,
|
onNotification,
|
||||||
onStdErrNotification,
|
onStdErrNotification,
|
||||||
onPendingRequest,
|
onPendingRequest,
|
||||||
@@ -96,31 +98,50 @@ export function useConnection({
|
|||||||
const makeRequest = async <T extends z.ZodType>(
|
const makeRequest = async <T extends z.ZodType>(
|
||||||
request: ClientRequest,
|
request: ClientRequest,
|
||||||
schema: T,
|
schema: T,
|
||||||
options?: RequestOptions,
|
options?: RequestOptions & { suppressToast?: boolean },
|
||||||
): Promise<z.output<T>> => {
|
): Promise<z.output<T>> => {
|
||||||
if (!mcpClient) {
|
if (!mcpClient) {
|
||||||
throw new Error("MCP client not connected");
|
throw new Error("MCP client not connected");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
const timeoutId = setTimeout(() => {
|
|
||||||
abortController.abort("Request timed out");
|
// prepare MCP Client request options
|
||||||
}, options?.timeout ?? requestTimeout);
|
const mcpRequestOptions: RequestOptions = {
|
||||||
|
signal: options?.signal ?? abortController.signal,
|
||||||
|
resetTimeoutOnProgress:
|
||||||
|
options?.resetTimeoutOnProgress ??
|
||||||
|
resetRequestTimeoutOnProgress(config),
|
||||||
|
timeout: options?.timeout ?? getMCPServerRequestTimeout(config),
|
||||||
|
maxTotalTimeout:
|
||||||
|
options?.maxTotalTimeout ??
|
||||||
|
getMCPServerRequestMaxTotalTimeout(config),
|
||||||
|
};
|
||||||
|
|
||||||
|
// If progress notifications are enabled, add an onprogress hook to the MCP Client request options
|
||||||
|
// This is required by SDK to reset the timeout on progress notifications
|
||||||
|
if (mcpRequestOptions.resetTimeoutOnProgress) {
|
||||||
|
mcpRequestOptions.onprogress = (params: Progress) => {
|
||||||
|
// Add progress notification to `Server Notification` window in the UI
|
||||||
|
if (onNotification) {
|
||||||
|
onNotification({
|
||||||
|
method: "notification/progress",
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
let response;
|
let response;
|
||||||
try {
|
try {
|
||||||
response = await mcpClient.request(request, schema, {
|
response = await mcpClient.request(request, schema, mcpRequestOptions);
|
||||||
signal: options?.signal ?? abortController.signal,
|
|
||||||
});
|
|
||||||
pushHistory(request, response);
|
pushHistory(request, response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
error instanceof Error ? error.message : String(error);
|
error instanceof Error ? error.message : String(error);
|
||||||
pushHistory(request, { error: errorMessage });
|
pushHistory(request, { error: errorMessage });
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
@@ -213,7 +234,7 @@ export function useConnection({
|
|||||||
|
|
||||||
const checkProxyHealth = async () => {
|
const checkProxyHealth = async () => {
|
||||||
try {
|
try {
|
||||||
const proxyHealthUrl = new URL(`${proxyServerUrl}/health`);
|
const proxyHealthUrl = new URL(`${getMCPProxyAddress(config)}/health`);
|
||||||
const proxyHealthResponse = await fetch(proxyHealthUrl);
|
const proxyHealthResponse = await fetch(proxyHealthUrl);
|
||||||
const proxyHealth = await proxyHealthResponse.json();
|
const proxyHealth = await proxyHealthResponse.json();
|
||||||
if (proxyHealth?.status !== "ok") {
|
if (proxyHealth?.status !== "ok") {
|
||||||
@@ -258,7 +279,7 @@ export function useConnection({
|
|||||||
setConnectionStatus("error-connecting-to-proxy");
|
setConnectionStatus("error-connecting-to-proxy");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const mcpProxyServerUrl = new URL(`${proxyServerUrl}/sse`);
|
const mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/sse`);
|
||||||
mcpProxyServerUrl.searchParams.append("transportType", transportType);
|
mcpProxyServerUrl.searchParams.append("transportType", transportType);
|
||||||
if (transportType === "stdio") {
|
if (transportType === "stdio") {
|
||||||
mcpProxyServerUrl.searchParams.append("command", command);
|
mcpProxyServerUrl.searchParams.append("command", command);
|
||||||
@@ -292,7 +313,6 @@ export function useConnection({
|
|||||||
if (onNotification) {
|
if (onNotification) {
|
||||||
[
|
[
|
||||||
CancelledNotificationSchema,
|
CancelledNotificationSchema,
|
||||||
ProgressNotificationSchema,
|
|
||||||
LoggingMessageNotificationSchema,
|
LoggingMessageNotificationSchema,
|
||||||
ResourceUpdatedNotificationSchema,
|
ResourceUpdatedNotificationSchema,
|
||||||
ResourceListChangedNotificationSchema,
|
ResourceListChangedNotificationSchema,
|
||||||
|
|||||||
@@ -1,5 +1,146 @@
|
|||||||
import { updateValueAtPath, getValueAtPath } from "../jsonPathUtils";
|
import {
|
||||||
import { JsonValue } from "../../components/DynamicJsonForm";
|
getDataType,
|
||||||
|
tryParseJson,
|
||||||
|
updateValueAtPath,
|
||||||
|
getValueAtPath,
|
||||||
|
} from "../jsonUtils";
|
||||||
|
import type { JsonValue } from "../jsonUtils";
|
||||||
|
|
||||||
|
describe("getDataType", () => {
|
||||||
|
test("should return 'string' for string values", () => {
|
||||||
|
expect(getDataType("hello")).toBe("string");
|
||||||
|
expect(getDataType("")).toBe("string");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return 'number' for number values", () => {
|
||||||
|
expect(getDataType(123)).toBe("number");
|
||||||
|
expect(getDataType(0)).toBe("number");
|
||||||
|
expect(getDataType(-10)).toBe("number");
|
||||||
|
expect(getDataType(1.5)).toBe("number");
|
||||||
|
expect(getDataType(NaN)).toBe("number");
|
||||||
|
expect(getDataType(Infinity)).toBe("number");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return 'boolean' for boolean values", () => {
|
||||||
|
expect(getDataType(true)).toBe("boolean");
|
||||||
|
expect(getDataType(false)).toBe("boolean");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return 'undefined' for undefined value", () => {
|
||||||
|
expect(getDataType(undefined)).toBe("undefined");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return 'object' for object values", () => {
|
||||||
|
expect(getDataType({})).toBe("object");
|
||||||
|
expect(getDataType({ key: "value" })).toBe("object");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return 'array' for array values", () => {
|
||||||
|
expect(getDataType([])).toBe("array");
|
||||||
|
expect(getDataType([1, 2, 3])).toBe("array");
|
||||||
|
expect(getDataType(["a", "b", "c"])).toBe("array");
|
||||||
|
expect(getDataType([{}, { nested: true }])).toBe("array");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return 'null' for null value", () => {
|
||||||
|
expect(getDataType(null)).toBe("null");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("tryParseJson", () => {
|
||||||
|
test("should correctly parse valid JSON object", () => {
|
||||||
|
const jsonString = '{"name":"test","value":123}';
|
||||||
|
const result = tryParseJson(jsonString);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.data).toEqual({ name: "test", value: 123 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should correctly parse valid JSON array", () => {
|
||||||
|
const jsonString = '[1,2,3,"test"]';
|
||||||
|
const result = tryParseJson(jsonString);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.data).toEqual([1, 2, 3, "test"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should correctly parse JSON with whitespace", () => {
|
||||||
|
const jsonString = ' { "name" : "test" } ';
|
||||||
|
const result = tryParseJson(jsonString);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.data).toEqual({ name: "test" });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should correctly parse nested JSON structures", () => {
|
||||||
|
const jsonString =
|
||||||
|
'{"user":{"name":"test","details":{"age":30}},"items":[1,2,3]}';
|
||||||
|
const result = tryParseJson(jsonString);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.data).toEqual({
|
||||||
|
user: {
|
||||||
|
name: "test",
|
||||||
|
details: {
|
||||||
|
age: 30,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
items: [1, 2, 3],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should correctly parse empty objects and arrays", () => {
|
||||||
|
expect(tryParseJson("{}").success).toBe(true);
|
||||||
|
expect(tryParseJson("{}").data).toEqual({});
|
||||||
|
|
||||||
|
expect(tryParseJson("[]").success).toBe(true);
|
||||||
|
expect(tryParseJson("[]").data).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return failure for non-JSON strings", () => {
|
||||||
|
const nonJsonString = "this is not json";
|
||||||
|
const result = tryParseJson(nonJsonString);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.data).toBe(nonJsonString);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return failure for malformed JSON", () => {
|
||||||
|
const malformedJson = '{"name":"test",}';
|
||||||
|
const result = tryParseJson(malformedJson);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.data).toBe(malformedJson);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return failure for strings with correct delimiters but invalid JSON", () => {
|
||||||
|
const invalidJson = "{name:test}";
|
||||||
|
const result = tryParseJson(invalidJson);
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.data).toBe(invalidJson);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle edge cases", () => {
|
||||||
|
expect(tryParseJson("").success).toBe(false);
|
||||||
|
expect(tryParseJson("").data).toBe("");
|
||||||
|
|
||||||
|
expect(tryParseJson(" ").success).toBe(false);
|
||||||
|
expect(tryParseJson(" ").data).toBe(" ");
|
||||||
|
|
||||||
|
expect(tryParseJson("null").success).toBe(false);
|
||||||
|
expect(tryParseJson("null").data).toBe("null");
|
||||||
|
|
||||||
|
expect(tryParseJson('"string"').success).toBe(false);
|
||||||
|
expect(tryParseJson('"string"').data).toBe('"string"');
|
||||||
|
|
||||||
|
expect(tryParseJson("123").success).toBe(false);
|
||||||
|
expect(tryParseJson("123").data).toBe("123");
|
||||||
|
|
||||||
|
expect(tryParseJson("true").success).toBe(false);
|
||||||
|
expect(tryParseJson("true").data).toBe("true");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("updateValueAtPath", () => {
|
describe("updateValueAtPath", () => {
|
||||||
// Basic functionality tests
|
// Basic functionality tests
|
||||||
@@ -8,17 +149,17 @@ describe("updateValueAtPath", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("initializes an empty object when input is null/undefined and path starts with a string", () => {
|
test("initializes an empty object when input is null/undefined and path starts with a string", () => {
|
||||||
expect(updateValueAtPath(null as any, ["foo"], "bar")).toEqual({
|
expect(updateValueAtPath(null, ["foo"], "bar")).toEqual({
|
||||||
foo: "bar",
|
foo: "bar",
|
||||||
});
|
});
|
||||||
expect(updateValueAtPath(undefined as any, ["foo"], "bar")).toEqual({
|
expect(updateValueAtPath(undefined, ["foo"], "bar")).toEqual({
|
||||||
foo: "bar",
|
foo: "bar",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("initializes an empty array when input is null/undefined and path starts with a number", () => {
|
test("initializes an empty array when input is null/undefined and path starts with a number", () => {
|
||||||
expect(updateValueAtPath(null as any, ["0"], "bar")).toEqual(["bar"]);
|
expect(updateValueAtPath(null, ["0"], "bar")).toEqual(["bar"]);
|
||||||
expect(updateValueAtPath(undefined as any, ["0"], "bar")).toEqual(["bar"]);
|
expect(updateValueAtPath(undefined, ["0"], "bar")).toEqual(["bar"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Object update tests
|
// Object update tests
|
||||||
@@ -152,10 +293,8 @@ describe("getValueAtPath", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("returns default value when input is null/undefined", () => {
|
test("returns default value when input is null/undefined", () => {
|
||||||
expect(getValueAtPath(null as any, ["foo"], "default")).toBe("default");
|
expect(getValueAtPath(null, ["foo"], "default")).toBe("default");
|
||||||
expect(getValueAtPath(undefined as any, ["foo"], "default")).toBe(
|
expect(getValueAtPath(undefined, ["foo"], "default")).toBe("default");
|
||||||
"default",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("handles array indices correctly", () => {
|
test("handles array indices correctly", () => {
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { generateDefaultValue, formatFieldLabel } from "../schemaUtils";
|
import { generateDefaultValue, formatFieldLabel } from "../schemaUtils";
|
||||||
import { JsonSchemaType } from "../../components/DynamicJsonForm";
|
import type { JsonSchemaType } from "../jsonUtils";
|
||||||
|
|
||||||
describe("generateDefaultValue", () => {
|
describe("generateDefaultValue", () => {
|
||||||
test("generates default string", () => {
|
test("generates default string", () => {
|
||||||
|
|||||||
@@ -12,3 +12,15 @@ export const getMCPProxyAddress = (config: InspectorConfig): string => {
|
|||||||
export const getMCPServerRequestTimeout = (config: InspectorConfig): number => {
|
export const getMCPServerRequestTimeout = (config: InspectorConfig): number => {
|
||||||
return config.MCP_SERVER_REQUEST_TIMEOUT.value as number;
|
return config.MCP_SERVER_REQUEST_TIMEOUT.value as number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const resetRequestTimeoutOnProgress = (
|
||||||
|
config: InspectorConfig,
|
||||||
|
): boolean => {
|
||||||
|
return config.MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS.value as boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMCPServerRequestMaxTotalTimeout = (
|
||||||
|
config: InspectorConfig,
|
||||||
|
): number => {
|
||||||
|
return config.MCP_REQUEST_MAX_TOTAL_TIMEOUT.value as number;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,7 +1,66 @@
|
|||||||
import { JsonValue } from "../components/DynamicJsonForm";
|
export type JsonValue =
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| null
|
||||||
|
| undefined
|
||||||
|
| JsonValue[]
|
||||||
|
| { [key: string]: JsonValue };
|
||||||
|
|
||||||
|
export type JsonSchemaType = {
|
||||||
|
type:
|
||||||
|
| "string"
|
||||||
|
| "number"
|
||||||
|
| "integer"
|
||||||
|
| "boolean"
|
||||||
|
| "array"
|
||||||
|
| "object"
|
||||||
|
| "null";
|
||||||
|
description?: string;
|
||||||
|
required?: boolean;
|
||||||
|
default?: JsonValue;
|
||||||
|
properties?: Record<string, JsonSchemaType>;
|
||||||
|
items?: JsonSchemaType;
|
||||||
|
};
|
||||||
|
|
||||||
export type JsonObject = { [key: string]: JsonValue };
|
export type JsonObject = { [key: string]: JsonValue };
|
||||||
|
|
||||||
|
export type DataType =
|
||||||
|
| "string"
|
||||||
|
| "number"
|
||||||
|
| "bigint"
|
||||||
|
| "boolean"
|
||||||
|
| "symbol"
|
||||||
|
| "undefined"
|
||||||
|
| "object"
|
||||||
|
| "function"
|
||||||
|
| "array"
|
||||||
|
| "null";
|
||||||
|
|
||||||
|
export function getDataType(value: JsonValue): DataType {
|
||||||
|
if (Array.isArray(value)) return "array";
|
||||||
|
if (value === null) return "null";
|
||||||
|
return typeof value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tryParseJson(str: string): {
|
||||||
|
success: boolean;
|
||||||
|
data: JsonValue;
|
||||||
|
} {
|
||||||
|
const trimmed = str.trim();
|
||||||
|
if (
|
||||||
|
!(trimmed.startsWith("{") && trimmed.endsWith("}")) &&
|
||||||
|
!(trimmed.startsWith("[") && trimmed.endsWith("]"))
|
||||||
|
) {
|
||||||
|
return { success: false, data: str };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return { success: true, data: JSON.parse(str) };
|
||||||
|
} catch {
|
||||||
|
return { success: false, data: str };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates a value at a specific path in a nested JSON structure
|
* Updates a value at a specific path in a nested JSON structure
|
||||||
* @param obj The original JSON value
|
* @param obj The original JSON value
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { JsonValue, JsonSchemaType } from "../components/DynamicJsonForm";
|
import type { JsonValue, JsonSchemaType, JsonObject } from "./jsonUtils";
|
||||||
import { JsonObject } from "./jsonPathUtils";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a default value based on a JSON schema type
|
* Generates a default value based on a JSON schema type
|
||||||
|
|||||||
42
package-lock.json
generated
42
package-lock.json
generated
@@ -1,20 +1,20 @@
|
|||||||
{
|
{
|
||||||
"name": "@modelcontextprotocol/inspector",
|
"name": "@modelcontextprotocol/inspector",
|
||||||
"version": "0.8.1",
|
"version": "0.8.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@modelcontextprotocol/inspector",
|
"name": "@modelcontextprotocol/inspector",
|
||||||
"version": "0.8.1",
|
"version": "0.8.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"client",
|
"client",
|
||||||
"server"
|
"server"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/inspector-client": "^0.8.1",
|
"@modelcontextprotocol/inspector-client": "^0.8.2",
|
||||||
"@modelcontextprotocol/inspector-server": "^0.8.1",
|
"@modelcontextprotocol/inspector-server": "^0.8.2",
|
||||||
"concurrently": "^9.0.1",
|
"concurrently": "^9.0.1",
|
||||||
"shell-quote": "^1.8.2",
|
"shell-quote": "^1.8.2",
|
||||||
"spawn-rx": "^5.1.2",
|
"spawn-rx": "^5.1.2",
|
||||||
@@ -32,10 +32,10 @@
|
|||||||
},
|
},
|
||||||
"client": {
|
"client": {
|
||||||
"name": "@modelcontextprotocol/inspector-client",
|
"name": "@modelcontextprotocol/inspector-client",
|
||||||
"version": "0.8.1",
|
"version": "0.8.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.8.0",
|
"@modelcontextprotocol/sdk": "^1.9.0",
|
||||||
"@radix-ui/react-checkbox": "^1.1.4",
|
"@radix-ui/react-checkbox": "^1.1.4",
|
||||||
"@radix-ui/react-dialog": "^1.1.3",
|
"@radix-ui/react-dialog": "^1.1.3",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
@@ -46,7 +46,6 @@
|
|||||||
"@radix-ui/react-tabs": "^1.1.1",
|
"@radix-ui/react-tabs": "^1.1.1",
|
||||||
"@radix-ui/react-toast": "^1.2.6",
|
"@radix-ui/react-toast": "^1.2.6",
|
||||||
"@radix-ui/react-tooltip": "^1.1.8",
|
"@radix-ui/react-tooltip": "^1.1.8",
|
||||||
"@types/prismjs": "^1.26.5",
|
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.0.4",
|
"cmdk": "^1.0.4",
|
||||||
@@ -70,6 +69,7 @@
|
|||||||
"@testing-library/react": "^16.2.0",
|
"@testing-library/react": "^16.2.0",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^22.7.5",
|
"@types/node": "^22.7.5",
|
||||||
|
"@types/prismjs": "^1.26.5",
|
||||||
"@types/react": "^18.3.10",
|
"@types/react": "^18.3.10",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@types/serve-handler": "^6.1.4",
|
"@types/serve-handler": "^6.1.4",
|
||||||
@@ -1329,9 +1329,9 @@
|
|||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
"node_modules/@modelcontextprotocol/sdk": {
|
"node_modules/@modelcontextprotocol/sdk": {
|
||||||
"version": "1.8.0",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.9.0.tgz",
|
||||||
"integrity": "sha512-e06W7SwrontJDHwCawNO5SGxG+nU9AAx+jpHHZqGl/WrDBdWOpvirC+s58VpJTB5QemI4jTRcjWT4Pt3Q1NPQQ==",
|
"integrity": "sha512-Jq2EUCQpe0iyO5FGpzVYDNFR6oR53AIrwph9yWl7uSc7IWUMsrmpmSaTGra5hQNunXpM+9oit85p924jWuHzUA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"content-type": "^1.0.5",
|
"content-type": "^1.0.5",
|
||||||
@@ -1340,7 +1340,7 @@
|
|||||||
"eventsource": "^3.0.2",
|
"eventsource": "^3.0.2",
|
||||||
"express": "^5.0.1",
|
"express": "^5.0.1",
|
||||||
"express-rate-limit": "^7.5.0",
|
"express-rate-limit": "^7.5.0",
|
||||||
"pkce-challenge": "^4.1.0",
|
"pkce-challenge": "^5.0.0",
|
||||||
"raw-body": "^3.0.0",
|
"raw-body": "^3.0.0",
|
||||||
"zod": "^3.23.8",
|
"zod": "^3.23.8",
|
||||||
"zod-to-json-schema": "^3.24.1"
|
"zod-to-json-schema": "^3.24.1"
|
||||||
@@ -1585,6 +1585,15 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/pkce-challenge": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@modelcontextprotocol/sdk/node_modules/send": {
|
"node_modules/@modelcontextprotocol/sdk/node_modules/send": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -3566,6 +3575,9 @@
|
|||||||
},
|
},
|
||||||
"node_modules/@types/prismjs": {
|
"node_modules/@types/prismjs": {
|
||||||
"version": "1.26.5",
|
"version": "1.26.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz",
|
||||||
|
"integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/prop-types": {
|
"node_modules/@types/prop-types": {
|
||||||
@@ -9124,7 +9136,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "5.4.16",
|
"version": "5.4.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.17.tgz",
|
||||||
|
"integrity": "sha512-5+VqZryDj4wgCs55o9Lp+p8GE78TLVg0lasCH5xFZ4jacZjtqZa6JUw9/p0WeAojaOfncSM6v77InkFPGnvPvg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -9486,10 +9500,10 @@
|
|||||||
},
|
},
|
||||||
"server": {
|
"server": {
|
||||||
"name": "@modelcontextprotocol/inspector-server",
|
"name": "@modelcontextprotocol/inspector-server",
|
||||||
"version": "0.8.1",
|
"version": "0.8.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.8.0",
|
"@modelcontextprotocol/sdk": "^1.9.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^4.21.0",
|
"express": "^4.21.0",
|
||||||
"ws": "^8.18.0",
|
"ws": "^8.18.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@modelcontextprotocol/inspector",
|
"name": "@modelcontextprotocol/inspector",
|
||||||
"version": "0.8.1",
|
"version": "0.8.2",
|
||||||
"description": "Model Context Protocol inspector",
|
"description": "Model Context Protocol inspector",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||||
@@ -36,8 +36,8 @@
|
|||||||
"publish-all": "npm publish --workspaces --access public && npm publish --access public"
|
"publish-all": "npm publish --workspaces --access public && npm publish --access public"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/inspector-client": "^0.8.1",
|
"@modelcontextprotocol/inspector-client": "^0.8.2",
|
||||||
"@modelcontextprotocol/inspector-server": "^0.8.1",
|
"@modelcontextprotocol/inspector-server": "^0.8.2",
|
||||||
"concurrently": "^9.0.1",
|
"concurrently": "^9.0.1",
|
||||||
"shell-quote": "^1.8.2",
|
"shell-quote": "^1.8.2",
|
||||||
"spawn-rx": "^5.1.2",
|
"spawn-rx": "^5.1.2",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@modelcontextprotocol/inspector-server",
|
"name": "@modelcontextprotocol/inspector-server",
|
||||||
"version": "0.8.1",
|
"version": "0.8.2",
|
||||||
"description": "Server-side application for the Model Context Protocol inspector",
|
"description": "Server-side application for the Model Context Protocol inspector",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.8.0",
|
"@modelcontextprotocol/sdk": "^1.9.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^4.21.0",
|
"express": "^4.21.0",
|
||||||
"ws": "^8.18.0",
|
"ws": "^8.18.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user