mcp: add detectForms tool for structured form discovery#1951
mcp: add detectForms tool for structured form discovery#1951mvanhorn wants to merge 13 commits intolightpanda-io:mainfrom
Conversation
Add a detectForms MCP tool and lp.detectForms CDP command that return structured form metadata from the current page. Each form includes its action URL, HTTP method, and fields with names, types, required status, values, select options, and backendNodeIds for use with the fill tool. This lets AI agents discover and fill forms in a single step instead of calling interactiveElements, filtering for form fields, and guessing which fields belong to which form. New files: - src/browser/forms.zig: FormInfo/FormField structs, collectForms() Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Form.getAction() resolves relative URLs against the page base, which causes test failures when the page URL is a test server address. Use the raw action attribute value instead, which matches what agents need to understand the form's target path. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Thank again for the PR, I just ran the tests locally and fixed the failing one. |
|
Yes, ready for review. The wba-test failure looks like a CI config issue (missing URL in curl step) - unrelated to this PR. All Zig tests pass. |
karlseguin
left a comment
There was a problem hiding this comment.
This is missing two things:
1 - a 'disabled' flag (which could come directly from the element, or from a parent) and form fields outside of the form. FormData handles this, we should extract the logic and make it as simple function call.
2 - Fields outside of the form. For example:
<input form="spice">
<form id=spice>...</form>We have code to handle this, but, looking at it, it could be better. For now, you'd have to do:
const form_id = self.asElement().getAttributeSafe(comptime .wrap("id"));
const root = if (form_id != null)
self.asNode().getRootNode(null) // Has ID: walk entire document to find form=ID controls
else
self.asNode(); // No ID: walk only form subtree (no external controls possible)
const node_live = collections.NodeLive(.form).init(root, self, page);
while (node_live.next()) |form_element| {
// ...
}
Meant to help things like #1951 Small optimization to form node_live iterator Disable iframes test (not related, but they are super-flaky, and I'm tired of CI's failing because of it. I'll look at them later today).
Co-authored-by: Karl Seguin <karlseguin@users.noreply.github.com>
Co-authored-by: Karl Seguin <karlseguin@users.noreply.github.com>
Co-authored-by: Karl Seguin <karlseguin@users.noreply.github.com>
Address review feedback from @karlseguin: 1. Use Form.getElements() instead of manual TreeWalker for field collection. This reuses NodeLive(.form) which handles fields outside the <form> via the form="id" attribute per spec. 2. Add disabled detection: checks both the element's disabled attribute and ancestor <fieldset disabled> (with first-legend exemption per spec). Fields are flagged rather than excluded - agents need visibility into disabled state. 3. allocator is now the first parameter in collectForms/helpers. 4. handleDetectForms returns InvalidParams on bad input instead of silently swallowing parse errors. 5. Added tests for disabled fields, disabled fieldsets, and external form fields via form="id". Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Addressed in 35551ac:
|
|
Thx all! |
|
Hmm. I thought that would fix the tests. They pass locally... |
|
The CI failure is in |
|
Yes, sorry about that. Hopefully that test become reliable in #1958 That failure won't stop us from merging. |
Summary
Adds a
detectFormsMCP tool and correspondinglp.detectFormsCDP command that return structured form metadata from the current page. Each form includes its action URL, HTTP method, and an array of fields with names, types, required status, current values, select options, and backendNodeIds for use with thefilltool.Why this matters
AI agents filling forms currently need multiple round-trips: call
interactiveElements, filter for form-related elements, figure out which fields belong to which form, then callfillper field. There is no way to get a form's structure - its action, method, field names, types, or select options - in a single call.No existing tool provides form structure.
interactiveElementsreturns flat lists without form grouping.structuredDataextracts JSON-LD/OpenGraph but not forms.Changes
New file:
src/browser/forms.zigFormInfo,FormField,SelectOptionstructs withjsonStringifycollectForms()walks the DOM via TreeWalker, finds<form>elements, collects child<input>,<textarea>, and<select>fields<select>elements, collects all<option>values and textModified:
src/mcp/tools.zigdetectFormstool in tool_list with optional URL parameterhandleDetectFormshandler: collects forms, registers nodes in node_registry, serializes with backendNodeIdsModified:
src/cdp/domains/lp.zigdetectFormsCDP command with same data, returns forms + formNodeIds arraysModified:
src/lightpanda.zigpub const formsExample output
[{ "backendNodeId": 42, "action": "/login", "method": "POST", "fields": [ {"backendNodeId": 43, "tagName": "input", "name": "email", "inputType": "email", "required": true, "placeholder": "you@example.com"}, {"backendNodeId": 44, "tagName": "select", "name": "role", "value": "user", "options": [{"value": "user", "text": "User"}, {"value": "admin", "text": "Admin"}]}, {"backendNodeId": 45, "tagName": "input", "name": "password", "inputType": "password", "required": true} ] }]Testing
src/browser/forms.zigincludes 6 inline test blocks covering: login forms, select options, textarea, empty form exclusion, hidden input exclusion, and multiple forms. Build passes withzig build.Implementation follows the
collectInteractiveElementspattern ininteractive.zig: TreeWalker over the DOM, collect matching elements, register nodes for backendNodeId references.This contribution was developed with AI assistance (Claude Code).