Skip to content

feat: add press_key MCP tool for keyboard input simulation - #82

Open
jasonanovak wants to merge 1 commit into
mozilla:mainfrom
jasonanovak:inputsimulation
Open

feat: add press_key MCP tool for keyboard input simulation#82
jasonanovak wants to merge 1 commit into
mozilla:mainfrom
jasonanovak:inputsimulation

Conversation

@jasonanovak

Copy link
Copy Markdown

Lets the model send keyboard input to the browser — named keys (Enter, Escape, Tab, arrows, F1–F12), modifier combinations (ctrl+l, ctrl+shift+t, alt+F4), or plain characters — directed at a specific snapshot element (uid) or at the currently focused element.

Implementation:

  • dom.ts: add KEY_MAP and MODIFIER_MAP (named keys and modifier aliases mapped to Selenium Key.* unicode constants) and a pressKey(key, uid?) method that parses "ctrl+shift+t"-style combos. With a uid it routes through Key.chord + sendKeys on the element; without one it drives selenium's W3C Actions keyboard source directly so the key goes to the active element.
  • firefox/index.ts: expose FirefoxClient.pressKey with a not-connected guard.
  • tools/input.ts: define pressKeyTool schema and handlePressKey, with the same UID error mapping used by the other input tools.
  • tools/index.ts, src/index.ts: register the tool and handler.

Also drops an orphaned yaml dev-dependency entry from package-lock.

@juliandescottes juliandescottes left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you amend the commit to not be co-authored by the agent?

(I had a PR to add this to AGENTS.md, but I only merged it today sorry!)

Otherwise the feature looks useful. There's a small gap in terms of inputs if you want to type text in a field, you either have fill_by_uid, which overwrites everything. Or we could reuse that new tool, but at the moment it's really meant for single key usage.

One thing to note is that after using the tool a bit, the agent started using it to type text, one command per character. Either we update the description to explain that you can actually send several keys at once, or we make it really clear that this should not be used for typing.

Could you add some tests as well?

Comment thread src/tools/input.ts Outdated

try {
await firefox.pressKey(key, uid);
return successResponse(uid ? `✅ press_key "${key}" on ${uid}` : `✅ press_key "${key}"`);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the emojis from the logs

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment thread package-lock.json

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the package-lock update necessary? If not, can you remove it from this changeset?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed it.

Comment thread src/firefox/dom.ts Outdated
// Key.chord concatenates modifiers + key + Key.NULL (releases all modifiers)
await el.sendKeys(Key.chord(...modifiers, mainKey));
} else {
// Send to the currently focused element via W3C Actions keyboard source

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe WebDriver instead of W3C ? https://www.w3.org/TR/webdriver2/#actions

W3C actions is a bit vague.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

made this webdriver consistently

Comment thread src/firefox/dom.ts Outdated
// Key.chord concatenates modifiers + key + Key.NULL (releases all modifiers)
await el.sendKeys(Key.chord(...modifiers, mainKey));
} else {
// Send to the currently focused element via W3C Actions keyboard source

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe WebDriver instead of W3C ? https://www.w3.org/TR/webdriver2/#actions

W3C actions is a bit vague.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

made this webdriver consistently

Comment thread src/firefox/dom.ts
// Map of lowercase key names → Selenium unicode values
const KEY_MAP: Record<string, string> = {
enter: Key.RETURN,
return: Key.RETURN,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RETURN and ENTER are mapped to different keys in selenium, I think we should stick to that. https://github.com/SeleniumHQ/selenium/blob/trunk/javascript/selenium-webdriver/lib/input.js#L53-L54

Overall we are missing a few keys, might be good to add them.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed the mapping and added the keys that are in selenium.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't realize that Selenium's Key.ENTER was the numpad one. I'm not sure what's the best approach between being faithful to selenium's Key definition or aliasing enter to Key.RETURN.

It probably doesn't change much in most cases, but maybe for now we should revert to what you had, and add numpadenter: Key.ENTER?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Measured: Return arrives as code=Enter, Enter as code=NumpadEnter. +1 to enter -> Key.RETURN plus numpadenter: Key.ENTER.

Comment thread src/firefox/dom.ts Outdated
if (part in MODIFIER_MAP) {
modifiers.push(MODIFIER_MAP[part]!);
} else {
mainKey = KEY_MAP[part] ?? part;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider throwing here if mainKey was already set. I asked it to press ctrl+k+l, which resulted in only ctrl+l being sent.

Maybe adapt the tool description as well to mention that you can use many modifiers, but only one key?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clarified.

@juliandescottes

Copy link
Copy Markdown
Collaborator

@jasonanovak we would still be interested to merge this PR if you have time to update it.

In case we don't hear back or if you can't update it, hopefully it's fine if I rebase it and apply my suggestions in a followup.

@jasonanovak

Copy link
Copy Markdown
Author

Hi @juliandescottes sorry for the delayed response. Life got nuts. I am going to update the PR now and address your comments.

Sends a single key, optionally with modifiers, either to a snapshot
element or to whatever currently has focus. Covers submitting (Return),
dismissing (Escape), navigating (Tab, arrows) and shortcuts such as
ctrl+shift+t.

Key names map to the WebDriver code points selenium exposes as Key.*,
keeping return and enter distinct as selenium does. Combinations accept
any number of modifiers but exactly one key, so "ctrl+k+l" is rejected
rather than silently dropping a key, and unknown multi-character names
are rejected rather than being typed as text.
@jasonanovak

Copy link
Copy Markdown
Author

Ok! Made the changes.

Added tests as well.

Finally, tried to make it clear that this wasn't for typing and that's more fill_by_uid.

@juliandescottes

Copy link
Copy Markdown
Collaborator

Thanks @jasonanovak !

Will take a look today, cc-ing @freema for review as well since he's working on a similar PR.

@juliandescottes
juliandescottes requested a review from freema August 18, 2026 08:59

@juliandescottes juliandescottes left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the delay! Main comment is that we should try to consistently use actions and stop falling back to sendKey if a uid is passed.

Once the last comments are addressed we should be good to go!

Thanks

Comment thread src/firefox/dom.ts
throw new Error('pressKey: resolveUid callback not set. Ensure snapshot is initialized.');
}
const el = await this.resolveUid(uid);
await el.sendKeys(Key.chord(...modifiers, mainKey));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of using sendKeys when we have a uid and actions in the other case, could we try to focus the element corresponding to the uid and then use actions ?

Otherwise I'm worried we would get slightly different behaviors when using a uid or not.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, measured: Key.chord appends Key.NULL and Element Send Keys dispatches it, so press_key Escape with a uid yields Escape plus a phantom Unidentified keydown/keyup (actions path: Escape only). On <input type=file> it fails with File not found:, which handleUidError rewrites to "stale uid, take_snapshot", so the model loops. Focus-then-actions fixes both locally; a non-focusable uid then needs its own error, and the caret lands at the start of a contenteditable instead of the end.

Comment thread src/firefox/dom.ts
// Map of lowercase key names → Selenium unicode values
const KEY_MAP: Record<string, string> = {
enter: Key.RETURN,
return: Key.RETURN,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't realize that Selenium's Key.ENTER was the numpad one. I'm not sure what's the best approach between being faithful to selenium's Key definition or aliasing enter to Key.RETURN.

It probably doesn't change much in most cases, but maybe for now we should revert to what you had, and add numpadenter: Key.ENTER?

Comment thread src/tools/input.ts
key: {
type: 'string',
description:
'One key, optionally preceded by "+"-separated modifiers, such as "Escape", "F5" or "ctrl+shift+t". Modifiers: ctrl, alt, shift, meta. Named keys: Enter (numpad), Return, Tab, Backspace, Delete, Insert, Space, Escape, Home, End, PageUp, PageDown, ArrowUp, ArrowDown, ArrowLeft, ArrowRight, F1-F12, Numpad0-Numpad9, Clear, Pause, Help, Cancel, Semicolon, Equals, Add, Subtract, Multiply, Divide, Decimal, Separator. Anything else must be a single character.',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be confusing to mention F5 here. Most readers (and models) would probably interpret that as the "reload" shortcut, but it would only work from Chrome/privileged context, and will be misleading in other situations.

To be safe I would remove it.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ctrl+shift+t has the same issue: from content neither triggers the browser action (checked: tab count unchanged, no reload). A page-level example like shift+Tab would be safer. On macOS ctrl+a also does not select all (meta+a does).

@freema freema left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for picking this up @jasonanovak, and thanks @juliandescottes for the ping. Ran the branch against Firefox 154 (macOS, headless): lint, typecheck, tests and build green, merges cleanly with main; measurements behind Julian's points are in the threads. Nothing exercises DomInteractions.pressKey itself, I can port the integration test from #153 if useful (the README tool overview could list press_key too). Once the uid path goes through actions I am happy and will rebase #153 on top so it shrinks to type_text.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants