Merge branch 'main' into fix/header
This commit is contained in:
commit
ee8c29b3ad
18 changed files with 986 additions and 166 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@modelcontextprotocol/inspector-cli",
|
"name": "@modelcontextprotocol/inspector-cli",
|
||||||
"version": "0.12.0",
|
"version": "0.13.0",
|
||||||
"description": "CLI for the Model Context Protocol inspector",
|
"description": "CLI for the Model Context Protocol inspector",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {},
|
"devDependencies": {},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.11.0",
|
"@modelcontextprotocol/sdk": "^1.11.5",
|
||||||
"commander": "^13.1.0",
|
"commander": "^13.1.0",
|
||||||
"spawn-rx": "^5.1.2"
|
"spawn-rx": "^5.1.2"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@modelcontextprotocol/inspector-client",
|
"name": "@modelcontextprotocol/inspector-client",
|
||||||
"version": "0.12.0",
|
"version": "0.13.0",
|
||||||
"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,8 +23,9 @@
|
||||||
"test:watch": "jest --config jest.config.cjs --watch"
|
"test:watch": "jest --config jest.config.cjs --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.11.0",
|
"@modelcontextprotocol/sdk": "^1.11.5",
|
||||||
"@radix-ui/react-checkbox": "^1.1.4",
|
"@radix-ui/react-checkbox": "^1.1.4",
|
||||||
|
"ajv": "^6.12.6",
|
||||||
"@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",
|
||||||
"@radix-ui/react-label": "^2.1.0",
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import {
|
||||||
import { OAuthTokensSchema } from "@modelcontextprotocol/sdk/shared/auth.js";
|
import { OAuthTokensSchema } from "@modelcontextprotocol/sdk/shared/auth.js";
|
||||||
import { SESSION_KEYS, getServerSpecificKey } from "./lib/constants";
|
import { SESSION_KEYS, getServerSpecificKey } from "./lib/constants";
|
||||||
import { AuthDebuggerState } from "./lib/auth-types";
|
import { AuthDebuggerState } from "./lib/auth-types";
|
||||||
|
import { cacheToolOutputSchemas } from "./utils/schemaUtils";
|
||||||
import React, {
|
import React, {
|
||||||
Suspense,
|
Suspense,
|
||||||
useCallback,
|
useCallback,
|
||||||
|
|
@ -234,7 +235,6 @@ const App = () => {
|
||||||
const onOAuthConnect = useCallback(
|
const onOAuthConnect = useCallback(
|
||||||
(serverUrl: string) => {
|
(serverUrl: string) => {
|
||||||
setSseUrl(serverUrl);
|
setSseUrl(serverUrl);
|
||||||
setTransportType("sse");
|
|
||||||
setIsAuthDebuggerVisible(false);
|
setIsAuthDebuggerVisible(false);
|
||||||
void connectMcpServer();
|
void connectMcpServer();
|
||||||
},
|
},
|
||||||
|
|
@ -474,6 +474,8 @@ const App = () => {
|
||||||
);
|
);
|
||||||
setTools(response.tools);
|
setTools(response.tools);
|
||||||
setNextToolCursor(response.nextCursor);
|
setNextToolCursor(response.nextCursor);
|
||||||
|
// Cache output schemas for validation
|
||||||
|
cacheToolOutputSchemas(response.tools);
|
||||||
};
|
};
|
||||||
|
|
||||||
const callTool = async (name: string, params: Record<string, unknown>) => {
|
const callTool = async (name: string, params: Record<string, unknown>) => {
|
||||||
|
|
@ -760,6 +762,8 @@ const App = () => {
|
||||||
clearTools={() => {
|
clearTools={() => {
|
||||||
setTools([]);
|
setTools([]);
|
||||||
setNextToolCursor(undefined);
|
setNextToolCursor(undefined);
|
||||||
|
// Clear cached output schemas
|
||||||
|
cacheToolOutputSchemas([]);
|
||||||
}}
|
}}
|
||||||
callTool={async (name, params) => {
|
callTool={async (name, params) => {
|
||||||
clearError("tools");
|
clearError("tools");
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,6 @@
|
||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { DebugInspectorOAuthClientProvider } from "../lib/auth";
|
import { DebugInspectorOAuthClientProvider } from "../lib/auth";
|
||||||
import {
|
|
||||||
auth,
|
|
||||||
discoverOAuthMetadata,
|
|
||||||
} from "@modelcontextprotocol/sdk/client/auth.js";
|
|
||||||
import { OAuthMetadataSchema } from "@modelcontextprotocol/sdk/shared/auth.js";
|
|
||||||
import { AlertCircle } from "lucide-react";
|
import { AlertCircle } from "lucide-react";
|
||||||
import { AuthDebuggerState } from "../lib/auth-types";
|
import { AuthDebuggerState } from "../lib/auth-types";
|
||||||
import { OAuthFlowProgress } from "./OAuthFlowProgress";
|
import { OAuthFlowProgress } from "./OAuthFlowProgress";
|
||||||
|
|
@ -124,22 +119,43 @@ const AuthDebugger = ({
|
||||||
|
|
||||||
updateAuthState({ isInitiatingAuth: true, statusMessage: null });
|
updateAuthState({ isInitiatingAuth: true, statusMessage: null });
|
||||||
try {
|
try {
|
||||||
const serverAuthProvider = new DebugInspectorOAuthClientProvider(
|
// Step through the OAuth flow using the state machine instead of the auth() function
|
||||||
serverUrl,
|
let currentState: AuthDebuggerState = {
|
||||||
);
|
...authState,
|
||||||
// First discover OAuth metadata separately so we can save it
|
oauthStep: "metadata_discovery",
|
||||||
const metadata = await discoverOAuthMetadata(serverUrl);
|
authorizationUrl: null,
|
||||||
if (!metadata) {
|
latestError: null,
|
||||||
throw new Error("Failed to discover OAuth metadata");
|
};
|
||||||
}
|
|
||||||
const parsedMetadata = await OAuthMetadataSchema.parseAsync(metadata);
|
|
||||||
serverAuthProvider.saveServerMetadata(parsedMetadata);
|
|
||||||
|
|
||||||
await auth(serverAuthProvider, { serverUrl: serverUrl });
|
const oauthMachine = new OAuthStateMachine(serverUrl, (updates) => {
|
||||||
|
// Update our temporary state during the process
|
||||||
|
currentState = { ...currentState, ...updates };
|
||||||
|
// But don't call updateAuthState yet
|
||||||
|
});
|
||||||
|
|
||||||
|
// Manually step through each stage of the OAuth flow
|
||||||
|
while (currentState.oauthStep !== "complete") {
|
||||||
|
await oauthMachine.executeStep(currentState);
|
||||||
|
// In quick mode, we'll just redirect to the authorization URL
|
||||||
|
if (
|
||||||
|
currentState.oauthStep === "authorization_code" &&
|
||||||
|
currentState.authorizationUrl
|
||||||
|
) {
|
||||||
|
// Open the authorization URL automatically
|
||||||
|
window.location.href = currentState.authorizationUrl;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// After the flow completes or reaches a user-input step, update the app state
|
||||||
updateAuthState({
|
updateAuthState({
|
||||||
|
...currentState,
|
||||||
statusMessage: {
|
statusMessage: {
|
||||||
type: "info",
|
type: "info",
|
||||||
message: "Starting OAuth authentication process...",
|
message:
|
||||||
|
currentState.oauthStep === "complete"
|
||||||
|
? "Authentication completed successfully"
|
||||||
|
: "Please complete authentication in the opened window and enter the code",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -153,7 +169,7 @@ const AuthDebugger = ({
|
||||||
} finally {
|
} finally {
|
||||||
updateAuthState({ isInitiatingAuth: false });
|
updateAuthState({ isInitiatingAuth: false });
|
||||||
}
|
}
|
||||||
}, [serverUrl, updateAuthState]);
|
}, [serverUrl, updateAuthState, authState]);
|
||||||
|
|
||||||
const handleClearOAuth = useCallback(() => {
|
const handleClearOAuth = useCallback(() => {
|
||||||
if (serverUrl) {
|
if (serverUrl) {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ import { AuthDebuggerState, OAuthStep } from "@/lib/auth-types";
|
||||||
import { CheckCircle2, Circle, ExternalLink } from "lucide-react";
|
import { CheckCircle2, Circle, ExternalLink } from "lucide-react";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
import { DebugInspectorOAuthClientProvider } from "@/lib/auth";
|
import { DebugInspectorOAuthClientProvider } from "@/lib/auth";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { OAuthClientInformation } from "@modelcontextprotocol/sdk/shared/auth.js";
|
||||||
|
|
||||||
interface OAuthStepProps {
|
interface OAuthStepProps {
|
||||||
label: string;
|
label: string;
|
||||||
|
|
@ -54,24 +56,57 @@ interface OAuthFlowProgressProps {
|
||||||
proceedToNextStep: () => Promise<void>;
|
proceedToNextStep: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const steps: Array<OAuthStep> = [
|
||||||
|
"metadata_discovery",
|
||||||
|
"client_registration",
|
||||||
|
"authorization_redirect",
|
||||||
|
"authorization_code",
|
||||||
|
"token_request",
|
||||||
|
"complete",
|
||||||
|
];
|
||||||
|
|
||||||
export const OAuthFlowProgress = ({
|
export const OAuthFlowProgress = ({
|
||||||
serverUrl,
|
serverUrl,
|
||||||
authState,
|
authState,
|
||||||
updateAuthState,
|
updateAuthState,
|
||||||
proceedToNextStep,
|
proceedToNextStep,
|
||||||
}: OAuthFlowProgressProps) => {
|
}: OAuthFlowProgressProps) => {
|
||||||
const provider = new DebugInspectorOAuthClientProvider(serverUrl);
|
const provider = useMemo(
|
||||||
|
() => new DebugInspectorOAuthClientProvider(serverUrl),
|
||||||
|
[serverUrl],
|
||||||
|
);
|
||||||
|
const [clientInfo, setClientInfo] = useState<OAuthClientInformation | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
const steps: Array<OAuthStep> = [
|
|
||||||
"metadata_discovery",
|
|
||||||
"client_registration",
|
|
||||||
"authorization_redirect",
|
|
||||||
"authorization_code",
|
|
||||||
"token_request",
|
|
||||||
"complete",
|
|
||||||
];
|
|
||||||
const currentStepIdx = steps.findIndex((s) => s === authState.oauthStep);
|
const currentStepIdx = steps.findIndex((s) => s === authState.oauthStep);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchClientInfo = async () => {
|
||||||
|
if (authState.oauthClientInfo) {
|
||||||
|
setClientInfo(authState.oauthClientInfo);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const info = await provider.clientInformation();
|
||||||
|
if (info) {
|
||||||
|
setClientInfo(info);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch client information:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (currentStepIdx > steps.indexOf("client_registration")) {
|
||||||
|
fetchClientInfo();
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
provider,
|
||||||
|
authState.oauthStep,
|
||||||
|
authState.oauthClientInfo,
|
||||||
|
currentStepIdx,
|
||||||
|
]);
|
||||||
|
|
||||||
// Helper to get step props
|
// Helper to get step props
|
||||||
const getStepProps = (stepName: OAuthStep) => ({
|
const getStepProps = (stepName: OAuthStep) => ({
|
||||||
isComplete:
|
isComplete:
|
||||||
|
|
@ -110,13 +145,13 @@ export const OAuthFlowProgress = ({
|
||||||
label="Client Registration"
|
label="Client Registration"
|
||||||
{...getStepProps("client_registration")}
|
{...getStepProps("client_registration")}
|
||||||
>
|
>
|
||||||
{authState.oauthClientInfo && (
|
{clientInfo && (
|
||||||
<details className="text-xs mt-2">
|
<details className="text-xs mt-2">
|
||||||
<summary className="cursor-pointer text-muted-foreground font-medium">
|
<summary className="cursor-pointer text-muted-foreground font-medium">
|
||||||
Registered Client Information
|
Registered Client Information
|
||||||
</summary>
|
</summary>
|
||||||
<pre className="mt-2 p-2 bg-muted rounded-md overflow-auto max-h-[300px]">
|
<pre className="mt-2 p-2 bg-muted rounded-md overflow-auto max-h-[300px]">
|
||||||
{JSON.stringify(authState.oauthClientInfo, null, 2)}
|
{JSON.stringify(clientInfo, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
</details>
|
</details>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -127,14 +127,14 @@ const Sidebar = ({
|
||||||
return {
|
return {
|
||||||
type: "sse",
|
type: "sse",
|
||||||
url: sseUrl,
|
url: sseUrl,
|
||||||
note: "For SSE connections, add this URL directly in Client",
|
note: "For SSE connections, add this URL directly in your MCP Client",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (transportType === "streamable-http") {
|
if (transportType === "streamable-http") {
|
||||||
return {
|
return {
|
||||||
type: "streamable-http",
|
type: "streamable-http",
|
||||||
url: sseUrl,
|
url: sseUrl,
|
||||||
note: "For Streamable HTTP connections, add this URL directly in Client",
|
note: "For Streamable HTTP connections, add this URL directly in your MCP Client",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
|
|
@ -172,7 +172,7 @@ const Sidebar = ({
|
||||||
description:
|
description:
|
||||||
transportType === "stdio"
|
transportType === "stdio"
|
||||||
? "Server configuration has been copied to clipboard. Add this to your mcp.json inside the 'mcpServers' object with your preferred server name."
|
? "Server configuration has been copied to clipboard. Add this to your mcp.json inside the 'mcpServers' object with your preferred server name."
|
||||||
: "SSE URL has been copied. Use this URL in Cursor directly.",
|
: "SSE URL has been copied. Use this URL directly in your MCP Client.",
|
||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
|
||||||
221
client/src/components/ToolResults.tsx
Normal file
221
client/src/components/ToolResults.tsx
Normal file
|
|
@ -0,0 +1,221 @@
|
||||||
|
import JsonView from "./JsonView";
|
||||||
|
import {
|
||||||
|
CallToolResultSchema,
|
||||||
|
CompatibilityCallToolResult,
|
||||||
|
Tool,
|
||||||
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
import { validateToolOutput, hasOutputSchema } from "@/utils/schemaUtils";
|
||||||
|
|
||||||
|
interface ToolResultsProps {
|
||||||
|
toolResult: CompatibilityCallToolResult | null;
|
||||||
|
selectedTool: Tool | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkContentCompatibility = (
|
||||||
|
structuredContent: unknown,
|
||||||
|
unstructuredContent: Array<{
|
||||||
|
type: string;
|
||||||
|
text?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}>,
|
||||||
|
): { isCompatible: boolean; message: string } => {
|
||||||
|
if (
|
||||||
|
unstructuredContent.length !== 1 ||
|
||||||
|
unstructuredContent[0].type !== "text"
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
isCompatible: false,
|
||||||
|
message: "Unstructured content is not a single text block",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const textContent = unstructuredContent[0].text;
|
||||||
|
if (!textContent) {
|
||||||
|
return {
|
||||||
|
isCompatible: false,
|
||||||
|
message: "Text content is empty",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsedContent = JSON.parse(textContent);
|
||||||
|
const isEqual =
|
||||||
|
JSON.stringify(parsedContent) === JSON.stringify(structuredContent);
|
||||||
|
|
||||||
|
if (isEqual) {
|
||||||
|
return {
|
||||||
|
isCompatible: true,
|
||||||
|
message: "Unstructured content matches structured content",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
isCompatible: false,
|
||||||
|
message: "Parsed JSON does not match structured content",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
isCompatible: false,
|
||||||
|
message: "Unstructured content is not valid JSON",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ToolResults = ({ toolResult, selectedTool }: ToolResultsProps) => {
|
||||||
|
if (!toolResult) return null;
|
||||||
|
|
||||||
|
if ("content" in toolResult) {
|
||||||
|
const parsedResult = CallToolResultSchema.safeParse(toolResult);
|
||||||
|
if (!parsedResult.success) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h4 className="font-semibold mb-2">Invalid Tool Result:</h4>
|
||||||
|
<JsonView data={toolResult} />
|
||||||
|
<h4 className="font-semibold mb-2">Errors:</h4>
|
||||||
|
{parsedResult.error.errors.map((error, idx) => (
|
||||||
|
<JsonView data={error} key={idx} />
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const structuredResult = parsedResult.data;
|
||||||
|
const isError = structuredResult.isError ?? false;
|
||||||
|
|
||||||
|
let validationResult = null;
|
||||||
|
const toolHasOutputSchema =
|
||||||
|
selectedTool && hasOutputSchema(selectedTool.name);
|
||||||
|
|
||||||
|
if (toolHasOutputSchema) {
|
||||||
|
if (!structuredResult.structuredContent && !isError) {
|
||||||
|
validationResult = {
|
||||||
|
isValid: false,
|
||||||
|
error:
|
||||||
|
"Tool has an output schema but did not return structured content",
|
||||||
|
};
|
||||||
|
} else if (structuredResult.structuredContent) {
|
||||||
|
validationResult = validateToolOutput(
|
||||||
|
selectedTool.name,
|
||||||
|
structuredResult.structuredContent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let compatibilityResult = null;
|
||||||
|
if (
|
||||||
|
structuredResult.structuredContent &&
|
||||||
|
structuredResult.content.length > 0 &&
|
||||||
|
selectedTool &&
|
||||||
|
hasOutputSchema(selectedTool.name)
|
||||||
|
) {
|
||||||
|
compatibilityResult = checkContentCompatibility(
|
||||||
|
structuredResult.structuredContent,
|
||||||
|
structuredResult.content,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h4 className="font-semibold mb-2">
|
||||||
|
Tool Result:{" "}
|
||||||
|
{isError ? (
|
||||||
|
<span className="text-red-600 font-semibold">Error</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-green-600 font-semibold">Success</span>
|
||||||
|
)}
|
||||||
|
</h4>
|
||||||
|
{structuredResult.structuredContent && (
|
||||||
|
<div className="mb-4">
|
||||||
|
<h5 className="font-semibold mb-2 text-sm">Structured Content:</h5>
|
||||||
|
<div className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg">
|
||||||
|
<JsonView data={structuredResult.structuredContent} />
|
||||||
|
{validationResult && (
|
||||||
|
<div
|
||||||
|
className={`mt-2 p-2 rounded text-sm ${
|
||||||
|
validationResult.isValid
|
||||||
|
? "bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200"
|
||||||
|
: "bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{validationResult.isValid ? (
|
||||||
|
"✓ Valid according to output schema"
|
||||||
|
) : (
|
||||||
|
<>✗ Validation Error: {validationResult.error}</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!structuredResult.structuredContent &&
|
||||||
|
validationResult &&
|
||||||
|
!validationResult.isValid && (
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200 p-2 rounded text-sm">
|
||||||
|
✗ Validation Error: {validationResult.error}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{structuredResult.content.length > 0 && (
|
||||||
|
<div className="mb-4">
|
||||||
|
{structuredResult.structuredContent && (
|
||||||
|
<>
|
||||||
|
<h5 className="font-semibold mb-2 text-sm">
|
||||||
|
Unstructured Content:
|
||||||
|
</h5>
|
||||||
|
{compatibilityResult && (
|
||||||
|
<div
|
||||||
|
className={`mb-2 p-2 rounded text-sm ${
|
||||||
|
compatibilityResult.isCompatible
|
||||||
|
? "bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200"
|
||||||
|
: "bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{compatibilityResult.isCompatible ? "✓" : "⚠"}{" "}
|
||||||
|
{compatibilityResult.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{structuredResult.content.map((item, index) => (
|
||||||
|
<div key={index} className="mb-2">
|
||||||
|
{item.type === "text" && (
|
||||||
|
<JsonView data={item.text} isError={isError} />
|
||||||
|
)}
|
||||||
|
{item.type === "image" && (
|
||||||
|
<img
|
||||||
|
src={`data:${item.mimeType};base64,${item.data}`}
|
||||||
|
alt="Tool result image"
|
||||||
|
className="max-w-full h-auto"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{item.type === "resource" &&
|
||||||
|
(item.resource?.mimeType?.startsWith("audio/") ? (
|
||||||
|
<audio
|
||||||
|
controls
|
||||||
|
src={`data:${item.resource.mimeType};base64,${item.resource.blob}`}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<p>Your browser does not support audio playback</p>
|
||||||
|
</audio>
|
||||||
|
) : (
|
||||||
|
<JsonView data={item.resource} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else if ("toolResult" in toolResult) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h4 className="font-semibold mb-2">Tool Result (Legacy):</h4>
|
||||||
|
<JsonView data={toolResult.toolResult} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ToolResults;
|
||||||
|
|
@ -9,15 +9,15 @@ import DynamicJsonForm from "./DynamicJsonForm";
|
||||||
import type { JsonValue, JsonSchemaType } from "@/utils/jsonUtils";
|
import type { JsonValue, JsonSchemaType } from "@/utils/jsonUtils";
|
||||||
import { generateDefaultValue } from "@/utils/schemaUtils";
|
import { generateDefaultValue } from "@/utils/schemaUtils";
|
||||||
import {
|
import {
|
||||||
CallToolResultSchema,
|
|
||||||
CompatibilityCallToolResult,
|
CompatibilityCallToolResult,
|
||||||
ListToolsResult,
|
ListToolsResult,
|
||||||
Tool,
|
Tool,
|
||||||
} from "@modelcontextprotocol/sdk/types.js";
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
import { Loader2, Send } from "lucide-react";
|
import { Loader2, Send, ChevronDown, ChevronUp } 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";
|
||||||
|
import ToolResults from "./ToolResults";
|
||||||
|
|
||||||
const ToolsTab = ({
|
const ToolsTab = ({
|
||||||
tools,
|
tools,
|
||||||
|
|
@ -41,6 +41,7 @@ const ToolsTab = ({
|
||||||
}) => {
|
}) => {
|
||||||
const [params, setParams] = useState<Record<string, unknown>>({});
|
const [params, setParams] = useState<Record<string, unknown>>({});
|
||||||
const [isToolRunning, setIsToolRunning] = useState(false);
|
const [isToolRunning, setIsToolRunning] = useState(false);
|
||||||
|
const [isOutputSchemaExpanded, setIsOutputSchemaExpanded] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const params = Object.entries(
|
const params = Object.entries(
|
||||||
|
|
@ -52,75 +53,6 @@ const ToolsTab = ({
|
||||||
setParams(Object.fromEntries(params));
|
setParams(Object.fromEntries(params));
|
||||||
}, [selectedTool]);
|
}, [selectedTool]);
|
||||||
|
|
||||||
const renderToolResult = () => {
|
|
||||||
if (!toolResult) return null;
|
|
||||||
|
|
||||||
if ("content" in toolResult) {
|
|
||||||
const parsedResult = CallToolResultSchema.safeParse(toolResult);
|
|
||||||
if (!parsedResult.success) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h4 className="font-semibold mb-2">Invalid Tool Result:</h4>
|
|
||||||
<JsonView data={toolResult} />
|
|
||||||
<h4 className="font-semibold mb-2">Errors:</h4>
|
|
||||||
{parsedResult.error.errors.map((error, idx) => (
|
|
||||||
<JsonView data={error} key={idx} />
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const structuredResult = parsedResult.data;
|
|
||||||
const isError = structuredResult.isError ?? false;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h4 className="font-semibold mb-2">
|
|
||||||
Tool Result:{" "}
|
|
||||||
{isError ? (
|
|
||||||
<span className="text-red-600 font-semibold">Error</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-green-600 font-semibold">Success</span>
|
|
||||||
)}
|
|
||||||
</h4>
|
|
||||||
{structuredResult.content.map((item, index) => (
|
|
||||||
<div key={index} className="mb-2">
|
|
||||||
{item.type === "text" && (
|
|
||||||
<JsonView data={item.text} isError={isError} />
|
|
||||||
)}
|
|
||||||
{item.type === "image" && (
|
|
||||||
<img
|
|
||||||
src={`data:${item.mimeType};base64,${item.data}`}
|
|
||||||
alt="Tool result image"
|
|
||||||
className="max-w-full h-auto"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{item.type === "resource" &&
|
|
||||||
(item.resource?.mimeType?.startsWith("audio/") ? (
|
|
||||||
<audio
|
|
||||||
controls
|
|
||||||
src={`data:${item.resource.mimeType};base64,${item.resource.blob}`}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
<p>Your browser does not support audio playback</p>
|
|
||||||
</audio>
|
|
||||||
) : (
|
|
||||||
<JsonView data={item.resource} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
} else if ("toolResult" in toolResult) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h4 className="font-semibold mb-2">Tool Result (Legacy):</h4>
|
|
||||||
|
|
||||||
<JsonView data={toolResult.toolResult} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabsContent value="tools">
|
<TabsContent value="tools">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
|
@ -262,6 +194,42 @@ const ToolsTab = ({
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
|
{selectedTool.outputSchema && (
|
||||||
|
<div className="bg-gray-50 dark:bg-gray-900 p-3 rounded-lg">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h4 className="text-sm font-semibold">Output Schema:</h4>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() =>
|
||||||
|
setIsOutputSchemaExpanded(!isOutputSchemaExpanded)
|
||||||
|
}
|
||||||
|
className="h-6 px-2"
|
||||||
|
>
|
||||||
|
{isOutputSchemaExpanded ? (
|
||||||
|
<>
|
||||||
|
<ChevronUp className="h-3 w-3 mr-1" />
|
||||||
|
Collapse
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ChevronDown className="h-3 w-3 mr-1" />
|
||||||
|
Expand
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`transition-all ${
|
||||||
|
isOutputSchemaExpanded
|
||||||
|
? ""
|
||||||
|
: "max-h-[8rem] overflow-y-auto"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<JsonView data={selectedTool.outputSchema} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -285,7 +253,10 @@ const ToolsTab = ({
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
{toolResult && renderToolResult()}
|
<ToolResults
|
||||||
|
toolResult={toolResult}
|
||||||
|
selectedTool={selectedTool}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Alert>
|
<Alert>
|
||||||
|
|
|
||||||
|
|
@ -207,16 +207,10 @@ describe("AuthDebugger", () => {
|
||||||
"https://example.com",
|
"https://example.com",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Then should call auth with the server provider
|
|
||||||
expect(mockAuth).toHaveBeenCalled();
|
|
||||||
|
|
||||||
// Check that updateAuthState was called with the right info message
|
// Check that updateAuthState was called with the right info message
|
||||||
expect(updateAuthState).toHaveBeenCalledWith(
|
expect(updateAuthState).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
statusMessage: {
|
oauthStep: "authorization_code",
|
||||||
type: "info",
|
|
||||||
message: "Starting OAuth authentication process...",
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -755,7 +755,7 @@ describe("Sidebar Environment Variables", () => {
|
||||||
{
|
{
|
||||||
type: "sse",
|
type: "sse",
|
||||||
url: sseUrl,
|
url: sseUrl,
|
||||||
note: "For SSE connections, add this URL directly in Client",
|
note: "For SSE connections, add this URL directly in your MCP Client",
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
4,
|
4,
|
||||||
|
|
@ -780,7 +780,7 @@ describe("Sidebar Environment Variables", () => {
|
||||||
"default-server": {
|
"default-server": {
|
||||||
type: "sse",
|
type: "sse",
|
||||||
url: sseUrl,
|
url: sseUrl,
|
||||||
note: "For SSE connections, add this URL directly in Client",
|
note: "For SSE connections, add this URL directly in your MCP Client",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -805,7 +805,7 @@ describe("Sidebar Environment Variables", () => {
|
||||||
{
|
{
|
||||||
type: "streamable-http",
|
type: "streamable-http",
|
||||||
url: sseUrl,
|
url: sseUrl,
|
||||||
note: "For Streamable HTTP connections, add this URL directly in Client",
|
note: "For Streamable HTTP connections, add this URL directly in your MCP Client",
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
4,
|
4,
|
||||||
|
|
@ -830,7 +830,7 @@ describe("Sidebar Environment Variables", () => {
|
||||||
"default-server": {
|
"default-server": {
|
||||||
type: "streamable-http",
|
type: "streamable-http",
|
||||||
url: sseUrl,
|
url: sseUrl,
|
||||||
note: "For Streamable HTTP connections, add this URL directly in Client",
|
note: "For Streamable HTTP connections, add this URL directly in your MCP Client",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,17 @@
|
||||||
import { render, screen, fireEvent, act } from "@testing-library/react";
|
import { render, screen, fireEvent, act } from "@testing-library/react";
|
||||||
import { describe, it, expect, jest } from "@jest/globals";
|
|
||||||
import "@testing-library/jest-dom";
|
import "@testing-library/jest-dom";
|
||||||
|
import { describe, it, jest, beforeEach } from "@jest/globals";
|
||||||
import ToolsTab from "../ToolsTab";
|
import ToolsTab from "../ToolsTab";
|
||||||
import { Tool } from "@modelcontextprotocol/sdk/types.js";
|
import { Tool } from "@modelcontextprotocol/sdk/types.js";
|
||||||
import { Tabs } from "@/components/ui/tabs";
|
import { Tabs } from "@/components/ui/tabs";
|
||||||
|
import { cacheToolOutputSchemas } from "@/utils/schemaUtils";
|
||||||
|
|
||||||
describe("ToolsTab", () => {
|
describe("ToolsTab", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Clear the output schema cache before each test
|
||||||
|
cacheToolOutputSchemas([]);
|
||||||
|
});
|
||||||
|
|
||||||
const mockTools: Tool[] = [
|
const mockTools: Tool[] = [
|
||||||
{
|
{
|
||||||
name: "tool1",
|
name: "tool1",
|
||||||
|
|
@ -141,4 +147,217 @@ describe("ToolsTab", () => {
|
||||||
|
|
||||||
expect(submitButton.getAttribute("disabled")).toBeNull();
|
expect(submitButton.getAttribute("disabled")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Output Schema Display", () => {
|
||||||
|
const toolWithOutputSchema: Tool = {
|
||||||
|
name: "weatherTool",
|
||||||
|
description: "Get weather",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object" as const,
|
||||||
|
properties: {
|
||||||
|
city: { type: "string" as const },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
outputSchema: {
|
||||||
|
type: "object" as const,
|
||||||
|
properties: {
|
||||||
|
temperature: { type: "number" as const },
|
||||||
|
humidity: { type: "number" as const },
|
||||||
|
},
|
||||||
|
required: ["temperature", "humidity"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it("should display output schema when tool has one", () => {
|
||||||
|
renderToolsTab({
|
||||||
|
tools: [toolWithOutputSchema],
|
||||||
|
selectedTool: toolWithOutputSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByText("Output Schema:")).toBeInTheDocument();
|
||||||
|
// Check for expand/collapse button
|
||||||
|
expect(
|
||||||
|
screen.getByRole("button", { name: /expand/i }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not display output schema section when tool doesn't have one", () => {
|
||||||
|
renderToolsTab({
|
||||||
|
selectedTool: mockTools[0], // Tool without outputSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.queryByText("Output Schema:")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should toggle output schema expansion", () => {
|
||||||
|
renderToolsTab({
|
||||||
|
tools: [toolWithOutputSchema],
|
||||||
|
selectedTool: toolWithOutputSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleButton = screen.getByRole("button", { name: /expand/i });
|
||||||
|
|
||||||
|
// Click to expand
|
||||||
|
fireEvent.click(toggleButton);
|
||||||
|
expect(
|
||||||
|
screen.getByRole("button", { name: /collapse/i }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Click to collapse
|
||||||
|
fireEvent.click(toggleButton);
|
||||||
|
expect(
|
||||||
|
screen.getByRole("button", { name: /expand/i }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Structured Output Results", () => {
|
||||||
|
const toolWithOutputSchema: Tool = {
|
||||||
|
name: "weatherTool",
|
||||||
|
description: "Get weather",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object" as const,
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
outputSchema: {
|
||||||
|
type: "object" as const,
|
||||||
|
properties: {
|
||||||
|
temperature: { type: "number" as const },
|
||||||
|
},
|
||||||
|
required: ["temperature"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it("should display structured content when present", () => {
|
||||||
|
// Cache the tool's output schema so hasOutputSchema returns true
|
||||||
|
cacheToolOutputSchemas([toolWithOutputSchema]);
|
||||||
|
|
||||||
|
const structuredResult = {
|
||||||
|
content: [],
|
||||||
|
structuredContent: {
|
||||||
|
temperature: 25,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
renderToolsTab({
|
||||||
|
selectedTool: toolWithOutputSchema,
|
||||||
|
toolResult: structuredResult,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByText("Structured Content:")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText(/Valid according to output schema/),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show validation error for invalid structured content", () => {
|
||||||
|
cacheToolOutputSchemas([toolWithOutputSchema]);
|
||||||
|
|
||||||
|
const invalidResult = {
|
||||||
|
content: [],
|
||||||
|
structuredContent: {
|
||||||
|
temperature: "25", // String instead of number
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
renderToolsTab({
|
||||||
|
selectedTool: toolWithOutputSchema,
|
||||||
|
toolResult: invalidResult,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByText(/Validation Error:/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show error when tool with output schema doesn't return structured content", () => {
|
||||||
|
cacheToolOutputSchemas([toolWithOutputSchema]);
|
||||||
|
|
||||||
|
const resultWithoutStructured = {
|
||||||
|
content: [{ type: "text", text: "some result" }],
|
||||||
|
// No structuredContent
|
||||||
|
};
|
||||||
|
|
||||||
|
renderToolsTab({
|
||||||
|
selectedTool: toolWithOutputSchema,
|
||||||
|
toolResult: resultWithoutStructured,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByText(
|
||||||
|
/Tool has an output schema but did not return structured content/,
|
||||||
|
),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show unstructured content title when both structured and unstructured exist", () => {
|
||||||
|
cacheToolOutputSchemas([toolWithOutputSchema]);
|
||||||
|
|
||||||
|
const resultWithBoth = {
|
||||||
|
content: [{ type: "text", text: '{"temperature": 25}' }],
|
||||||
|
structuredContent: { temperature: 25 },
|
||||||
|
};
|
||||||
|
|
||||||
|
renderToolsTab({
|
||||||
|
selectedTool: toolWithOutputSchema,
|
||||||
|
toolResult: resultWithBoth,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByText("Structured Content:")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Unstructured Content:")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not show unstructured content title when only unstructured exists", () => {
|
||||||
|
const resultWithUnstructuredOnly = {
|
||||||
|
content: [{ type: "text", text: "some result" }],
|
||||||
|
};
|
||||||
|
|
||||||
|
renderToolsTab({
|
||||||
|
selectedTool: mockTools[0], // Tool without output schema
|
||||||
|
toolResult: resultWithUnstructuredOnly,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.queryByText("Unstructured Content:"),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show compatibility check when tool has output schema", () => {
|
||||||
|
cacheToolOutputSchemas([toolWithOutputSchema]);
|
||||||
|
|
||||||
|
const compatibleResult = {
|
||||||
|
content: [{ type: "text", text: '{"temperature": 25}' }],
|
||||||
|
structuredContent: { temperature: 25 },
|
||||||
|
};
|
||||||
|
|
||||||
|
renderToolsTab({
|
||||||
|
selectedTool: toolWithOutputSchema,
|
||||||
|
toolResult: compatibleResult,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should show compatibility result
|
||||||
|
expect(
|
||||||
|
screen.getByText(
|
||||||
|
/matches structured content|not a single text block|not valid JSON|does not match/,
|
||||||
|
),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not show compatibility check when tool has no output schema", () => {
|
||||||
|
const resultWithBoth = {
|
||||||
|
content: [{ type: "text", text: '{"data": "value"}' }],
|
||||||
|
structuredContent: { different: "data" },
|
||||||
|
};
|
||||||
|
|
||||||
|
renderToolsTab({
|
||||||
|
selectedTool: mockTools[0], // Tool without output schema
|
||||||
|
toolResult: resultWithBoth,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should not show any compatibility messages
|
||||||
|
expect(
|
||||||
|
screen.queryByText(
|
||||||
|
/matches structured content|not a single text block|not valid JSON|does not match/,
|
||||||
|
),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -249,9 +249,16 @@ export function useConnection({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const is401Error = (error: unknown): boolean => {
|
||||||
|
return (
|
||||||
|
(error instanceof SseError && error.code === 401) ||
|
||||||
|
(error instanceof Error && error.message.includes("401")) ||
|
||||||
|
(error instanceof Error && error.message.includes("Unauthorized"))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const handleAuthError = async (error: unknown) => {
|
const handleAuthError = async (error: unknown) => {
|
||||||
if (error instanceof SseError && error.code === 401) {
|
if (is401Error(error)) {
|
||||||
// Create a new auth provider with the current server URL
|
|
||||||
const serverAuthProvider = new InspectorOAuthClientProvider(sseUrl);
|
const serverAuthProvider = new InspectorOAuthClientProvider(sseUrl);
|
||||||
|
|
||||||
const result = await auth(serverAuthProvider, { serverUrl: sseUrl });
|
const result = await auth(serverAuthProvider, { serverUrl: sseUrl });
|
||||||
|
|
@ -337,7 +344,6 @@ export function useConnection({
|
||||||
mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/sse`);
|
mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/sse`);
|
||||||
mcpProxyServerUrl.searchParams.append("url", sseUrl);
|
mcpProxyServerUrl.searchParams.append("url", sseUrl);
|
||||||
transportOptions = {
|
transportOptions = {
|
||||||
authProvider: serverAuthProvider,
|
|
||||||
eventSourceInit: {
|
eventSourceInit: {
|
||||||
fetch: (
|
fetch: (
|
||||||
url: string | URL | globalThis.Request,
|
url: string | URL | globalThis.Request,
|
||||||
|
|
@ -354,7 +360,6 @@ export function useConnection({
|
||||||
mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/mcp`);
|
mcpProxyServerUrl = new URL(`${getMCPProxyAddress(config)}/mcp`);
|
||||||
mcpProxyServerUrl.searchParams.append("url", sseUrl);
|
mcpProxyServerUrl.searchParams.append("url", sseUrl);
|
||||||
transportOptions = {
|
transportOptions = {
|
||||||
authProvider: serverAuthProvider,
|
|
||||||
eventSourceInit: {
|
eventSourceInit: {
|
||||||
fetch: (
|
fetch: (
|
||||||
url: string | URL | globalThis.Request,
|
url: string | URL | globalThis.Request,
|
||||||
|
|
@ -432,13 +437,14 @@ export function useConnection({
|
||||||
`Failed to connect to MCP Server via the MCP Inspector Proxy: ${mcpProxyServerUrl}:`,
|
`Failed to connect to MCP Server via the MCP Inspector Proxy: ${mcpProxyServerUrl}:`,
|
||||||
error,
|
error,
|
||||||
);
|
);
|
||||||
|
|
||||||
const shouldRetry = await handleAuthError(error);
|
const shouldRetry = await handleAuthError(error);
|
||||||
if (shouldRetry) {
|
if (shouldRetry) {
|
||||||
return connect(undefined, retryCount + 1);
|
return connect(undefined, retryCount + 1);
|
||||||
}
|
}
|
||||||
|
if (is401Error(error)) {
|
||||||
if (error instanceof SseError && error.code === 401) {
|
|
||||||
// Don't set error state if we're about to redirect for auth
|
// Don't set error state if we're about to redirect for auth
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,13 @@
|
||||||
import { generateDefaultValue, formatFieldLabel } from "../schemaUtils";
|
import {
|
||||||
|
generateDefaultValue,
|
||||||
|
formatFieldLabel,
|
||||||
|
cacheToolOutputSchemas,
|
||||||
|
getToolOutputValidator,
|
||||||
|
validateToolOutput,
|
||||||
|
hasOutputSchema,
|
||||||
|
} from "../schemaUtils";
|
||||||
import type { JsonSchemaType } from "../jsonUtils";
|
import type { JsonSchemaType } from "../jsonUtils";
|
||||||
|
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
|
||||||
describe("generateDefaultValue", () => {
|
describe("generateDefaultValue", () => {
|
||||||
test("generates default string", () => {
|
test("generates default string", () => {
|
||||||
|
|
@ -137,3 +145,220 @@ describe("formatFieldLabel", () => {
|
||||||
expect(formatFieldLabel("")).toBe("");
|
expect(formatFieldLabel("")).toBe("");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Output Schema Validation", () => {
|
||||||
|
const mockTools: Tool[] = [
|
||||||
|
{
|
||||||
|
name: "weatherTool",
|
||||||
|
description: "Get weather information",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
city: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
outputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
temperature: { type: "number" },
|
||||||
|
humidity: { type: "number" },
|
||||||
|
},
|
||||||
|
required: ["temperature", "humidity"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "noOutputSchema",
|
||||||
|
description: "Tool without output schema",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "complexOutputSchema",
|
||||||
|
description: "Tool with complex output schema",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
outputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
user: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
name: { type: "string" },
|
||||||
|
age: { type: "number" },
|
||||||
|
},
|
||||||
|
required: ["name"],
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
type: "array",
|
||||||
|
items: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["user"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Clear cache before each test
|
||||||
|
cacheToolOutputSchemas([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("cacheToolOutputSchemas", () => {
|
||||||
|
test("caches validators for tools with output schemas", () => {
|
||||||
|
cacheToolOutputSchemas(mockTools);
|
||||||
|
|
||||||
|
expect(hasOutputSchema("weatherTool")).toBe(true);
|
||||||
|
expect(hasOutputSchema("complexOutputSchema")).toBe(true);
|
||||||
|
expect(hasOutputSchema("noOutputSchema")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("clears existing cache when called", () => {
|
||||||
|
cacheToolOutputSchemas(mockTools);
|
||||||
|
expect(hasOutputSchema("weatherTool")).toBe(true);
|
||||||
|
|
||||||
|
cacheToolOutputSchemas([]);
|
||||||
|
expect(hasOutputSchema("weatherTool")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles invalid output schemas gracefully", () => {
|
||||||
|
const toolsWithInvalidSchema: Tool[] = [
|
||||||
|
{
|
||||||
|
name: "invalidSchemaTool",
|
||||||
|
description: "Tool with invalid schema",
|
||||||
|
inputSchema: { type: "object", properties: {} },
|
||||||
|
outputSchema: {
|
||||||
|
// @ts-expect-error Testing with invalid type
|
||||||
|
type: "invalid-type",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Should not throw
|
||||||
|
expect(() =>
|
||||||
|
cacheToolOutputSchemas(toolsWithInvalidSchema),
|
||||||
|
).not.toThrow();
|
||||||
|
expect(hasOutputSchema("invalidSchemaTool")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("validateToolOutput", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cacheToolOutputSchemas(mockTools);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("validates correct structured content", () => {
|
||||||
|
const result = validateToolOutput("weatherTool", {
|
||||||
|
temperature: 25.5,
|
||||||
|
humidity: 60,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.isValid).toBe(true);
|
||||||
|
expect(result.error).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects invalid structured content", () => {
|
||||||
|
const result = validateToolOutput("weatherTool", {
|
||||||
|
temperature: "25.5", // Should be number
|
||||||
|
humidity: 60,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.isValid).toBe(false);
|
||||||
|
expect(result.error).toContain("should be number");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects missing required fields", () => {
|
||||||
|
const result = validateToolOutput("weatherTool", {
|
||||||
|
temperature: 25.5,
|
||||||
|
// Missing humidity
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.isValid).toBe(false);
|
||||||
|
expect(result.error).toContain("required");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("validates complex nested structures", () => {
|
||||||
|
const validResult = validateToolOutput("complexOutputSchema", {
|
||||||
|
user: {
|
||||||
|
name: "John",
|
||||||
|
age: 30,
|
||||||
|
},
|
||||||
|
tags: ["tag1", "tag2"],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(validResult.isValid).toBe(true);
|
||||||
|
|
||||||
|
const invalidResult = validateToolOutput("complexOutputSchema", {
|
||||||
|
user: {
|
||||||
|
// Missing required 'name'
|
||||||
|
age: 30,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(invalidResult.isValid).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns valid for tools without validators", () => {
|
||||||
|
const result = validateToolOutput("nonExistentTool", { any: "data" });
|
||||||
|
|
||||||
|
expect(result.isValid).toBe(true);
|
||||||
|
expect(result.error).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("validates additional properties restriction", () => {
|
||||||
|
const result = validateToolOutput("weatherTool", {
|
||||||
|
temperature: 25.5,
|
||||||
|
humidity: 60,
|
||||||
|
extraField: "should not be here",
|
||||||
|
});
|
||||||
|
|
||||||
|
// This depends on whether additionalProperties is set to false in the schema
|
||||||
|
// If it is, this should fail
|
||||||
|
expect(result.isValid).toBe(true); // By default, additional properties are allowed
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getToolOutputValidator", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cacheToolOutputSchemas(mockTools);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns validator for cached tool", () => {
|
||||||
|
const validator = getToolOutputValidator("weatherTool");
|
||||||
|
expect(validator).toBeDefined();
|
||||||
|
expect(typeof validator).toBe("function");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns undefined for tool without output schema", () => {
|
||||||
|
const validator = getToolOutputValidator("noOutputSchema");
|
||||||
|
expect(validator).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns undefined for non-existent tool", () => {
|
||||||
|
const validator = getToolOutputValidator("nonExistentTool");
|
||||||
|
expect(validator).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("hasOutputSchema", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cacheToolOutputSchemas(mockTools);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns true for tools with output schemas", () => {
|
||||||
|
expect(hasOutputSchema("weatherTool")).toBe(true);
|
||||||
|
expect(hasOutputSchema("complexOutputSchema")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns false for tools without output schemas", () => {
|
||||||
|
expect(hasOutputSchema("noOutputSchema")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns false for non-existent tools", () => {
|
||||||
|
expect(hasOutputSchema("nonExistentTool")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,82 @@
|
||||||
import type { JsonValue, JsonSchemaType, JsonObject } from "./jsonUtils";
|
import type { JsonValue, JsonSchemaType, JsonObject } from "./jsonUtils";
|
||||||
|
import Ajv from "ajv";
|
||||||
|
import type { ValidateFunction } from "ajv";
|
||||||
|
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
|
||||||
|
const ajv = new Ajv();
|
||||||
|
|
||||||
|
// Cache for compiled validators
|
||||||
|
const toolOutputValidators = new Map<string, ValidateFunction>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compiles and caches output schema validators for a list of tools
|
||||||
|
* Following the same pattern as SDK's Client.cacheToolOutputSchemas
|
||||||
|
* @param tools Array of tools that may have output schemas
|
||||||
|
*/
|
||||||
|
export function cacheToolOutputSchemas(tools: Tool[]): void {
|
||||||
|
toolOutputValidators.clear();
|
||||||
|
for (const tool of tools) {
|
||||||
|
if (tool.outputSchema) {
|
||||||
|
try {
|
||||||
|
const validator = ajv.compile(tool.outputSchema);
|
||||||
|
toolOutputValidators.set(tool.name, validator);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
`Failed to compile output schema for tool ${tool.name}:`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the cached output schema validator for a tool
|
||||||
|
* Following the same pattern as SDK's Client.getToolOutputValidator
|
||||||
|
* @param toolName Name of the tool
|
||||||
|
* @returns The compiled validator function, or undefined if not found
|
||||||
|
*/
|
||||||
|
export function getToolOutputValidator(
|
||||||
|
toolName: string,
|
||||||
|
): ValidateFunction | undefined {
|
||||||
|
return toolOutputValidators.get(toolName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates structured content against a tool's output schema
|
||||||
|
* Returns validation result with detailed error messages
|
||||||
|
* @param toolName Name of the tool
|
||||||
|
* @param structuredContent The structured content to validate
|
||||||
|
* @returns An object with isValid boolean and optional error message
|
||||||
|
*/
|
||||||
|
export function validateToolOutput(
|
||||||
|
toolName: string,
|
||||||
|
structuredContent: unknown,
|
||||||
|
): { isValid: boolean; error?: string } {
|
||||||
|
const validator = getToolOutputValidator(toolName);
|
||||||
|
if (!validator) {
|
||||||
|
return { isValid: true }; // No validator means no schema to validate against
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = validator(structuredContent);
|
||||||
|
if (!isValid) {
|
||||||
|
return {
|
||||||
|
isValid: false,
|
||||||
|
error: ajv.errorsText(validator.errors),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isValid: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a tool has an output schema
|
||||||
|
* @param toolName Name of the tool
|
||||||
|
* @returns true if the tool has an output schema
|
||||||
|
*/
|
||||||
|
export function hasOutputSchema(toolName: string): boolean {
|
||||||
|
return toolOutputValidators.has(toolName);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a default value based on a JSON schema type
|
* Generates a default value based on a JSON schema type
|
||||||
|
|
|
||||||
97
package-lock.json
generated
97
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "@modelcontextprotocol/inspector",
|
"name": "@modelcontextprotocol/inspector",
|
||||||
"version": "0.12.0",
|
"version": "0.13.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@modelcontextprotocol/inspector",
|
"name": "@modelcontextprotocol/inspector",
|
||||||
"version": "0.12.0",
|
"version": "0.13.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"client",
|
"client",
|
||||||
|
|
@ -14,10 +14,10 @@
|
||||||
"cli"
|
"cli"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/inspector-cli": "^0.12.0",
|
"@modelcontextprotocol/inspector-cli": "^0.13.0",
|
||||||
"@modelcontextprotocol/inspector-client": "^0.12.0",
|
"@modelcontextprotocol/inspector-client": "^0.13.0",
|
||||||
"@modelcontextprotocol/inspector-server": "^0.12.0",
|
"@modelcontextprotocol/inspector-server": "^0.13.0",
|
||||||
"@modelcontextprotocol/sdk": "^1.11.2",
|
"@modelcontextprotocol/sdk": "^1.11.5",
|
||||||
"concurrently": "^9.0.1",
|
"concurrently": "^9.0.1",
|
||||||
"open": "^10.1.0",
|
"open": "^10.1.0",
|
||||||
"shell-quote": "^1.8.2",
|
"shell-quote": "^1.8.2",
|
||||||
|
|
@ -40,10 +40,10 @@
|
||||||
},
|
},
|
||||||
"cli": {
|
"cli": {
|
||||||
"name": "@modelcontextprotocol/inspector-cli",
|
"name": "@modelcontextprotocol/inspector-cli",
|
||||||
"version": "0.12.0",
|
"version": "0.13.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.11.0",
|
"@modelcontextprotocol/sdk": "^1.11.5",
|
||||||
"commander": "^13.1.0",
|
"commander": "^13.1.0",
|
||||||
"spawn-rx": "^5.1.2"
|
"spawn-rx": "^5.1.2"
|
||||||
},
|
},
|
||||||
|
|
@ -63,10 +63,10 @@
|
||||||
},
|
},
|
||||||
"client": {
|
"client": {
|
||||||
"name": "@modelcontextprotocol/inspector-client",
|
"name": "@modelcontextprotocol/inspector-client",
|
||||||
"version": "0.12.0",
|
"version": "0.13.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.11.0",
|
"@modelcontextprotocol/sdk": "^1.11.5",
|
||||||
"@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",
|
||||||
|
|
@ -77,6 +77,7 @@
|
||||||
"@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",
|
||||||
|
"ajv": "^6.12.6",
|
||||||
"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",
|
||||||
|
|
@ -2004,13 +2005,15 @@
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
"node_modules/@modelcontextprotocol/sdk": {
|
"node_modules/@modelcontextprotocol/sdk": {
|
||||||
"version": "1.11.2",
|
"version": "1.11.5",
|
||||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.2.tgz",
|
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.5.tgz",
|
||||||
"integrity": "sha512-H9vwztj5OAqHg9GockCQC06k1natgcxWQSRpQcPJf6i5+MWBzfKkRtxGbjQf0X2ihii0ffLZCRGbYV2f2bjNCQ==",
|
"integrity": "sha512-gS7Q7IHpKxjVaNLMUZyTtatZ63ca3h418zPPntAhu/MvG5yfz/8HMcDAOpvpQfx3V3dsw9QQxk8RuFNrQhLlgA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"ajv": "^8.17.1",
|
||||||
"content-type": "^1.0.5",
|
"content-type": "^1.0.5",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"cross-spawn": "^7.0.3",
|
"cross-spawn": "^7.0.5",
|
||||||
"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",
|
||||||
|
|
@ -2023,6 +2026,28 @@
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/ajv": {
|
||||||
|
"version": "8.17.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||||
|
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"fast-uri": "^3.0.1",
|
||||||
|
"json-schema-traverse": "^1.0.0",
|
||||||
|
"require-from-string": "^2.0.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
|
|
@ -3978,7 +4003,6 @@
|
||||||
"version": "6.12.6",
|
"version": "6.12.6",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.1",
|
"fast-deep-equal": "^3.1.1",
|
||||||
|
|
@ -5652,7 +5676,6 @@
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fast-glob": {
|
"node_modules/fast-glob": {
|
||||||
|
|
@ -5687,7 +5710,6 @@
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fast-levenshtein": {
|
"node_modules/fast-levenshtein": {
|
||||||
|
|
@ -5697,6 +5719,22 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-uri": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fastify"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/fastify"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/fastq": {
|
"node_modules/fastq": {
|
||||||
"version": "1.19.1",
|
"version": "1.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
|
||||||
|
|
@ -7701,7 +7739,6 @@
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/json-stable-stringify-without-jsonify": {
|
"node_modules/json-stable-stringify-without-jsonify": {
|
||||||
|
|
@ -8407,15 +8444,6 @@
|
||||||
"node": "20 || >=22"
|
"node": "20 || >=22"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/path-to-regexp": {
|
|
||||||
"version": "8.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
|
|
||||||
"integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
|
|
@ -8782,7 +8810,6 @@
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
|
|
@ -9037,6 +9064,15 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/require-from-string": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/requires-port": {
|
"node_modules/requires-port": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||||
|
|
@ -10449,7 +10485,6 @@
|
||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||||
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
|
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
|
|
@ -10933,10 +10968,10 @@
|
||||||
},
|
},
|
||||||
"server": {
|
"server": {
|
||||||
"name": "@modelcontextprotocol/inspector-server",
|
"name": "@modelcontextprotocol/inspector-server",
|
||||||
"version": "0.12.0",
|
"version": "0.13.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.11.0",
|
"@modelcontextprotocol/sdk": "^1.11.5",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"ws": "^8.18.0",
|
"ws": "^8.18.0",
|
||||||
|
|
|
||||||
10
package.json
10
package.json
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@modelcontextprotocol/inspector",
|
"name": "@modelcontextprotocol/inspector",
|
||||||
"version": "0.12.0",
|
"version": "0.13.0",
|
||||||
"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)",
|
||||||
|
|
@ -40,10 +40,10 @@
|
||||||
"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-cli": "^0.12.0",
|
"@modelcontextprotocol/inspector-cli": "^0.13.0",
|
||||||
"@modelcontextprotocol/inspector-client": "^0.12.0",
|
"@modelcontextprotocol/inspector-client": "^0.13.0",
|
||||||
"@modelcontextprotocol/inspector-server": "^0.12.0",
|
"@modelcontextprotocol/inspector-server": "^0.13.0",
|
||||||
"@modelcontextprotocol/sdk": "^1.11.2",
|
"@modelcontextprotocol/sdk": "^1.11.5",
|
||||||
"concurrently": "^9.0.1",
|
"concurrently": "^9.0.1",
|
||||||
"open": "^10.1.0",
|
"open": "^10.1.0",
|
||||||
"shell-quote": "^1.8.2",
|
"shell-quote": "^1.8.2",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@modelcontextprotocol/inspector-server",
|
"name": "@modelcontextprotocol/inspector-server",
|
||||||
"version": "0.12.0",
|
"version": "0.13.0",
|
||||||
"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.11.0",
|
"@modelcontextprotocol/sdk": "^1.11.5",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"ws": "^8.18.0",
|
"ws": "^8.18.0",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
||||||
|
import { isJSONRPCRequest } from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
|
||||||
function onClientError(error: Error) {
|
function onClientError(error: Error) {
|
||||||
console.error("Error from inspector client:", error);
|
console.error("Error from inspector client:", error);
|
||||||
|
|
@ -19,7 +20,21 @@ export default function mcpProxy({
|
||||||
let transportToServerClosed = false;
|
let transportToServerClosed = false;
|
||||||
|
|
||||||
transportToClient.onmessage = (message) => {
|
transportToClient.onmessage = (message) => {
|
||||||
transportToServer.send(message).catch(onServerError);
|
transportToServer.send(message).catch((error) => {
|
||||||
|
// Send error response back to client if it was a request (has id) and connection is still open
|
||||||
|
if (isJSONRPCRequest(message) && !transportToClientClosed) {
|
||||||
|
const errorResponse = {
|
||||||
|
jsonrpc: "2.0" as const,
|
||||||
|
id: message.id,
|
||||||
|
error: {
|
||||||
|
code: -32001,
|
||||||
|
message: error.message,
|
||||||
|
data: error,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
transportToClient.send(errorResponse).catch(onClientError);
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
transportToServer.onmessage = (message) => {
|
transportToServer.onmessage = (message) => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue