diff --git a/README.md b/README.md
index 1853ed754..e32db77e0 100644
--- a/README.md
+++ b/README.md
@@ -414,7 +414,7 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
-- **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)
@@ -422,6 +422,7 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
- [`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)
diff --git a/docs/tool-reference.md b/docs/tool-reference.md
index 9111d6fd0..f2b4e6234 100644
--- a/docs/tool-reference.md
+++ b/docs/tool-reference.md
@@ -1,8 +1,8 @@
-# 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)
@@ -10,6 +10,7 @@
- [`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)
@@ -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.
diff --git a/src/tools/input.ts b/src/tools/input.ts
index c309326e4..2c338a520 100644
--- a/src/tools/input.ts
+++ b/src/tools/input.ts
@@ -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';
@@ -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(
@@ -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`,
diff --git a/tests/tools/input.test.ts b/tests/tools/input.test.ts
index 37f861add..b50d46b0d 100644
--- a/tests/tools/input.test.ts
+++ b/tests/tools/input.test.ts
@@ -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';
@@ -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``);
+ await page.setContent(html``);
await context.createTextSnapshot();
await fill.handler(
{
@@ -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``);
+ await page.setContent(html``);
await context.createTextSnapshot();
page.setDefaultTimeout(1000);
await fill.handler(
@@ -411,6 +412,90 @@ describe('input', () => {
});
});
+ it('types text', async () => {
+ await withMcpContext(async (response, context) => {
+ const page = context.getSelectedPage();
+ await page.setContent(html``);
+ 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``);
+ 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``);
+ 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();