Skip to content

Commit cbb3ebc

Browse files
Enhance content compatibility checks for tool results with multiple blocks
Co-authored-by: me <[email protected]>
1 parent 38bead3 commit cbb3ebc

File tree

3 files changed

+313
-33
lines changed

3 files changed

+313
-33
lines changed

client/src/components/ToolResults.tsx

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -19,46 +19,44 @@ const checkContentCompatibility = (
1919
[key: string]: unknown;
2020
}>,
2121
): { isCompatible: boolean; message: string } => {
22-
if (
23-
unstructuredContent.length !== 1 ||
24-
unstructuredContent[0].type !== "text"
25-
) {
22+
// Look for at least one text content block that matches the structured content
23+
const textBlocks = unstructuredContent.filter(block => block.type === "text");
24+
25+
if (textBlocks.length === 0) {
2626
return {
2727
isCompatible: false,
28-
message: "Unstructured content is not a single text block",
28+
message: "No text content blocks found to match structured content",
2929
};
3030
}
3131

32-
const textContent = unstructuredContent[0].text;
33-
if (!textContent) {
34-
return {
35-
isCompatible: false,
36-
message: "Text content is empty",
37-
};
38-
}
32+
// Check if any text block contains JSON that matches the structured content
33+
for (const textBlock of textBlocks) {
34+
const textContent = textBlock.text;
35+
if (!textContent) {
36+
continue;
37+
}
3938

40-
try {
41-
const parsedContent = JSON.parse(textContent);
42-
const isEqual =
43-
JSON.stringify(parsedContent) === JSON.stringify(structuredContent);
39+
try {
40+
const parsedContent = JSON.parse(textContent);
41+
const isEqual =
42+
JSON.stringify(parsedContent) === JSON.stringify(structuredContent);
4443

45-
if (isEqual) {
46-
return {
47-
isCompatible: true,
48-
message: "Unstructured content matches structured content",
49-
};
50-
} else {
51-
return {
52-
isCompatible: false,
53-
message: "Parsed JSON does not match structured content",
54-
};
44+
if (isEqual) {
45+
return {
46+
isCompatible: true,
47+
message: `Found matching JSON content (${textBlocks.length > 1 ? 'among multiple text blocks' : 'in single text block'})${unstructuredContent.length > textBlocks.length ? ' with additional content blocks' : ''}`,
48+
};
49+
}
50+
} catch {
51+
// Continue to next text block if this one doesn't parse as JSON
52+
continue;
5553
}
56-
} catch {
57-
return {
58-
isCompatible: false,
59-
message: "Unstructured content is not valid JSON",
60-
};
6154
}
55+
56+
return {
57+
isCompatible: false,
58+
message: "No text content block contains JSON matching structured content",
59+
};
6260
};
6361

6462
const ToolResults = ({ toolResult, selectedTool }: ToolResultsProps) => {
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import { render, screen } from "@testing-library/react";
2+
import "@testing-library/jest-dom";
3+
import { describe, it, beforeEach } from "@jest/globals";
4+
import ToolResults from "../ToolResults";
5+
import { Tool } from "@modelcontextprotocol/sdk/types.js";
6+
import { cacheToolOutputSchemas } from "@/utils/schemaUtils";
7+
8+
describe("ToolResults", () => {
9+
const mockTool: Tool = {
10+
name: "testTool",
11+
description: "Test tool",
12+
inputSchema: {
13+
type: "object",
14+
properties: {},
15+
},
16+
outputSchema: {
17+
type: "object",
18+
properties: {
19+
result: { type: "string" },
20+
},
21+
required: ["result"],
22+
},
23+
};
24+
25+
beforeEach(() => {
26+
cacheToolOutputSchemas([mockTool]);
27+
});
28+
29+
describe("Content Compatibility Validation", () => {
30+
it("should accept single text block with matching JSON", () => {
31+
const toolResult = {
32+
content: [{ type: "text", text: '{"result": "success"}' }],
33+
structuredContent: { result: "success" },
34+
};
35+
36+
render(<ToolResults toolResult={toolResult} selectedTool={mockTool} />);
37+
38+
expect(screen.getByText(/Found matching JSON content.*in single text block/)).toBeInTheDocument();
39+
});
40+
41+
it("should accept multiple text blocks with one matching JSON", () => {
42+
const toolResult = {
43+
content: [
44+
{ type: "text", text: "Processing..." },
45+
{ type: "text", text: '{"result": "success"}' },
46+
{ type: "text", text: "Done!" },
47+
],
48+
structuredContent: { result: "success" },
49+
};
50+
51+
render(<ToolResults toolResult={toolResult} selectedTool={mockTool} />);
52+
53+
expect(screen.getByText(/Found matching JSON content.*among multiple text blocks/)).toBeInTheDocument();
54+
});
55+
56+
it("should accept mixed content types with matching JSON", () => {
57+
const toolResult = {
58+
content: [
59+
{ type: "text", text: "Result:" },
60+
{ type: "text", text: '{"result": "success"}' },
61+
{ type: "image", data: "base64data", mimeType: "image/png" },
62+
{ type: "resource", resource: { uri: "file://test.txt" } },
63+
],
64+
structuredContent: { result: "success" },
65+
};
66+
67+
render(<ToolResults toolResult={toolResult} selectedTool={mockTool} />);
68+
69+
expect(screen.getByText(/Found matching JSON content.*with additional content blocks/)).toBeInTheDocument();
70+
});
71+
72+
it("should reject when no text blocks are present", () => {
73+
const toolResult = {
74+
content: [
75+
{ type: "image", data: "base64data", mimeType: "image/png" },
76+
],
77+
structuredContent: { result: "success" },
78+
};
79+
80+
render(<ToolResults toolResult={toolResult} selectedTool={mockTool} />);
81+
82+
expect(screen.getByText(/No text content blocks found to match structured content/)).toBeInTheDocument();
83+
});
84+
85+
it("should reject when no text blocks contain matching JSON", () => {
86+
const toolResult = {
87+
content: [
88+
{ type: "text", text: "Some text" },
89+
{ type: "text", text: '{"different": "data"}' },
90+
],
91+
structuredContent: { result: "success" },
92+
};
93+
94+
render(<ToolResults toolResult={toolResult} selectedTool={mockTool} />);
95+
96+
expect(screen.getByText(/No text content block contains JSON matching structured content/)).toBeInTheDocument();
97+
});
98+
99+
it("should reject when text blocks contain invalid JSON", () => {
100+
const toolResult = {
101+
content: [
102+
{ type: "text", text: "Not JSON" },
103+
{ type: "text", text: '{"invalid": json}' },
104+
],
105+
structuredContent: { result: "success" },
106+
};
107+
108+
render(<ToolResults toolResult={toolResult} selectedTool={mockTool} />);
109+
110+
expect(screen.getByText(/No text content block contains JSON matching structured content/)).toBeInTheDocument();
111+
});
112+
113+
it("should handle empty text blocks gracefully", () => {
114+
const toolResult = {
115+
content: [
116+
{ type: "text", text: "" },
117+
{ type: "text", text: '{"result": "success"}' },
118+
],
119+
structuredContent: { result: "success" },
120+
};
121+
122+
render(<ToolResults toolResult={toolResult} selectedTool={mockTool} />);
123+
124+
expect(screen.getByText(/Found matching JSON content.*among multiple text blocks/)).toBeInTheDocument();
125+
});
126+
127+
it("should not show compatibility check when tool has no output schema", () => {
128+
const toolWithoutSchema: Tool = {
129+
name: "noSchemaTool",
130+
description: "Tool without schema",
131+
inputSchema: {
132+
type: "object",
133+
properties: {},
134+
},
135+
};
136+
137+
const toolResult = {
138+
content: [{ type: "text", text: '{"any": "data"}' }],
139+
structuredContent: { any: "data" },
140+
};
141+
142+
render(<ToolResults toolResult={toolResult} selectedTool={toolWithoutSchema} />);
143+
144+
// Should not show any compatibility messages
145+
expect(screen.queryByText(/Found matching JSON content/)).not.toBeInTheDocument();
146+
expect(screen.queryByText(/No text content blocks found/)).not.toBeInTheDocument();
147+
expect(screen.queryByText(/No text content block contains JSON/)).not.toBeInTheDocument();
148+
});
149+
});
150+
151+
describe("Structured Content Validation", () => {
152+
it("should show validation success for valid structured content", () => {
153+
const toolResult = {
154+
content: [],
155+
structuredContent: { result: "success" },
156+
};
157+
158+
render(<ToolResults toolResult={toolResult} selectedTool={mockTool} />);
159+
160+
expect(screen.getByText(/Valid according to output schema/)).toBeInTheDocument();
161+
});
162+
163+
it("should show validation error for invalid structured content", () => {
164+
const toolResult = {
165+
content: [],
166+
structuredContent: { result: 123 }, // Should be string
167+
};
168+
169+
render(<ToolResults toolResult={toolResult} selectedTool={mockTool} />);
170+
171+
expect(screen.getByText(/Validation Error:/)).toBeInTheDocument();
172+
});
173+
174+
it("should show error when structured content is missing for tool with output schema", () => {
175+
const toolResult = {
176+
content: [{ type: "text", text: "Some result" }],
177+
// No structuredContent
178+
};
179+
180+
render(<ToolResults toolResult={toolResult} selectedTool={mockTool} />);
181+
182+
expect(screen.getByText(/Tool has an output schema but did not return structured content/)).toBeInTheDocument();
183+
});
184+
});
185+
});

client/src/components/__tests__/ToolsTab.test.tsx

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,104 @@ describe("ToolsTab", () => {
357357
// Should show compatibility result
358358
expect(
359359
screen.getByText(
360-
/matches structured content|not a single text block|not valid JSON|does not match/,
360+
/Found matching JSON content/,
361+
),
362+
).toBeInTheDocument();
363+
});
364+
365+
it("should accept multiple content blocks with structured output", () => {
366+
cacheToolOutputSchemas([toolWithOutputSchema]);
367+
368+
const multipleBlocksResult = {
369+
content: [
370+
{ type: "text", text: "Here is the weather data:" },
371+
{ type: "text", text: '{"temperature": 25}' },
372+
{ type: "text", text: "Have a nice day!" },
373+
],
374+
structuredContent: { temperature: 25 },
375+
};
376+
377+
renderToolsTab({
378+
selectedTool: toolWithOutputSchema,
379+
toolResult: multipleBlocksResult,
380+
});
381+
382+
// Should show compatible result with multiple blocks
383+
expect(
384+
screen.getByText(
385+
/Found matching JSON content.*among multiple text blocks/,
386+
),
387+
).toBeInTheDocument();
388+
});
389+
390+
it("should accept mixed content types with structured output", () => {
391+
cacheToolOutputSchemas([toolWithOutputSchema]);
392+
393+
const mixedContentResult = {
394+
content: [
395+
{ type: "text", text: "Weather report:" },
396+
{ type: "text", text: '{"temperature": 25}' },
397+
{ type: "image", data: "base64data", mimeType: "image/png" },
398+
],
399+
structuredContent: { temperature: 25 },
400+
};
401+
402+
renderToolsTab({
403+
selectedTool: toolWithOutputSchema,
404+
toolResult: mixedContentResult,
405+
});
406+
407+
// Should show compatible result with additional content blocks
408+
expect(
409+
screen.getByText(
410+
/Found matching JSON content.*with additional content blocks/,
411+
),
412+
).toBeInTheDocument();
413+
});
414+
415+
it("should reject when no text blocks match structured content", () => {
416+
cacheToolOutputSchemas([toolWithOutputSchema]);
417+
418+
const noMatchResult = {
419+
content: [
420+
{ type: "text", text: "Some text" },
421+
{ type: "text", text: '{"humidity": 60}' }, // Different structure
422+
],
423+
structuredContent: { temperature: 25 },
424+
};
425+
426+
renderToolsTab({
427+
selectedTool: toolWithOutputSchema,
428+
toolResult: noMatchResult,
429+
});
430+
431+
// Should show incompatible result
432+
expect(
433+
screen.getByText(
434+
/No text content block contains JSON matching structured content/,
435+
),
436+
).toBeInTheDocument();
437+
});
438+
439+
it("should reject when no text blocks are present", () => {
440+
cacheToolOutputSchemas([toolWithOutputSchema]);
441+
442+
const noTextBlocksResult = {
443+
content: [
444+
{ type: "image", data: "base64data", mimeType: "image/png" },
445+
],
446+
structuredContent: { temperature: 25 },
447+
};
448+
449+
renderToolsTab({
450+
selectedTool: toolWithOutputSchema,
451+
toolResult: noTextBlocksResult,
452+
});
453+
454+
// Should show incompatible result
455+
expect(
456+
screen.getByText(
457+
/No text content blocks found to match structured content/,
361458
),
362459
).toBeInTheDocument();
363460
});
@@ -376,7 +473,7 @@ describe("ToolsTab", () => {
376473
// Should not show any compatibility messages
377474
expect(
378475
screen.queryByText(
379-
/matches structured content|not a single text block|not valid JSON|does not match/,
476+
/Found matching JSON content|No text content blocks found|No text content block contains JSON/,
380477
),
381478
).not.toBeInTheDocument();
382479
});

0 commit comments

Comments
 (0)