Skip to content

Commit e46fae7

Browse files
fix: add image content support to MCP tool responses (#10874)
Co-authored-by: Roo Code <roomote@roocode.com>
1 parent f97a5c2 commit e46fae7

2 files changed

Lines changed: 272 additions & 12 deletions

File tree

src/core/tools/UseMcpToolTool.ts

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -255,12 +255,14 @@ export class UseMcpToolTool extends BaseTool<"use_mcp_tool"> {
255255
})
256256
}
257257

258-
private processToolContent(toolResult: any): string {
258+
private processToolContent(toolResult: any): { text: string; images: string[] } {
259259
if (!toolResult?.content || toolResult.content.length === 0) {
260-
return ""
260+
return { text: "", images: [] }
261261
}
262262

263-
return toolResult.content
263+
const images: string[] = []
264+
265+
const textContent = toolResult.content
264266
.map((item: any) => {
265267
if (item.type === "text") {
266268
return item.text
@@ -269,10 +271,23 @@ export class UseMcpToolTool extends BaseTool<"use_mcp_tool"> {
269271
const { blob: _, ...rest } = item.resource
270272
return JSON.stringify(rest, null, 2)
271273
}
274+
if (item.type === "image") {
275+
// Handle image content (MCP image content has mimeType and data properties)
276+
if (item.mimeType && item.data) {
277+
if (item.data.startsWith("data:")) {
278+
images.push(item.data)
279+
} else {
280+
images.push(`data:${item.mimeType};base64,${item.data}`)
281+
}
282+
}
283+
return ""
284+
}
272285
return ""
273286
})
274287
.filter(Boolean)
275288
.join("\n\n")
289+
290+
return { text: textContent, images }
276291
}
277292

278293
private async executeToolAndProcessResult(
@@ -296,18 +311,22 @@ export class UseMcpToolTool extends BaseTool<"use_mcp_tool"> {
296311
const toolResult = await task.providerRef.deref()?.getMcpHub()?.callTool(serverName, toolName, parsedArguments)
297312

298313
let toolResultPretty = "(No response)"
314+
let images: string[] = []
299315

300316
if (toolResult) {
301-
const outputText = this.processToolContent(toolResult)
317+
const { text: outputText, images: extractedImages } = this.processToolContent(toolResult)
318+
images = extractedImages
302319

303-
if (outputText) {
320+
if (outputText || images.length > 0) {
304321
await this.sendExecutionStatus(task, {
305322
executionId,
306323
status: "output",
307-
response: outputText,
324+
response: outputText || (images.length > 0 ? `[${images.length} image(s)]` : ""),
308325
})
309326

310-
toolResultPretty = (toolResult.isError ? "Error:\n" : "") + outputText
327+
toolResultPretty =
328+
(toolResult.isError ? "Error:\n" : "") +
329+
(outputText || (images.length > 0 ? `[${images.length} image(s) received]` : ""))
311330
}
312331

313332
// Send completion status
@@ -326,8 +345,8 @@ export class UseMcpToolTool extends BaseTool<"use_mcp_tool"> {
326345
})
327346
}
328347

329-
await task.say("mcp_server_response", toolResultPretty)
330-
pushToolResult(formatResponse.toolResult(toolResultPretty))
348+
await task.say("mcp_server_response", toolResultPretty, images)
349+
pushToolResult(formatResponse.toolResult(toolResultPretty, images))
331350
}
332351
}
333352

src/core/tools/__tests__/useMcpToolTool.spec.ts

Lines changed: 244 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ import { ToolUse } from "../../../shared/tools"
77
// Mock dependencies
88
vi.mock("../../prompts/responses", () => ({
99
formatResponse: {
10-
toolResult: vi.fn((result: string) => `Tool result: ${result}`),
10+
toolResult: vi.fn((result: string, images?: string[]) => {
11+
if (images && images.length > 0) {
12+
return `Tool result: ${result} [with ${images.length} image(s)]`
13+
}
14+
return `Tool result: ${result}`
15+
}),
1116
toolError: vi.fn((error: string) => `Tool error: ${error}`),
1217
invalidMcpToolArgumentError: vi.fn((server: string, tool: string) => `Invalid args for ${server}:${tool}`),
1318
unknownMcpToolError: vi.fn((server: string, tool: string, availableTools: string[]) => {
@@ -245,7 +250,7 @@ describe("useMcpToolTool", () => {
245250
expect(mockTask.consecutiveMistakeCount).toBe(0)
246251
expect(mockAskApproval).toHaveBeenCalled()
247252
expect(mockTask.say).toHaveBeenCalledWith("mcp_server_request_started")
248-
expect(mockTask.say).toHaveBeenCalledWith("mcp_server_response", "Tool executed successfully")
253+
expect(mockTask.say).toHaveBeenCalledWith("mcp_server_response", "Tool executed successfully", [])
249254
expect(mockPushToolResult).toHaveBeenCalledWith("Tool result: Tool executed successfully")
250255
})
251256

@@ -483,7 +488,7 @@ describe("useMcpToolTool", () => {
483488
expect(mockTask.consecutiveMistakeCount).toBe(0)
484489
expect(mockTask.recordToolError).not.toHaveBeenCalled()
485490
expect(mockTask.say).toHaveBeenCalledWith("mcp_server_request_started")
486-
expect(mockTask.say).toHaveBeenCalledWith("mcp_server_response", "Tool executed successfully")
491+
expect(mockTask.say).toHaveBeenCalledWith("mcp_server_response", "Tool executed successfully", [])
487492
})
488493

489494
it("should reject unknown server names with available servers listed", async () => {
@@ -636,4 +641,240 @@ describe("useMcpToolTool", () => {
636641
expect(callToolMock).toHaveBeenCalledWith("test-server", "get-user-profile", {})
637642
})
638643
})
644+
645+
describe("image handling", () => {
646+
it("should handle tool response with image content", async () => {
647+
const block: ToolUse = {
648+
type: "tool_use",
649+
name: "use_mcp_tool",
650+
params: {
651+
server_name: "figma-server",
652+
tool_name: "get_screenshot",
653+
arguments: '{"nodeId": "123"}',
654+
},
655+
nativeArgs: {
656+
server_name: "figma-server",
657+
tool_name: "get_screenshot",
658+
arguments: { nodeId: "123" },
659+
},
660+
partial: false,
661+
}
662+
663+
mockAskApproval.mockResolvedValue(true)
664+
665+
const mockToolResult = {
666+
content: [
667+
{
668+
type: "image",
669+
mimeType: "image/png",
670+
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJ",
671+
},
672+
],
673+
isError: false,
674+
}
675+
676+
mockProviderRef.deref.mockReturnValue({
677+
getMcpHub: () => ({
678+
callTool: vi.fn().mockResolvedValue(mockToolResult),
679+
getAllServers: vi
680+
.fn()
681+
.mockReturnValue([
682+
{
683+
name: "figma-server",
684+
tools: [{ name: "get_screenshot", description: "Get screenshot" }],
685+
},
686+
]),
687+
}),
688+
postMessageToWebview: vi.fn(),
689+
})
690+
691+
await useMcpToolTool.handle(mockTask as Task, block as any, {
692+
askApproval: mockAskApproval,
693+
handleError: mockHandleError,
694+
pushToolResult: mockPushToolResult,
695+
})
696+
697+
expect(mockTask.say).toHaveBeenCalledWith("mcp_server_request_started")
698+
expect(mockTask.say).toHaveBeenCalledWith("mcp_server_response", "[1 image(s) received]", [
699+
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJ",
700+
])
701+
expect(mockPushToolResult).toHaveBeenCalledWith(expect.stringContaining("with 1 image(s)"))
702+
})
703+
704+
it("should handle tool response with both text and image content", async () => {
705+
const block: ToolUse = {
706+
type: "tool_use",
707+
name: "use_mcp_tool",
708+
params: {
709+
server_name: "figma-server",
710+
tool_name: "get_node_info",
711+
arguments: '{"nodeId": "123"}',
712+
},
713+
nativeArgs: {
714+
server_name: "figma-server",
715+
tool_name: "get_node_info",
716+
arguments: { nodeId: "123" },
717+
},
718+
partial: false,
719+
}
720+
721+
mockAskApproval.mockResolvedValue(true)
722+
723+
const mockToolResult = {
724+
content: [
725+
{ type: "text", text: "Node name: Button" },
726+
{
727+
type: "image",
728+
mimeType: "image/png",
729+
data: "base64imagedata",
730+
},
731+
],
732+
isError: false,
733+
}
734+
735+
mockProviderRef.deref.mockReturnValue({
736+
getMcpHub: () => ({
737+
callTool: vi.fn().mockResolvedValue(mockToolResult),
738+
getAllServers: vi
739+
.fn()
740+
.mockReturnValue([
741+
{ name: "figma-server", tools: [{ name: "get_node_info", description: "Get node info" }] },
742+
]),
743+
}),
744+
postMessageToWebview: vi.fn(),
745+
})
746+
747+
await useMcpToolTool.handle(mockTask as Task, block as any, {
748+
askApproval: mockAskApproval,
749+
handleError: mockHandleError,
750+
pushToolResult: mockPushToolResult,
751+
})
752+
753+
expect(mockTask.say).toHaveBeenCalledWith("mcp_server_request_started")
754+
expect(mockTask.say).toHaveBeenCalledWith("mcp_server_response", "Node name: Button", [
755+
"data:image/png;base64,base64imagedata",
756+
])
757+
expect(mockPushToolResult).toHaveBeenCalledWith(expect.stringContaining("with 1 image(s)"))
758+
})
759+
760+
it("should handle image with data URL already formatted", async () => {
761+
const block: ToolUse = {
762+
type: "tool_use",
763+
name: "use_mcp_tool",
764+
params: {
765+
server_name: "figma-server",
766+
tool_name: "get_screenshot",
767+
arguments: '{"nodeId": "123"}',
768+
},
769+
nativeArgs: {
770+
server_name: "figma-server",
771+
tool_name: "get_screenshot",
772+
arguments: { nodeId: "123" },
773+
},
774+
partial: false,
775+
}
776+
777+
mockAskApproval.mockResolvedValue(true)
778+
779+
const mockToolResult = {
780+
content: [
781+
{
782+
type: "image",
783+
mimeType: "image/jpeg",
784+
data: "data:image/jpeg;base64,/9j/4AAQSkZJRg==",
785+
},
786+
],
787+
isError: false,
788+
}
789+
790+
mockProviderRef.deref.mockReturnValue({
791+
getMcpHub: () => ({
792+
callTool: vi.fn().mockResolvedValue(mockToolResult),
793+
getAllServers: vi
794+
.fn()
795+
.mockReturnValue([
796+
{
797+
name: "figma-server",
798+
tools: [{ name: "get_screenshot", description: "Get screenshot" }],
799+
},
800+
]),
801+
}),
802+
postMessageToWebview: vi.fn(),
803+
})
804+
805+
await useMcpToolTool.handle(mockTask as Task, block as any, {
806+
askApproval: mockAskApproval,
807+
handleError: mockHandleError,
808+
pushToolResult: mockPushToolResult,
809+
})
810+
811+
// Should not double-prefix the data URL
812+
expect(mockTask.say).toHaveBeenCalledWith("mcp_server_response", "[1 image(s) received]", [
813+
"data:image/jpeg;base64,/9j/4AAQSkZJRg==",
814+
])
815+
})
816+
817+
it("should handle multiple images in response", async () => {
818+
const block: ToolUse = {
819+
type: "tool_use",
820+
name: "use_mcp_tool",
821+
params: {
822+
server_name: "figma-server",
823+
tool_name: "get_screenshots",
824+
arguments: '{"nodeIds": ["1", "2"]}',
825+
},
826+
nativeArgs: {
827+
server_name: "figma-server",
828+
tool_name: "get_screenshots",
829+
arguments: { nodeIds: ["1", "2"] },
830+
},
831+
partial: false,
832+
}
833+
834+
mockAskApproval.mockResolvedValue(true)
835+
836+
const mockToolResult = {
837+
content: [
838+
{
839+
type: "image",
840+
mimeType: "image/png",
841+
data: "image1data",
842+
},
843+
{
844+
type: "image",
845+
mimeType: "image/png",
846+
data: "image2data",
847+
},
848+
],
849+
isError: false,
850+
}
851+
852+
mockProviderRef.deref.mockReturnValue({
853+
getMcpHub: () => ({
854+
callTool: vi.fn().mockResolvedValue(mockToolResult),
855+
getAllServers: vi
856+
.fn()
857+
.mockReturnValue([
858+
{
859+
name: "figma-server",
860+
tools: [{ name: "get_screenshots", description: "Get screenshots" }],
861+
},
862+
]),
863+
}),
864+
postMessageToWebview: vi.fn(),
865+
})
866+
867+
await useMcpToolTool.handle(mockTask as Task, block as any, {
868+
askApproval: mockAskApproval,
869+
handleError: mockHandleError,
870+
pushToolResult: mockPushToolResult,
871+
})
872+
873+
expect(mockTask.say).toHaveBeenCalledWith("mcp_server_response", "[2 image(s) received]", [
874+
"data:image/png;base64,image1data",
875+
"data:image/png;base64,image2data",
876+
])
877+
expect(mockPushToolResult).toHaveBeenCalledWith(expect.stringContaining("with 2 image(s)"))
878+
})
879+
})
639880
})

0 commit comments

Comments
 (0)