Skip to content

Commit 86e5ff3

Browse files
committed
Fixes #5252: Handle images without data URI format in access_mcp_resource
- Modified accessMcpResourceTool.ts to detect and convert raw base64 data to proper data URI format - Updated formatImagesIntoBlocks in responses.ts to handle both data URI and raw base64 formats - Added comprehensive test suite covering standard data URI, raw base64, mixed formats, and different MIME types - Maintains backward compatibility with existing data URI format - Uses 'image/png' as default MIME type for raw base64 data
1 parent 3a8ba27 commit 86e5ff3

File tree

3 files changed

+259
-8
lines changed

3 files changed

+259
-8
lines changed
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
// npx vitest core/prompts/__tests__/responses-image-handling.spec.ts
2+
3+
import { vi, describe, it, expect } from "vitest"
4+
import { formatResponse } from "../responses"
5+
6+
// Mock VSCode dependencies
7+
vi.mock("vscode", () => {
8+
const mockDisposable = { dispose: vi.fn() }
9+
return {
10+
workspace: {
11+
createFileSystemWatcher: vi.fn(() => ({
12+
onDidCreate: vi.fn(() => mockDisposable),
13+
onDidChange: vi.fn(() => mockDisposable),
14+
onDidDelete: vi.fn(() => mockDisposable),
15+
dispose: vi.fn(),
16+
})),
17+
},
18+
RelativePattern: vi.fn(),
19+
}
20+
})
21+
22+
// Mock fs dependencies
23+
vi.mock("../../../utils/fs", () => ({
24+
fileExistsAtPath: vi.fn().mockResolvedValue(false),
25+
}))
26+
27+
vi.mock("fs/promises", () => ({
28+
readFile: vi.fn().mockResolvedValue(""),
29+
}))
30+
31+
describe("Image Handling in formatResponse", () => {
32+
describe("formatResponse.toolResult with images", () => {
33+
it("should handle standard data URI format images", () => {
34+
const text = "Here is an image:"
35+
const images = [
36+
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
37+
]
38+
39+
const result = formatResponse.toolResult(text, images)
40+
41+
// Should return an array with text and image blocks
42+
expect(Array.isArray(result)).toBe(true)
43+
const resultArray = result as any[]
44+
45+
// First block should be text
46+
expect(resultArray[0]).toEqual({
47+
type: "text",
48+
text: "Here is an image:",
49+
})
50+
51+
// Second block should be image with correct format
52+
expect(resultArray[1]).toEqual({
53+
type: "image",
54+
source: {
55+
type: "base64",
56+
media_type: "image/png",
57+
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
58+
},
59+
})
60+
})
61+
62+
it("should handle raw base64 data without data URI prefix", () => {
63+
const text = "Here is an image:"
64+
const images = [
65+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
66+
]
67+
68+
const result = formatResponse.toolResult(text, images)
69+
70+
// Should return an array with text and image blocks
71+
expect(Array.isArray(result)).toBe(true)
72+
const resultArray = result as any[]
73+
74+
// First block should be text
75+
expect(resultArray[0]).toEqual({
76+
type: "text",
77+
text: "Here is an image:",
78+
})
79+
80+
// Second block should be image with default PNG mime type
81+
expect(resultArray[1]).toEqual({
82+
type: "image",
83+
source: {
84+
type: "base64",
85+
media_type: "image/png", // Default fallback
86+
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
87+
},
88+
})
89+
})
90+
91+
it("should handle different image MIME types in data URI", () => {
92+
const text = "Here are different image types:"
93+
const images = [
94+
"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwA/8A8A",
95+
"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",
96+
"data:image/webp;base64,UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA",
97+
]
98+
99+
const result = formatResponse.toolResult(text, images)
100+
101+
// Should return an array with text and image blocks
102+
expect(Array.isArray(result)).toBe(true)
103+
const resultArray = result as any[]
104+
105+
// First block should be text
106+
expect(resultArray[0]).toEqual({
107+
type: "text",
108+
text: "Here are different image types:",
109+
})
110+
111+
// Should handle JPEG
112+
expect(resultArray[1]).toEqual({
113+
type: "image",
114+
source: {
115+
type: "base64",
116+
media_type: "image/jpeg",
117+
data: "/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwA/8A8A",
118+
},
119+
})
120+
121+
// Should handle GIF
122+
expect(resultArray[2]).toEqual({
123+
type: "image",
124+
source: {
125+
type: "base64",
126+
media_type: "image/gif",
127+
data: "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",
128+
},
129+
})
130+
131+
// Should handle WebP
132+
expect(resultArray[3]).toEqual({
133+
type: "image",
134+
source: {
135+
type: "base64",
136+
media_type: "image/webp",
137+
data: "UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA",
138+
},
139+
})
140+
})
141+
142+
it("should handle mixed data URI and raw base64 images", () => {
143+
const text = "Mixed image formats:"
144+
const images = [
145+
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
146+
"R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7", // Raw base64 GIF
147+
]
148+
149+
const result = formatResponse.toolResult(text, images)
150+
151+
// Should return an array with text and image blocks
152+
expect(Array.isArray(result)).toBe(true)
153+
const resultArray = result as any[]
154+
155+
// First image should preserve original PNG format
156+
expect(resultArray[1]).toEqual({
157+
type: "image",
158+
source: {
159+
type: "base64",
160+
media_type: "image/png",
161+
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
162+
},
163+
})
164+
165+
// Second image should default to PNG for raw base64
166+
expect(resultArray[2]).toEqual({
167+
type: "image",
168+
source: {
169+
type: "base64",
170+
media_type: "image/png", // Default fallback
171+
data: "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",
172+
},
173+
})
174+
})
175+
176+
it("should return just text when no images provided", () => {
177+
const text = "Just text, no images"
178+
179+
const result = formatResponse.toolResult(text)
180+
181+
// Should return just the text string
182+
expect(result).toBe("Just text, no images")
183+
})
184+
185+
it("should return just text when empty images array provided", () => {
186+
const text = "Just text, empty images array"
187+
const images: string[] = []
188+
189+
const result = formatResponse.toolResult(text, images)
190+
191+
// Should return just the text string
192+
expect(result).toBe("Just text, empty images array")
193+
})
194+
})
195+
196+
describe("formatResponse.imageBlocks", () => {
197+
it("should handle undefined images", () => {
198+
const result = formatResponse.imageBlocks(undefined)
199+
200+
expect(result).toEqual([])
201+
})
202+
203+
it("should handle empty images array", () => {
204+
const result = formatResponse.imageBlocks([])
205+
206+
expect(result).toEqual([])
207+
})
208+
209+
it("should format raw base64 images correctly", () => {
210+
const images = [
211+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
212+
]
213+
214+
const result = formatResponse.imageBlocks(images)
215+
216+
expect(result).toEqual([
217+
{
218+
type: "image",
219+
source: {
220+
type: "base64",
221+
media_type: "image/png",
222+
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
223+
},
224+
},
225+
])
226+
})
227+
})
228+
})

src/core/prompts/responses.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -179,13 +179,27 @@ Otherwise, if you have not completed the task and do not need additional informa
179179
const formatImagesIntoBlocks = (images?: string[]): Anthropic.ImageBlockParam[] => {
180180
return images
181181
? images.map((dataUrl) => {
182-
// data:image/png;base64,base64string
183-
const [rest, base64] = dataUrl.split(",")
184-
const mimeType = rest.split(":")[1].split(";")[0]
185-
return {
186-
type: "image",
187-
source: { type: "base64", media_type: mimeType, data: base64 },
188-
} as Anthropic.ImageBlockParam
182+
// Handle different image formats:
183+
// 1. data:image/png;base64,base64string (standard data URI)
184+
// 2. base64string (raw base64 data)
185+
// 3. other formats that might be provided by MCP servers
186+
187+
if (dataUrl.startsWith("data:")) {
188+
// Standard data URI format: data:image/png;base64,base64string
189+
const [rest, base64] = dataUrl.split(",")
190+
const mimeType = rest.split(":")[1].split(";")[0]
191+
return {
192+
type: "image",
193+
source: { type: "base64", media_type: mimeType, data: base64 },
194+
} as Anthropic.ImageBlockParam
195+
} else {
196+
// Assume it's raw base64 data, default to image/png
197+
// This handles cases where MCP servers provide just the base64 string
198+
return {
199+
type: "image",
200+
source: { type: "base64", media_type: "image/png", data: dataUrl },
201+
} as Anthropic.ImageBlockParam
202+
}
189203
})
190204
: []
191205
}

src/core/tools/accessMcpResourceTool.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,16 @@ export async function accessMcpResourceTool(
7373

7474
resourceResult?.contents.forEach((item) => {
7575
if (item.mimeType?.startsWith("image") && item.blob) {
76-
images.push(item.blob)
76+
// Check if blob is already a data URI
77+
if (item.blob.startsWith("data:")) {
78+
// Already in data URI format, use as-is
79+
images.push(item.blob)
80+
} else {
81+
// Assume it's raw base64 data, create proper data URI
82+
const mimeType = item.mimeType || "image/png"
83+
const dataUri = `data:${mimeType};base64,${item.blob}`
84+
images.push(dataUri)
85+
}
7786
}
7887
})
7988

0 commit comments

Comments
 (0)