Skip to content
Merged
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 @@ -414,14 +414,15 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles

<!-- BEGIN AUTO GENERATED TOOLS -->

- **Input automation** (8 tools)
- **Input automation** (9 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)
- [`type_text`](docs/tool-reference.md#type_text)
- [`upload_file`](docs/tool-reference.md#upload_file)
- **Navigation automation** (6 tools)
- [`close_page`](docs/tool-reference.md#close_page)
Expand Down
16 changes: 14 additions & 2 deletions docs/tool-reference.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
<!-- AUTO GENERATED DO NOT EDIT - run 'npm run docs' to update-->

# Chrome DevTools MCP Tool Reference (~6916 cl100k_base tokens)
# Chrome DevTools MCP Tool Reference (~7094 cl100k_base tokens)

- **[Input automation](#input-automation)** (8 tools)
- **[Input automation](#input-automation)** (9 tools)
- [`click`](#click)
- [`drag`](#drag)
- [`fill`](#fill)
- [`fill_form`](#fill_form)
- [`handle_dialog`](#handle_dialog)
- [`hover`](#hover)
- [`press_key`](#press_key)
- [`type_text`](#type_text)
- [`upload_file`](#upload_file)
- **[Navigation automation](#navigation-automation)** (6 tools)
- [`close_page`](#close_page)
Expand Down Expand Up @@ -118,6 +119,17 @@

---

### `type_text`

**Description:** Type text using keyboard into a previously focused input

**Parameters:**

- **text** (string) **(required)**: The text to type
- **submitKey** (string) _(optional)_: Optional key to press after typing. E.g., "Enter", "Tab", "Escape"

---

### `upload_file`

**Description:** Upload a file through a provided element.
Expand Down
34 changes: 33 additions & 1 deletion src/tools/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import {logger} from '../logger.js';
import type {McpContext, TextSnapshotNode} from '../McpContext.js';
import {zod} from '../third_party/index.js';
import type {ElementHandle} from '../third_party/index.js';
import type {ElementHandle, KeyInput} from '../third_party/index.js';
import {parseKey} from '../utils/keyboard.js';

import {ToolCategory} from './categories.js';
Expand All @@ -23,6 +23,13 @@ const includeSnapshotSchema = zod
.optional()
.describe('Whether to include a snapshot in the response. Default is false.');

const submitKeySchema = zod
.string()
.optional()
.describe(
'Optional key to press after typing. E.g., "Enter", "Tab", "Escape"',
);

function handleActionError(error: unknown, uid: string) {
logger('failed to act using a locator', error);
throw new Error(
Expand Down Expand Up @@ -239,6 +246,31 @@ export const fill = defineTool({
},
});

export const typeText = defineTool({
name: 'type_text',
description: `Type text using keyboard into a previously focused input`,
annotations: {
category: ToolCategory.INPUT,
readOnlyHint: false,
},
schema: {
text: zod.string().describe('The text to type'),
submitKey: submitKeySchema,
},
handler: async (request, response, context) => {
await context.waitForEventsAfterAction(async () => {
const page = context.getSelectedPage();
await page.keyboard.type(request.params.text);
if (request.params.submitKey) {
await page.keyboard.press(request.params.submitKey as KeyInput);
}
});
response.appendResponseLine(
`Typed text "${request.params.text}${request.params.submitKey ? ` + ${request.params.submitKey}` : ''}"`,
);
},
});

export const drag = defineTool({
name: 'drag',
description: `Drag an element onto another element`,
Expand Down
89 changes: 87 additions & 2 deletions tests/tools/input.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
uploadFile,
pressKey,
clickAt,
typeText,
} from '../../src/tools/input.js';
import {parseKey} from '../../src/utils/keyboard.js';
import {serverHooks} from '../server.js';
Expand Down Expand Up @@ -355,7 +356,7 @@ describe('input', () => {
it('fills out a textarea marked as combobox', async () => {
await withMcpContext(async (response, context) => {
const page = context.getSelectedPage();
await page.setContent(html`<textarea role="combobox" />`);
await page.setContent(html`<textarea role="combobox"></textarea>`);
await context.createTextSnapshot();
await fill.handler(
{
Expand Down Expand Up @@ -383,7 +384,7 @@ describe('input', () => {
it('fills out a textarea with long text', async () => {
await withMcpContext(async (response, context) => {
const page = context.getSelectedPage();
await page.setContent(html`<textarea />`);
await page.setContent(html`<textarea></textarea>`);
await context.createTextSnapshot();
page.setDefaultTimeout(1000);
await fill.handler(
Expand Down Expand Up @@ -411,6 +412,90 @@ describe('input', () => {
});
});

it('types text', async () => {
await withMcpContext(async (response, context) => {
const page = context.getSelectedPage();
await page.setContent(html`<textarea></textarea>`);
await page.click('textarea');
await context.createTextSnapshot();
await typeText.handler(
{
params: {
text: 'test',
},
},
response,
context,
);
assert.strictEqual(response.responseLines[0], 'Typed text "test"');
assert.strictEqual(
await page.evaluate(() => {
return document.body.querySelector('textarea')?.value;
}),
'test',
);
});
});

it('types text with submit key', async () => {
await withMcpContext(async (response, context) => {
const page = context.getSelectedPage();
await page.setContent(html`<textarea></textarea>`);
await page.click('textarea');
await context.createTextSnapshot();
await typeText.handler(
{
params: {
text: 'test',
submitKey: 'Tab',
},
},
response,
context,
);
assert.strictEqual(
response.responseLines[0],
'Typed text "test + Tab"',
);
assert.strictEqual(
await page.evaluate(() => {
return document.body.querySelector('textarea')?.value;
}),
'test',
);
assert.ok(
await page.evaluate(() => {
return (
document.body.querySelector('textarea') !== document.activeElement
);
}),
);
});
});

it('errors on invalid submit key', async () => {
await withMcpContext(async (response, context) => {
const page = context.getSelectedPage();
await page.setContent(html`<textarea></textarea>`);
await page.click('textarea');
await context.createTextSnapshot();
try {
await typeText.handler(
{
params: {
text: 'test',
submitKey: 'XXX',
},
},
response,
context,
);
} catch (err) {
assert.strictEqual(err.message, 'Unknown key: "XXX"');
}
});
});

it('reproduction: fill isolation', async () => {
await withMcpContext(async (_response, context) => {
const page = context.getSelectedPage();
Expand Down