diff --git a/client/src/components/ToolResults.tsx b/client/src/components/ToolResults.tsx
index c6d90700..661507aa 100644
--- a/client/src/components/ToolResults.tsx
+++ b/client/src/components/ToolResults.tsx
@@ -19,46 +19,44 @@ const checkContentCompatibility = (
[key: string]: unknown;
}>,
): { isCompatible: boolean; message: string } => {
- if (
- unstructuredContent.length !== 1 ||
- unstructuredContent[0].type !== "text"
- ) {
+ // Look for at least one text content block that matches the structured content
+ const textBlocks = unstructuredContent.filter(block => block.type === "text");
+
+ if (textBlocks.length === 0) {
return {
isCompatible: false,
- message: "Unstructured content is not a single text block",
+ message: "No text content blocks found to match structured content",
};
}
- 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",
- };
+ // Check if any text block contains JSON that matches the structured content
+ for (const textBlock of textBlocks) {
+ const textContent = textBlock.text;
+ if (!textContent) {
+ continue;
+ }
+
+ try {
+ const parsedContent = JSON.parse(textContent);
+ const isEqual =
+ JSON.stringify(parsedContent) === JSON.stringify(structuredContent);
+
+ if (isEqual) {
+ return {
+ isCompatible: true,
+ message: `Found matching JSON content (${textBlocks.length > 1 ? 'among multiple text blocks' : 'in single text block'})${unstructuredContent.length > textBlocks.length ? ' with additional content blocks' : ''}`,
+ };
+ }
+ } catch {
+ // Continue to next text block if this one doesn't parse as JSON
+ continue;
}
- } catch {
- return {
- isCompatible: false,
- message: "Unstructured content is not valid JSON",
- };
}
+
+ return {
+ isCompatible: false,
+ message: "No text content block contains JSON matching structured content",
+ };
};
const ToolResults = ({ toolResult, selectedTool }: ToolResultsProps) => {
diff --git a/client/src/components/__tests__/ToolResults.test.tsx b/client/src/components/__tests__/ToolResults.test.tsx
new file mode 100644
index 00000000..be4de4ec
--- /dev/null
+++ b/client/src/components/__tests__/ToolResults.test.tsx
@@ -0,0 +1,185 @@
+import { render, screen } from "@testing-library/react";
+import "@testing-library/jest-dom";
+import { describe, it, beforeEach } from "@jest/globals";
+import ToolResults from "../ToolResults";
+import { Tool } from "@modelcontextprotocol/sdk/types.js";
+import { cacheToolOutputSchemas } from "@/utils/schemaUtils";
+
+describe("ToolResults", () => {
+ const mockTool: Tool = {
+ name: "testTool",
+ description: "Test tool",
+ inputSchema: {
+ type: "object",
+ properties: {},
+ },
+ outputSchema: {
+ type: "object",
+ properties: {
+ result: { type: "string" },
+ },
+ required: ["result"],
+ },
+ };
+
+ beforeEach(() => {
+ cacheToolOutputSchemas([mockTool]);
+ });
+
+ describe("Content Compatibility Validation", () => {
+ it("should accept single text block with matching JSON", () => {
+ const toolResult = {
+ content: [{ type: "text", text: '{"result": "success"}' }],
+ structuredContent: { result: "success" },
+ };
+
+ render();
+
+ expect(screen.getByText(/Found matching JSON content.*in single text block/)).toBeInTheDocument();
+ });
+
+ it("should accept multiple text blocks with one matching JSON", () => {
+ const toolResult = {
+ content: [
+ { type: "text", text: "Processing..." },
+ { type: "text", text: '{"result": "success"}' },
+ { type: "text", text: "Done!" },
+ ],
+ structuredContent: { result: "success" },
+ };
+
+ render();
+
+ expect(screen.getByText(/Found matching JSON content.*among multiple text blocks/)).toBeInTheDocument();
+ });
+
+ it("should accept mixed content types with matching JSON", () => {
+ const toolResult = {
+ content: [
+ { type: "text", text: "Result:" },
+ { type: "text", text: '{"result": "success"}' },
+ { type: "image", data: "base64data", mimeType: "image/png" },
+ { type: "resource", resource: { uri: "file://test.txt" } },
+ ],
+ structuredContent: { result: "success" },
+ };
+
+ render();
+
+ expect(screen.getByText(/Found matching JSON content.*with additional content blocks/)).toBeInTheDocument();
+ });
+
+ it("should reject when no text blocks are present", () => {
+ const toolResult = {
+ content: [
+ { type: "image", data: "base64data", mimeType: "image/png" },
+ ],
+ structuredContent: { result: "success" },
+ };
+
+ render();
+
+ expect(screen.getByText(/No text content blocks found to match structured content/)).toBeInTheDocument();
+ });
+
+ it("should reject when no text blocks contain matching JSON", () => {
+ const toolResult = {
+ content: [
+ { type: "text", text: "Some text" },
+ { type: "text", text: '{"different": "data"}' },
+ ],
+ structuredContent: { result: "success" },
+ };
+
+ render();
+
+ expect(screen.getByText(/No text content block contains JSON matching structured content/)).toBeInTheDocument();
+ });
+
+ it("should reject when text blocks contain invalid JSON", () => {
+ const toolResult = {
+ content: [
+ { type: "text", text: "Not JSON" },
+ { type: "text", text: '{"invalid": json}' },
+ ],
+ structuredContent: { result: "success" },
+ };
+
+ render();
+
+ expect(screen.getByText(/No text content block contains JSON matching structured content/)).toBeInTheDocument();
+ });
+
+ it("should handle empty text blocks gracefully", () => {
+ const toolResult = {
+ content: [
+ { type: "text", text: "" },
+ { type: "text", text: '{"result": "success"}' },
+ ],
+ structuredContent: { result: "success" },
+ };
+
+ render();
+
+ expect(screen.getByText(/Found matching JSON content.*among multiple text blocks/)).toBeInTheDocument();
+ });
+
+ it("should not show compatibility check when tool has no output schema", () => {
+ const toolWithoutSchema: Tool = {
+ name: "noSchemaTool",
+ description: "Tool without schema",
+ inputSchema: {
+ type: "object",
+ properties: {},
+ },
+ };
+
+ const toolResult = {
+ content: [{ type: "text", text: '{"any": "data"}' }],
+ structuredContent: { any: "data" },
+ };
+
+ render();
+
+ // Should not show any compatibility messages
+ expect(screen.queryByText(/Found matching JSON content/)).not.toBeInTheDocument();
+ expect(screen.queryByText(/No text content blocks found/)).not.toBeInTheDocument();
+ expect(screen.queryByText(/No text content block contains JSON/)).not.toBeInTheDocument();
+ });
+ });
+
+ describe("Structured Content Validation", () => {
+ it("should show validation success for valid structured content", () => {
+ const toolResult = {
+ content: [],
+ structuredContent: { result: "success" },
+ };
+
+ render();
+
+ expect(screen.getByText(/Valid according to output schema/)).toBeInTheDocument();
+ });
+
+ it("should show validation error for invalid structured content", () => {
+ const toolResult = {
+ content: [],
+ structuredContent: { result: 123 }, // Should be string
+ };
+
+ render();
+
+ expect(screen.getByText(/Validation Error:/)).toBeInTheDocument();
+ });
+
+ it("should show error when structured content is missing for tool with output schema", () => {
+ const toolResult = {
+ content: [{ type: "text", text: "Some result" }],
+ // No structuredContent
+ };
+
+ render();
+
+ expect(screen.getByText(/Tool has an output schema but did not return structured content/)).toBeInTheDocument();
+ });
+ });
+});
\ No newline at end of file
diff --git a/client/src/components/__tests__/ToolsTab.test.tsx b/client/src/components/__tests__/ToolsTab.test.tsx
index dc353ba5..bf8b1294 100644
--- a/client/src/components/__tests__/ToolsTab.test.tsx
+++ b/client/src/components/__tests__/ToolsTab.test.tsx
@@ -357,7 +357,104 @@ describe("ToolsTab", () => {
// Should show compatibility result
expect(
screen.getByText(
- /matches structured content|not a single text block|not valid JSON|does not match/,
+ /Found matching JSON content/,
+ ),
+ ).toBeInTheDocument();
+ });
+
+ it("should accept multiple content blocks with structured output", () => {
+ cacheToolOutputSchemas([toolWithOutputSchema]);
+
+ const multipleBlocksResult = {
+ content: [
+ { type: "text", text: "Here is the weather data:" },
+ { type: "text", text: '{"temperature": 25}' },
+ { type: "text", text: "Have a nice day!" },
+ ],
+ structuredContent: { temperature: 25 },
+ };
+
+ renderToolsTab({
+ selectedTool: toolWithOutputSchema,
+ toolResult: multipleBlocksResult,
+ });
+
+ // Should show compatible result with multiple blocks
+ expect(
+ screen.getByText(
+ /Found matching JSON content.*among multiple text blocks/,
+ ),
+ ).toBeInTheDocument();
+ });
+
+ it("should accept mixed content types with structured output", () => {
+ cacheToolOutputSchemas([toolWithOutputSchema]);
+
+ const mixedContentResult = {
+ content: [
+ { type: "text", text: "Weather report:" },
+ { type: "text", text: '{"temperature": 25}' },
+ { type: "image", data: "base64data", mimeType: "image/png" },
+ ],
+ structuredContent: { temperature: 25 },
+ };
+
+ renderToolsTab({
+ selectedTool: toolWithOutputSchema,
+ toolResult: mixedContentResult,
+ });
+
+ // Should show compatible result with additional content blocks
+ expect(
+ screen.getByText(
+ /Found matching JSON content.*with additional content blocks/,
+ ),
+ ).toBeInTheDocument();
+ });
+
+ it("should reject when no text blocks match structured content", () => {
+ cacheToolOutputSchemas([toolWithOutputSchema]);
+
+ const noMatchResult = {
+ content: [
+ { type: "text", text: "Some text" },
+ { type: "text", text: '{"humidity": 60}' }, // Different structure
+ ],
+ structuredContent: { temperature: 25 },
+ };
+
+ renderToolsTab({
+ selectedTool: toolWithOutputSchema,
+ toolResult: noMatchResult,
+ });
+
+ // Should show incompatible result
+ expect(
+ screen.getByText(
+ /No text content block contains JSON matching structured content/,
+ ),
+ ).toBeInTheDocument();
+ });
+
+ it("should reject when no text blocks are present", () => {
+ cacheToolOutputSchemas([toolWithOutputSchema]);
+
+ const noTextBlocksResult = {
+ content: [
+ { type: "image", data: "base64data", mimeType: "image/png" },
+ ],
+ structuredContent: { temperature: 25 },
+ };
+
+ renderToolsTab({
+ selectedTool: toolWithOutputSchema,
+ toolResult: noTextBlocksResult,
+ });
+
+ // Should show incompatible result
+ expect(
+ screen.getByText(
+ /No text content blocks found to match structured content/,
),
).toBeInTheDocument();
});
@@ -376,7 +473,7 @@ describe("ToolsTab", () => {
// Should not show any compatibility messages
expect(
screen.queryByText(
- /matches structured content|not a single text block|not valid JSON|does not match/,
+ /Found matching JSON content|No text content blocks found|No text content block contains JSON/,
),
).not.toBeInTheDocument();
});