diff --git a/src/__tests__/accessMcpResourceTool.spec.ts b/src/__tests__/accessMcpResourceTool.spec.ts new file mode 100644 index 00000000000..0f466ebb0f0 --- /dev/null +++ b/src/__tests__/accessMcpResourceTool.spec.ts @@ -0,0 +1,526 @@ +import * as vscode from "vscode" +import { accessMcpResourceTool } from "../core/tools/accessMcpResourceTool" +import { Task } from "../core/task/Task" +import { AccessMcpResourceToolUse } from "../shared/tools" + +// Mock dependencies +vitest.mock("vscode") + +describe("accessMcpResourceTool", () => { + let mockTask: Partial + let mockAskApproval: any + let mockHandleError: any + let mockPushToolResult: any + let mockRemoveClosingTag: any + let mockProvider: any + let mockMcpHub: any + + beforeEach(() => { + vitest.clearAllMocks() + + // Mock MCP Hub + mockMcpHub = { + readResource: vitest.fn(), + } + + // Mock provider + mockProvider = { + context: { + workspaceState: { + get: vitest.fn().mockImplementation((key: string) => { + switch (key) { + case "mcpMaxImagesPerResponse": + return 10 + case "mcpMaxImageSizeMB": + return 10 + default: + return undefined + } + }), + }, + globalState: { + get: vitest.fn(), + }, + }, + getMcpHub: vitest.fn().mockReturnValue(mockMcpHub), + } + + // Mock Task + mockTask = { + consecutiveMistakeCount: 0, + recordToolError: vitest.fn(), + sayAndCreateMissingParamError: vitest.fn().mockResolvedValue("Missing parameter error"), + say: vitest.fn().mockResolvedValue(undefined), + ask: vitest.fn().mockResolvedValue(undefined), + providerRef: { + deref: vitest.fn().mockReturnValue(mockProvider), + [Symbol.toStringTag]: "WeakRef", + } as any, + } + + // Mock functions + mockAskApproval = vitest.fn().mockResolvedValue(true) + mockHandleError = vitest.fn().mockResolvedValue(undefined) + mockPushToolResult = vitest.fn() + mockRemoveClosingTag = vitest.fn().mockImplementation((tag: string, value: string) => value) + }) + + describe("Parameter Validation", () => { + it("should handle missing server_name parameter", async () => { + const toolUse: AccessMcpResourceToolUse = { + type: "tool_use", + name: "access_mcp_resource", + params: { + uri: "test://resource", + // server_name is missing + }, + partial: false, + } + + await accessMcpResourceTool( + mockTask as Task, + toolUse, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + expect(mockTask.consecutiveMistakeCount).toBe(1) + expect(mockTask.recordToolError).toHaveBeenCalledWith("access_mcp_resource") + expect(mockTask.sayAndCreateMissingParamError).toHaveBeenCalledWith("access_mcp_resource", "server_name") + }) + + it("should handle missing uri parameter", async () => { + const toolUse: AccessMcpResourceToolUse = { + type: "tool_use", + name: "access_mcp_resource", + params: { + server_name: "test-server", + // uri is missing + }, + partial: false, + } + + await accessMcpResourceTool( + mockTask as Task, + toolUse, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + expect(mockTask.consecutiveMistakeCount).toBe(1) + expect(mockTask.recordToolError).toHaveBeenCalledWith("access_mcp_resource") + expect(mockTask.sayAndCreateMissingParamError).toHaveBeenCalledWith("access_mcp_resource", "uri") + }) + + it("should handle partial tool use", async () => { + const toolUse: AccessMcpResourceToolUse = { + type: "tool_use", + name: "access_mcp_resource", + params: { + server_name: "test-server", + uri: "test://resource", + }, + partial: true, + } + + await accessMcpResourceTool( + mockTask as Task, + toolUse, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + expect(mockTask.ask).toHaveBeenCalledWith( + "use_mcp_server", + expect.stringContaining("access_mcp_resource"), + true, + ) + }) + }) + + describe("Image Processing", () => { + it("should process valid JPEG images", async () => { + // Valid JPEG base64 (minimal JPEG header: FFD8FF) + const validJpegBase64 = + "/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwA/8A" + + mockMcpHub.readResource.mockResolvedValue({ + contents: [ + { + uri: "test://image1.jpg", + mimeType: "image/jpeg", + blob: validJpegBase64, + }, + ], + }) + + const toolUse: AccessMcpResourceToolUse = { + type: "tool_use", + name: "access_mcp_resource", + params: { + server_name: "test-server", + uri: "test://resource", + }, + partial: false, + } + + await accessMcpResourceTool( + mockTask as Task, + toolUse, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + expect(mockTask.say).toHaveBeenCalledWith( + "mcp_server_response", + expect.stringContaining("Successfully processed 1 image(s)"), + expect.arrayContaining([expect.stringContaining("data:image/jpeg;base64,")]), + ) + }) + + it("should handle corrupted images gracefully", async () => { + // Invalid image data (doesn't match any known format) + const invalidImageBase64 = "aW52YWxpZCBpbWFnZSBkYXRh" // "invalid image data" in base64 + + mockMcpHub.readResource.mockResolvedValue({ + contents: [ + { + uri: "test://corrupted.jpg", + mimeType: "image/jpeg", + blob: invalidImageBase64, + }, + ], + }) + + const toolUse: AccessMcpResourceToolUse = { + type: "tool_use", + name: "access_mcp_resource", + params: { + server_name: "test-server", + uri: "test://resource", + }, + partial: false, + } + + await accessMcpResourceTool( + mockTask as Task, + toolUse, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + expect(mockTask.say).toHaveBeenCalledWith( + "mcp_server_response", + expect.stringContaining("Image Processing Errors"), + [], + ) + }) + + it("should enforce maximum number of images limit", async () => { + // Set max images to 2 + mockProvider.context.workspaceState.get.mockImplementation((key: string) => { + switch (key) { + case "mcpMaxImagesPerResponse": + return 2 + case "mcpMaxImageSizeMB": + return 10 + default: + return undefined + } + }) + + const validJpegBase64 = + "/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwA/8A" + + mockMcpHub.readResource.mockResolvedValue({ + contents: [ + { + uri: "test://image1.jpg", + mimeType: "image/jpeg", + blob: validJpegBase64, + }, + { + uri: "test://image2.jpg", + mimeType: "image/jpeg", + blob: validJpegBase64, + }, + { + uri: "test://image3.jpg", + mimeType: "image/jpeg", + blob: validJpegBase64, + }, + ], + }) + + const toolUse: AccessMcpResourceToolUse = { + type: "tool_use", + name: "access_mcp_resource", + params: { + server_name: "test-server", + uri: "test://resource", + }, + partial: false, + } + + await accessMcpResourceTool( + mockTask as Task, + toolUse, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + expect(mockTask.say).toHaveBeenCalledWith( + "mcp_server_response", + expect.stringContaining("Image Processing Warnings"), + expect.arrayContaining([ + expect.stringContaining("data:image/jpeg;base64,"), + expect.stringContaining("data:image/jpeg;base64,"), + ]), + ) + + // Should only process 2 images, not 3 + const lastCall = (mockTask.say as any).mock.calls.find((call: any) => call[0] === "mcp_server_response") + expect(lastCall[2]).toHaveLength(2) // Only 2 images processed + }) + + it("should handle text content alongside images", async () => { + const validJpegBase64 = + "/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwA/8A" + + mockMcpHub.readResource.mockResolvedValue({ + contents: [ + { + uri: "test://text.txt", + mimeType: "text/plain", + text: "This is text content", + }, + { + uri: "test://image.jpg", + mimeType: "image/jpeg", + blob: validJpegBase64, + }, + ], + }) + + const toolUse: AccessMcpResourceToolUse = { + type: "tool_use", + name: "access_mcp_resource", + params: { + server_name: "test-server", + uri: "test://resource", + }, + partial: false, + } + + await accessMcpResourceTool( + mockTask as Task, + toolUse, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + expect(mockTask.say).toHaveBeenCalledWith( + "mcp_server_response", + expect.stringContaining("This is text content"), + expect.arrayContaining([expect.stringContaining("data:image/jpeg;base64,")]), + ) + }) + + it("should handle empty response gracefully", async () => { + mockMcpHub.readResource.mockResolvedValue({ + contents: [], + }) + + const toolUse: AccessMcpResourceToolUse = { + type: "tool_use", + name: "access_mcp_resource", + params: { + server_name: "test-server", + uri: "test://resource", + }, + partial: false, + } + + await accessMcpResourceTool( + mockTask as Task, + toolUse, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + expect(mockTask.say).toHaveBeenCalledWith("mcp_server_response", "(Empty response)", []) + }) + }) + + describe("Error Handling", () => { + it("should handle MCP hub errors gracefully", async () => { + mockMcpHub.readResource.mockRejectedValue(new Error("MCP server error")) + + const toolUse: AccessMcpResourceToolUse = { + type: "tool_use", + name: "access_mcp_resource", + params: { + server_name: "test-server", + uri: "test://resource", + }, + partial: false, + } + + await accessMcpResourceTool( + mockTask as Task, + toolUse, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + expect(mockHandleError).toHaveBeenCalledWith("accessing MCP resource", expect.any(Error)) + }) + + it("should handle approval rejection", async () => { + mockAskApproval.mockResolvedValue(false) + + const toolUse: AccessMcpResourceToolUse = { + type: "tool_use", + name: "access_mcp_resource", + params: { + server_name: "test-server", + uri: "test://resource", + }, + partial: false, + } + + await accessMcpResourceTool( + mockTask as Task, + toolUse, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + expect(mockMcpHub.readResource).not.toHaveBeenCalled() + expect(mockTask.say).not.toHaveBeenCalledWith("mcp_server_response", expect.any(String), expect.any(Array)) + }) + }) + + describe("Configuration Handling", () => { + it("should use default values when configuration is not available", async () => { + // Mock configuration to return undefined + mockProvider.context.workspaceState.get.mockReturnValue(undefined) + mockProvider.context.globalState.get.mockReturnValue(undefined) + + const validJpegBase64 = + "/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwA/8A" + + mockMcpHub.readResource.mockResolvedValue({ + contents: [ + { + uri: "test://image.jpg", + mimeType: "image/jpeg", + blob: validJpegBase64, + }, + ], + }) + + const toolUse: AccessMcpResourceToolUse = { + type: "tool_use", + name: "access_mcp_resource", + params: { + server_name: "test-server", + uri: "test://resource", + }, + partial: false, + } + + await accessMcpResourceTool( + mockTask as Task, + toolUse, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + expect(mockTask.say).toHaveBeenCalledWith( + "mcp_server_response", + expect.stringContaining("Successfully processed 1 image(s)"), + expect.arrayContaining([expect.stringContaining("data:image/jpeg;base64,")]), + ) + }) + + it("should prefer workspace settings over global settings", async () => { + mockProvider.context.workspaceState.get.mockImplementation((key: string) => { + switch (key) { + case "mcpMaxImagesPerResponse": + return 5 // Workspace setting + default: + return undefined + } + }) + + mockProvider.context.globalState.get.mockImplementation((key: string) => { + switch (key) { + case "mcpMaxImagesPerResponse": + return 15 // Global setting (should be ignored) + default: + return undefined + } + }) + + // This test would need to verify that workspace setting (5) is used instead of global (15) + // We can verify this by providing 6 images and checking that only 5 are processed + const validJpegBase64 = + "/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwA/8A" + + mockMcpHub.readResource.mockResolvedValue({ + contents: Array.from({ length: 6 }, (_, i) => ({ + uri: `test://image${i + 1}.jpg`, + mimeType: "image/jpeg", + blob: validJpegBase64, + })), + }) + + const toolUse: AccessMcpResourceToolUse = { + type: "tool_use", + name: "access_mcp_resource", + params: { + server_name: "test-server", + uri: "test://resource", + }, + partial: false, + } + + await accessMcpResourceTool( + mockTask as Task, + toolUse, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + // Should process 5 images (workspace setting) and show warning about exceeding limit + const lastCall = (mockTask.say as any).mock.calls.find((call: any) => call[0] === "mcp_server_response") + expect(lastCall[2]).toHaveLength(5) // Only 5 images processed + expect(lastCall[1]).toContain("Image Processing Warnings") // Warning about exceeding limit + }) + }) +}) diff --git a/src/core/tools/accessMcpResourceTool.ts b/src/core/tools/accessMcpResourceTool.ts index 22b1aba9095..fb4e61cd364 100644 --- a/src/core/tools/accessMcpResourceTool.ts +++ b/src/core/tools/accessMcpResourceTool.ts @@ -3,6 +3,173 @@ import { ToolUse, RemoveClosingTag, AskApproval, HandleError, PushToolResult } f import { Task } from "../task/Task" import { formatResponse } from "../prompts/responses" +// Default limits for MCP image handling +const DEFAULT_MAX_IMAGES_PER_RESPONSE = 10 +const DEFAULT_MAX_IMAGE_SIZE_MB = 10 + +/** + * Validates if a base64 string represents a valid image + * @param base64Data The base64 data to validate + * @param mimeType The MIME type of the image + * @returns true if valid, false otherwise + */ +function isValidImageData(base64Data: string, mimeType: string): boolean { + try { + // Check if it's a valid base64 string + if (!/^[A-Za-z0-9+/]*={0,2}$/.test(base64Data)) { + return false + } + + // Check if MIME type is a supported image format + const supportedImageTypes = [ + "image/jpeg", + "image/jpg", + "image/png", + "image/gif", + "image/webp", + "image/bmp", + "image/svg+xml", + ] + + if (!supportedImageTypes.includes(mimeType.toLowerCase())) { + return false + } + + // Decode base64 to check if it's valid + const binaryString = atob(base64Data) + + // Basic validation: check if decoded data has reasonable length + if (binaryString.length === 0) { + return false + } + + // For common image formats, check magic bytes + const bytes = new Uint8Array(binaryString.length) + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i) + } + + // Check magic bytes for common image formats + if (mimeType.toLowerCase().includes("jpeg") || mimeType.toLowerCase().includes("jpg")) { + // JPEG magic bytes: FF D8 FF + return bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff + } else if (mimeType.toLowerCase().includes("png")) { + // PNG magic bytes: 89 50 4E 47 0D 0A 1A 0A + return ( + bytes.length >= 8 && + bytes[0] === 0x89 && + bytes[1] === 0x50 && + bytes[2] === 0x4e && + bytes[3] === 0x47 && + bytes[4] === 0x0d && + bytes[5] === 0x0a && + bytes[6] === 0x1a && + bytes[7] === 0x0a + ) + } else if (mimeType.toLowerCase().includes("gif")) { + // GIF magic bytes: 47 49 46 38 (GIF8) + return bytes.length >= 4 && bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x38 + } else if (mimeType.toLowerCase().includes("webp")) { + // WebP magic bytes: 52 49 46 46 (RIFF) at start and 57 45 42 50 (WEBP) at offset 8 + return ( + bytes.length >= 12 && + bytes[0] === 0x52 && + bytes[1] === 0x49 && + bytes[2] === 0x46 && + bytes[3] === 0x46 && + bytes[8] === 0x57 && + bytes[9] === 0x45 && + bytes[10] === 0x42 && + bytes[11] === 0x50 + ) + } else if (mimeType.toLowerCase().includes("bmp")) { + // BMP magic bytes: 42 4D (BM) + return bytes.length >= 2 && bytes[0] === 0x42 && bytes[1] === 0x4d + } + + // For other formats or if magic byte check is not implemented, assume valid if base64 decoding worked + return true + } catch (error) { + // If any error occurs during validation, consider it invalid + return false + } +} + +/** + * Calculates the size of a base64 encoded image in MB + * @param base64Data The base64 data + * @returns Size in MB + */ +function getImageSizeInMB(base64Data: string): number { + // Base64 encoding increases size by ~33%, so actual size = (base64Length * 3) / 4 + const actualSizeBytes = (base64Data.length * 3) / 4 + return actualSizeBytes / (1024 * 1024) // Convert to MB +} + +/** + * Processes and validates images from MCP resource response with security controls + * @param resourceContents The contents from MCP resource response + * @param maxImages Maximum number of images allowed per response + * @param maxImageSizeMB Maximum size per image in MB + * @returns Object containing valid images and any warnings/errors + */ +function processImagesWithValidation( + resourceContents: Array<{ uri: string; mimeType?: string; text?: string; blob?: string }>, + maxImages: number, + maxImageSizeMB: number, +): { images: string[]; warnings: string[]; errors: string[] } { + const images: string[] = [] + const warnings: string[] = [] + const errors: string[] = [] + + let imageCount = 0 + + for (const item of resourceContents) { + // Skip non-image items + if (!item.mimeType?.startsWith("image") || !item.blob) { + continue + } + + imageCount++ + + // Check maximum number of images limit + if (imageCount > maxImages) { + warnings.push( + `Exceeded maximum number of images per response (${maxImages}). Additional images were skipped.`, + ) + break + } + + try { + // Validate image data + if (!isValidImageData(item.blob, item.mimeType)) { + errors.push(`Invalid or corrupted image data detected for ${item.mimeType} image. Skipping this image.`) + continue + } + + // Check image size limit + const imageSizeMB = getImageSizeInMB(item.blob) + if (imageSizeMB > maxImageSizeMB) { + warnings.push( + `Image size (${imageSizeMB.toFixed(2)}MB) exceeds maximum allowed size (${maxImageSizeMB}MB). Skipping this image.`, + ) + continue + } + + // Add data URL prefix if not present + const dataUrl = item.blob.startsWith("data:") ? item.blob : `data:${item.mimeType};base64,${item.blob}` + + images.push(dataUrl) + } catch (error) { + errors.push( + `Error processing image: ${error instanceof Error ? error.message : "Unknown error"}. Skipping this image.`, + ) + } + } + + return { images, warnings, errors } +} + export async function accessMcpResourceTool( cline: Task, block: ToolUse, @@ -68,17 +235,46 @@ export async function accessMcpResourceTool( .filter(Boolean) .join("\n\n") || "(Empty response)" - // Handle images (image must contain mimetype and blob) - let images: string[] = [] + // Get MCP image handling settings from VSCode configuration + const provider = cline.providerRef.deref() + const workspaceConfig = provider?.context.workspaceState + const globalConfig = provider?.context.globalState + + // Get settings with fallback to defaults + const maxImagesPerResponse = + workspaceConfig?.get("mcpMaxImagesPerResponse") ?? + globalConfig?.get("mcpMaxImagesPerResponse") ?? + DEFAULT_MAX_IMAGES_PER_RESPONSE + + const maxImageSizeMB = + workspaceConfig?.get("mcpMaxImageSizeMB") ?? + globalConfig?.get("mcpMaxImageSizeMB") ?? + DEFAULT_MAX_IMAGE_SIZE_MB + + // Process images with enhanced validation and security controls + const imageProcessingResult = processImagesWithValidation( + resourceResult?.contents || [], + maxImagesPerResponse, + maxImageSizeMB, + ) + + // Prepare response message with any warnings or errors + let responseMessage = resourceResultPretty - resourceResult?.contents.forEach((item) => { - if (item.mimeType?.startsWith("image") && item.blob) { - images.push(item.blob) - } - }) + if (imageProcessingResult.warnings.length > 0) { + responseMessage += "\n\n⚠️ Image Processing Warnings:\n" + imageProcessingResult.warnings.join("\n") + } + + if (imageProcessingResult.errors.length > 0) { + responseMessage += "\n\n❌ Image Processing Errors:\n" + imageProcessingResult.errors.join("\n") + } + + if (imageProcessingResult.images.length > 0) { + responseMessage += `\n\n✅ Successfully processed ${imageProcessingResult.images.length} image(s).` + } - await cline.say("mcp_server_response", resourceResultPretty, images) - pushToolResult(formatResponse.toolResult(resourceResultPretty, images)) + await cline.say("mcp_server_response", responseMessage, imageProcessingResult.images) + pushToolResult(formatResponse.toolResult(responseMessage, imageProcessingResult.images)) return } diff --git a/src/package.json b/src/package.json index 6412b4b858e..daaeea760b0 100644 --- a/src/package.json +++ b/src/package.json @@ -333,6 +333,20 @@ "type": "boolean", "default": true, "description": "%settings.enableCodeActions.description%" + }, + "roo-cline.mcpMaxImagesPerResponse": { + "type": "number", + "default": 10, + "minimum": 1, + "maximum": 50, + "description": "%settings.mcpMaxImagesPerResponse.description%" + }, + "roo-cline.mcpMaxImageSizeMB": { + "type": "number", + "default": 10, + "minimum": 1, + "maximum": 100, + "description": "%settings.mcpMaxImageSizeMB.description%" } } } diff --git a/src/package.nls.json b/src/package.nls.json index b6880b8bfe5..afc5a1970a7 100644 --- a/src/package.nls.json +++ b/src/package.nls.json @@ -31,5 +31,7 @@ "settings.vsCodeLmModelSelector.vendor.description": "The vendor of the language model (e.g. copilot)", "settings.vsCodeLmModelSelector.family.description": "The family of the language model (e.g. gpt-4)", "settings.customStoragePath.description": "Custom storage path. Leave empty to use the default location. Supports absolute paths (e.g. 'D:\\RooCodeStorage')", - "settings.enableCodeActions.description": "Enable Roo Code quick fixes" + "settings.enableCodeActions.description": "Enable Roo Code quick fixes", + "settings.mcpMaxImagesPerResponse.description": "Maximum number of images allowed per MCP resource response (1-50)", + "settings.mcpMaxImageSizeMB.description": "Maximum size per image in MB for MCP resources (1-100)" }