auto-webmcp 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -37,10 +37,11 @@ await autoWebMCP();
37
37
  ## What it does
38
38
 
39
39
  1. Scans the page for all `<form>` elements (on load + dynamically via `MutationObserver`)
40
- 2. Infers a meaningful **tool name**, **description**, and **JSON Schema** from the form's DOM
41
- 3. Registers each form as a WebMCP tool via `navigator.modelContext.registerTool()`
42
- 4. Intercepts submissions to return structured results back to the agent
43
- 5. Degrades silently in browsers without WebMCP
40
+ 2. Also discovers inputs **outside** `<form>` tags (orphan input groups) and inputs inside **Web Component shadow roots**
41
+ 3. Infers a meaningful **tool name**, **description**, and **JSON Schema** from the form's DOM
42
+ 4. Registers each form as a WebMCP tool via `navigator.modelContext.registerTool()`
43
+ 5. Intercepts submissions to return structured results back to the agent
44
+ 6. Degrades silently in browsers without WebMCP
44
45
 
45
46
  ---
46
47
 
@@ -50,15 +51,12 @@ await autoWebMCP();
50
51
  import { autoWebMCP } from 'auto-webmcp';
51
52
 
52
53
  await autoWebMCP({
53
- // Skip specific forms
54
+ // Skip specific forms (CSS selectors)
54
55
  exclude: ['#login-form', '[data-no-webmcp]'],
55
56
 
56
57
  // Auto-submit when agent invokes (default: false — human must click submit)
57
58
  autoSubmit: false,
58
59
 
59
- // Optional AI enrichment for richer descriptions
60
- enhance: { provider: 'claude', apiKey: 'sk-...' },
61
-
62
60
  // Per-form name / description overrides
63
61
  overrides: {
64
62
  '#checkout-form': {
@@ -74,7 +72,23 @@ await autoWebMCP({
74
72
 
75
73
  ### Per-form HTML overrides
76
74
 
77
- Override individual forms without JavaScript:
75
+ **Native WebMCP spec attributes** (highest priority):
76
+
77
+ ```html
78
+ <form
79
+ toolname="book_appointment"
80
+ tooldescription="Book a doctor appointment"
81
+ toolautosubmit
82
+ >
83
+ <input
84
+ name="date"
85
+ type="date"
86
+ toolparamdescription="Preferred appointment date"
87
+ >
88
+ </form>
89
+ ```
90
+
91
+ **data-webmcp-* attributes** (fallback, useful when you cannot edit the form element directly):
78
92
 
79
93
  ```html
80
94
  <form
@@ -88,7 +102,6 @@ Override individual forms without JavaScript:
88
102
  data-webmcp-title="Appointment Date"
89
103
  data-webmcp-description="Preferred appointment date"
90
104
  >
91
-
92
105
  </form>
93
106
  ```
94
107
 
@@ -102,36 +115,131 @@ Override individual forms without JavaScript:
102
115
 
103
116
  ## Tool name inference (priority order)
104
117
 
105
- 1. `data-webmcp-name` attribute on `<form>`
106
- 2. Submit button text (e.g. "Search Flights" → `search_flights`)
107
- 3. Nearest `<h1>`–`<h3>` heading above the form
108
- 4. Form `id` or `name` attribute
109
- 5. Last segment of form `action` URL
110
- 6. Fallback: `form_N`
118
+ 1. `toolname` native attribute on `<form>` (WebMCP spec)
119
+ 2. `data-webmcp-name` attribute on `<form>`
120
+ 3. Submit button text (e.g. "Search Flights" → `search_flights`)
121
+ 4. Nearest `<h1>`–`<h3>` heading above the form
122
+ 5. Form `id` or `name` attribute
123
+ 6. Last segment of form `action` URL
124
+ 7. Fallback: `form_N`
111
125
 
112
126
  ## Tool description inference (priority order)
113
127
 
114
- 1. `data-webmcp-description` attribute on `<form>`
115
- 2. `<legend>` text inside the form
116
- 3. `aria-label` on the form
117
- 4. `aria-describedby` target
118
- 5. Nearest heading + page `<title>`
128
+ 1. `tooldescription` native attribute on `<form>` (WebMCP spec)
129
+ 2. `data-webmcp-description` attribute on `<form>`
130
+ 3. `<legend>` text inside the form
131
+ 4. `aria-label` on the form
132
+ 5. `aria-describedby` target
133
+ 6. Nearest heading + page `<title>`
134
+
135
+ ## Field title inference (priority order)
136
+
137
+ 1. `data-webmcp-title` attribute on the field
138
+ 2. Associated `<label>` text
139
+ 3. `name` attribute (humanized)
140
+ 4. `id` attribute (humanized)
141
+ 5. `placeholder` text
142
+
143
+ ## Field description inference (priority order)
144
+
145
+ 1. `toolparamdescription` native attribute on the field (WebMCP spec)
146
+ 2. `data-webmcp-description` attribute on the field
147
+ 3. `aria-description` / `aria-describedby` target
148
+ 4. `placeholder` text
149
+
150
+ ---
119
151
 
120
152
  ## HTML → JSON Schema mapping
121
153
 
122
- | HTML type | JSON Schema |
123
- |------------------------|-------------------------|
124
- | `text`, `search`, `tel`| `string` |
125
- | `email` | `string` + format:email |
126
- | `url` | `string` + format:uri |
127
- | `number`, `range` | `number` (+ min/max) |
128
- | `date` | `string` + format:date |
129
- | `datetime-local` | `string` + format:date-time |
130
- | `checkbox` | `boolean` |
131
- | `radio` | `string` + enum |
132
- | `select` | `string` + enum |
133
- | `textarea` | `string` |
134
- | `file`, `hidden`, `password` | _skipped_ |
154
+ | HTML input | JSON Schema |
155
+ |------------------------------|------------------------------------|
156
+ | `text`, `search`, `tel` | `string` |
157
+ | `email` | `string` + `format: email` |
158
+ | `url` | `string` + `format: uri` |
159
+ | `number`, `range` | `number` (+ `min` / `max`) |
160
+ | `date` | `string` + `format: date` |
161
+ | `datetime-local` | `string` + `format: date-time` |
162
+ | `checkbox` | `boolean` |
163
+ | checkbox group (same `name`) | `array` + `items.enum` |
164
+ | `radio` group | `string` + `enum` + `oneOf` |
165
+ | `select` | `string` + `enum` + `oneOf` |
166
+ | `select[multiple]` | `array` + `items.enum` |
167
+ | `textarea` | `string` |
168
+ | ARIA role inputs | mapped by role (textbox, checkbox…) |
169
+ | `file`, `hidden`, `password` | _skipped_ |
170
+
171
+ Additional schema enrichment:
172
+ - Pre-filled field values are exposed as `schema.default`
173
+ - `<optgroup>` labels and `<datalist>` suggestions are included as metadata
174
+ - Disabled select options and placeholder options (`"Select..."`, `"---"`) are excluded
175
+
176
+ ---
177
+
178
+ ## ToolAnnotations
179
+
180
+ auto-webmcp automatically infers [WebMCP ToolAnnotations](https://webmachinelearning.github.io/webmcp/) from your HTML:
181
+
182
+ | Annotation | Auto-inferred when |
183
+ |---|---|
184
+ | `readOnlyHint` | Form method is `GET`, or submit button says "Search", "Find", etc. |
185
+ | `destructiveHint` | Submit button says "Delete", "Remove", "Cancel", etc. |
186
+ | `idempotentHint` | Form is read-only or GET |
187
+ | `openWorldHint` | Form modifies data (default for POST forms) |
188
+
189
+ Override any annotation with data attributes:
190
+
191
+ ```html
192
+ <form
193
+ data-webmcp-destructive="true"
194
+ data-webmcp-openworld="true"
195
+ >
196
+ <!-- delete account form -->
197
+ </form>
198
+ ```
199
+
200
+ ---
201
+
202
+ ## Advanced discovery
203
+
204
+ ### Shadow DOM (Web Components)
205
+
206
+ Inputs inside custom element shadow roots are automatically discovered and included in the schema. No extra configuration needed.
207
+
208
+ ```html
209
+ <form>
210
+ <custom-date-picker></custom-date-picker> <!-- shadow root inputs discovered -->
211
+ <button type="submit">Book</button>
212
+ </form>
213
+ ```
214
+
215
+ ### Orphan inputs (no `<form>` tag)
216
+
217
+ Input groups not wrapped in a `<form>` element (common in SPAs) are detected by finding the nearest ancestor that contains a visible submit button. Each group is registered as a separate tool.
218
+
219
+ ### React and framework-managed forms
220
+
221
+ auto-webmcp fills React-controlled inputs using native `HTMLInputElement` prototype setters and `execCommand` to trigger `onChange`. A post-fill snapshot preserves field values if the framework re-renders before submit.
222
+
223
+ ---
224
+
225
+ ## Structured execute result
226
+
227
+ Every tool execution returns a two-item `content` array:
228
+
229
+ - `content[0].text` — human-readable summary: `"Form submitted. Fields: {...}"`
230
+ - `content[1].text` — JSON-stringified structured data:
231
+
232
+ ```json
233
+ {
234
+ "status": "success",
235
+ "filled_fields": { "email": "a@b.com", "frequency": "weekly" },
236
+ "skipped_fields": [],
237
+ "missing_required": [],
238
+ "warnings": []
239
+ }
240
+ ```
241
+
242
+ `status` is `"partial"` when required fields were missing or a value was clamped. Each warning in the `warnings` array has a `field`, `type` (`clamped`, `not_filled`, `missing_required`, `type_mismatch`), `message`, and optionally `original` / `actual` values.
135
243
 
136
244
  ---
137
245
 
@@ -148,12 +256,23 @@ handle.destroy() // Promise<void> — unregister all tools & stop observing
148
256
  ### Events
149
257
 
150
258
  ```js
259
+ // auto-webmcp lifecycle events
151
260
  window.addEventListener('form:registered', (e) => {
152
261
  console.log('Registered:', e.detail.toolName, e.detail.form);
153
262
  });
154
263
  window.addEventListener('form:unregistered', (e) => {
155
264
  console.log('Removed:', e.detail.toolName);
156
265
  });
266
+
267
+ // WebMCP spec events
268
+ window.addEventListener('toolactivated', (e) => {
269
+ // Agent has filled the form fields, waiting for submit
270
+ console.log('Agent activated:', e.detail.toolName);
271
+ });
272
+ window.addEventListener('toolcancel', (e) => {
273
+ // User reset the form or agent cancelled
274
+ console.log('Cancelled:', e.detail.toolName);
275
+ });
157
276
  ```
158
277
 
159
278
  ### Prevent auto-init (IIFE build)
@@ -177,16 +296,15 @@ npm install
177
296
  npm run build # compile to dist/
178
297
  npm run build:watch # rebuild on change
179
298
  npm run typecheck # TypeScript type check only
180
- npm test # Playwright integration tests
299
+ npm test # Playwright integration tests (Chromium)
181
300
  ```
182
301
 
183
302
  ---
184
303
 
185
304
  ## Browser support
186
305
 
187
- - Chrome 146+ with `#enable-webmcp-testing` flag (or WebMCP GA) for full functionality
188
- - All other browsers: forms are analyzed and the library loads without error
189
- `navigator.modelContext` calls are silently no-opped (progressive enhancement)
306
+ - Chrome 146+ with `chrome://flags/#enable-webmcp-testing` enabled for full functionality
307
+ - All other browsers: the library loads, analyzes forms, and silently no-ops `navigator.modelContext` calls (progressive enhancement)
190
308
 
191
309
  ---
192
310
 
@@ -102,7 +102,6 @@ function resolveConfig(userConfig) {
102
102
  return {
103
103
  exclude: userConfig?.exclude ?? [],
104
104
  autoSubmit: userConfig?.autoSubmit ?? false,
105
- enhance: userConfig?.enhance ?? null,
106
105
  overrides: userConfig?.overrides ?? {},
107
106
  debug: userConfig?.debug ?? false
108
107
  };
@@ -430,7 +429,7 @@ function inferToolDescription(form) {
430
429
  const heading = getNearestHeadingText(form);
431
430
  const pageTitle = document.title?.trim();
432
431
  if (heading && pageTitle)
433
- return `${heading} \u2014 ${pageTitle}`;
432
+ return `${heading}: ${pageTitle}`;
434
433
  if (heading)
435
434
  return heading;
436
435
  if (pageTitle)
@@ -959,6 +958,7 @@ function buildExecuteHandler(form, config, toolName, metadata) {
959
958
  attachSubmitInterceptor(form, toolName);
960
959
  return async (params) => {
961
960
  pendingFillWarnings.set(form, []);
961
+ pendingWarnings.delete(form);
962
962
  fillFormFields(form, params);
963
963
  const missingNow = getMissingRequired(metadata, params);
964
964
  if (missingNow.length > 0)
@@ -1384,74 +1384,6 @@ function getMissingRequired(metadata, params) {
1384
1384
  return metadata.inputSchema.required.filter((fieldKey) => !(fieldKey in params));
1385
1385
  }
1386
1386
 
1387
- // src/enhancer.ts
1388
- async function enrichMetadata(metadata, enhancer) {
1389
- try {
1390
- const enriched = await callLLM(metadata, enhancer);
1391
- return { ...metadata, description: enriched };
1392
- } catch (err) {
1393
- console.warn("[auto-webmcp] Enrichment failed, using heuristic description:", err);
1394
- return metadata;
1395
- }
1396
- }
1397
- async function callLLM(metadata, config) {
1398
- const prompt = buildPrompt(metadata);
1399
- if (config.provider === "claude") {
1400
- return callClaude(prompt, config);
1401
- } else {
1402
- return callGemini(prompt, config);
1403
- }
1404
- }
1405
- function buildPrompt(metadata) {
1406
- const fields = Object.entries(metadata.inputSchema.properties).map(([name, prop]) => `- ${prop.title ?? name} (${prop.type}): ${prop.description ?? ""}`).join("\n");
1407
- return `You are helping describe a web form as an AI tool. Given this form information:
1408
-
1409
- Name: ${metadata.name}
1410
- Current description: ${metadata.description}
1411
- Fields:
1412
- ${fields}
1413
-
1414
- Write a concise (1-2 sentence) description of what this tool does and when an AI agent should use it. Be specific and actionable. Respond with only the description, no preamble.`;
1415
- }
1416
- async function callClaude(prompt, config) {
1417
- const model = config.model ?? "claude-haiku-4-5-20251001";
1418
- const response = await fetch("https://api.anthropic.com/v1/messages", {
1419
- method: "POST",
1420
- headers: {
1421
- "x-api-key": config.apiKey,
1422
- "anthropic-version": "2023-06-01",
1423
- "content-type": "application/json"
1424
- },
1425
- body: JSON.stringify({
1426
- model,
1427
- max_tokens: 150,
1428
- messages: [{ role: "user", content: prompt }]
1429
- })
1430
- });
1431
- if (!response.ok) {
1432
- throw new Error(`Claude API error: ${response.status}`);
1433
- }
1434
- const data = await response.json();
1435
- return data.content.filter((block) => block.type === "text").map((block) => block.text).join("").trim();
1436
- }
1437
- async function callGemini(prompt, config) {
1438
- const model = config.model ?? "gemini-1.5-flash";
1439
- const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${config.apiKey}`;
1440
- const response = await fetch(url, {
1441
- method: "POST",
1442
- headers: { "content-type": "application/json" },
1443
- body: JSON.stringify({
1444
- contents: [{ parts: [{ text: prompt }] }],
1445
- generationConfig: { maxOutputTokens: 150, temperature: 0.2 }
1446
- })
1447
- });
1448
- if (!response.ok) {
1449
- throw new Error(`Gemini API error: ${response.status}`);
1450
- }
1451
- const data = await response.json();
1452
- return data.candidates[0]?.content.parts.map((p) => p.text).join("").trim() ?? "";
1453
- }
1454
-
1455
1387
  // src/discovery.ts
1456
1388
  function emit(type, form, toolName) {
1457
1389
  window.dispatchEvent(
@@ -1483,12 +1415,7 @@ async function registerForm(form, config) {
1483
1415
  } catch {
1484
1416
  }
1485
1417
  }
1486
- let metadata = analyzeForm(form, override);
1487
- if (config.enhance) {
1488
- if (config.debug)
1489
- console.debug(`[auto-webmcp] Enriching: ${metadata.name}\u2026`);
1490
- metadata = await enrichMetadata(metadata, config.enhance);
1491
- }
1418
+ const metadata = analyzeForm(form, override);
1492
1419
  if (config.debug) {
1493
1420
  warnToolQuality(metadata.name, metadata.description);
1494
1421
  }