Skip to main content
Glama
Leviathangk

Playwright MCP Server

by Leviathangk
get-page-structure.ts12.2 kB
import { SessionManager } from '../session-manager.js'; import { ErrorResponse } from '../errors.js'; export interface GetPageStructureArgs { sessionId: string; selector?: string; includeHidden?: boolean; maxElements?: number; } interface PageElement { type: string; text: string; selector: string; visible: boolean; clickable: boolean; tag: string; attributes?: Record<string, string>; children?: PageElement[]; } export async function handleGetPageStructure( sessionManager: SessionManager, args: any ): Promise<any> { const { sessionId, selector, includeHidden = false, maxElements = 100 } = args as GetPageStructureArgs; // Validate session const validation = sessionManager.validateSession(sessionId); if (!validation.valid) { return { content: [{ type: 'text', text: JSON.stringify(validation.error) }], isError: true, }; } const session = sessionManager.getSession(sessionId); if (!session) { return { content: [ { type: 'text', text: JSON.stringify({ errorCode: 'SESSION_NOT_FOUND', message: `Session not found: ${sessionId}`, } as ErrorResponse), }, ], isError: true, }; } try { // Get page structure using Playwright const structure = await session.page.evaluate( ({ rootSelector, includeHidden, maxElements }): any => { const elements: any[] = []; let elementCount = 0; // Interactive element selectors const interactiveSelectors = [ 'a', 'button', 'input', 'select', 'textarea', '[role="button"]', '[role="link"]', '[onclick]', '.clickable', ]; function isVisible(element: any): boolean { // @ts-ignore - browser context const style = window.getComputedStyle(element); return ( style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0' ); } function isClickable(element: any): boolean { const tag = element.tagName.toLowerCase(); if (['a', 'button', 'input', 'select', 'textarea'].includes(tag)) { return true; } if (element.hasAttribute('onclick') || element.hasAttribute('role')) { return true; } return false; } function getSelector(element: any): string { function isUnique(el: any, selector: string): boolean { try { // @ts-ignore - browser context const matches = document.querySelectorAll(selector); return matches.length === 1 && matches[0] === el; } catch { return false; } } function getNodeSelector(node: any): string { const tag = node.tagName.toLowerCase(); const classes = Array.from(node.classList).filter((c: any) => c && !c.startsWith('_')); if (classes.length > 0) { const classSelector = `.${classes.join('.')}`; const parent = node.parentElement; if (parent) { const siblings = Array.from(parent.children).filter( (el: any) => { const elClasses = Array.from(el.classList).filter((c: any) => c && !c.startsWith('_')); return elClasses.length > 0 && elClasses.join('.') === classes.join('.'); } ); if (siblings.length === 1) { return `${tag}${classSelector}`; } else { const index = siblings.indexOf(node) + 1; return `${tag}${classSelector}:nth-of-type(${index})`; } } return `${tag}${classSelector}`; } else { const parent = node.parentElement; if (parent) { const siblings = Array.from(parent.children); const index = siblings.indexOf(node) + 1; return `${tag}:nth-child(${index})`; } return tag; } } function buildPathFrom(ancestor: any, target: any, maxLevels: number = 5): string { const path: string[] = []; let current = target; let levels = 0; while (current && current !== ancestor && levels < maxLevels) { path.unshift(getNodeSelector(current)); current = current.parentElement; levels++; } // Add ancestor selector if (ancestor.id) { path.unshift(`#${ancestor.id}`); } else if (ancestor.classList && ancestor.classList.length > 0) { const classes = Array.from(ancestor.classList).filter((c: any) => c && !c.startsWith('_')); if (classes.length > 0) { path.unshift(`${ancestor.tagName.toLowerCase()}.${classes.join('.')}`); } } return path.join(' > '); } // 1. If element has ID, return it directly if (element.id) { return `#${element.id}`; } // 2. Try using only classes if unique const classes = Array.from(element.classList).filter((c: any) => c && !c.startsWith('_')); if (classes.length > 0) { const tag = element.tagName.toLowerCase(); const classSelector = `${tag}.${classes.join('.')}`; if (isUnique(element, classSelector)) { return classSelector; } } // 3. Find unique ancestor (max 10 levels up) let ancestor = element.parentElement; let depth = 0; const maxAncestorDepth = 10; while (ancestor && depth < maxAncestorDepth) { // If ancestor has ID, build path from here if (ancestor.id) { return buildPathFrom(ancestor, element); } // If ancestor has unique class combination const ancestorClasses = Array.from(ancestor.classList).filter((c: any) => c && !c.startsWith('_')); if (ancestorClasses.length > 0) { const ancestorTag = ancestor.tagName.toLowerCase(); const ancestorSelector = `${ancestorTag}.${ancestorClasses.join('.')}`; if (isUnique(ancestor, ancestorSelector)) { return buildPathFrom(ancestor, element); } } ancestor = ancestor.parentElement; depth++; } // 4. Fallback: build path from body with max 5 levels // @ts-ignore - browser context return buildPathFrom(document.body, element, 5); } function getText(element: any): string { // Get direct text content (not from children) let text = ''; for (const node of element.childNodes) { if (node.nodeType === 3) { // TEXT_NODE text += node.textContent?.trim() || ''; } } // If no direct text, try common text attributes if (!text) { const el = element; text = el.getAttribute('aria-label') || el.getAttribute('title') || el.getAttribute('placeholder') || el.getAttribute('alt') || ''; } return text.trim().substring(0, 100); // Limit text length } function extractElement(element: any, depth: number = 0): any | null { if (elementCount >= maxElements) return null; if (depth > 5) return null; // Limit depth const visible = isVisible(element); if (!includeHidden && !visible) return null; const clickable = isClickable(element); const text = getText(element); // Skip elements with no text and not interactive if (!text && !clickable && depth > 0) return null; elementCount++; const elementData: any = { type: element.getAttribute('role') || element.tagName.toLowerCase(), text, selector: getSelector(element), visible, clickable, tag: element.tagName.toLowerCase(), }; // Add useful attributes const attrs: Record<string, string> = {}; const el = element; if (el.href) attrs.href = el.href; if (el.getAttribute('type')) attrs.type = el.getAttribute('type')!; if (el.className) attrs.class = el.className; if (Object.keys(attrs).length > 0) { elementData.attributes = attrs; } // Process children for interactive or container elements if (clickable || ['nav', 'menu', 'ul', 'ol', 'div', 'section'].includes(elementData.type)) { const children: any[] = []; for (const child of element.children) { if (elementCount >= maxElements) break; const childData = extractElement(child, depth + 1); if (childData) { children.push(childData); } } if (children.length > 0) { elementData.children = children; } } return elementData; } // Start extraction // @ts-ignore - browser context const root = rootSelector ? document.querySelector(rootSelector) : document.body; if (!root) { return { elements: [], error: 'Root element not found' }; } // First, collect all interactive elements const interactiveElements: any[] = []; const allFoundElements: any[] = []; // 用于日志记录所有找到的元素 for (const selector of interactiveSelectors) { const elements = root.querySelectorAll(selector); console.log(`[DEBUG] 选择器 "${selector}" 找到 ${elements.length} 个元素`); for (const element of elements) { const visible = isVisible(element); const text = getText(element); const tag = element.tagName.toLowerCase(); const elementSelector = getSelector(element); // 记录所有找到的元素(用于调试) allFoundElements.push({ selector: selector, tag: tag, text: text || '(无文本)', visible: visible, elementSelector: elementSelector, outerHTML: element.outerHTML?.substring(0, 200) || '', // 前200字符 }); if (elementCount >= maxElements) break; if (!includeHidden && !visible) continue; interactiveElements.push({ type: element.getAttribute('role') || element.tagName.toLowerCase(), text: text, selector: elementSelector, visible, clickable: true, tag: tag, }); elementCount++; } } // 打印所有找到的元素日志 console.log(`[DEBUG] 总共找到 ${allFoundElements.length} 个元素(过滤前)`); console.log('[DEBUG] 所有元素详情:', JSON.stringify(allFoundElements, null, 2)); return { elements: interactiveElements, totalFound: elementCount, }; }, { rootSelector: selector, includeHidden, maxElements } ); return { content: [ { type: 'text', text: JSON.stringify(structure), }, ], }; } catch (error: any) { return { content: [ { type: 'text', text: JSON.stringify({ errorCode: 'GET_PAGE_STRUCTURE_FAILED', message: error.message || 'Failed to get page structure', sessionId, } as ErrorResponse), }, ], isError: true, }; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Leviathangk/PlaywrightMCPForCrawler'

If you have feedback or need assistance with the MCP directory API, please join our Discord server