Skip to content
Open
Show file tree
Hide file tree
Changes from all 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');
});
});
});
});