fix: sharp fallback with viewport clipping when unavailable

This commit is contained in:
Tommy D. Rossi
2026-01-19 14:07:01 +01:00
parent 6af2373fe6
commit 495035ccef
5 changed files with 67 additions and 29 deletions
+21 -14
View File
@@ -47,7 +47,14 @@
}
```
Restart your MCP client and you're ready to go! Your AI assistant can now control the browser through the extension.
Or use the following command to automatically add the MCP setting for your client:
```sh
npx -y install-mcp playwriter # pass `--client claude` or `--client opencode` for the client you want to use
```
Restart your MCP client and you're ready to go! Your AI assistant can now control the browser through the extension.
## Usage
@@ -161,11 +168,11 @@ When set, the MCP will automatically create an initial tab when a Playwright cli
{
"mcpServers": {
"playwriter": {
"command": "npx",
"args": ["-y", "playwriter@latest"],
"env": {
"PLAYWRITER_AUTO_ENABLE": "1"
}
"command": "npx",
"args": ["-y", "playwriter@latest"],
"env": {
"PLAYWRITER_AUTO_ENABLE": "1"
}
}
}
}
@@ -359,8 +366,8 @@ Configure your MCP client with the host and token. You can pass them as CLI argu
{
"mcpServers": {
"playwriter": {
"command": "npx",
"args": ["-y", "playwriter@latest", "--host", "host.docker.internal", "--token", "<secret>"]
"command": "npx",
"args": ["-y", "playwriter@latest", "--host", "host.docker.internal", "--token", "<secret>"]
}
}
}
@@ -372,12 +379,12 @@ Or use environment variables (useful if you want to set them globally in your pr
{
"mcpServers": {
"playwriter": {
"command": "npx",
"args": ["-y", "playwriter@latest"],
"env": {
"PLAYWRITER_HOST": "host.docker.internal",
"PLAYWRITER_TOKEN": "<secret>"
}
"command": "npx",
"args": ["-y", "playwriter@latest"],
"env": {
"PLAYWRITER_HOST": "host.docker.internal",
"PLAYWRITER_TOKEN": "<secret>"
}
}
}
}
+7
View File
@@ -1,5 +1,12 @@
# Changelog
## 0.0.50
### Bug Fixes
- **Sharp fallback with viewport clipping**: When sharp is unavailable (optional dependency), screenshots now clip to max 1568px instead of relying on Claude's auto-resize
- **Error logging for sharp failures**: Added logging when sharp import or resize fails, making it easier to debug screenshot optimization issues
## 0.0.49
### Features
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "playwriter",
"description": "",
"version": "0.0.49",
"version": "0.0.50",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
+32 -13
View File
@@ -2,6 +2,11 @@ import type { Page, Locator, ElementHandle } from 'playwright-core'
import fs from 'node:fs'
import path from 'node:path'
// Import sharp at module level - resolves to null if not available
const sharpPromise = import('sharp')
.then((m) => { return m.default })
.catch(() => { return null })
export interface AriaRef {
role: string
name: string
@@ -558,10 +563,11 @@ export async function hideAriaRefLabels({ page }: { page: Page }): Promise<void>
* await page.locator('aria-ref=e5').click()
* ```
*/
export async function screenshotWithAccessibilityLabels({ page, interactiveOnly = true, collector }: {
export async function screenshotWithAccessibilityLabels({ page, interactiveOnly = true, collector, logger }: {
page: Page
interactiveOnly?: boolean
collector: ScreenshotResult[]
logger?: { error: (...args: unknown[]) => void }
}): Promise<void> {
// Show labels and get snapshot
const { snapshot, labelCount } = await showAriaRefLabels({ page, interactiveOnly })
@@ -581,23 +587,33 @@ export async function screenshotWithAccessibilityLabels({ page, interactiveOnly
// Get viewport size to clip screenshot to visible area
const viewport = await page.evaluate('({ width: window.innerWidth, height: window.innerHeight })') as { width: number; height: number }
// Max 1568px on any edge (larger gets auto-resized by Claude, adding latency)
// Token formula: tokens = (width * height) / 750
const MAX_DIMENSION = 1568
// Check if sharp is available for resizing
const sharp = await sharpPromise
// Clip dimensions: if sharp unavailable, limit capture area to MAX_DIMENSION
const clipWidth = sharp ? viewport.width : Math.min(viewport.width, MAX_DIMENSION)
const clipHeight = sharp ? viewport.height : Math.min(viewport.height, MAX_DIMENSION)
// Take viewport screenshot with scale: 'css' to ignore device pixel ratio
const rawBuffer = await page.screenshot({
type: 'jpeg',
quality: 80,
scale: 'css',
clip: { x: 0, y: 0, width: viewport.width, height: viewport.height },
clip: { x: 0, y: 0, width: clipWidth, height: clipHeight },
})
// Resize to fit within Claude's optimal limits:
// - Max 1568px on any edge (larger gets auto-resized by Claude, adding latency)
// - Target ~1.15 megapixels for optimal token usage (~1,533 tokens)
// Token formula: tokens = (width * height) / 750
const buffer = await Promise.resolve()
.then(() => import('sharp'))
.then(async ({ default: sharp }) => {
const MAX_DIMENSION = 1568
return sharp(rawBuffer)
// Resize with sharp if available, otherwise use clipped raw buffer
const buffer = await (async () => {
if (!sharp) {
logger?.error('[playwriter] sharp not available, using clipped screenshot (max', MAX_DIMENSION, 'px)')
return rawBuffer
}
try {
return await sharp(rawBuffer)
.resize({
width: MAX_DIMENSION,
height: MAX_DIMENSION,
@@ -606,8 +622,11 @@ export async function screenshotWithAccessibilityLabels({ page, interactiveOnly
})
.jpeg({ quality: 80 })
.toBuffer()
})
.catch(() => rawBuffer) // sharp not available, Claude will auto-resize
} catch (err) {
logger?.error('[playwriter] sharp resize failed, using raw buffer:', err)
return rawBuffer
}
})()
// Save to file
fs.writeFileSync(screenshotPath, buffer)
+6 -1
View File
@@ -918,7 +918,11 @@ server.tool(
const screenshotCollector: ScreenshotResult[] = []
const screenshotWithAccessibilityLabelsFn = async (options: { page: Page; interactiveOnly?: boolean }) => {
return screenshotWithAccessibilityLabels({ ...options, collector: screenshotCollector })
return screenshotWithAccessibilityLabels({
...options,
collector: screenshotCollector,
logger: { error: (...args) => { console.error('[playwriter]', ...args) } },
})
}
let vmContextObj: VMContextWithGlobals = {
@@ -1134,6 +1138,7 @@ export async function startMcp(options: { host?: string; token?: string } = {})
mcpLog(`Using remote CDP relay server: ${remote.host}:${remote.port}`)
await checkRemoteServer(remote)
}
const transport = new StdioServerTransport()
await server.connect(transport)
}