Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,13 +204,14 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles

<!-- BEGIN AUTO GENERATED TOOLS -->

- **Input automation** (7 tools)
- **Input automation** (8 tools)
- [`click`](docs/tool-reference.md#click)
- [`drag`](docs/tool-reference.md#drag)
- [`fill`](docs/tool-reference.md#fill)
- [`fill_form`](docs/tool-reference.md#fill_form)
- [`handle_dialog`](docs/tool-reference.md#handle_dialog)
- [`hover`](docs/tool-reference.md#hover)
- [`press_key`](docs/tool-reference.md#press_key)
- [`upload_file`](docs/tool-reference.md#upload_file)
- **Navigation automation** (7 tools)
- [`close_page`](docs/tool-reference.md#close_page)
Expand Down
13 changes: 12 additions & 1 deletion docs/tool-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

# Chrome DevTools MCP Tool Reference

- **[Input automation](#input-automation)** (7 tools)
- **[Input automation](#input-automation)** (8 tools)
- [`click`](#click)
- [`drag`](#drag)
- [`fill`](#fill)
- [`fill_form`](#fill_form)
- [`handle_dialog`](#handle_dialog)
- [`hover`](#hover)
- [`press_key`](#press_key)
- [`upload_file`](#upload_file)
- **[Navigation automation](#navigation-automation)** (7 tools)
- [`close_page`](#close_page)
Expand Down Expand Up @@ -101,6 +102,16 @@

---

### `press_key`

**Description:** Press a key or key combination on the keyboard. Supports modifier keys and combinations.

**Parameters:**

- **key** (string) **(required)**: Key to press. Can be a single key (e.g., "Enter", "Escape", "a") or a combination with modifiers (e.g., "Control+A", "Control+Shift+T", "Control++"). Modifier keys: Control, Shift, Alt, Meta.

---

### `upload_file`

**Description:** Upload a file through a provided element.
Expand Down
66 changes: 66 additions & 0 deletions src/tools/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,3 +216,69 @@ export const uploadFile = defineTool({
}
},
});

/**
* Split a key combination string into individual keys.
* Handles combinations like "Control+A" and special cases like "Control++".
* Based on Playwright's implementation.
*/
function splitKeyCombo(keyString: string): string[] {
const keys: string[] = [];
let building = '';
for (const char of keyString) {
if (char === '+' && building) {
// Only split if there's text before +
keys.push(building);
building = '';
} else {
building += char;
}
}
keys.push(building);
return keys;
}

export const pressKey = defineTool({
name: 'press_key',
description: `Press a key or key combination on the keyboard. Supports modifier keys and combinations.`,
annotations: {
category: ToolCategories.INPUT_AUTOMATION,
readOnlyHint: false,
},
schema: {
key: z
.string()
.describe(
'Key to press. Can be a single key (e.g., "Enter", "Escape", "a") or a combination with modifiers (e.g., "Control+A", "Control+Shift+T", "Control++"). Modifier keys: Control, Shift, Alt, Meta.',
),
},
handler: async (request, response, context) => {
const page = context.getSelectedPage();
const tokens = splitKeyCombo(request.params.key);
const key = tokens[tokens.length - 1];
const modifiers = tokens.slice(0, -1);

await context.waitForEventsAfterAction(async () => {
// Press down modifiers
for (const modifier of modifiers) {
// @ts-expect-error - Puppeteer KeyInput type is too restrictive for dynamic input
await page.keyboard.down(modifier);
}

// Press the key
// @ts-expect-error - Puppeteer KeyInput type is too restrictive for dynamic input
await page.keyboard.press(key);

// Release modifiers in reverse order
for (let i = modifiers.length - 1; i >= 0; i--) {
// @ts-expect-error - Puppeteer KeyInput type is too restrictive for dynamic input
await page.keyboard.up(modifiers[i]);
}
});

response.appendResponseLine(
`Successfully pressed key: ${request.params.key}`,
);
response.setIncludeSnapshot(true);
},
});
147 changes: 147 additions & 0 deletions tests/tools/input.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
drag,
fillForm,
uploadFile,
pressKey,
} from '../../src/tools/input.js';
import {serverHooks} from '../server.js';
import {html, withBrowser} from '../utils.js';
Expand Down Expand Up @@ -402,4 +403,150 @@ describe('input', () => {
});
});
});

describe('pressKey', () => {
it('presses a simple key', async () => {
await withBrowser(async (response, context) => {
const page = context.getSelectedPage();
await page.setContent(`<!DOCTYPE html>
<input id="test-input" />
<div id="result"></div>
<script>
document.getElementById('test-input').addEventListener('keydown', (e) => {
document.getElementById('result').innerText = e.key;
});
</script>`);
await context.createTextSnapshot();
await page.focus('#test-input');
await pressKey.handler(
{
params: {
key: 'Enter',
},
},
response,
context,
);
assert.strictEqual(
response.responseLines[0],
'Successfully pressed key: Enter',
);
assert.ok(response.includeSnapshot);
const result = await page.$eval(
'#result',
el => (el as HTMLElement).innerText,
);
assert.strictEqual(result, 'Enter');
});
});

it('presses a key combination', async () => {
await withBrowser(async (response, context) => {
const page = context.getSelectedPage();
await page.setContent(`<!DOCTYPE html>
<textarea id="test-input">Hello World</textarea>
<script>
const input = document.getElementById('test-input');
input.focus();
input.setSelectionRange(0, 0);
</script>`);
await context.createTextSnapshot();
await pressKey.handler(
{
params: {
key: 'Control+A',
},
},
response,
context,
);
assert.strictEqual(
response.responseLines[0],
'Successfully pressed key: Control+A',
);
assert.ok(response.includeSnapshot);
// Verify text is selected by getting selection
const selected = await page.evaluate(() => {
const input = document.getElementById(
'test-input',
) as HTMLTextAreaElement;
return (
input.selectionStart === 0 &&
input.selectionEnd === input.value.length
);
});
assert.ok(selected, 'Text should be selected');
});
});

it('presses plus key with modifier (Control++)', async () => {
await withBrowser(async (response, context) => {
const page = context.getSelectedPage();
await page.setContent(`<!DOCTYPE html>
<div id="result"></div>
<script>
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === '+') {
document.getElementById('result').innerText = 'ctrl-plus';
}
});
</script>`);
await context.createTextSnapshot();
await pressKey.handler(
{
params: {
key: 'Control++',
},
},
response,
context,
);
assert.strictEqual(
response.responseLines[0],
'Successfully pressed key: Control++',
);
assert.ok(response.includeSnapshot);
const result = await page.$eval(
'#result',
el => (el as HTMLElement).innerText,
);
assert.strictEqual(result, 'ctrl-plus');
});
});

it('presses multiple modifiers', async () => {
await withBrowser(async (response, context) => {
const page = context.getSelectedPage();
await page.setContent(`<!DOCTYPE html>
<div id="result"></div>
<script>
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.shiftKey && e.key === 'T') {
document.getElementById('result').innerText = 'ctrl-shift-t';
}
});
</script>`);
await context.createTextSnapshot();
await pressKey.handler(
{
params: {
key: 'Control+Shift+T',
},
},
response,
context,
);
assert.strictEqual(
response.responseLines[0],
'Successfully pressed key: Control+Shift+T',
);
assert.ok(response.includeSnapshot);
const result = await page.$eval(
'#result',
el => (el as HTMLElement).innerText,
);
assert.strictEqual(result, 'ctrl-shift-t');
});
});
});
});