@democratize-quality/mcp-server
Version:
MCP Server for democratizing quality through browser automation and comprehensive API testing capabilities
344 lines (303 loc) • 11.3 kB
JavaScript
const ToolBase = require('../../base/ToolBase');
const browserService = require('../../../services/browserService');
/**
* Enhanced Keyboard Tool - Provides comprehensive keyboard interactions
* Inspired by Playwright MCP keyboard capabilities
*/
class BrowserKeyboardTool extends ToolBase {
static definition = {
name: "browser_keyboard",
description: "Perform keyboard actions including typing, key presses, and keyboard shortcuts. Supports special keys and modifier combinations.",
input_schema: {
type: "object",
properties: {
browserId: {
type: "string",
description: "The ID of the browser instance"
},
action: {
type: "string",
enum: ["type", "press", "down", "up", "shortcut"],
description: "The keyboard action to perform"
},
text: {
type: "string",
description: "Text to type (for 'type' action)"
},
key: {
type: "string",
description: "Key to press (for 'press', 'down', 'up' actions). Examples: 'Enter', 'Tab', 'ArrowLeft', 'F1'"
},
shortcut: {
type: "string",
description: "Keyboard shortcut (for 'shortcut' action). Examples: 'Ctrl+C', 'Cmd+V', 'Ctrl+Shift+I'"
},
target: {
type: "object",
properties: {
selector: {
type: "string",
description: "CSS selector of element to focus before typing (optional)"
}
},
description: "Target element to focus before keyboard action"
},
delay: {
type: "number",
default: 0,
description: "Delay between keystrokes in milliseconds"
},
modifiers: {
type: "array",
items: { type: "string", enum: ["ctrl", "shift", "alt", "meta"] },
description: "Modifier keys to hold during action"
}
},
required: ["browserId", "action"]
},
output_schema: {
type: "object",
properties: {
success: { type: "boolean", description: "Whether the keyboard action was successful" },
action: { type: "string", description: "The action that was performed" },
text: { type: "string", description: "Text that was typed" },
key: { type: "string", description: "Key that was pressed" },
target: { type: "string", description: "CSS selector of targeted element" },
browserId: { type: "string", description: "Browser instance ID" }
},
required: ["success", "action", "browserId"]
}
};
async execute(parameters) {
const { browserId, action, text, key, shortcut, target, delay = 0, modifiers = [] } = parameters;
const browser = browserService.getBrowserInstance(browserId);
if (!browser) {
throw new Error(`Browser instance '${browserId}' not found`);
}
const client = browser.client;
// Focus target element if specified
if (target?.selector) {
await this.focusElement(client, target.selector);
}
let result = {
success: true,
action: action,
browserId: browserId
};
// Convert modifiers to CDP format
const cdpModifiers = this.convertModifiers(modifiers);
switch (action) {
case 'type':
if (!text) {
throw new Error('Text is required for type action');
}
await this.typeText(client, text, delay);
result.text = text;
break;
case 'press':
if (!key) {
throw new Error('Key is required for press action');
}
await this.pressKey(client, key, cdpModifiers);
result.key = key;
break;
case 'down':
if (!key) {
throw new Error('Key is required for down action');
}
await this.keyDown(client, key, cdpModifiers);
result.key = key;
break;
case 'up':
if (!key) {
throw new Error('Key is required for up action');
}
await this.keyUp(client, key, cdpModifiers);
result.key = key;
break;
case 'shortcut':
if (!shortcut) {
throw new Error('Shortcut is required for shortcut action');
}
await this.executeShortcut(client, shortcut);
result.shortcut = shortcut;
break;
default:
throw new Error(`Unsupported keyboard action: ${action}`);
}
if (target?.selector) {
result.target = target.selector;
}
return result;
}
/**
* Focus an element by CSS selector
*/
async focusElement(client, selector) {
const result = await client.Runtime.evaluate({
expression: `
(() => {
const element = document.querySelector('${selector}');
if (element) {
element.focus();
return true;
}
return false;
})()
`
});
if (!result.result.value) {
throw new Error(`Could not focus element with selector: ${selector}`);
}
}
/**
* Type text with optional delay between keystrokes
*/
async typeText(client, text, delay) {
for (const char of text) {
await client.Input.dispatchKeyEvent({
type: 'keyDown',
text: char
});
await client.Input.dispatchKeyEvent({
type: 'keyUp',
text: char
});
if (delay > 0) {
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
/**
* Press a key (down and up)
*/
async pressKey(client, key, modifiers = 0) {
const keyInfo = this.getKeyInfo(key);
await client.Input.dispatchKeyEvent({
type: 'keyDown',
key: keyInfo.key,
code: keyInfo.code,
modifiers: modifiers
});
await client.Input.dispatchKeyEvent({
type: 'keyUp',
key: keyInfo.key,
code: keyInfo.code,
modifiers: modifiers
});
}
/**
* Key down
*/
async keyDown(client, key, modifiers = 0) {
const keyInfo = this.getKeyInfo(key);
await client.Input.dispatchKeyEvent({
type: 'keyDown',
key: keyInfo.key,
code: keyInfo.code,
modifiers: modifiers
});
}
/**
* Key up
*/
async keyUp(client, key, modifiers = 0) {
const keyInfo = this.getKeyInfo(key);
await client.Input.dispatchKeyEvent({
type: 'keyUp',
key: keyInfo.key,
code: keyInfo.code,
modifiers: modifiers
});
}
/**
* Execute keyboard shortcut
*/
async executeShortcut(client, shortcut) {
const keys = this.parseShortcut(shortcut);
// Press modifier keys
for (const modifier of keys.modifiers) {
await this.keyDown(client, modifier);
}
// Press main key
if (keys.key) {
await this.pressKey(client, keys.key, this.convertModifiers(keys.modifiers));
}
// Release modifier keys in reverse order
for (let i = keys.modifiers.length - 1; i >= 0; i--) {
await this.keyUp(client, keys.modifiers[i]);
}
}
/**
* Parse keyboard shortcut string
*/
parseShortcut(shortcut) {
const parts = shortcut.split('+');
const key = parts.pop();
const modifiers = parts.map(mod => mod.toLowerCase());
return { key, modifiers };
}
/**
* Get key information for CDP
*/
getKeyInfo(key) {
const keyMap = {
'Enter': { key: 'Enter', code: 'Enter' },
'Tab': { key: 'Tab', code: 'Tab' },
'Escape': { key: 'Escape', code: 'Escape' },
'Space': { key: ' ', code: 'Space' },
'ArrowLeft': { key: 'ArrowLeft', code: 'ArrowLeft' },
'ArrowRight': { key: 'ArrowRight', code: 'ArrowRight' },
'ArrowUp': { key: 'ArrowUp', code: 'ArrowUp' },
'ArrowDown': { key: 'ArrowDown', code: 'ArrowDown' },
'Backspace': { key: 'Backspace', code: 'Backspace' },
'Delete': { key: 'Delete', code: 'Delete' },
'Home': { key: 'Home', code: 'Home' },
'End': { key: 'End', code: 'End' },
'PageUp': { key: 'PageUp', code: 'PageUp' },
'PageDown': { key: 'PageDown', code: 'PageDown' },
'F1': { key: 'F1', code: 'F1' },
'F2': { key: 'F2', code: 'F2' },
'F3': { key: 'F3', code: 'F3' },
'F4': { key: 'F4', code: 'F4' },
'F5': { key: 'F5', code: 'F5' },
'F6': { key: 'F6', code: 'F6' },
'F7': { key: 'F7', code: 'F7' },
'F8': { key: 'F8', code: 'F8' },
'F9': { key: 'F9', code: 'F9' },
'F10': { key: 'F10', code: 'F10' },
'F11': { key: 'F11', code: 'F11' },
'F12': { key: 'F12', code: 'F12' }
};
if (keyMap[key]) {
return keyMap[key];
}
// For single characters
return { key: key, code: `Key${key.toUpperCase()}` };
}
/**
* Convert modifier keys to CDP format
*/
convertModifiers(modifiers) {
let cdpModifiers = 0;
for (const modifier of modifiers) {
switch (modifier.toLowerCase()) {
case 'ctrl':
cdpModifiers |= 2; // Ctrl
break;
case 'shift':
cdpModifiers |= 8; // Shift
break;
case 'alt':
cdpModifiers |= 1; // Alt
break;
case 'meta':
case 'cmd':
cdpModifiers |= 4; // Meta/Cmd
break;
}
}
return cdpModifiers;
}
}
module.exports = BrowserKeyboardTool;