Merge branch 'main' into sampling-form
This commit is contained in:
commit
5696c19df9
37 changed files with 1273 additions and 646 deletions
4
.github/workflows/main.yml
vendored
4
.github/workflows/main.yml
vendored
|
|
@ -26,6 +26,10 @@ jobs:
|
||||||
# - run: npm ci
|
# - run: npm ci
|
||||||
- run: npm install --no-package-lock
|
- run: npm install --no-package-lock
|
||||||
|
|
||||||
|
- name: Check linting
|
||||||
|
working-directory: ./client
|
||||||
|
run: npm run lint
|
||||||
|
|
||||||
- name: Run client tests
|
- name: Run client tests
|
||||||
working-directory: ./client
|
working-directory: ./client
|
||||||
run: npm test
|
run: npm test
|
||||||
|
|
|
||||||
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;
|
||||||
});
|
});
|
||||||
|
|
@ -148,7 +156,7 @@ const App = () => {
|
||||||
serverCapabilities,
|
serverCapabilities,
|
||||||
mcpClient,
|
mcpClient,
|
||||||
requestHistory,
|
requestHistory,
|
||||||
makeRequest: makeConnectionRequest,
|
makeRequest,
|
||||||
sendNotification,
|
sendNotification,
|
||||||
handleCompletion,
|
handleCompletion,
|
||||||
completionsSupported,
|
completionsSupported,
|
||||||
|
|
@ -161,8 +169,7 @@ const App = () => {
|
||||||
sseUrl,
|
sseUrl,
|
||||||
env,
|
env,
|
||||||
bearerToken,
|
bearerToken,
|
||||||
proxyServerUrl: getMCPProxyAddress(config),
|
config,
|
||||||
requestTimeout: getMCPServerRequestTimeout(config),
|
|
||||||
onNotification: (notification) => {
|
onNotification: (notification) => {
|
||||||
setNotifications((prev) => [...prev, notification as ServerNotification]);
|
setNotifications((prev) => [...prev, notification as ServerNotification]);
|
||||||
},
|
},
|
||||||
|
|
@ -279,13 +286,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);
|
||||||
}
|
}
|
||||||
|
|
@ -303,7 +310,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 } : {},
|
||||||
|
|
@ -316,7 +323,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
|
||||||
|
|
@ -333,7 +340,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 },
|
||||||
|
|
@ -346,7 +353,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 },
|
||||||
|
|
@ -362,7 +369,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 },
|
||||||
|
|
@ -377,7 +384,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 } : {},
|
||||||
|
|
@ -390,7 +397,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 },
|
||||||
|
|
@ -402,7 +409,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 } : {},
|
||||||
|
|
@ -415,7 +422,8 @@ 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,
|
method: "tools/call" as const,
|
||||||
params: {
|
params: {
|
||||||
|
|
@ -430,6 +438,18 @@ const App = () => {
|
||||||
"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 () => {
|
||||||
|
|
@ -437,7 +457,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 },
|
||||||
|
|
@ -637,9 +657,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) => {
|
||||||
|
|
@ -654,7 +675,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"}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ const ListPane = <T extends object>({
|
||||||
isButtonDisabled,
|
isButtonDisabled,
|
||||||
}: ListPaneProps<T>) => (
|
}: ListPaneProps<T>) => (
|
||||||
<div className="bg-card rounded-lg shadow">
|
<div className="bg-card rounded-lg shadow">
|
||||||
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
|
<div className="p-4 border-b border-gray-200 dark:border-gray-800">
|
||||||
<h3 className="font-semibold dark:text-white">{title}</h3>
|
<h3 className="font-semibold dark:text-white">{title}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@ 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="grid grid-cols-2 gap-4">
|
||||||
<div className="col-span-2 flex justify-center items-center">
|
<div className="col-span-2 flex justify-center items-center">
|
||||||
<Button
|
<Button
|
||||||
onClick={onPingClick}
|
onClick={onPingClick}
|
||||||
|
|
@ -12,6 +13,7 @@ const PingTab = ({ onPingClick }: { onPingClick: () => void }) => {
|
||||||
Ping Server
|
Ping Server
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,8 @@ const PromptsTab = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabsContent value="prompts" className="grid grid-cols-2 gap-4">
|
<TabsContent value="prompts">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<ListPane
|
<ListPane
|
||||||
items={prompts}
|
items={prompts}
|
||||||
listItems={listPrompts}
|
listItems={listPrompts}
|
||||||
|
|
@ -96,7 +97,9 @@ const PromptsTab = ({
|
||||||
renderItem={(prompt) => (
|
renderItem={(prompt) => (
|
||||||
<>
|
<>
|
||||||
<span className="flex-1">{prompt.name}</span>
|
<span className="flex-1">{prompt.name}</span>
|
||||||
<span className="text-sm text-gray-500">{prompt.description}</span>
|
<span className="text-sm text-gray-500">
|
||||||
|
{prompt.description}
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
title="Prompts"
|
title="Prompts"
|
||||||
|
|
@ -105,7 +108,7 @@ const PromptsTab = ({
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<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 dark:border-gray-800">
|
||||||
<h3 className="font-semibold">
|
<h3 className="font-semibold">
|
||||||
{selectedPrompt ? selectedPrompt.name : "Select a prompt"}
|
{selectedPrompt ? selectedPrompt.name : "Select a prompt"}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
@ -164,6 +167,7 @@ const PromptsTab = ({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,8 @@ const ResourcesTab = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabsContent value="resources" className="grid grid-cols-3 gap-4">
|
<TabsContent value="resources">
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
<ListPane
|
<ListPane
|
||||||
items={resources}
|
items={resources}
|
||||||
listItems={listResources}
|
listItems={listResources}
|
||||||
|
|
@ -161,7 +162,7 @@ const ResourcesTab = ({
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="bg-card rounded-lg shadow">
|
<div className="bg-card rounded-lg shadow">
|
||||||
<div className="p-4 border-b border-gray-200 flex justify-between items-center">
|
<div className="p-4 border-b border-gray-200 dark:border-gray-800 flex justify-between items-center">
|
||||||
<h3
|
<h3
|
||||||
className="font-semibold truncate"
|
className="font-semibold truncate"
|
||||||
title={selectedResource?.name || selectedTemplate?.name}
|
title={selectedResource?.name || selectedTemplate?.name}
|
||||||
|
|
@ -256,12 +257,14 @@ const ResourcesTab = ({
|
||||||
) : (
|
) : (
|
||||||
<Alert>
|
<Alert>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
Select a resource or template from the list to view its contents
|
Select a resource or template from the list to view its
|
||||||
|
contents
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,8 @@ const RootsTab = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabsContent value="roots" className="space-y-4">
|
<TabsContent value="roots">
|
||||||
|
<div className="space-y-4">
|
||||||
<Alert>
|
<Alert>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
Configure the root directories that the server can access
|
Configure the root directories that the server can access
|
||||||
|
|
@ -70,6 +71,7 @@ const RootsTab = ({
|
||||||
Save Changes
|
Save Changes
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,9 @@ import {
|
||||||
CreateMessageResultSchema,
|
CreateMessageResultSchema,
|
||||||
} from "@modelcontextprotocol/sdk/types.js";
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
import { PendingRequest } from "./SamplingTab";
|
import { PendingRequest } from "./SamplingTab";
|
||||||
import DynamicJsonForm, { JsonSchemaType, JsonValue } from "./DynamicJsonForm";
|
import DynamicJsonForm from "./DynamicJsonForm";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { JsonSchemaType, JsonValue } from "@/utils/jsonUtils";
|
||||||
|
|
||||||
export type SamplingRequestProps = {
|
export type SamplingRequestProps = {
|
||||||
request: PendingRequest;
|
request: PendingRequest;
|
||||||
|
|
@ -23,7 +24,7 @@ const SamplingRequest = ({
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const [messageResult, setMessageResult] = useState<JsonValue>({
|
const [messageResult, setMessageResult] = useState<JsonValue>({
|
||||||
model: "GPT-4o",
|
model: "stub-model",
|
||||||
stopReason: "endTurn",
|
stopReason: "endTurn",
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: {
|
content: {
|
||||||
|
|
@ -32,8 +33,14 @@ const SamplingRequest = ({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const s = useMemo(() => {
|
const contentType = (
|
||||||
const schema: JsonSchemaType = {
|
(messageResult as { [key: string]: JsonValue })?.content as {
|
||||||
|
[key: string]: JsonValue;
|
||||||
|
}
|
||||||
|
)?.type;
|
||||||
|
|
||||||
|
const schema = useMemo(() => {
|
||||||
|
const s: JsonSchemaType = {
|
||||||
type: "object",
|
type: "object",
|
||||||
description: "Message result",
|
description: "Message result",
|
||||||
properties: {
|
properties: {
|
||||||
|
|
@ -65,10 +72,9 @@ const SamplingRequest = ({
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const contentType = (messageResult as any)?.content?.type;
|
if (contentType === "text" && s.properties) {
|
||||||
if (contentType === "text" && schema.properties) {
|
s.properties.content.properties = {
|
||||||
schema.properties.content.properties = {
|
...s.properties.content.properties,
|
||||||
...schema.properties.content.properties,
|
|
||||||
text: {
|
text: {
|
||||||
type: "string",
|
type: "string",
|
||||||
default: "",
|
default: "",
|
||||||
|
|
@ -82,9 +88,9 @@ const SamplingRequest = ({
|
||||||
text: "",
|
text: "",
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
} else if (contentType === "image" && schema.properties) {
|
} else if (contentType === "image" && s.properties) {
|
||||||
schema.properties.content.properties = {
|
s.properties.content.properties = {
|
||||||
...schema.properties.content.properties,
|
...s.properties.content.properties,
|
||||||
data: {
|
data: {
|
||||||
type: "string",
|
type: "string",
|
||||||
default: "",
|
default: "",
|
||||||
|
|
@ -106,8 +112,8 @@ const SamplingRequest = ({
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
return schema;
|
return s;
|
||||||
}, [(messageResult as any)?.content?.type]);
|
}, [contentType]);
|
||||||
|
|
||||||
const handleApprove = (id: number) => {
|
const handleApprove = (id: number) => {
|
||||||
const validationResult = CreateMessageResultSchema.safeParse(messageResult);
|
const validationResult = CreateMessageResultSchema.safeParse(messageResult);
|
||||||
|
|
@ -135,7 +141,7 @@ const SamplingRequest = ({
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<DynamicJsonForm
|
<DynamicJsonForm
|
||||||
defaultIsJsonMode={true}
|
defaultIsJsonMode={true}
|
||||||
schema={s}
|
schema={schema}
|
||||||
value={messageResult}
|
value={messageResult}
|
||||||
onChange={(newValue: JsonValue) => {
|
onChange={(newValue: JsonValue) => {
|
||||||
setMessageResult(newValue);
|
setMessageResult(newValue);
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,8 @@ export type Props = {
|
||||||
|
|
||||||
const SamplingTab = ({ pendingRequests, onApprove, onReject }: Props) => {
|
const SamplingTab = ({ pendingRequests, onApprove, onReject }: Props) => {
|
||||||
return (
|
return (
|
||||||
<TabsContent value="sampling" className="mh-96">
|
<TabsContent value="sampling">
|
||||||
|
<div className="h-96">
|
||||||
<Alert>
|
<Alert>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
When the server requests LLM sampling, requests will appear here for
|
When the server requests LLM sampling, requests will appear here for
|
||||||
|
|
@ -40,6 +41,7 @@ const SamplingTab = ({ pendingRequests, onApprove, onReject }: Props) => {
|
||||||
<p className="text-gray-500">No pending requests</p>
|
<p className="text-gray-500">No pending requests</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ const Sidebar = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-80 bg-card border-r border-border flex flex-col h-full">
|
<div className="w-80 bg-card border-r border-border flex flex-col h-full">
|
||||||
<div className="flex items-center justify-between p-4 border-b border-gray-200">
|
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-800">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<h1 className="ml-2 text-lg font-semibold">
|
<h1 className="ml-2 text-lg font-semibold">
|
||||||
MCP Inspector v{version}
|
MCP Inspector v{version}
|
||||||
|
|
@ -103,14 +103,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>
|
||||||
|
|
@ -123,8 +128,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)}
|
||||||
|
|
@ -132,8 +140,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)}
|
||||||
|
|
@ -144,8 +158,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)}
|
||||||
|
|
@ -157,6 +174,7 @@ const Sidebar = ({
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setShowBearerToken(!showBearerToken)}
|
onClick={() => setShowBearerToken(!showBearerToken)}
|
||||||
className="flex items-center w-full"
|
className="flex items-center w-full"
|
||||||
|
aria-expanded={showBearerToken}
|
||||||
>
|
>
|
||||||
{showBearerToken ? (
|
{showBearerToken ? (
|
||||||
<ChevronDown className="w-4 h-4 mr-2" />
|
<ChevronDown className="w-4 h-4 mr-2" />
|
||||||
|
|
@ -167,8 +185,14 @@ const Sidebar = ({
|
||||||
</Button>
|
</Button>
|
||||||
{showBearerToken && (
|
{showBearerToken && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<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)}
|
||||||
|
|
@ -187,6 +211,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" />
|
||||||
|
|
@ -201,6 +226,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) => {
|
||||||
|
|
@ -243,6 +269,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}
|
||||||
|
|
@ -309,6 +336,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" />
|
||||||
|
|
@ -325,8 +353,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>
|
||||||
|
|
@ -339,6 +370,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}
|
||||||
|
|
@ -365,7 +397,7 @@ const Sidebar = ({
|
||||||
setConfig(newConfig);
|
setConfig(newConfig);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger id={`${configKey}-input`}>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
@ -375,6 +407,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) => {
|
||||||
|
|
@ -398,7 +431,13 @@ const Sidebar = ({
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{connectionStatus === "connected" && (
|
{connectionStatus === "connected" && (
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<Button data-testid="connect-button" onClick={onConnect}>
|
<Button
|
||||||
|
data-testid="connect-button"
|
||||||
|
onClick={() => {
|
||||||
|
onDisconnect();
|
||||||
|
onConnect();
|
||||||
|
}}
|
||||||
|
>
|
||||||
<RotateCcw className="w-4 h-4 mr-2" />
|
<RotateCcw className="w-4 h-4 mr-2" />
|
||||||
{transportType === "stdio" ? "Restart" : "Reconnect"}
|
{transportType === "stdio" ? "Restart" : "Reconnect"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -448,14 +487,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,7 +116,8 @@ const ToolsTab = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabsContent value="tools" className="grid grid-cols-2 gap-4">
|
<TabsContent value="tools">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<ListPane
|
<ListPane
|
||||||
items={tools}
|
items={tools}
|
||||||
listItems={listTools}
|
listItems={listTools}
|
||||||
|
|
@ -129,7 +140,7 @@ const ToolsTab = ({
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<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 dark:border-gray-800">
|
||||||
<h3 className="font-semibold">
|
<h3 className="font-semibold">
|
||||||
{selectedTool ? selectedTool.name : "Select a tool"}
|
{selectedTool ? selectedTool.name : "Select a tool"}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
@ -234,9 +245,28 @@ const ToolsTab = ({
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
<Button onClick={() => callTool(selectedTool.name, params)}>
|
<Button
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
setIsToolRunning(true);
|
||||||
|
await callTool(selectedTool.name, params);
|
||||||
|
} finally {
|
||||||
|
setIsToolRunning(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={isToolRunning}
|
||||||
|
>
|
||||||
|
{isToolRunning ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
|
Running...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<Send className="w-4 h-4 mr-2" />
|
<Send className="w-4 h-4 mr-2" />
|
||||||
Run Tool
|
Run Tool
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
{toolResult && renderToolResult()}
|
{toolResult && renderToolResult()}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -249,6 +279,7 @@ const ToolsTab = ({
|
||||||
)}
|
)}
|
||||||
</div>
|
</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 = {}) => {
|
||||||
|
|
|
||||||
|
|
@ -343,6 +343,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,
|
||||||
},
|
},
|
||||||
|
|
@ -350,6 +351,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 });
|
||||||
|
|
@ -364,6 +415,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,
|
||||||
},
|
},
|
||||||
|
|
@ -409,6 +461,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;
|
||||||
|
await act(async () => {
|
||||||
fireEvent.change(input, { target: { value: "42" } });
|
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 });
|
||||||
|
await act(async () => {
|
||||||
fireEvent.click(submitButton);
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ describe("Form to handle sampling response", () => {
|
||||||
|
|
||||||
// Assert that onApprove is called with the correct arguments
|
// Assert that onApprove is called with the correct arguments
|
||||||
expect(mockOnApprove).toHaveBeenCalledWith(mockRequest.id, {
|
expect(mockOnApprove).toHaveBeenCalledWith(mockRequest.id, {
|
||||||
model: "GPT-4o",
|
model: "stub-model",
|
||||||
stopReason: "endTurn",
|
stopReason: "endTurn",
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: {
|
content: {
|
||||||
|
|
|
||||||
|
|
@ -54,4 +54,4 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
);
|
);
|
||||||
Button.displayName = "Button";
|
Button.displayName = "Button";
|
||||||
|
|
||||||
export { Button, buttonVariants };
|
export { Button };
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue