fix: Prevent format 'data' for screen captures to avoid stack overflow

- Screen captures now reject format: 'data' with clear error message
- Large screen images cause JavaScript stack overflow when base64 encoded
- Application window captures can still use format: 'data'
- Update tests and documentation to reflect this limitation
This commit is contained in:
Peter Steinberger 2025-06-08 05:39:55 +01:00
parent 338b994ac9
commit 30277bbf6c
5 changed files with 47 additions and 10 deletions

View file

@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Added
- Restriction on using `format: "data"` for screen captures to prevent JavaScript stack overflow errors
- Screen captures must use `format: "png"` or omit the format parameter
- Application window captures can still use `format: "data"`
## [1.0.0-beta.18] - 2025-06-08 ## [1.0.0-beta.18] - 2025-06-08
### Added ### Added

View file

@ -289,15 +289,17 @@ Peekaboo provides three main tools for AI agents:
Captures macOS screen content with automatic shadow/frame removal. Captures macOS screen content with automatic shadow/frame removal.
**Important:** Screen captures cannot use `format: "data"` due to the large size of screen images causing JavaScript stack overflow errors. Always use `format: "png"` (or omit format) with a `path` for screen captures.
**Examples:** **Examples:**
```javascript ```javascript
// Capture entire screen // Capture entire screen (must save to file)
await use_mcp_tool("peekaboo", "image", { await use_mcp_tool("peekaboo", "image", {
app_target: "screen:0", app_target: "screen:0",
path: "~/Desktop/screenshot.png" path: "~/Desktop/screenshot.png"
}); });
// Capture specific app window with analysis // Capture specific app window with analysis (can use format: "data")
await use_mcp_tool("peekaboo", "image", { await use_mcp_tool("peekaboo", "image", {
app_target: "Safari", app_target: "Safari",
question: "What website is currently open?", question: "What website is currently open?",

View file

@ -1,4 +1,4 @@
// This file is auto-generated by the build script. Do not edit manually. // This file is auto-generated by the build script. Do not edit manually.
enum Version { enum Version {
static let current = "1.0.0-beta.17" static let current = "1.0.0-beta.18"
} }

View file

@ -28,6 +28,23 @@ export async function imageToolHandler(
try { try {
logger.debug({ input }, "Processing peekaboo.image tool call"); logger.debug({ input }, "Processing peekaboo.image tool call");
// Validate format restrictions for screen captures
const isScreenCapture = !input.app_target || input.app_target.startsWith("screen:");
if (isScreenCapture && input.format === "data") {
logger.warn("Screen capture with format 'data' is not allowed due to size constraints");
return {
content: [
{
type: "text",
text: "Screen captures cannot use format 'data' because they produce images too large for base64 encoding. " +
"Please use format 'png' to save to a file instead.",
},
],
isError: true,
_meta: { backend_error_code: "FORMAT_NOT_ALLOWED_FOR_SCREEN" },
};
}
// Determine effective path and format for Swift CLI // Determine effective path and format for Swift CLI
const swiftFormat = input.format === "data" ? "png" : (input.format || "png"); const swiftFormat = input.format === "data" ? "png" : (input.format || "png");

View file

@ -109,14 +109,26 @@ describe("Image Tool", () => {
expect(mockFsRm).not.toHaveBeenCalled(); expect(mockFsRm).not.toHaveBeenCalled();
}); });
it("should capture screen with format: 'data'", async () => { it("should reject screen capture with format: 'data'", async () => {
const result = await imageToolHandler(
{ format: "data" },
mockContext,
);
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain("Screen captures cannot use format 'data'");
expect(result._meta?.backend_error_code).toBe("FORMAT_NOT_ALLOWED_FOR_SCREEN");
expect(mockExecuteSwiftCli).not.toHaveBeenCalled();
});
it("should allow app capture with format: 'data'", async () => {
// Mock resolveImagePath to return a temp directory for format: "data" // Mock resolveImagePath to return a temp directory for format: "data"
mockResolveImagePath.mockResolvedValue({ mockResolveImagePath.mockResolvedValue({
effectivePath: MOCK_TEMP_IMAGE_DIR, effectivePath: MOCK_TEMP_IMAGE_DIR,
tempDirUsed: MOCK_TEMP_IMAGE_DIR, tempDirUsed: MOCK_TEMP_IMAGE_DIR,
}); });
const mockResponse = mockSwiftCli.captureImage("screen", { const mockResponse = mockSwiftCli.captureImage("Safari", {
path: MOCK_SAVED_FILE_PATH, path: MOCK_SAVED_FILE_PATH,
format: "png", format: "png",
}); });
@ -124,7 +136,7 @@ describe("Image Tool", () => {
mockReadImageAsBase64.mockResolvedValue("base64imagedata"); mockReadImageAsBase64.mockResolvedValue("base64imagedata");
const result = await imageToolHandler( const result = await imageToolHandler(
{ format: "data" }, { app_target: "Safari", format: "data" },
mockContext, mockContext,
); );
@ -140,7 +152,7 @@ describe("Image Tool", () => {
expect(mockFsRm).not.toHaveBeenCalled(); expect(mockFsRm).not.toHaveBeenCalled();
}); });
it("should save file and return base64 when format: 'data' with path", async () => { it("should save file and return base64 when format: 'data' with path for app capture", async () => {
const userPath = "/user/test.png"; const userPath = "/user/test.png";
// Mock resolveImagePath to return the user path (no temp dir) // Mock resolveImagePath to return the user path (no temp dir)
mockResolveImagePath.mockResolvedValue({ mockResolveImagePath.mockResolvedValue({
@ -151,7 +163,7 @@ describe("Image Tool", () => {
const mockSavedFile: SavedFile = { const mockSavedFile: SavedFile = {
path: userPath, path: userPath,
mime_type: "image/png", mime_type: "image/png",
item_label: "Screen 1", item_label: "Safari",
}; };
const mockResponse = { const mockResponse = {
success: true, success: true,
@ -162,7 +174,7 @@ describe("Image Tool", () => {
mockReadImageAsBase64.mockResolvedValue("base64imagedata"); mockReadImageAsBase64.mockResolvedValue("base64imagedata");
const result = await imageToolHandler( const result = await imageToolHandler(
{ format: "data", path: userPath }, { app_target: "Safari", format: "data", path: userPath },
mockContext, mockContext,
); );
@ -644,7 +656,7 @@ describe("Image Tool", () => {
tempDirUsed: MOCK_TEMP_IMAGE_DIR, tempDirUsed: MOCK_TEMP_IMAGE_DIR,
}); });
const mockCliResponse = mockSwiftCli.captureImage("screen", { const mockCliResponse = mockSwiftCli.captureImage("Safari", {
path: MOCK_SAVED_FILE_PATH, path: MOCK_SAVED_FILE_PATH,
format: "png", format: "png",
}); });
@ -652,6 +664,7 @@ describe("Image Tool", () => {
const result = await imageToolHandler( const result = await imageToolHandler(
{ {
app_target: "Safari", // Use app capture to allow format: "data"
question: MOCK_QUESTION, question: MOCK_QUESTION,
format: "data", // Even with format: "data" format: "data", // Even with format: "data"
}, },