Skip to content

Commit f7fc4de

Browse files
shivangagclaude
andcommitted
perf: optimize MCP image processing with parallel validation
- Extract image validation into separate validateAndProcessImage function - Replace sequential forEach with parallel Promise.all processing - Pre-filter and limit images before validation for better efficiency - Improve performance for MCP tools returning multiple images 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 337ef77 commit f7fc4de

File tree

1 file changed

+65
-55
lines changed

1 file changed

+65
-55
lines changed

src/core/tools/useMcpToolTool.ts

Lines changed: 65 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -207,82 +207,92 @@ function calculateImageSizeMB(base64Data: string): number {
207207
return sizeInBytes / (1024 * 1024) // Convert to MB
208208
}
209209

210+
async function validateAndProcessImage(item: any, maxImageSizeMB: number): Promise<string | null> {
211+
if (!item.mimeType || item.data === undefined || item.data === null) {
212+
console.warn("Invalid MCP ImageContent: missing data or mimeType")
213+
return null
214+
}
215+
216+
if (!SUPPORTED_IMAGE_TYPES.includes(item.mimeType)) {
217+
console.warn(`Unsupported image MIME type: ${item.mimeType}`)
218+
return null
219+
}
220+
221+
try {
222+
// Validate base64 data before constructing data URL
223+
if (typeof item.data !== "string" || item.data.trim() === "") {
224+
console.warn("Invalid MCP ImageContent: base64 data is not a valid string")
225+
return null
226+
}
227+
228+
// Quick size check before full validation to prevent memory spikes
229+
const approximateSizeMB = (item.data.length * 0.75) / (1024 * 1024)
230+
if (approximateSizeMB > maxImageSizeMB * 1.5) {
231+
console.warn(
232+
`MCP image likely exceeds size limit based on string length: ~${approximateSizeMB.toFixed(2)}MB`,
233+
)
234+
return null
235+
}
236+
237+
// Basic validation for base64 format
238+
if (!BASE64_REGEX.test(item.data.replace(/\s/g, ""))) {
239+
console.warn("Invalid MCP ImageContent: base64 data contains invalid characters")
240+
return null
241+
}
242+
243+
// Check image size
244+
const imageSizeMB = calculateImageSizeMB(item.data)
245+
if (imageSizeMB > maxImageSizeMB) {
246+
console.warn(
247+
`MCP image exceeds size limit: ${imageSizeMB.toFixed(2)}MB > ${maxImageSizeMB}MB. Image will be ignored.`,
248+
)
249+
return null
250+
}
251+
252+
return `data:${item.mimeType};base64,${item.data}`
253+
} catch (error) {
254+
console.warn("Failed to process MCP image content:", error)
255+
return null
256+
}
257+
}
258+
210259
async function processToolContent(toolResult: any, cline: Task): Promise<{ text: string; images: string[] }> {
211260
if (!toolResult?.content || toolResult.content.length === 0) {
212261
return { text: "", images: [] }
213262
}
214263

215264
const textParts: string[] = []
216-
const images: string[] = []
217265

218266
// Get MCP settings from the extension's global state
219267
const state = await cline.providerRef.deref()?.getState()
220268
const maxImagesPerResponse = Math.max(1, Math.min(100, state?.mcpMaxImagesPerResponse ?? 20))
221269
const maxImageSizeMB = Math.max(0.1, Math.min(50, state?.mcpMaxImageSizeMB ?? 10))
222270

271+
// Separate content by type for efficient processing
272+
const imageItems = toolResult.content.filter((item: any) => item.type === "image").slice(0, maxImagesPerResponse) // Limit images before processing
273+
274+
// Process images in parallel
275+
const validatedImages = await Promise.all(
276+
imageItems.map((item: any) => validateAndProcessImage(item, maxImageSizeMB)),
277+
)
278+
const images = validatedImages.filter(Boolean) as string[]
279+
280+
// Process other content types
223281
toolResult.content.forEach((item: any) => {
224282
if (item.type === "text") {
225283
textParts.push(item.text)
226-
} else if (item.type === "image") {
227-
// Check if we've exceeded the maximum number of images
228-
if (images.length >= maxImagesPerResponse) {
229-
console.warn(
230-
`MCP response contains more than ${maxImagesPerResponse} images. Additional images will be ignored to prevent performance issues.`,
231-
)
232-
return // Skip processing additional images
233-
}
234-
235-
if (item.mimeType && item.data !== undefined && item.data !== null) {
236-
if (SUPPORTED_IMAGE_TYPES.includes(item.mimeType)) {
237-
try {
238-
// Validate base64 data before constructing data URL
239-
if (typeof item.data !== "string" || item.data.trim() === "") {
240-
console.warn("Invalid MCP ImageContent: base64 data is not a valid string")
241-
return
242-
}
243-
244-
// Quick size check before full validation to prevent memory spikes
245-
const approximateSizeMB = (item.data.length * 0.75) / (1024 * 1024)
246-
if (approximateSizeMB > maxImageSizeMB * 1.5) {
247-
console.warn(
248-
`MCP image likely exceeds size limit based on string length: ~${approximateSizeMB.toFixed(2)}MB`,
249-
)
250-
return
251-
}
252-
253-
// Basic validation for base64 format
254-
if (!BASE64_REGEX.test(item.data.replace(/\s/g, ""))) {
255-
console.warn("Invalid MCP ImageContent: base64 data contains invalid characters")
256-
return
257-
}
258-
259-
// Check image size
260-
const imageSizeMB = calculateImageSizeMB(item.data)
261-
if (imageSizeMB > maxImageSizeMB) {
262-
console.warn(
263-
`MCP image exceeds size limit: ${imageSizeMB.toFixed(2)}MB > ${maxImageSizeMB}MB. Image will be ignored.`,
264-
)
265-
return
266-
}
267-
268-
const dataUrl = `data:${item.mimeType};base64,${item.data}`
269-
images.push(dataUrl)
270-
} catch (error) {
271-
console.warn("Failed to process MCP image content:", error)
272-
// Continue processing other content instead of failing entirely
273-
}
274-
} else {
275-
console.warn(`Unsupported image MIME type: ${item.mimeType}`)
276-
}
277-
} else {
278-
console.warn("Invalid MCP ImageContent: missing data or mimeType")
279-
}
280284
} else if (item.type === "resource") {
281285
const { blob: _, ...rest } = item.resource
282286
textParts.push(JSON.stringify(rest, null, 2))
283287
}
284288
})
285289

290+
if (imageItems.length > maxImagesPerResponse) {
291+
console.warn(
292+
`MCP response contains more than ${maxImagesPerResponse} images. Additional images will be ignored to prevent performance issues.`,
293+
)
294+
}
295+
286296
return {
287297
text: textParts.filter(Boolean).join("\n\n"),
288298
images,

0 commit comments

Comments
 (0)