query_shadow_dom
Query elements inside a shadow DOM by providing the host element selector and the inner selector within the shadow root.
Instructions
Query an element inside a shadow DOM root. Provide the host element selector and the inner selector within the shadow root.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| hostSelector | Yes | CSS selector for the shadow host element | |
| innerSelector | Yes | CSS selector within the shadow root | |
| tabId | No | Target tab ID (defaults to currently active tab) | |
| apiKey | No | API key for authentication if enabled |
Implementation Reference
- src/tools/elements.ts:6-155 (registration)The registerElementTools function registers all element-related MCP tools including query_shadow_dom on the server via server.tool().
export function registerElementTools(server: McpServer, bridge: WebSocketBridge) { server.tool( 'find_element', 'Find an element by visible text, ARIA role, ARIA label, CSS selector, or XPath. Returns a unique CSS selector and element details. Use this when you need to locate an element but don\'t know its exact selector. Role matching supports both explicit role="..." attributes AND semantic HTML (e.g., <button> matches role "button", <a> matches role "link"). When multiple filters are provided (e.g., role + text), they are combined with AND logic.', { text: z.string().optional().describe('Find by visible text content (substring match). Can be combined with role for precise targeting.'), role: z.string().optional().describe('Find by ARIA role (e.g., "button", "link", "navigation", "main"). Falls back to semantic HTML roles if no explicit role attribute is present.'), ariaLabel: z.string().optional().describe('Find by aria-label attribute (substring match)'), css: z.string().optional().describe('CSS selector (most direct — bypasses all other filters)'), xpath: z.string().optional().describe('XPath expression (bypasses all other filters)'), nth: z.number().optional().describe('Select the Nth match (0-indexed), default: 0'), tabId: z.number().optional().describe('Target tab ID (defaults to currently active tab)'), apiKey: z.string().optional().describe('API key for authentication if enabled'), }, async ({ text, role, ariaLabel, css, xpath, nth, tabId, apiKey }) => { const result = await bridge.sendCommand({ command: 'find_element', params: { text, role, ariaLabel, css, xpath, nth }, tabId, apiKey, timeout: LONG_TIMEOUT, }); if (!result.success) { return { content: [{ type: 'text', text: `Error: ${result.error?.message}` }], isError: true }; } return { content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }] }; } ); server.tool( 'get_element_state', 'Get the state of an element: exists, visible, enabled, focused, checked, selected, readOnly, required, valid, and more. Essential for verifying element conditions during testing.', { selector: z.string().describe('CSS selector or XPath expression'), selectorType: z.enum(['css', 'xpath']).optional().default('css').describe('Selector type'), tabId: z.number().optional().describe('Target tab ID (defaults to currently active tab)'), apiKey: z.string().optional().describe('API key for authentication if enabled'), }, async ({ selector, selectorType, tabId, apiKey }) => { const result = await bridge.sendCommand({ command: 'get_element_state', params: { selector, selectorType }, tabId, apiKey, timeout: LONG_TIMEOUT, }); if (!result.success) { return { content: [{ type: 'text', text: `Error: ${result.error?.message}` }], isError: true }; } return { content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }] }; } ); server.tool( 'query_shadow_dom', 'Query an element inside a shadow DOM root. Provide the host element selector and the inner selector within the shadow root.', { hostSelector: z.string().describe('CSS selector for the shadow host element'), innerSelector: z.string().describe('CSS selector within the shadow root'), tabId: z.number().optional().describe('Target tab ID (defaults to currently active tab)'), apiKey: z.string().optional().describe('API key for authentication if enabled'), }, async ({ hostSelector, innerSelector, tabId, apiKey }) => { const result = await bridge.sendCommand({ command: 'query_shadow_dom', params: { hostSelector, innerSelector }, tabId, apiKey, timeout: LONG_TIMEOUT, }); if (!result.success) { return { content: [{ type: 'text', text: `Error: ${result.error?.message}` }], isError: true }; } return { content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }] }; } ); server.tool( 'get_computed_styles', 'Get computed CSS styles for an element. Optionally filter to specific properties or query pseudo-elements like ::before and ::after.', { selector: z.string().describe('CSS selector'), properties: z.array(z.string()).optional().describe('Specific CSS properties to query (default: all computed styles)'), pseudoElement: z.string().optional().describe('Pseudo-element (e.g., "::before", "::after")'), tabId: z.number().optional().describe('Target tab ID (defaults to currently active tab)'), apiKey: z.string().optional().describe('API key for authentication if enabled'), }, async ({ selector, properties, pseudoElement, tabId, apiKey }) => { const result = await bridge.sendCommand({ command: 'get_computed_styles', params: { selector, properties, pseudoElement }, tabId, apiKey, timeout: LONG_TIMEOUT, }); if (!result.success) { return { content: [{ type: 'text', text: `Error: ${result.error?.message}` }], isError: true }; } return { content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }] }; } ); server.tool( 'deep_query_shadow_dom', 'Query an element inside a deep shadow DOM tree by providing a path of host selectors.', { hostPath: z.array(z.string()).describe('Ordered array of shadow host CSS selectors to traverse'), innerSelector: z.string().describe('CSS selector within the deepest shadow root'), tabId: z.number().optional().describe('Target tab ID (defaults to currently active tab)'), apiKey: z.string().optional().describe('API key for authentication if enabled'), }, async ({ hostPath, innerSelector, tabId, apiKey }) => { const result = await bridge.sendCommand({ command: 'deep_query_shadow_dom', params: { hostPath, innerSelector }, tabId, apiKey, timeout: LONG_TIMEOUT, }); if (!result.success) { return { content: [{ type: 'text', text: `Error: ${result.error?.message}` }], isError: true }; } return { content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }] }; } ); server.tool( 'get_shadow_dom_tree', 'Return the full shadow DOM tree structure as JSON for a given host element.', { hostSelector: z.string().describe('CSS selector for the shadow host element'), maxDepth: z.number().optional().default(5).describe('Maximum depth to traverse'), tabId: z.number().optional().describe('Target tab ID (defaults to currently active tab)'), apiKey: z.string().optional().describe('API key for authentication if enabled'), }, async ({ hostSelector, maxDepth, tabId, apiKey }) => { const result = await bridge.sendCommand({ command: 'get_shadow_dom_tree', params: { hostSelector, maxDepth }, tabId, apiKey, timeout: LONG_TIMEOUT, }); if (!result.success) { return { content: [{ type: 'text', text: `Error: ${result.error?.message}` }], isError: true }; } return { content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }] }; } ); } - src/tools/elements.ts:62-67 (schema)Input schema for query_shadow_dom tool: hostSelector (string), innerSelector (string), optional tabId (number), optional apiKey (string).
{ hostSelector: z.string().describe('CSS selector for the shadow host element'), innerSelector: z.string().describe('CSS selector within the shadow root'), tabId: z.number().optional().describe('Target tab ID (defaults to currently active tab)'), apiKey: z.string().optional().describe('API key for authentication if enabled'), }, - src/tools/elements.ts:68-81 (handler)Handler function for query_shadow_dom tool. Sends a 'query_shadow_dom' command via WebSocket bridge with hostSelector and innerSelector params. Returns the result or an error.
async ({ hostSelector, innerSelector, tabId, apiKey }) => { const result = await bridge.sendCommand({ command: 'query_shadow_dom', params: { hostSelector, innerSelector }, tabId, apiKey, timeout: LONG_TIMEOUT, }); if (!result.success) { return { content: [{ type: 'text', text: `Error: ${result.error?.message}` }], isError: true }; } return { content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }] }; } ); - src/websocket-bridge.ts:63-103 (helper)WebSocketBridge.sendCommand() is the helper that forwards the command to the Chrome extension over WebSocket and resolves the response.
async sendCommand(cmd: BridgeCommand): Promise<BridgeResponse> { if (!this.isConnected()) { return { success: false, error: { code: 'NOT_CONNECTED', message: 'Chrome extension is not connected. Ensure the extension is installed, enabled, and the browser is running.', }, }; } const id = crypto.randomUUID(); const timeout = cmd.timeout ?? DEFAULT_TIMEOUT; return new Promise<BridgeResponse>((resolve, reject) => { const timer = setTimeout(() => { this.pending.delete(id); resolve({ success: false, error: { code: 'TIMEOUT', message: `Command '${cmd.command}' timed out after ${timeout}ms`, }, }); }, timeout); this.pending.set(id, { resolve, reject, timer }); const message = { id, type: 'request', command: cmd.command, params: cmd.params, tabId: cmd.tabId, apiKey: cmd.apiKey, timestamp: Date.now(), }; this.client!.send(JSON.stringify(message)); }); } - src/index.ts:7-8 (helper)Entry point creating the WebSocketBridge and server, which ultimately registers the query_shadow_dom tool.
const bridge = new WebSocketBridge(WEBSOCKET_PORT); const server = createServer(bridge);