@cgaspard/webappmcp
Version:
WebApp MCP - Model Context Protocol integration for web applications with server-side debugging tools
1,318 lines (1,258 loc) • 54.8 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.WebAppMCP = {}));
})(this, (function (exports) { 'use strict';
class MCPDevTools {
constructor(config = {}) {
this.container = null;
this.isExpanded = false;
this.logs = [];
this.maxLogs = 500;
this.connectionStatus = 'disconnected';
this.config = {
position: 'bottom-right',
theme: 'dark',
...config
};
this.addStyles();
this.createDevToolsUI();
this.setupEventListeners();
}
createDevToolsUI() {
this.container = document.createElement('div');
this.container.id = 'mcp-devtools';
this.container.className = `mcp-theme-${this.config.theme} mcp-position-${this.config.position}`;
this.container.innerHTML = `
<div class="mcp-devtools-indicator" id="mcp-indicator">
<div class="mcp-status-dot" id="mcp-status-dot"></div>
<span class="mcp-label">MCP</span>
</div>
<div class="mcp-devtools-panel" id="mcp-panel" style="display: none;">
<div class="mcp-devtools-header">
<span>MCP DevTools</span>
<div class="mcp-header-controls">
<button class="mcp-theme-toggle" id="mcp-theme-btn" title="Toggle theme">🌓</button>
<button class="mcp-close-btn" id="mcp-close-btn">×</button>
</div>
</div>
<div class="mcp-devtools-content">
<div class="mcp-status-section">
<div class="mcp-status-item">
<label>WebSocket:</label>
<span id="mcp-ws-status">Disconnected</span>
</div>
<div class="mcp-status-item">
<label>MCP Server:</label>
<span id="mcp-server-status">Disconnected</span>
</div>
</div>
<div class="mcp-logs-section">
<div class="mcp-logs-header">
<span>Activity Log</span>
<div class="mcp-logs-controls">
<label class="mcp-checkbox">
<input type="checkbox" id="mcp-autoscroll" checked>
Auto-scroll
</label>
<button class="mcp-clear-btn" id="mcp-clear-btn">Clear</button>
</div>
</div>
<div class="mcp-logs-container" id="mcp-logs"></div>
</div>
</div>
</div>
`;
this.addStyles();
document.body.appendChild(this.container);
}
addStyles() {
const style = document.createElement('style');
style.textContent = `
#mcp-devtools {
position: fixed;
z-index: 10000;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
/* Positioning */
#mcp-devtools.mcp-position-bottom-right {
bottom: 20px;
right: 20px;
}
#mcp-devtools.mcp-position-bottom-left {
bottom: 20px;
left: 20px;
}
#mcp-devtools.mcp-position-top-right {
top: 20px;
right: 20px;
}
#mcp-devtools.mcp-position-top-left {
top: 20px;
left: 20px;
}
.mcp-devtools-indicator {
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
background: rgba(255, 255, 255, 0.9);
border-radius: 4px;
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.mcp-label {
font-size: 10px;
font-weight: 600;
color: #495057;
user-select: none;
}
.mcp-status-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background-color: #dc3545;
border: 2px solid rgba(255, 255, 255, 0.8);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
transition: background-color 0.3s ease;
}
.mcp-status-dot.connecting {
background-color: #ffc107;
animation: pulse 1.5s infinite;
}
.mcp-status-dot.connected {
background-color: #28a745;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.mcp-devtools-panel {
position: absolute;
width: 500px;
height: 400px;
background: white;
border: 1px solid #e0e0e0;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
}
/* Panel positioning based on indicator position */
.mcp-position-bottom-right .mcp-devtools-panel {
bottom: 40px;
right: 0;
}
.mcp-position-bottom-left .mcp-devtools-panel {
bottom: 40px;
left: 0;
}
.mcp-position-top-right .mcp-devtools-panel {
top: 40px;
right: 0;
}
.mcp-position-top-left .mcp-devtools-panel {
top: 40px;
left: 0;
}
.mcp-devtools-header {
padding: 12px 16px;
background: #f8f9fa;
border-bottom: 1px solid #e0e0e0;
border-radius: 8px 8px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 600;
font-size: 14px;
}
.mcp-close-btn {
background: none;
border: none;
font-size: 18px;
cursor: pointer;
color: #6c757d;
padding: 0;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.mcp-close-btn:hover {
color: #495057;
}
.mcp-devtools-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.mcp-status-section {
padding: 12px 16px;
border-bottom: 1px solid #e0e0e0;
background: #f8f9fa;
}
.mcp-status-item {
display: flex;
justify-content: space-between;
margin-bottom: 4px;
font-size: 12px;
}
.mcp-status-item label {
font-weight: 500;
color: #495057;
}
.mcp-status-item span {
color: #6c757d;
}
.mcp-logs-section {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.mcp-logs-header {
padding: 8px 16px;
background: #f8f9fa;
border-bottom: 1px solid #e0e0e0;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
font-weight: 500;
}
.mcp-clear-btn {
background: none;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 2px 8px;
font-size: 11px;
cursor: pointer;
color: #6c757d;
}
.mcp-clear-btn:hover {
background: #e9ecef;
}
.mcp-logs-container {
flex: 1;
overflow-y: auto;
padding: 8px;
font-size: 11px;
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
}
.mcp-log-entry {
margin-bottom: 4px;
padding: 4px 8px;
border-radius: 4px;
white-space: pre-wrap;
word-break: break-word;
}
.mcp-log-entry.info {
background: #e7f3ff;
color: #0c5460;
}
.mcp-log-entry.warning {
background: #fff3cd;
color: #856404;
}
.mcp-log-entry.error {
background: #f8d7da;
color: #721c24;
}
.mcp-log-timestamp {
color: #6c757d;
font-size: 10px;
}
/* Header controls */
.mcp-header-controls {
display: flex;
gap: 8px;
align-items: center;
}
.mcp-theme-toggle {
background: none;
border: none;
font-size: 16px;
cursor: pointer;
padding: 0;
opacity: 0.6;
transition: opacity 0.2s;
}
.mcp-theme-toggle:hover {
opacity: 1;
}
/* Log controls */
.mcp-logs-controls {
display: flex;
gap: 8px;
align-items: center;
}
.mcp-checkbox {
display: flex;
align-items: center;
gap: 4px;
font-size: 11px;
cursor: pointer;
}
.mcp-checkbox input {
cursor: pointer;
}
/* Tool execution logs */
.mcp-log-entry.tool {
background: #e7f3ff;
color: #004085;
border-left: 3px solid #004085;
}
.mcp-tool-details {
font-size: 10px;
margin-top: 4px;
padding-left: 16px;
opacity: 0.8;
}
/* Dark theme */
.mcp-theme-dark .mcp-devtools-indicator {
background: rgba(30, 30, 30, 0.9);
border-color: rgba(255, 255, 255, 0.1);
}
.mcp-theme-dark .mcp-label {
color: #e0e0e0;
}
.mcp-theme-dark .mcp-devtools-panel {
background: #1e1e1e;
border-color: #333;
}
.mcp-theme-dark .mcp-devtools-header {
background: #2d2d2d;
border-color: #333;
color: #e0e0e0;
}
.mcp-theme-dark .mcp-devtools-content {
background: #1e1e1e;
}
.mcp-theme-dark .mcp-status-section {
background: #2d2d2d;
border-color: #333;
}
.mcp-theme-dark .mcp-status-item label {
color: #b0b0b0;
}
.mcp-theme-dark .mcp-status-item span {
color: #e0e0e0;
}
.mcp-theme-dark .mcp-logs-header {
background: #2d2d2d;
border-color: #333;
color: #e0e0e0;
}
.mcp-theme-dark .mcp-logs-container {
background: #252525;
border-color: #333;
}
.mcp-theme-dark .mcp-log-entry {
background: #2d2d2d;
color: #e0e0e0;
border-color: #333;
}
.mcp-theme-dark .mcp-log-entry.warning {
background: #4a3800;
color: #ffc107;
}
.mcp-theme-dark .mcp-log-entry.error {
background: #4a0000;
color: #ff6b6b;
}
.mcp-theme-dark .mcp-log-entry.tool {
background: #003366;
color: #66b3ff;
border-left-color: #66b3ff;
}
.mcp-theme-dark .mcp-log-timestamp {
color: #888;
}
.mcp-theme-dark .mcp-clear-btn,
.mcp-theme-dark .mcp-close-btn {
color: #e0e0e0;
border-color: #555;
}
.mcp-theme-dark .mcp-clear-btn:hover,
.mcp-theme-dark .mcp-close-btn:hover {
background: #444;
}
}
`;
document.head.appendChild(style);
}
setupEventListeners() {
const indicator = this.container?.querySelector('#mcp-indicator');
const closeBtn = this.container?.querySelector('#mcp-close-btn');
const clearBtn = this.container?.querySelector('#mcp-clear-btn');
const themeBtn = this.container?.querySelector('#mcp-theme-btn');
indicator?.addEventListener('click', () => this.togglePanel());
closeBtn?.addEventListener('click', () => this.togglePanel());
clearBtn?.addEventListener('click', () => this.clearLogs());
themeBtn?.addEventListener('click', () => this.toggleTheme());
}
toggleTheme() {
if (!this.container)
return;
this.config.theme = this.config.theme === 'light' ? 'dark' : 'light';
this.container.className = `mcp-theme-${this.config.theme} mcp-position-${this.config.position}`;
}
togglePanel() {
this.isExpanded = !this.isExpanded;
const panel = this.container?.querySelector('#mcp-panel');
if (panel) {
panel.style.display = this.isExpanded ? 'flex' : 'none';
}
}
setConnectionStatus(status) {
this.connectionStatus = status;
const dot = this.container?.querySelector('#mcp-status-dot');
const wsStatus = this.container?.querySelector('#mcp-ws-status');
const serverStatus = this.container?.querySelector('#mcp-server-status');
if (dot) {
dot.className = `mcp-status-dot ${status}`;
}
if (wsStatus && serverStatus) {
const statusText = status.charAt(0).toUpperCase() + status.slice(1);
wsStatus.textContent = statusText;
serverStatus.textContent = statusText;
}
this.addLog('info', 'client', `Connection status: ${status}`);
}
addLog(level, source, message, data) {
const log = {
timestamp: new Date().toISOString(),
level,
source,
message,
data
};
this.logs.unshift(log);
if (this.logs.length > this.maxLogs) {
this.logs = this.logs.slice(0, this.maxLogs);
}
this.renderLogs();
}
renderLogs() {
const logsContainer = this.container?.querySelector('#mcp-logs');
if (!logsContainer)
return;
logsContainer.innerHTML = this.logs.map(log => {
const time = new Date(log.timestamp).toLocaleTimeString();
if (log.level === 'tool' && log.data) {
// Special formatting for tool logs
const details = log.data;
let detailsHtml = '';
if (details.args) {
detailsHtml += `<div class="mcp-tool-details">Args: ${this.safeStringify(details.args)}</div>`;
}
if (details.error) {
detailsHtml += `<div class="mcp-tool-details">Error: ${details.error}</div>`;
}
if (details.result && details.status === 'completed') {
const resultStr = this.safeStringify(details.result);
if (resultStr.length > 100) {
detailsHtml += `<div class="mcp-tool-details">Result: ${resultStr.substring(0, 100)}...</div>`;
}
else {
detailsHtml += `<div class="mcp-tool-details">Result: ${resultStr}</div>`;
}
}
return `
<div class="mcp-log-entry ${log.level}">
<span class="mcp-log-timestamp">[${time}]</span> [${log.source.toUpperCase()}] ${log.message}
${detailsHtml}
</div>
`;
}
else {
// Regular log formatting
const dataStr = log.data ? ` ${this.safeStringify(log.data)}` : '';
return `
<div class="mcp-log-entry ${log.level}">
<span class="mcp-log-timestamp">[${time}]</span> [${log.source.toUpperCase()}] ${log.message}${dataStr}
</div>
`;
}
}).join('');
// Auto-scroll to top if enabled (latest items are at the top)
const autoScrollCheckbox = this.container?.querySelector('#mcp-autoscroll');
if (autoScrollCheckbox?.checked) {
logsContainer.scrollTop = 0;
}
}
safeStringify(obj) {
try {
// Handle simple types
if (obj === null || obj === undefined) {
return String(obj);
}
if (typeof obj === 'string' || typeof obj === 'number' || typeof obj === 'boolean') {
return String(obj);
}
// Handle arrays and objects with circular reference protection
const seen = new Set();
return JSON.stringify(obj, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return '[Circular Reference]';
}
seen.add(value);
}
return value;
});
}
catch (error) {
return `[Error stringifying: ${error instanceof Error ? error.message : 'Unknown error'}]`;
}
}
clearLogs() {
this.logs = [];
this.renderLogs();
}
logWebSocketEvent(event, data) {
this.addLog('info', 'websocket', event, data);
}
logMCPEvent(event, data) {
this.addLog('info', 'mcp', event, data);
}
logError(source, message, error) {
this.addLog('error', source, message, error);
}
logToolExecution(toolName, args, success, message, executionTime, result) {
const status = success === null ? 'started' : (success ? 'completed' : 'failed');
const details = {
tool: toolName,
status,
args,
executionTime: executionTime ? `${executionTime}ms` : undefined,
result: success && result ? result : undefined,
error: !success && message !== 'Success' ? message : undefined
};
const logMessage = `Tool ${toolName} ${status}${executionTime ? ` (${executionTime}ms)` : ''}`;
this.addLog(success === false ? 'error' : 'tool', 'tool', logMessage, details);
}
}
class WebAppMCPClient {
get isConnected() {
return this._isConnected && this.ws?.readyState === WebSocket.OPEN;
}
constructor(config) {
this.ws = null;
this.reconnectAttempts = 0;
this.messageHandlers = new Map();
this.consoleLogs = [];
this._isConnected = false;
this.devTools = null;
this.pluginHandlers = {};
this.config = {
reconnectInterval: 5000,
maxReconnectAttempts: 10,
enableDevTools: true,
debug: false,
enableConnection: true,
interceptConsole: true,
enabledTools: [], // Empty array means all tools enabled
devToolsPosition: 'bottom-right',
devToolsTheme: 'dark',
...config,
};
// Only intercept console if enabled
if (this.config.interceptConsole) {
this.setupConsoleInterception();
}
// Auto-load html2canvas for screenshot functionality
this.loadHtml2Canvas();
if (this.config.enableDevTools && this.config.enableConnection) {
this.devTools = new MCPDevTools({
position: this.config.devToolsPosition,
theme: this.config.devToolsTheme,
});
this.devTools.setConnectionStatus('disconnected');
}
}
log(...args) {
if (this.config.debug) {
console.log('[webappmcp]', ...args);
}
}
logError(...args) {
// Always log errors regardless of debug setting
console.error('[webappmcp]', ...args);
}
connect() {
// Bypass connection if disabled (e.g., in production)
if (!this.config.enableConnection) {
this.log('Connection disabled by configuration');
return;
}
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
return;
}
this.devTools?.setConnectionStatus('connecting');
this.devTools?.logWebSocketEvent('Attempting to connect', { url: this.config.serverUrl });
try {
const url = new URL(this.config.serverUrl);
const headers = {};
if (this.config.authToken) {
headers['Authorization'] = `Bearer ${this.config.authToken}`;
}
// WebSocket in browser doesn't support custom headers directly
// So we'll pass the token in the URL
if (this.config.authToken) {
url.searchParams.set('token', this.config.authToken);
}
this.ws = new WebSocket(url.toString());
this.setupWebSocketHandlers();
}
catch (error) {
this.logError('Failed to connect to WebApp MCP server:', error);
this.devTools?.logError('websocket', 'Failed to connect', error);
this.scheduleReconnect();
}
}
disconnect() {
this._isConnected = false;
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
setupWebSocketHandlers() {
if (!this.ws)
return;
this.ws.onopen = () => {
this.log('Connected to WebApp MCP server');
this._isConnected = true;
this.reconnectAttempts = 0;
this.devTools?.setConnectionStatus('connected');
this.devTools?.logWebSocketEvent('Connected to WebApp MCP server');
this.sendMessage({
type: 'init',
url: window.location.href,
});
};
this.ws.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
this.devTools?.logWebSocketEvent('Message received', message);
this.handleMessage(message);
}
catch (error) {
this.logError('Failed to parse WebSocket message:', error);
this.devTools?.logError('websocket', 'Failed to parse message', error);
}
};
this.ws.onerror = (error) => {
this.logError('WebSocket error:', error);
this.devTools?.logError('websocket', 'WebSocket error', error);
};
this.ws.onclose = () => {
this.log('Disconnected from WebApp MCP server');
this._isConnected = false;
this.devTools?.setConnectionStatus('disconnected');
this.devTools?.logWebSocketEvent('Disconnected from WebApp MCP server');
this.scheduleReconnect();
};
}
scheduleReconnect() {
if (this.reconnectAttempts >= (this.config.maxReconnectAttempts || 10) ||
this.reconnectTimer) {
return;
}
this.reconnectAttempts++;
this.log(`Scheduling reconnect attempt ${this.reconnectAttempts}/${this.config.maxReconnectAttempts}`);
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null;
this.connect();
}, this.config.reconnectInterval);
}
sendMessage(message) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(message));
this.devTools?.logWebSocketEvent('Message sent', message);
}
else {
this.logError('WebSocket is not connected');
}
}
handleMessage(message) {
const { type, requestId, tool, args } = message;
this.log('[WebApp Client] Received message:', JSON.stringify(message));
if (type === 'connected') {
this.log('WebApp MCP client registered:', message.clientId);
this.devTools?.logMCPEvent('Client registered', { clientId: message.clientId });
return;
}
if (type === 'execute_tool') {
this.log(`[WebApp Client] Executing tool: ${tool} with requestId: ${requestId}`);
this.devTools?.logMCPEvent(`Executing tool: ${tool}`, { requestId, args });
this.executeToolHandler(requestId, tool, args);
return;
}
if (type === 'plugin_extension') {
this.log(`[WebApp Client] Loading plugin extension`);
this.loadPluginExtension(message.extension);
return;
}
const handler = this.messageHandlers.get(type);
if (handler) {
handler(message);
}
}
async executeToolHandler(requestId, toolName, args) {
// Check if tool is enabled
if (this.config.enabledTools && this.config.enabledTools.length > 0) {
if (!this.config.enabledTools.includes(toolName)) {
const error = `Tool ${toolName} is not enabled`;
this.logError(error);
this.devTools?.logToolExecution(toolName, args, false, error);
this.sendMessage({
type: 'tool_response',
requestId,
success: false,
error,
});
return;
}
}
this.log(`[WebApp Client] Executing tool handler for ${toolName}`);
this.log(`[WebApp Client] Tool args:`, JSON.stringify(args));
this.devTools?.logToolExecution(toolName, args, null, 'Started');
const startTime = Date.now();
try {
let result;
switch (toolName) {
case 'dom_query':
result = await this.domQuery(args);
break;
case 'dom_get_properties':
result = await this.domGetProperties(args);
break;
case 'dom_get_text':
result = await this.domGetText(args);
break;
case 'dom_get_html':
result = await this.domGetHTML(args);
break;
case 'interaction_click':
result = await this.interactionClick(args);
break;
case 'interaction_type':
result = await this.interactionType(args);
break;
case 'interaction_scroll':
result = await this.interactionScroll(args);
break;
case 'interaction_hover':
result = await this.interactionHover(args);
break;
case 'capture_screenshot':
result = await this.captureScreenshot(args);
break;
case 'capture_element_screenshot':
result = await this.captureElementScreenshot(args);
break;
case 'state_get_variable':
result = await this.stateGetVariable(args);
break;
case 'state_local_storage':
result = await this.stateLocalStorage(args);
break;
case 'console_get_logs':
result = await this.consoleGetLogs(args);
break;
case 'dom_manipulate':
result = await this.domManipulate(args);
break;
case 'javascript_inject':
result = await this.javascriptInject(args);
break;
case 'webapp_list_clients':
result = await this.webappListClients(args);
break;
case 'execute_javascript':
result = await this.executeJavascript(args);
break;
default:
// Check if this is a plugin-provided tool
if (this.pluginHandlers && this.pluginHandlers[toolName]) {
result = await this.pluginHandlers[toolName](args);
}
else {
throw new Error(`Unknown tool: ${toolName}`);
}
}
const executionTime = Date.now() - startTime;
this.log(`[WebApp Client] Tool execution successful, sending response`);
this.devTools?.logToolExecution(toolName, args, true, 'Success', executionTime, result);
this.sendMessage({
type: 'tool_response',
requestId,
result,
success: true,
});
}
catch (error) {
const executionTime = Date.now() - startTime;
const errorMessage = error instanceof Error ? error.message : String(error);
this.logError(`[WebApp Client] Tool execution failed:`, error);
this.devTools?.logToolExecution(toolName, args, false, errorMessage, executionTime);
this.sendMessage({
type: 'tool_response',
requestId,
success: false,
error: errorMessage,
});
}
}
loadHtml2Canvas() {
// Check if html2canvas is already loaded
if (typeof window.html2canvas !== 'undefined') {
return;
}
// Create script element
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js';
script.async = true;
script.onload = () => {
if (this.config.debug) {
console.log('[WebAppMCP] html2canvas loaded successfully');
}
};
script.onerror = () => {
console.warn('[WebAppMCP] Failed to load html2canvas - screenshots will use fallback mode');
};
document.head.appendChild(script);
}
setupConsoleInterception() {
const originalConsole = {
log: console.log,
info: console.info,
warn: console.warn,
error: console.error,
};
const interceptor = (level) => {
return (...args) => {
this.consoleLogs.push({
level,
timestamp: new Date().toISOString(),
args: args.map((arg) => {
try {
return typeof arg === 'object' ? JSON.stringify(arg) : String(arg);
}
catch {
return String(arg);
}
}),
});
if (this.consoleLogs.length > 1000) {
this.consoleLogs.shift();
}
originalConsole[level](...args);
};
};
console.log = interceptor('log');
console.info = interceptor('info');
console.warn = interceptor('warn');
console.error = interceptor('error');
}
async domQuery(args) {
const { selector, limit = 10 } = args;
const elements = Array.from(document.querySelectorAll(selector)).slice(0, limit);
return {
elements: elements.map((el) => ({
selector,
tagName: el.tagName.toLowerCase(),
id: el.id || undefined,
className: el.className || undefined,
text: el.textContent?.trim().substring(0, 100),
attributes: (() => {
const attrs = {};
for (let i = 0; i < el.attributes.length; i++) {
const attr = el.attributes[i];
attrs[attr.name] = attr.value;
}
return attrs;
})(),
})),
};
}
async domGetProperties(args) {
const { selector, properties = [] } = args;
const element = document.querySelector(selector);
if (!element) {
throw new Error(`Element not found: ${selector}`);
}
const result = {};
for (const prop of properties) {
try {
result[prop] = element[prop];
}
catch {
result[prop] = undefined;
}
}
return result;
}
async domGetText(args) {
const { selector, includeHidden = false } = args;
const elements = document.querySelectorAll(selector);
const texts = [];
elements.forEach((el) => {
if (includeHidden || el.offsetParent !== null) {
const text = el.textContent?.trim();
if (text)
texts.push(text);
}
});
return { texts };
}
async domGetHTML(args) {
const { selector, outerHTML = false } = args;
const element = document.querySelector(selector);
if (!element) {
throw new Error(`Element not found: ${selector}`);
}
return {
html: outerHTML ? element.outerHTML : element.innerHTML,
};
}
async interactionClick(args) {
const { selector, button = 'left' } = args;
const element = document.querySelector(selector);
if (!element) {
throw new Error(`Element not found: ${selector}`);
}
const event = new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true,
button: button === 'right' ? 2 : button === 'middle' ? 1 : 0,
});
element.dispatchEvent(event);
return { success: true };
}
async interactionType(args) {
const { selector, text, clear = false } = args;
const element = document.querySelector(selector);
if (!element) {
throw new Error(`Element not found: ${selector}`);
}
if (clear) {
element.value = '';
}
element.focus();
element.value += text;
element.dispatchEvent(new Event('input', { bubbles: true }));
element.dispatchEvent(new Event('change', { bubbles: true }));
return { success: true };
}
async interactionScroll(args) {
const { selector, direction, amount = 100 } = args;
const element = selector ? document.querySelector(selector) : window;
if (!element && selector) {
throw new Error(`Element not found: ${selector}`);
}
const scrollOptions = {
behavior: 'smooth',
};
if (direction === 'up' || direction === 'down') {
scrollOptions.top = direction === 'down' ? amount : -amount;
}
else {
scrollOptions.left = direction === 'right' ? amount : -amount;
}
if (element === window) {
window.scrollBy(scrollOptions);
}
else {
element.scrollBy(scrollOptions);
}
return { success: true };
}
async interactionHover(args) {
const { selector } = args;
const element = document.querySelector(selector);
if (!element) {
throw new Error(`Element not found: ${selector}`);
}
element.dispatchEvent(new MouseEvent('mouseenter', {
view: window,
bubbles: true,
cancelable: true,
}));
element.dispatchEvent(new MouseEvent('mouseover', {
view: window,
bubbles: true,
cancelable: true,
}));
return { success: true };
}
async captureScreenshot(args) {
const { fullPage = true, format = 'png' } = args;
try {
// Use a more sophisticated approach to capture actual content
const width = fullPage ? Math.max(document.documentElement.scrollWidth, document.body.scrollWidth, document.documentElement.offsetWidth, document.body.offsetWidth, document.documentElement.clientWidth) : window.innerWidth;
const height = fullPage ? Math.max(document.documentElement.scrollHeight, document.body.scrollHeight, document.documentElement.offsetHeight, document.body.offsetHeight, document.documentElement.clientHeight) : window.innerHeight;
// Create a canvas to draw the content
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('Failed to create canvas context');
}
// Try to use html2canvas if available
if (typeof window.html2canvas !== 'undefined') {
const html2canvas = window.html2canvas;
const capturedCanvas = await html2canvas(document.body, {
width: width,
height: height,
windowWidth: width,
windowHeight: height,
x: 0,
y: 0,
scrollX: fullPage ? 0 : window.scrollX,
scrollY: fullPage ? 0 : window.scrollY,
useCORS: true,
allowTaint: true
});
const dataUrl = capturedCanvas.toDataURL(`image/${format}`);
return {
success: true,
dataUrl,
width,
height,
message: 'Screenshot captured successfully'
};
}
// Fallback: Create a more detailed representation
// This is still a fallback but provides more information than a blank placeholder
// Fill background
const bgColor = window.getComputedStyle(document.body).backgroundColor || '#ffffff';
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, width, height);
// Add some context about the page
ctx.fillStyle = '#666';
ctx.font = '14px system-ui, -apple-system, sans-serif';
ctx.textAlign = 'left';
const info = [
`Page Title: ${document.title}`,
`URL: ${window.location.href}`,
`Dimensions: ${width}x${height}`,
'',
'Note: For full screenshot functionality, include html2canvas library:',
'<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>'
];
let y = 30;
info.forEach(line => {
ctx.fillText(line, 20, y);
y += 25;
});
// Draw a border
ctx.strokeStyle = '#ddd';
ctx.lineWidth = 2;
ctx.strokeRect(1, 1, width - 2, height - 2);
const dataUrl = canvas.toDataURL(`image/${format}`);
return {
success: true,
dataUrl,
width,
height,
message: 'Screenshot captured (basic mode - add html2canvas for full rendering)'
};
}
catch (error) {
throw new Error(`Failed to capture screenshot: ${error}`);
}
}
async captureElementScreenshot(args) {
const { selector, format = 'png' } = args;
if (!selector) {
throw new Error('Selector is required for element screenshot');
}
const element = document.querySelector(selector);
if (!element) {
throw new Error(`Element not found: ${selector}`);
}
try {
const rect = element.getBoundingClientRect();
// Try to use html2canvas if available
if (typeof window.html2canvas !== 'undefined') {
const html2canvas = window.html2canvas;
const capturedCanvas = await html2canvas(element, {
width: rect.width,
height: rect.height,
x: rect.left + window.scrollX,
y: rect.top + window.scrollY,
scrollX: -rect.left,
scrollY: -rect.top,
useCORS: true,
allowTaint: true
});
const dataUrl = capturedCanvas.toDataURL(`image/${format}`);
return {
success: true,
dataUrl,
width: rect.width,
height: rect.height,
selector,
message: 'Element screenshot captured successfully'
};
}
// Fallback approach
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('Failed to create canvas context');
}
canvas.width = rect.width;
canvas.height = rect.height;
// Get element styles
const styles = window.getComputedStyle(element);
const bgColor = styles.backgroundColor || '#ffffff';
// Draw element representation
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, rect.width, rect.height);
// Draw border
ctx.strokeStyle = styles.borderColor || '#ddd';
ctx.lineWidth = parseInt(styles.borderWidth) || 1;
ctx.strokeRect(0, 0, rect.width, rect.height);
// Add element info
ctx.fillStyle = '#666';
ctx.font = '12px system-ui, -apple-system, sans-serif';
ctx.textAlign = 'center';
const lines = [
`Element: ${selector}`,
`Size: ${Math.round(rect.width)}x${Math.round(rect.height)}`,
`Tag: ${element.tagName.toLowerCase()}`,
element.className ? `Class: ${element.className}` : '',
'Add html2canvas for full rendering'
].filter(Boolean);
let y = Math.max(20, rect.height / 2 - (lines.length * 15) / 2);
lines.forEach(line => {
ctx.fillText(line, rect.width / 2, y);
y += 15;
});
const dataUrl = canvas.toDataURL(`image/${format}`);
return {
success: true,
dataUrl,
width: rect.width,
height: rect.height,
selector,
message: 'Element screenshot captured (basic mode - add html2canvas for full rendering)'
};
}
catch (error) {
throw new Error(`Failed to capture element screenshot: ${error}`);
}
}
async stateGetVariable(args) {
const { path } = args;
const parts = path.split('.');
let current = window;
for (const part of parts) {
if (current && typeof current === 'object' && part in current) {
current = current[part];
}
else {
throw new Error(`Variable not found: ${path}`);
}
}
return { value: current };
}
async stateLocalStorage(args) {
const { operation, key, value } = args;
switch (operation) {
case 'get':
return { value: localStorage.getItem(key) };
case 'set':
localStorage.setItem(key, value);
return { success: true };
case 'remove':
localStorage.removeItem(key);
return { success: true };
case 'clear':
localStorage.clear();
return { success: true };
case 'getAll':
const items = {};
for (let i = 0; i < localStorage.length; i++) {
const k = localStorage.key(i);
if (k)
items[k] = localStorage.getItem(k) || '';
}
return { items };
default:
throw new Error(`Unknown localStorage operation: ${operation}`);
}
}
async consoleGetLogs(args) {
const { level = 'all', limit = 100 } = args;
let logs = this.consoleLogs;
if (level !== 'all') {
logs = logs.filter((log) => log.level === level);
}
return { logs: logs.slice(-limit) };
}
async domManipulate(args) {
const { action, selector, value, attribute, property } = args;
if (!selector) {
throw new Error('Selector is required for DOM manipulation');
}
const element = document.querySelector(selector);
if (!element) {
throw new Error(`Element not found: ${selector}`);
}
switch (action) {
case 'setAttribute':
if (!attribute || value === undefined) {
throw new Error('Attribute name and value are required for setAttribute');
}
element.setAttribute(attribute, value);
return { success: true, message: `Set attribute ${attribute}="${value}" on ${selector}` };
case 'removeAttribute':
if (!attribute) {
throw new Error('Attribute name is required for removeAttribute');
}
element.removeAttribute(attribute);
return { success: true, message: `Removed attribute ${attribute} from ${selector}` };
case 'setProperty':
if (!property || value === undefined) {
throw new Error('Property name and value are required for setProperty');
}
element[property] = value;
return { success: true, message: `Set property ${property}=${value} on ${selector}` };
case 'addClass':
if (!value) {
throw new Error('Class name is required for addClass');
}
element.classList.add(value);
return { success: true, message: `Added class "${value}" to ${selector}` };
case 'removeClass':
if (!value) {
throw new Error('Class name is required for removeClass');
}
element.classList.remove(value);
return { success: true, message: `Removed class "${value}" from ${selector}` };
case 'setInnerHTML':
if (value === undefined) {
throw new Error('HTML content is required for setInnerHTML');
}
element.innerHTML = value;
return { success: true, message: `Set innerHTML on ${selector}` };
case 'setTextContent':
if (value === undefined) {
throw new Error('Text content is required for setTextContent');
}
element.textContent = value;
return { s