Skip to main content
Glama
aj47
by aj47
cdp-client.ts7.6 kB
/** * Chrome DevTools Protocol Client * Manages connections to Electron apps for DOM inspection */ // @ts-ignore - no types available for chrome-remote-interface import CDP from 'chrome-remote-interface'; import type { CDPTarget, CDPConnection, DOMNode } from '../types/index.js'; import { CDPError } from '../types/index.js'; export class CDPClient { private connections: Map<string, CDPConnection> = new Map(); private host: string; private port: number; constructor(host = 'localhost', port = 9222) { this.host = host; this.port = port; } /** * List all available CDP targets (Electron windows/webviews) */ async listTargets(): Promise<CDPTarget[]> { try { const targets = await CDP.List({ host: this.host, port: this.port }); return targets.map((target: any) => ({ id: target.id, type: target.type, title: target.title, url: target.url, webSocketDebuggerUrl: target.webSocketDebuggerUrl, devtoolsFrontendUrl: target.devtoolsFrontendUrl, })); } catch (error: any) { throw new CDPError( `Failed to list CDP targets. Make sure Electron is running with --inspect flag.`, { originalError: error.message, host: this.host, port: this.port } ); } } /** * Connect to a specific target */ async connect(targetId: string): Promise<void> { try { // Check if already connected if (this.connections.has(targetId)) { const existing = this.connections.get(targetId)!; if (existing.connected) { return; } } // Create new connection const client = await CDP({ host: this.host, port: this.port, target: targetId, }); // Enable required domains await Promise.all([ client.DOM.enable(), client.Runtime.enable(), client.Page.enable(), client.Network.enable(), ]); this.connections.set(targetId, { targetId, client, connected: true, }); } catch (error: any) { throw new CDPError(`Failed to connect to target ${targetId}`, { originalError: error.message, }); } } /** * Disconnect from a target */ async disconnect(targetId: string): Promise<void> { const connection = this.connections.get(targetId); if (connection && connection.connected) { try { await connection.client.close(); connection.connected = false; } catch (error) { // Ignore errors on disconnect } this.connections.delete(targetId); } } /** * Disconnect from all targets */ async disconnectAll(): Promise<void> { const disconnectPromises = Array.from(this.connections.keys()).map((targetId) => this.disconnect(targetId) ); await Promise.all(disconnectPromises); } /** * Get the DOM document for a target */ async getDocument(targetId: string): Promise<DOMNode> { const connection = this.getConnection(targetId); try { const { root } = await connection.client.DOM.getDocument({ depth: -1 }); return root as DOMNode; } catch (error: any) { throw new CDPError(`Failed to get document for target ${targetId}`, { originalError: error.message, }); } } /** * Query selector in the DOM */ async querySelector(targetId: string, selector: string): Promise<number | null> { const connection = this.getConnection(targetId); try { const { root } = await connection.client.DOM.getDocument(); const { nodeId } = await connection.client.DOM.querySelector({ nodeId: root.nodeId, selector, }); return nodeId || null; } catch (error: any) { throw new CDPError(`Failed to query selector "${selector}"`, { originalError: error.message, }); } } /** * Query all matching selectors */ async querySelectorAll(targetId: string, selector: string): Promise<number[]> { const connection = this.getConnection(targetId); try { const { root } = await connection.client.DOM.getDocument(); const { nodeIds } = await connection.client.DOM.querySelectorAll({ nodeId: root.nodeId, selector, }); return nodeIds || []; } catch (error: any) { throw new CDPError(`Failed to query selector all "${selector}"`, { originalError: error.message, }); } } /** * Get attributes of a DOM node */ async getNodeAttributes(targetId: string, nodeId: number): Promise<Record<string, string>> { const connection = this.getConnection(targetId); try { const { attributes } = await connection.client.DOM.getAttributes({ nodeId }); const result: Record<string, string> = {}; // Attributes come as [name1, value1, name2, value2, ...] for (let i = 0; i < attributes.length; i += 2) { result[attributes[i]] = attributes[i + 1]; } return result; } catch (error: any) { throw new CDPError(`Failed to get attributes for node ${nodeId}`, { originalError: error.message, }); } } /** * Get outer HTML of a node */ async getOuterHTML(targetId: string, nodeId: number): Promise<string> { const connection = this.getConnection(targetId); try { const { outerHTML } = await connection.client.DOM.getOuterHTML({ nodeId }); return outerHTML; } catch (error: any) { throw new CDPError(`Failed to get outer HTML for node ${nodeId}`, { originalError: error.message, }); } } /** * Execute JavaScript in the page context */ async evaluate(targetId: string, expression: string): Promise<any> { const connection = this.getConnection(targetId); try { const { result, exceptionDetails } = await connection.client.Runtime.evaluate({ expression, returnByValue: true, awaitPromise: true, }); if (exceptionDetails) { throw new Error(exceptionDetails.text || 'JavaScript execution failed'); } return result.value; } catch (error: any) { throw new CDPError(`Failed to evaluate JavaScript`, { originalError: error.message, expression, }); } } /** * Take a screenshot of the page */ async screenshot( targetId: string, options?: { format?: 'png' | 'jpeg'; quality?: number } ): Promise<string> { const connection = this.getConnection(targetId); try { const { data } = await connection.client.Page.captureScreenshot({ format: options?.format || 'png', quality: options?.quality, }); return data; } catch (error: any) { throw new CDPError(`Failed to capture screenshot`, { originalError: error.message, }); } } /** * Get a connection or throw error */ private getConnection(targetId: string): CDPConnection { const connection = this.connections.get(targetId); if (!connection || !connection.connected) { throw new CDPError(`Not connected to target ${targetId}. Call connect() first.`); } return connection; } /** * Check if connected to a target */ isConnected(targetId: string): boolean { const connection = this.connections.get(targetId); return connection?.connected || false; } /** * Get all active connections */ getActiveConnections(): string[] { return Array.from(this.connections.entries()) .filter(([_, conn]) => conn.connected) .map(([targetId]) => targetId); } }

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/aj47/electron-native-mcp'

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