Skip to content

Commit 9503e43

Browse files
committed
feat: add image support for MCP tool responses
- Update processToolContent to handle image content types - Construct proper base64 data URIs for images - Pass images array to cline.say and pushToolResult - Add comprehensive tests for image handling Fixes #6163
1 parent 1e17b3b commit 9503e43

File tree

2 files changed

+134
-8
lines changed

2 files changed

+134
-8
lines changed

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

Lines changed: 112 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ 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+
images && images.length > 0
12+
? `Tool result: ${result} [with ${images.length} image(s)]`
13+
: `Tool result: ${result}`,
14+
),
1115
toolError: vi.fn((error: string) => `Tool error: ${error}`),
1216
invalidMcpToolArgumentError: vi.fn((server: string, tool: string) => `Invalid args for ${server}:${tool}`),
1317
},
@@ -208,10 +212,116 @@ describe("useMcpToolTool", () => {
208212
expect(mockTask.consecutiveMistakeCount).toBe(0)
209213
expect(mockAskApproval).toHaveBeenCalled()
210214
expect(mockTask.say).toHaveBeenCalledWith("mcp_server_request_started")
211-
expect(mockTask.say).toHaveBeenCalledWith("mcp_server_response", "Tool executed successfully")
215+
expect(mockTask.say).toHaveBeenCalledWith("mcp_server_response", "Tool executed successfully", [])
212216
expect(mockPushToolResult).toHaveBeenCalledWith("Tool result: Tool executed successfully")
213217
})
214218

219+
it("should handle tool response with images", async () => {
220+
const block: ToolUse = {
221+
type: "tool_use",
222+
name: "use_mcp_tool",
223+
params: {
224+
server_name: "screenshot_server",
225+
tool_name: "capture_screenshot",
226+
arguments: '{"url": "https://example.com"}',
227+
},
228+
partial: false,
229+
}
230+
231+
mockAskApproval.mockResolvedValue(true)
232+
233+
const mockToolResult = {
234+
content: [
235+
{ type: "text", text: "Screenshot captured successfully" },
236+
{
237+
type: "image",
238+
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==",
239+
mimeType: "image/png",
240+
},
241+
],
242+
isError: false,
243+
}
244+
245+
mockProviderRef.deref.mockReturnValue({
246+
getMcpHub: () => ({
247+
callTool: vi.fn().mockResolvedValue(mockToolResult),
248+
}),
249+
postMessageToWebview: vi.fn(),
250+
})
251+
252+
await useMcpToolTool(
253+
mockTask as Task,
254+
block,
255+
mockAskApproval,
256+
mockHandleError,
257+
mockPushToolResult,
258+
mockRemoveClosingTag,
259+
)
260+
261+
expect(mockTask.consecutiveMistakeCount).toBe(0)
262+
expect(mockAskApproval).toHaveBeenCalled()
263+
expect(mockTask.say).toHaveBeenCalledWith("mcp_server_request_started")
264+
expect(mockTask.say).toHaveBeenCalledWith(
265+
"mcp_server_response",
266+
"Screenshot captured successfully\n\n[Image: image/png]",
267+
[
268+
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==",
269+
],
270+
)
271+
expect(mockPushToolResult).toHaveBeenCalledWith(
272+
"Tool result: Screenshot captured successfully\n\n[Image: image/png] [with 1 image(s)]",
273+
)
274+
})
275+
276+
it("should handle tool response with multiple images", async () => {
277+
const block: ToolUse = {
278+
type: "tool_use",
279+
name: "use_mcp_tool",
280+
params: {
281+
server_name: "image_processor",
282+
tool_name: "process_images",
283+
arguments: "{}",
284+
},
285+
partial: false,
286+
}
287+
288+
mockAskApproval.mockResolvedValue(true)
289+
290+
const mockToolResult = {
291+
content: [
292+
{ type: "text", text: "Processed 2 images" },
293+
{ type: "image", data: "data:image/png;base64,ABC123", mimeType: "image/png" },
294+
{ type: "image", data: "XYZ789", mimeType: "image/jpeg" },
295+
],
296+
isError: false,
297+
}
298+
299+
mockProviderRef.deref.mockReturnValue({
300+
getMcpHub: () => ({
301+
callTool: vi.fn().mockResolvedValue(mockToolResult),
302+
}),
303+
postMessageToWebview: vi.fn(),
304+
})
305+
306+
await useMcpToolTool(
307+
mockTask as Task,
308+
block,
309+
mockAskApproval,
310+
mockHandleError,
311+
mockPushToolResult,
312+
mockRemoveClosingTag,
313+
)
314+
315+
expect(mockTask.say).toHaveBeenCalledWith(
316+
"mcp_server_response",
317+
"Processed 2 images\n\n[Image: image/png]\n\n[Image: image/jpeg]",
318+
["data:image/png;base64,ABC123", "data:image/jpeg;base64,XYZ789"],
319+
)
320+
expect(mockPushToolResult).toHaveBeenCalledWith(
321+
"Tool result: Processed 2 images\n\n[Image: image/png]\n\n[Image: image/jpeg] [with 2 image(s)]",
322+
)
323+
})
324+
215325
it("should handle user rejection", async () => {
216326
const block: ToolUse = {
217327
type: "tool_use",

src/core/tools/useMcpToolTool.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,16 +89,27 @@ async function sendExecutionStatus(cline: Task, status: McpExecutionStatus): Pro
8989
})
9090
}
9191

92-
function processToolContent(toolResult: any): string {
92+
function processToolContent(toolResult: any): { text: string; images: string[] } {
93+
const images: string[] = []
94+
9395
if (!toolResult?.content || toolResult.content.length === 0) {
94-
return ""
96+
return { text: "", images }
9597
}
9698

97-
return toolResult.content
99+
const text = toolResult.content
98100
.map((item: any) => {
99101
if (item.type === "text") {
100102
return item.text
101103
}
104+
if (item.type === "image" && item.data && item.mimeType) {
105+
// Handle base64 image data
106+
if (item.data.startsWith("data:")) {
107+
images.push(item.data)
108+
} else {
109+
images.push(`data:${item.mimeType};base64,${item.data}`)
110+
}
111+
return `[Image: ${item.mimeType}]`
112+
}
102113
if (item.type === "resource") {
103114
const { blob: _, ...rest } = item.resource
104115
return JSON.stringify(rest, null, 2)
@@ -107,6 +118,8 @@ function processToolContent(toolResult: any): string {
107118
})
108119
.filter(Boolean)
109120
.join("\n\n")
121+
122+
return { text, images }
110123
}
111124

112125
async function executeToolAndProcessResult(
@@ -130,9 +143,12 @@ async function executeToolAndProcessResult(
130143
const toolResult = await cline.providerRef.deref()?.getMcpHub()?.callTool(serverName, toolName, parsedArguments)
131144

132145
let toolResultPretty = "(No response)"
146+
let images: string[] = []
133147

134148
if (toolResult) {
135-
const outputText = processToolContent(toolResult)
149+
const processedContent = processToolContent(toolResult)
150+
const outputText = processedContent.text
151+
images = processedContent.images
136152

137153
if (outputText) {
138154
await sendExecutionStatus(cline, {
@@ -160,8 +176,8 @@ async function executeToolAndProcessResult(
160176
})
161177
}
162178

163-
await cline.say("mcp_server_response", toolResultPretty)
164-
pushToolResult(formatResponse.toolResult(toolResultPretty))
179+
await cline.say("mcp_server_response", toolResultPretty, images)
180+
pushToolResult(formatResponse.toolResult(toolResultPretty, images))
165181
}
166182

167183
export async function useMcpToolTool(

0 commit comments

Comments
 (0)