Skip to content

Commit 69c95af

Browse files
committed
docs
2 parents a061909 + b5d01b5 commit 69c95af

10 files changed

Lines changed: 221 additions & 28 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -414,14 +414,15 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
414414

415415
<!-- BEGIN AUTO GENERATED TOOLS -->
416416

417-
- **Input automation** (8 tools)
417+
- **Input automation** (9 tools)
418418
- [`click`](docs/tool-reference.md#click)
419419
- [`drag`](docs/tool-reference.md#drag)
420420
- [`fill`](docs/tool-reference.md#fill)
421421
- [`fill_form`](docs/tool-reference.md#fill_form)
422422
- [`handle_dialog`](docs/tool-reference.md#handle_dialog)
423423
- [`hover`](docs/tool-reference.md#hover)
424424
- [`press_key`](docs/tool-reference.md#press_key)
425+
- [`type_text`](docs/tool-reference.md#type_text)
425426
- [`upload_file`](docs/tool-reference.md#upload_file)
426427
- **Navigation automation** (6 tools)
427428
- [`close_page`](docs/tool-reference.md#close_page)

docs/tool-reference.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
<!-- AUTO GENERATED DO NOT EDIT - run 'npm run docs' to update-->
22

3-
# Chrome DevTools MCP Tool Reference (~7125 cl100k_base tokens)
3+
# Chrome DevTools MCP Tool Reference (~7334 cl100k_base tokens)
44

5-
- **[Input automation](#input-automation)** (8 tools)
5+
- **[Input automation](#input-automation)** (9 tools)
66
- [`click`](#click)
77
- [`drag`](#drag)
88
- [`fill`](#fill)
99
- [`fill_form`](#fill_form)
1010
- [`handle_dialog`](#handle_dialog)
1111
- [`hover`](#hover)
1212
- [`press_key`](#press_key)
13+
- [`type_text`](#type_text)
1314
- [`upload_file`](#upload_file)
1415
- **[Navigation automation](#navigation-automation)** (6 tools)
1516
- [`close_page`](#close_page)
@@ -119,6 +120,17 @@
119120

120121
---
121122

123+
### `type_text`
124+
125+
**Description:** Type text using keyboard into a previously focused input
126+
127+
**Parameters:**
128+
129+
- **text** (string) **(required)**: The text to type
130+
- **submitKey** (string) _(optional)_: Optional key to press after typing. E.g., "Enter", "Tab", "Escape"
131+
132+
---
133+
122134
### `upload_file`
123135

124136
**Description:** Upload a file through a provided element.
@@ -196,7 +208,7 @@
196208

197209
**Parameters:**
198210

199-
- **text** (string) **(required)**: Text to appear on the page
211+
- **text** (array) **(required)**: Non-empty list of texts. Resolves when any value appears on the page.
200212
- **timeout** (integer) _(optional)_: Maximum wait time in milliseconds. If set to 0, the default timeout will be used.
201213

202214
---

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
"@types/yargs": "^17.0.33",
5656
"@typescript-eslint/eslint-plugin": "^8.43.0",
5757
"@typescript-eslint/parser": "^8.43.0",
58-
"chrome-devtools-frontend": "1.0.1583146",
58+
"chrome-devtools-frontend": "1.0.1587572",
5959
"core-js": "3.48.0",
6060
"debug": "4.4.3",
6161
"eslint": "^9.35.0",

src/McpContext.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -873,15 +873,17 @@ export class McpContext implements Context {
873873
return this.#networkCollector.getIdForResource(request);
874874
}
875875

876-
waitForTextOnPage(text: string, timeout?: number): Promise<Element> {
876+
waitForTextOnPage(text: string[], timeout?: number): Promise<Element> {
877877
const page = this.getSelectedPage();
878878
const frames = page.frames();
879879

880880
let locator = this.#locatorClass.race(
881-
frames.flatMap(frame => [
882-
frame.locator(`aria/${text}`),
883-
frame.locator(`text/${text}`),
884-
]),
881+
frames.flatMap(frame =>
882+
text.flatMap(value => [
883+
frame.locator(`aria/${value}`),
884+
frame.locator(`text/${value}`),
885+
]),
886+
),
885887
);
886888

887889
if (timeout) {

src/tools/ToolDefinition.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ export type Context = Readonly<{
167167
action: () => Promise<unknown>,
168168
options?: {timeout?: number},
169169
): Promise<void>;
170-
waitForTextOnPage(text: string, timeout?: number): Promise<Element>;
170+
waitForTextOnPage(text: string[], timeout?: number): Promise<Element>;
171171
getDevToolsData(): Promise<DevToolsData>;
172172
/**
173173
* Returns a reqid for a cdpRequestId.

src/tools/input.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import {logger} from '../logger.js';
88
import type {McpContext, TextSnapshotNode} from '../McpContext.js';
99
import {zod} from '../third_party/index.js';
10-
import type {ElementHandle} from '../third_party/index.js';
10+
import type {ElementHandle, KeyInput} from '../third_party/index.js';
1111
import {parseKey} from '../utils/keyboard.js';
1212

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

26+
const submitKeySchema = zod
27+
.string()
28+
.optional()
29+
.describe(
30+
'Optional key to press after typing. E.g., "Enter", "Tab", "Escape"',
31+
);
32+
2633
function handleActionError(error: unknown, uid: string) {
2734
logger('failed to act using a locator', error);
2835
throw new Error(
@@ -239,6 +246,31 @@ export const fill = defineTool({
239246
},
240247
});
241248

249+
export const typeText = defineTool({
250+
name: 'type_text',
251+
description: `Type text using keyboard into a previously focused input`,
252+
annotations: {
253+
category: ToolCategory.INPUT,
254+
readOnlyHint: false,
255+
},
256+
schema: {
257+
text: zod.string().describe('The text to type'),
258+
submitKey: submitKeySchema,
259+
},
260+
handler: async (request, response, context) => {
261+
await context.waitForEventsAfterAction(async () => {
262+
const page = context.getSelectedPage();
263+
await page.keyboard.type(request.params.text);
264+
if (request.params.submitKey) {
265+
await page.keyboard.press(request.params.submitKey as KeyInput);
266+
}
267+
});
268+
response.appendResponseLine(
269+
`Typed text "${request.params.text}${request.params.submitKey ? ` + ${request.params.submitKey}` : ''}"`,
270+
);
271+
},
272+
});
273+
242274
export const drag = defineTool({
243275
name: 'drag',
244276
description: `Drag an element onto another element`,

src/tools/snapshot.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,12 @@ export const waitFor = defineTool({
4949
readOnlyHint: true,
5050
},
5151
schema: {
52-
text: zod.string().describe('Text to appear on the page'),
52+
text: zod
53+
.array(zod.string())
54+
.min(1)
55+
.describe(
56+
'Non-empty list of texts. Resolves when any value appears on the page.',
57+
),
5358
...timeoutSchema,
5459
},
5560
handler: async (request, response, context) => {
@@ -59,7 +64,7 @@ export const waitFor = defineTool({
5964
);
6065

6166
response.appendResponseLine(
62-
`Element with text "${request.params.text}" found.`,
67+
`Element matching one of ${JSON.stringify(request.params.text)} found.`,
6368
);
6469

6570
response.includeSnapshot();

tests/tools/input.test.ts

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
uploadFile,
2020
pressKey,
2121
clickAt,
22+
typeText,
2223
} from '../../src/tools/input.js';
2324
import {parseKey} from '../../src/utils/keyboard.js';
2425
import {serverHooks} from '../server.js';
@@ -355,7 +356,7 @@ describe('input', () => {
355356
it('fills out a textarea marked as combobox', async () => {
356357
await withMcpContext(async (response, context) => {
357358
const page = context.getSelectedPage();
358-
await page.setContent(html`<textarea role="combobox" />`);
359+
await page.setContent(html`<textarea role="combobox"></textarea>`);
359360
await context.createTextSnapshot();
360361
await fill.handler(
361362
{
@@ -383,7 +384,7 @@ describe('input', () => {
383384
it('fills out a textarea with long text', async () => {
384385
await withMcpContext(async (response, context) => {
385386
const page = context.getSelectedPage();
386-
await page.setContent(html`<textarea />`);
387+
await page.setContent(html`<textarea></textarea>`);
387388
await context.createTextSnapshot();
388389
page.setDefaultTimeout(1000);
389390
await fill.handler(
@@ -411,6 +412,90 @@ describe('input', () => {
411412
});
412413
});
413414

415+
it('types text', async () => {
416+
await withMcpContext(async (response, context) => {
417+
const page = context.getSelectedPage();
418+
await page.setContent(html`<textarea></textarea>`);
419+
await page.click('textarea');
420+
await context.createTextSnapshot();
421+
await typeText.handler(
422+
{
423+
params: {
424+
text: 'test',
425+
},
426+
},
427+
response,
428+
context,
429+
);
430+
assert.strictEqual(response.responseLines[0], 'Typed text "test"');
431+
assert.strictEqual(
432+
await page.evaluate(() => {
433+
return document.body.querySelector('textarea')?.value;
434+
}),
435+
'test',
436+
);
437+
});
438+
});
439+
440+
it('types text with submit key', async () => {
441+
await withMcpContext(async (response, context) => {
442+
const page = context.getSelectedPage();
443+
await page.setContent(html`<textarea></textarea>`);
444+
await page.click('textarea');
445+
await context.createTextSnapshot();
446+
await typeText.handler(
447+
{
448+
params: {
449+
text: 'test',
450+
submitKey: 'Tab',
451+
},
452+
},
453+
response,
454+
context,
455+
);
456+
assert.strictEqual(
457+
response.responseLines[0],
458+
'Typed text "test + Tab"',
459+
);
460+
assert.strictEqual(
461+
await page.evaluate(() => {
462+
return document.body.querySelector('textarea')?.value;
463+
}),
464+
'test',
465+
);
466+
assert.ok(
467+
await page.evaluate(() => {
468+
return (
469+
document.body.querySelector('textarea') !== document.activeElement
470+
);
471+
}),
472+
);
473+
});
474+
});
475+
476+
it('errors on invalid submit key', async () => {
477+
await withMcpContext(async (response, context) => {
478+
const page = context.getSelectedPage();
479+
await page.setContent(html`<textarea></textarea>`);
480+
await page.click('textarea');
481+
await context.createTextSnapshot();
482+
try {
483+
await typeText.handler(
484+
{
485+
params: {
486+
text: 'test',
487+
submitKey: 'XXX',
488+
},
489+
},
490+
response,
491+
context,
492+
);
493+
} catch (err) {
494+
assert.strictEqual(err.message, 'Unknown key: "XXX"');
495+
}
496+
});
497+
});
498+
414499
it('reproduction: fill isolation', async () => {
415500
await withMcpContext(async (_response, context) => {
416501
const page = context.getSelectedPage();

0 commit comments

Comments
 (0)