From de84c472ce2862abc77b7e9e98377b6583785adb Mon Sep 17 00:00:00 2001 From: Basil Hosmer Date: Tue, 27 May 2025 16:39:39 -0400 Subject: [PATCH 1/6] feat: Add structured output support to browser UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds comprehensive support for structured output validation in tool calls, following the MCP specification for tools with output schemas. UI Changes: 1. Output Schema Display: - Added collapsible output schema section in ToolsTab - Shows output schemas after input fields, before the Run Tool button - Default view shows 8 lines with scrolling, expandable to full view - Expand/Collapse button with chevron icons for better UX 2. Structured Content Display: - New "Structured Content" section when tools return structuredContent - Shows structured data in a formatted JSON view - Validation status indicator (green checkmark for valid, red X for errors) - Detailed validation error messages when content doesn't match schema 3. Unstructured Content Labeling: - Added "Unstructured Content" heading when both structured and unstructured content exist - Only shows label when structured content is also present - Maintains clean UI when only unstructured content exists 4. Compatibility Checking: - Checks if unstructured content matches structured content (when output schema exists) - Shows compatibility status with blue (compatible) or yellow (incompatible) indicators - Detailed messages explain why content doesn't match - Only runs compatibility check for tools with output schemas 5. Validation Error Handling: - Shows error when tool with output schema doesn't return structured content - Clear error messages for schema validation failures - Maintains proper error state display Technical Implementation: - Added schema validation utilities using Ajv (same as SDK) - Caches compiled validators for performance - Validates on tool result display, not during the call - Follows SDK's Client.callTool validation pattern 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- client/src/App.tsx | 5 + client/src/components/ToolsTab.tsx | 170 +++++++++++++++++++++++++++-- client/src/utils/schemaUtils.ts | 74 +++++++++++++ 3 files changed, 242 insertions(+), 7 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 68065e24..cb411452 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -20,6 +20,7 @@ import { import { OAuthTokensSchema } from "@modelcontextprotocol/sdk/shared/auth.js"; import { SESSION_KEYS, getServerSpecificKey } from "./lib/constants"; import { AuthDebuggerState } from "./lib/auth-types"; +import { cacheToolOutputSchemas } from "./utils/schemaUtils"; import React, { Suspense, useCallback, @@ -473,6 +474,8 @@ const App = () => { ); setTools(response.tools); setNextToolCursor(response.nextCursor); + // Cache output schemas for validation + cacheToolOutputSchemas(response.tools); }; const callTool = async (name: string, params: Record) => { @@ -759,6 +762,8 @@ const App = () => { clearTools={() => { setTools([]); setNextToolCursor(undefined); + // Clear cached output schemas + cacheToolOutputSchemas([]); }} callTool={async (name, params) => { clearError("tools"); diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index aa67bfcf..4f4e235f 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -7,14 +7,14 @@ import { TabsContent } from "@/components/ui/tabs"; import { Textarea } from "@/components/ui/textarea"; import DynamicJsonForm from "./DynamicJsonForm"; import type { JsonValue, JsonSchemaType } from "@/utils/jsonUtils"; -import { generateDefaultValue } from "@/utils/schemaUtils"; +import { generateDefaultValue, validateToolOutput, hasOutputSchema } from "@/utils/schemaUtils"; import { CallToolResultSchema, CompatibilityCallToolResult, ListToolsResult, Tool, } 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 ListPane from "./ListPane"; import JsonView from "./JsonView"; @@ -41,6 +41,7 @@ const ToolsTab = ({ }) => { const [params, setParams] = useState>({}); const [isToolRunning, setIsToolRunning] = useState(false); + const [isOutputSchemaExpanded, setIsOutputSchemaExpanded] = useState(false); useEffect(() => { const params = Object.entries( @@ -52,6 +53,53 @@ const ToolsTab = ({ setParams(Object.fromEntries(params)); }, [selectedTool]); + // Check compatibility between structured and unstructured content + const checkContentCompatibility = ( + structuredContent: unknown, + unstructuredContent: Array<{ type: string; text?: string; [key: string]: unknown }> + ): { isCompatible: boolean; message: string } => { + // Check if unstructured content is a single text block + 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 { + // Try to parse the text as JSON + const parsedContent = JSON.parse(textContent); + + // Deep equality check + 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 (e) { + return { + isCompatible: false, + message: "Unstructured content is not valid JSON" + }; + } + }; + const renderToolResult = () => { if (!toolResult) return null; @@ -72,6 +120,36 @@ const ToolsTab = ({ const structuredResult = parsedResult.data; const isError = structuredResult.isError ?? false; + // Validate structured content if present and tool has output schema + let validationResult = null; + const toolHasOutputSchema = selectedTool && hasOutputSchema(selectedTool.name); + + if (toolHasOutputSchema) { + if (!structuredResult.structuredContent && !isError) { + // Tool has output schema but didn't return structured content (and it's not an error) + validationResult = { + isValid: false, + error: "Tool has an output schema but did not return structured content" + }; + } else if (structuredResult.structuredContent) { + // Validate the structured content + validationResult = validateToolOutput(selectedTool.name, structuredResult.structuredContent); + } + } + + // Check compatibility if both structured and unstructured content exist + // AND the tool has an output schema + let compatibilityResult = null; + if (structuredResult.structuredContent && + structuredResult.content.length > 0 && + selectedTool && + hasOutputSchema(selectedTool.name)) { + compatibilityResult = checkContentCompatibility( + structuredResult.structuredContent, + structuredResult.content + ); + } + return ( <>

@@ -82,11 +160,57 @@ const ToolsTab = ({ Success )}

- {structuredResult.content.map((item, index) => ( -
- {item.type === "text" && ( - + {structuredResult.structuredContent && ( +
+
Structured Content:
+
+ + {validationResult && ( +
+ {validationResult.isValid ? ( + "✓ Valid according to output schema" + ) : ( + <> + ✗ Validation Error: {validationResult.error} + + )} +
+ )} +
+
+ )} + {!structuredResult.structuredContent && validationResult && !validationResult.isValid && ( +
+
+ ✗ Validation Error: {validationResult.error} +
+
+ )} + {structuredResult.content.length > 0 && ( +
+ {structuredResult.structuredContent && ( + <> +
Unstructured Content:
+ {compatibilityResult && ( +
+ {compatibilityResult.isCompatible ? "✓" : "⚠"} {compatibilityResult.message} +
+ )} + )} + {structuredResult.content.map((item, index) => ( +
+ {item.type === "text" && ( + + )} {item.type === "image" && ( ))} +
+ ))}
- ))} + )} ); } else if ("toolResult" in toolResult) { @@ -262,6 +388,36 @@ const ToolsTab = ({ ); }, )} + {selectedTool.outputSchema && ( +
+
+

Output Schema:

+ +
+
+ +
+
+ )}
-
+
diff --git a/client/src/utils/schemaUtils.ts b/client/src/utils/schemaUtils.ts index db620e28..d2440875 100644 --- a/client/src/utils/schemaUtils.ts +++ b/client/src/utils/schemaUtils.ts @@ -22,7 +22,10 @@ export function cacheToolOutputSchemas(tools: Tool[]): void { 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); + console.warn( + `Failed to compile output schema for tool ${tool.name}:`, + error, + ); } } } @@ -34,7 +37,9 @@ export function cacheToolOutputSchemas(tools: Tool[]): void { * @param toolName Name of the tool * @returns The compiled validator function, or undefined if not found */ -export function getToolOutputValidator(toolName: string): ValidateFunction | undefined { +export function getToolOutputValidator( + toolName: string, +): ValidateFunction | undefined { return toolOutputValidators.get(toolName); } @@ -47,7 +52,7 @@ export function getToolOutputValidator(toolName: string): ValidateFunction | und */ export function validateToolOutput( toolName: string, - structuredContent: unknown + structuredContent: unknown, ): { isValid: boolean; error?: string } { const validator = getToolOutputValidator(toolName); if (!validator) { From e7e47454641535b4c936b5b15543d554f0119b0f Mon Sep 17 00:00:00 2001 From: Basil Hosmer Date: Tue, 27 May 2025 19:19:40 -0400 Subject: [PATCH 4/6] fix: Fix TypeScript build errors in ToolsTab tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous test implementation caused build failures due to Jest hoisting issues with mock variables. This commit fixes the build by removing mocking in favor of using real schema validation. - Remove jest.mock() of schemaUtils module that caused initialization errors - Add beforeEach to clear schema cache between tests - Use cacheToolOutputSchemas() to set up test scenarios - Let real validation logic run for more accurate tests - All tests pass and build succeeds 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../components/__tests__/ToolsTab.test.tsx | 50 ++++--------------- 1 file changed, 10 insertions(+), 40 deletions(-) diff --git a/client/src/components/__tests__/ToolsTab.test.tsx b/client/src/components/__tests__/ToolsTab.test.tsx index 828808bb..c9f9b315 100644 --- a/client/src/components/__tests__/ToolsTab.test.tsx +++ b/client/src/components/__tests__/ToolsTab.test.tsx @@ -1,36 +1,15 @@ import { render, screen, fireEvent, act } from "@testing-library/react"; -import { describe, it, expect, jest } from "@jest/globals"; import "@testing-library/jest-dom"; +import { describe, it, jest, beforeEach } from "@jest/globals"; import ToolsTab from "../ToolsTab"; import { Tool } from "@modelcontextprotocol/sdk/types.js"; import { Tabs } from "@/components/ui/tabs"; -import * as schemaUtils from "@/utils/schemaUtils"; - -// Mock the schemaUtils module -// Note: hasOutputSchema checks if a tool's output schema validator has been compiled and cached -// by cacheToolOutputSchemas. In these tests, we mock it to avoid needing to call -// cacheToolOutputSchemas for every test that uses tools with output schemas. -// This keeps the tests focused on the component's behavior rather than schema compilation. -jest.mock("@/utils/schemaUtils", () => ({ - ...jest.requireActual("@/utils/schemaUtils"), - hasOutputSchema: jest.fn(), - validateToolOutput: jest.fn(), -})); +import { cacheToolOutputSchemas } from "@/utils/schemaUtils"; describe("ToolsTab", () => { beforeEach(() => { - jest.clearAllMocks(); - // Reset to default behavior - (schemaUtils.hasOutputSchema as jest.Mock).mockImplementation( - (toolName) => { - // Only tools with outputSchema property should return true - return false; - }, - ); - (schemaUtils.validateToolOutput as jest.Mock).mockReturnValue({ - isValid: true, - error: null, - }); + // Clear the output schema cache before each test + cacheToolOutputSchemas([]); }); const mockTools: Tool[] = [ @@ -250,8 +229,8 @@ describe("ToolsTab", () => { }; it("should display structured content when present", () => { - // Mock hasOutputSchema to return true for this tool - (schemaUtils.hasOutputSchema as jest.Mock).mockReturnValue(true); + // Cache the tool's output schema so hasOutputSchema returns true + cacheToolOutputSchemas([toolWithOutputSchema]); const structuredResult = { content: [], @@ -272,13 +251,7 @@ describe("ToolsTab", () => { }); it("should show validation error for invalid structured content", () => { - // Mock hasOutputSchema to return true for this tool - (schemaUtils.hasOutputSchema as jest.Mock).mockReturnValue(true); - // Mock the validation to fail - (schemaUtils.validateToolOutput as jest.Mock).mockReturnValue({ - isValid: false, - error: "temperature must be number", - }); + cacheToolOutputSchemas([toolWithOutputSchema]); const invalidResult = { content: [], @@ -296,8 +269,7 @@ describe("ToolsTab", () => { }); it("should show error when tool with output schema doesn't return structured content", () => { - // Mock hasOutputSchema to return true for this tool - (schemaUtils.hasOutputSchema as jest.Mock).mockReturnValue(true); + cacheToolOutputSchemas([toolWithOutputSchema]); const resultWithoutStructured = { content: [{ type: "text", text: "some result" }], @@ -317,8 +289,7 @@ describe("ToolsTab", () => { }); it("should show unstructured content title when both structured and unstructured exist", () => { - // Mock hasOutputSchema to return true for this tool - (schemaUtils.hasOutputSchema as jest.Mock).mockReturnValue(true); + cacheToolOutputSchemas([toolWithOutputSchema]); const resultWithBoth = { content: [{ type: "text", text: '{"temperature": 25}' }], @@ -350,8 +321,7 @@ describe("ToolsTab", () => { }); it("should show compatibility check when tool has output schema", () => { - // Mock hasOutputSchema to return true for this tool - (schemaUtils.hasOutputSchema as jest.Mock).mockReturnValue(true); + cacheToolOutputSchemas([toolWithOutputSchema]); const compatibleResult = { content: [{ type: "text", text: '{"temperature": 25}' }], From 5a628c7a00e7efcedd8b9351b907f8a2f81cbd48 Mon Sep 17 00:00:00 2001 From: Basil Hosmer Date: Tue, 27 May 2025 19:35:43 -0400 Subject: [PATCH 5/6] fix: Fix lint errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove unused catch parameter in ToolsTab.tsx - Replace 'as any' with @ts-expect-error in test for invalid schema 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- client/src/components/ToolsTab.tsx | 2 +- client/src/utils/__tests__/schemaUtils.test.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index ecadc8b9..01ea25f7 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -104,7 +104,7 @@ const ToolsTab = ({ message: "Parsed JSON does not match structured content", }; } - } catch (e) { + } catch { return { isCompatible: false, message: "Unstructured content is not valid JSON", diff --git a/client/src/utils/__tests__/schemaUtils.test.ts b/client/src/utils/__tests__/schemaUtils.test.ts index f908bf51..ca6b65ca 100644 --- a/client/src/utils/__tests__/schemaUtils.test.ts +++ b/client/src/utils/__tests__/schemaUtils.test.ts @@ -231,7 +231,8 @@ describe("Output Schema Validation", () => { description: "Tool with invalid schema", inputSchema: { type: "object", properties: {} }, outputSchema: { - type: "invalid-type" as any, + // @ts-expect-error Testing with invalid type + type: "invalid-type", }, }, ]; From d8a2821344d1d06225c01a12bf13d91863fe20b3 Mon Sep 17 00:00:00 2001 From: Basil Hosmer Date: Wed, 28 May 2025 22:18:19 -0400 Subject: [PATCH 6/6] refactor: Address PR comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract tool results rendering into separate ToolResults component - Hoist checkContentCompatibility function to module level following codebase conventions - Add ajv@6.12.6 as direct dependency to client package.json - All tests pass and linting is clean 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- client/package.json | 1 + client/src/components/ToolResults.tsx | 221 ++++++++++++++++++++++++ client/src/components/ToolsTab.tsx | 231 +------------------------- client/src/utils/schemaUtils.ts | 1 - package-lock.json | 6 +- 5 files changed, 229 insertions(+), 231 deletions(-) create mode 100644 client/src/components/ToolResults.tsx diff --git a/client/package.json b/client/package.json index 5b5ec19a..ee54df3d 100644 --- a/client/package.json +++ b/client/package.json @@ -25,6 +25,7 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.11.5", "@radix-ui/react-checkbox": "^1.1.4", + "ajv": "^6.12.6", "@radix-ui/react-dialog": "^1.1.3", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.1.0", diff --git a/client/src/components/ToolResults.tsx b/client/src/components/ToolResults.tsx new file mode 100644 index 00000000..c6d90700 --- /dev/null +++ b/client/src/components/ToolResults.tsx @@ -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 ( + <> +

Invalid Tool Result:

+ +

Errors:

+ {parsedResult.error.errors.map((error, 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 ( + <> +

+ Tool Result:{" "} + {isError ? ( + Error + ) : ( + Success + )} +

+ {structuredResult.structuredContent && ( +
+
Structured Content:
+
+ + {validationResult && ( +
+ {validationResult.isValid ? ( + "✓ Valid according to output schema" + ) : ( + <>✗ Validation Error: {validationResult.error} + )} +
+ )} +
+
+ )} + {!structuredResult.structuredContent && + validationResult && + !validationResult.isValid && ( +
+
+ ✗ Validation Error: {validationResult.error} +
+
+ )} + {structuredResult.content.length > 0 && ( +
+ {structuredResult.structuredContent && ( + <> +
+ Unstructured Content: +
+ {compatibilityResult && ( +
+ {compatibilityResult.isCompatible ? "✓" : "⚠"}{" "} + {compatibilityResult.message} +
+ )} + + )} + {structuredResult.content.map((item, index) => ( +
+ {item.type === "text" && ( + + )} + {item.type === "image" && ( + Tool result image + )} + {item.type === "resource" && + (item.resource?.mimeType?.startsWith("audio/") ? ( + + ) : ( + + ))} +
+ ))} +
+ )} + + ); + } else if ("toolResult" in toolResult) { + return ( + <> +

Tool Result (Legacy):

+ + + ); + } + + return null; +}; + +export default ToolResults; diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index 01ea25f7..8c72bd2a 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -7,13 +7,8 @@ import { TabsContent } from "@/components/ui/tabs"; import { Textarea } from "@/components/ui/textarea"; import DynamicJsonForm from "./DynamicJsonForm"; import type { JsonValue, JsonSchemaType } from "@/utils/jsonUtils"; +import { generateDefaultValue } from "@/utils/schemaUtils"; import { - generateDefaultValue, - validateToolOutput, - hasOutputSchema, -} from "@/utils/schemaUtils"; -import { - CallToolResultSchema, CompatibilityCallToolResult, ListToolsResult, Tool, @@ -22,6 +17,7 @@ import { Loader2, Send, ChevronDown, ChevronUp } from "lucide-react"; import { useEffect, useState } from "react"; import ListPane from "./ListPane"; import JsonView from "./JsonView"; +import ToolResults from "./ToolResults"; const ToolsTab = ({ tools, @@ -57,224 +53,6 @@ const ToolsTab = ({ setParams(Object.fromEntries(params)); }, [selectedTool]); - // Check compatibility between structured and unstructured content - const checkContentCompatibility = ( - structuredContent: unknown, - unstructuredContent: Array<{ - type: string; - text?: string; - [key: string]: unknown; - }>, - ): { isCompatible: boolean; message: string } => { - // Check if unstructured content is a single text block - 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 { - // Try to parse the text as JSON - const parsedContent = JSON.parse(textContent); - - // Deep equality check - 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 renderToolResult = () => { - if (!toolResult) return null; - - if ("content" in toolResult) { - const parsedResult = CallToolResultSchema.safeParse(toolResult); - if (!parsedResult.success) { - return ( - <> -

Invalid Tool Result:

- -

Errors:

- {parsedResult.error.errors.map((error, idx) => ( - - ))} - - ); - } - const structuredResult = parsedResult.data; - const isError = structuredResult.isError ?? false; - - // Validate structured content if present and tool has output schema - let validationResult = null; - const toolHasOutputSchema = - selectedTool && hasOutputSchema(selectedTool.name); - - if (toolHasOutputSchema) { - if (!structuredResult.structuredContent && !isError) { - // Tool has output schema but didn't return structured content (and it's not an error) - validationResult = { - isValid: false, - error: - "Tool has an output schema but did not return structured content", - }; - } else if (structuredResult.structuredContent) { - // Validate the structured content - validationResult = validateToolOutput( - selectedTool.name, - structuredResult.structuredContent, - ); - } - } - - // Check compatibility if both structured and unstructured content exist - // AND the tool has an output schema - let compatibilityResult = null; - if ( - structuredResult.structuredContent && - structuredResult.content.length > 0 && - selectedTool && - hasOutputSchema(selectedTool.name) - ) { - compatibilityResult = checkContentCompatibility( - structuredResult.structuredContent, - structuredResult.content, - ); - } - - return ( - <> -

- Tool Result:{" "} - {isError ? ( - Error - ) : ( - Success - )} -

- {structuredResult.structuredContent && ( -
-
- Structured Content: -
-
- - {validationResult && ( -
- {validationResult.isValid ? ( - "✓ Valid according to output schema" - ) : ( - <>✗ Validation Error: {validationResult.error} - )} -
- )} -
-
- )} - {!structuredResult.structuredContent && - validationResult && - !validationResult.isValid && ( -
-
- ✗ Validation Error: {validationResult.error} -
-
- )} - {structuredResult.content.length > 0 && ( -
- {structuredResult.structuredContent && ( - <> -
- Unstructured Content: -
- {compatibilityResult && ( -
- {compatibilityResult.isCompatible ? "✓" : "⚠"}{" "} - {compatibilityResult.message} -
- )} - - )} - {structuredResult.content.map((item, index) => ( -
- {item.type === "text" && ( - - )} - {item.type === "image" && ( - Tool result image - )} - {item.type === "resource" && - (item.resource?.mimeType?.startsWith("audio/") ? ( - - ) : ( - - ))} -
- ))} -
- )} - - ); - } else if ("toolResult" in toolResult) { - return ( - <> -

Tool Result (Legacy):

- - - - ); - } - }; - return (
@@ -475,7 +253,10 @@ const ToolsTab = ({ )} - {toolResult && renderToolResult()} +
) : ( diff --git a/client/src/utils/schemaUtils.ts b/client/src/utils/schemaUtils.ts index d2440875..30210532 100644 --- a/client/src/utils/schemaUtils.ts +++ b/client/src/utils/schemaUtils.ts @@ -3,7 +3,6 @@ import Ajv from "ajv"; import type { ValidateFunction } from "ajv"; import type { Tool } from "@modelcontextprotocol/sdk/types.js"; -// Create a single Ajv instance following the SDK pattern const ajv = new Ajv(); // Cache for compiled validators diff --git a/package-lock.json b/package-lock.json index 0274b038..2920ca59 100644 --- a/package-lock.json +++ b/package-lock.json @@ -77,6 +77,7 @@ "@radix-ui/react-tabs": "^1.1.1", "@radix-ui/react-toast": "^1.2.6", "@radix-ui/react-tooltip": "^1.1.8", + "ajv": "^6.12.6", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cmdk": "^1.0.4", @@ -4002,7 +4003,6 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -5710,7 +5710,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { @@ -7740,7 +7739,6 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { @@ -8812,7 +8810,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -10488,7 +10485,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0"