feat: add press_key MCP tool for keyboard input simulation - #82
feat: add press_key MCP tool for keyboard input simulation#82jasonanovak wants to merge 1 commit into
Conversation
juliandescottes
left a comment
There was a problem hiding this comment.
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?
|
|
||
| try { | ||
| await firefox.pressKey(key, uid); | ||
| return successResponse(uid ? `✅ press_key "${key}" on ${uid}` : `✅ press_key "${key}"`); |
There was a problem hiding this comment.
Remove the emojis from the logs
There was a problem hiding this comment.
Is the package-lock update necessary? If not, can you remove it from this changeset?
| // 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 |
There was a problem hiding this comment.
Maybe WebDriver instead of W3C ? https://www.w3.org/TR/webdriver2/#actions
W3C actions is a bit vague.
There was a problem hiding this comment.
made this webdriver consistently
| // 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 |
There was a problem hiding this comment.
Maybe WebDriver instead of W3C ? https://www.w3.org/TR/webdriver2/#actions
W3C actions is a bit vague.
There was a problem hiding this comment.
made this webdriver consistently
| // Map of lowercase key names → Selenium unicode values | ||
| const KEY_MAP: Record<string, string> = { | ||
| enter: Key.RETURN, | ||
| return: Key.RETURN, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Changed the mapping and added the keys that are in selenium.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Measured: Return arrives as code=Enter, Enter as code=NumpadEnter. +1 to enter -> Key.RETURN plus numpadenter: Key.ENTER.
| if (part in MODIFIER_MAP) { | ||
| modifiers.push(MODIFIER_MAP[part]!); | ||
| } else { | ||
| mainKey = KEY_MAP[part] ?? part; |
There was a problem hiding this comment.
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?
|
@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. |
|
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.
19ee6e2 to
0663bd7
Compare
|
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. |
|
Thanks @jasonanovak ! Will take a look today, cc-ing @freema for review as well since he's working on a similar PR. |
juliandescottes
left a comment
There was a problem hiding this comment.
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
| 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)); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
+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.
| // Map of lowercase key names → Selenium unicode values | ||
| const KEY_MAP: Record<string, string> = { | ||
| enter: Key.RETURN, | ||
| return: Key.RETURN, |
There was a problem hiding this comment.
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?
| 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.', |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
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:
Also drops an orphaned yaml dev-dependency entry from package-lock.