@@ -10,6 +10,189 @@ import type {ElementHandle, Page} from '../third_party/index.js';
1010import { ToolCategory } from './categories.js' ;
1111import { defineTool } from './ToolDefinition.js' ;
1212
13+ /**
14+ * Takes a full-page screenshot of an iframe's content by temporarily
15+ * expanding the iframe to show all scrollable content.
16+ */
17+ async function takeIframeFullPageScreenshot (
18+ iframeHandle : ElementHandle < Element > ,
19+ options : { type : 'png' | 'jpeg' | 'webp' ; quality ?: number } ,
20+ ) : Promise < Uint8Array > {
21+ const contentFrame = await iframeHandle . contentFrame ( ) ;
22+ if ( ! contentFrame ) {
23+ throw new Error (
24+ 'The specified element is not an iframe or its content is not accessible.' ,
25+ ) ;
26+ }
27+
28+ // Get the full scroll dimensions of the iframe content
29+ const { scrollWidth, scrollHeight} = await contentFrame . evaluate ( ( ) => {
30+ return {
31+ scrollWidth : document . documentElement . scrollWidth ,
32+ scrollHeight : document . documentElement . scrollHeight ,
33+ } ;
34+ } ) ;
35+
36+ // Get the original iframe styles to restore later
37+ const originalIframeStyle = await iframeHandle . evaluate ( el => {
38+ const iframe = el as HTMLIFrameElement ;
39+ return {
40+ width : iframe . style . width ,
41+ height : iframe . style . height ,
42+ maxWidth : iframe . style . maxWidth ,
43+ maxHeight : iframe . style . maxHeight ,
44+ position : iframe . style . position ,
45+ } ;
46+ } ) ;
47+
48+ // Get the original iframe content styles to restore later
49+ const originalContentStyle = await contentFrame . evaluate ( ( ) => {
50+ return {
51+ htmlHeight : document . documentElement . style . height ,
52+ htmlOverflow : document . documentElement . style . overflow ,
53+ bodyHeight : document . body . style . height ,
54+ bodyOverflow : document . body . style . overflow ,
55+ } ;
56+ } ) ;
57+
58+ try {
59+ // Temporarily expand the iframe to show all content
60+ // Setting position:absolute helps escape flex/grid layout constraints
61+ await iframeHandle . evaluate (
62+ ( el , dims ) => {
63+ const iframe = el as HTMLIFrameElement ;
64+ iframe . style . width = `${ dims . scrollWidth } px` ;
65+ iframe . style . height = `${ dims . scrollHeight } px` ;
66+ iframe . style . maxWidth = 'none' ;
67+ iframe . style . maxHeight = 'none' ;
68+ iframe . style . position = 'absolute' ;
69+ } ,
70+ { scrollWidth, scrollHeight} ,
71+ ) ;
72+
73+ // Set overflow:visible on iframe content to allow content to expand
74+ await contentFrame . evaluate ( dims => {
75+ document . documentElement . style . height = `${ dims . scrollHeight } px` ;
76+ document . documentElement . style . overflow = 'visible' ;
77+ document . body . style . height = `${ dims . scrollHeight } px` ;
78+ document . body . style . overflow = 'visible' ;
79+ } , { scrollHeight} ) ;
80+
81+ // Scroll to top-left to ensure we capture from the beginning
82+ await contentFrame . evaluate ( ( ) => {
83+ window . scrollTo ( 0 , 0 ) ;
84+ } ) ;
85+
86+ // Small delay to allow the iframe to resize and render
87+ await new Promise ( resolve => setTimeout ( resolve , 150 ) ) ;
88+
89+ // Take screenshot of the expanded iframe
90+ const screenshot = await iframeHandle . screenshot ( {
91+ type : options . type ,
92+ quality : options . quality ,
93+ optimizeForSpeed : true ,
94+ } ) ;
95+
96+ return screenshot ;
97+ } finally {
98+ // Restore original iframe content styles
99+ await contentFrame . evaluate ( style => {
100+ document . documentElement . style . height = style . htmlHeight ;
101+ document . documentElement . style . overflow = style . htmlOverflow ;
102+ document . body . style . height = style . bodyHeight ;
103+ document . body . style . overflow = style . bodyOverflow ;
104+ } , originalContentStyle ) ;
105+
106+ // Restore original iframe styles
107+ await iframeHandle . evaluate (
108+ ( el , style ) => {
109+ const iframe = el as HTMLIFrameElement ;
110+ iframe . style . width = style . width ;
111+ iframe . style . height = style . height ;
112+ iframe . style . maxWidth = style . maxWidth ;
113+ iframe . style . maxHeight = style . maxHeight ;
114+ iframe . style . position = style . position ;
115+ } ,
116+ originalIframeStyle ,
117+ ) ;
118+ }
119+ }
120+
121+ /**
122+ * Finds the main content iframe on the page if one exists.
123+ * Returns the iframe element handle if found, null otherwise.
124+ */
125+ async function findMainContentIframe (
126+ page : Page ,
127+ ) : Promise < ElementHandle < Element > | null > {
128+ // Look for iframes that take up a significant portion of the viewport
129+ const iframeHandle = await page . evaluateHandle ( ( ) => {
130+ const iframes = Array . from ( document . querySelectorAll ( 'iframe' ) ) ;
131+ if ( iframes . length === 0 ) return null ;
132+
133+ // Find the largest iframe that has scrollable content
134+ let bestIframe : HTMLIFrameElement | null = null ;
135+ let bestScore = 0 ;
136+
137+ for ( let i = 0 ; i < iframes . length ; i ++ ) {
138+ const iframe = iframes [ i ] ! ;
139+ try {
140+ const rect = iframe . getBoundingClientRect ( ) ;
141+ const contentDoc = iframe . contentDocument ;
142+
143+ // Skip tiny iframes or iframes we can't access
144+ if ( rect . width < 100 || rect . height < 100 || ! contentDoc ) continue ;
145+
146+ // Calculate score based on size and scrollable content
147+ const scrollHeight = contentDoc . documentElement . scrollHeight ;
148+ const hasScrollableContent = scrollHeight > rect . height ;
149+ const areaScore = rect . width * rect . height ;
150+ const scrollScore = hasScrollableContent ? scrollHeight : 0 ;
151+ const score = areaScore + scrollScore * 100 ;
152+
153+ if ( score > bestScore ) {
154+ bestScore = score ;
155+ bestIframe = iframe ;
156+ }
157+ } catch {
158+ // Skip iframes we can't access (cross-origin)
159+ continue ;
160+ }
161+ }
162+
163+ return bestIframe ;
164+ } ) ;
165+
166+ const iframeElement = iframeHandle . asElement ( ) ;
167+ if ( ! iframeElement ) {
168+ await iframeHandle . dispose ( ) ;
169+ return null ;
170+ }
171+
172+ // Cast to the correct type
173+ const iframe = iframeElement as ElementHandle < Element > ;
174+
175+ // Verify the iframe has scrollable content
176+ const contentFrame = await iframe . contentFrame ( ) ;
177+ if ( ! contentFrame ) {
178+ await iframe . dispose ( ) ;
179+ return null ;
180+ }
181+
182+ const { scrollHeight, clientHeight} = await contentFrame . evaluate ( ( ) => ( {
183+ scrollHeight : document . documentElement . scrollHeight ,
184+ clientHeight : document . documentElement . clientHeight ,
185+ } ) ) ;
186+
187+ // Only return if there's actually scrollable content
188+ if ( scrollHeight > clientHeight ) {
189+ return iframe ;
190+ }
191+
192+ await iframe . dispose ( ) ;
193+ return null ;
194+ }
195+
13196export const screenshot = defineTool ( {
14197 name : 'take_screenshot' ,
15198 description : `Take a screenshot of the page or element.` ,
@@ -41,7 +224,13 @@ export const screenshot = defineTool({
41224 . boolean ( )
42225 . optional ( )
43226 . describe (
44- 'If set to true takes a screenshot of the full page instead of the currently visible viewport. Incompatible with uid.' ,
227+ 'If set to true takes a screenshot of the full page instead of the currently visible viewport. Incompatible with uid unless iframeUid is also provided.' ,
228+ ) ,
229+ iframeUid : zod
230+ . string ( )
231+ . optional ( )
232+ . describe (
233+ 'The uid of an iframe element. When used with fullPage=true, captures the full scrollable content of the iframe by temporarily expanding it.' ,
45234 ) ,
46235 filePath : zod
47236 . string ( )
@@ -51,41 +240,93 @@ export const screenshot = defineTool({
51240 ) ,
52241 } ,
53242 handler : async ( request , response , context ) => {
54- if ( request . params . uid && request . params . fullPage ) {
243+ const { uid, fullPage, iframeUid} = request . params ;
244+
245+ // Validate parameter combinations
246+ if ( uid && fullPage && ! iframeUid ) {
55247 throw new Error ( 'Providing both "uid" and "fullPage" is not allowed.' ) ;
56248 }
57-
58- let pageOrHandle : Page | ElementHandle ;
59- if ( request . params . uid ) {
60- pageOrHandle = await context . getElementByUid ( request . params . uid ) ;
61- } else {
62- pageOrHandle = context . getSelectedPage ( ) ;
249+ if ( uid && iframeUid ) {
250+ throw new Error ( 'Providing both "uid" and "iframeUid" is not allowed.' ) ;
251+ }
252+ if ( iframeUid && ! fullPage ) {
253+ throw new Error (
254+ 'iframeUid requires fullPage=true to capture the full iframe content.' ,
255+ ) ;
63256 }
64257
65258 const format = request . params . format ;
66259 const quality = format === 'png' ? undefined : request . params . quality ;
67260
68- const screenshot = await pageOrHandle . screenshot ( {
69- type : format ,
70- fullPage : request . params . fullPage ,
71- quality,
72- optimizeForSpeed : true , // Bonus: optimize encoding for speed
73- } ) ;
261+ let screenshot : Uint8Array ;
262+ let responseMessage : string ;
74263
75- if ( request . params . uid ) {
76- response . appendResponseLine (
77- `Took a screenshot of node with uid "${ request . params . uid } ".` ,
78- ) ;
79- } else if ( request . params . fullPage ) {
80- response . appendResponseLine (
81- 'Took a screenshot of the full current page.' ,
82- ) ;
264+ if ( iframeUid && fullPage ) {
265+ // Full-page screenshot of iframe content (explicit iframe specified)
266+ const iframeHandle = await context . getElementByUid ( iframeUid ) ;
267+ try {
268+ screenshot = await takeIframeFullPageScreenshot ( iframeHandle , {
269+ type : format ,
270+ quality,
271+ } ) ;
272+ responseMessage = `Took a full-page screenshot of iframe with uid "${ iframeUid } ".` ;
273+ } finally {
274+ void iframeHandle . dispose ( ) ;
275+ }
276+ } else if ( uid ) {
277+ // Screenshot of a specific element
278+ const handle = await context . getElementByUid ( uid ) ;
279+ try {
280+ screenshot = await handle . screenshot ( {
281+ type : format ,
282+ quality,
283+ optimizeForSpeed : true ,
284+ } ) ;
285+ responseMessage = `Took a screenshot of node with uid "${ uid } ".` ;
286+ } finally {
287+ void handle . dispose ( ) ;
288+ }
289+ } else if ( fullPage ) {
290+ // Full-page screenshot - auto-detect iframe with scrollable content
291+ const page : Page = context . getSelectedPage ( ) ;
292+ const mainIframe = await findMainContentIframe ( page ) ;
293+
294+ if ( mainIframe ) {
295+ // Found an iframe with scrollable content - capture its full content
296+ try {
297+ screenshot = await takeIframeFullPageScreenshot ( mainIframe , {
298+ type : format ,
299+ quality,
300+ } ) ;
301+ responseMessage =
302+ 'Took a full-page screenshot of the main content iframe.' ;
303+ } finally {
304+ void mainIframe . dispose ( ) ;
305+ }
306+ } else {
307+ // No significant iframe found - take regular full page screenshot
308+ screenshot = await page . screenshot ( {
309+ type : format ,
310+ fullPage : true ,
311+ quality,
312+ optimizeForSpeed : true ,
313+ } ) ;
314+ responseMessage = 'Took a screenshot of the full current page.' ;
315+ }
83316 } else {
84- response . appendResponseLine (
85- "Took a screenshot of the current page's viewport." ,
86- ) ;
317+ // Viewport screenshot
318+ const page : Page = context . getSelectedPage ( ) ;
319+ screenshot = await page . screenshot ( {
320+ type : format ,
321+ fullPage : false ,
322+ quality,
323+ optimizeForSpeed : true ,
324+ } ) ;
325+ responseMessage = "Took a screenshot of the current page's viewport." ;
87326 }
88327
328+ response . appendResponseLine ( responseMessage ) ;
329+
89330 if ( request . params . filePath ) {
90331 const file = await context . saveFile ( screenshot , request . params . filePath ) ;
91332 response . appendResponseLine ( `Saved screenshot to ${ file . filename } .` ) ;
0 commit comments