auto-webmcp 0.3.0 → 0.3.2
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 +156 -38
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/auto-webmcp.cjs.js +76 -100
- package/dist/auto-webmcp.cjs.js.map +3 -3
- package/dist/auto-webmcp.esm.js +76 -100
- package/dist/auto-webmcp.esm.js.map +3 -3
- package/dist/auto-webmcp.iife.js +1 -9
- package/dist/auto-webmcp.iife.js.map +4 -4
- package/dist/config.d.ts +0 -10
- package/dist/config.d.ts.map +1 -1
- package/dist/discovery.d.ts.map +1 -1
- package/dist/interceptor.d.ts +14 -0
- package/dist/interceptor.d.ts.map +1 -1
- package/package.json +1 -1
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.
|
|
41
|
-
3.
|
|
42
|
-
4.
|
|
43
|
-
5.
|
|
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
|
-
|
|
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. `
|
|
106
|
-
2.
|
|
107
|
-
3.
|
|
108
|
-
4.
|
|
109
|
-
5.
|
|
110
|
-
6.
|
|
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. `
|
|
115
|
-
2.
|
|
116
|
-
3.
|
|
117
|
-
4. `aria-
|
|
118
|
-
5.
|
|
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
|
|
123
|
-
|
|
124
|
-
| `text`, `search`, `tel
|
|
125
|
-
| `email`
|
|
126
|
-
| `url`
|
|
127
|
-
| `number`, `range`
|
|
128
|
-
| `date`
|
|
129
|
-
| `datetime-local`
|
|
130
|
-
| `checkbox`
|
|
131
|
-
| `
|
|
132
|
-
| `
|
|
133
|
-
| `
|
|
134
|
-
| `
|
|
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
|
|
188
|
-
- All other browsers:
|
|
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
|
|
package/dist/analyzer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,UAAU,EAAmJ,MAAM,aAAa,CAAC;AAC1L,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,WAAW,eAAe;IAC9B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,UAAU,CAAC;IACxB,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B,6FAA6F;IAC7F,aAAa,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAKD,iDAAiD;AACjD,wBAAgB,cAAc,IAAI,IAAI,CAErC;AAED,gDAAgD;AAChD,wBAAgB,WAAW,CAAC,IAAI,EAAE,eAAe,EAAE,QAAQ,CAAC,EAAE,YAAY,GAAG,YAAY,CAOxF;
|
|
1
|
+
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,UAAU,EAAmJ,MAAM,aAAa,CAAC;AAC1L,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,WAAW,eAAe;IAC9B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,UAAU,CAAC;IACxB,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B,6FAA6F;IAC7F,aAAa,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAKD,iDAAiD;AACjD,wBAAgB,cAAc,IAAI,IAAI,CAErC;AAED,gDAAgD;AAChD,wBAAgB,WAAW,CAAC,IAAI,EAAE,eAAe,EAAE,QAAQ,CAAC,EAAE,YAAY,GAAG,YAAY,CAOxF;AAipBD;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,OAAO,EAClB,MAAM,EAAE,KAAK,CAAC,gBAAgB,GAAG,mBAAmB,GAAG,iBAAiB,CAAC,EACzE,SAAS,EAAE,iBAAiB,GAAG,gBAAgB,GAAG,IAAI,GACrD,YAAY,CAKd"}
|
package/dist/auto-webmcp.cjs.js
CHANGED
|
@@ -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
|
};
|
|
@@ -503,17 +502,39 @@ function extractDefaultValue(control) {
|
|
|
503
502
|
}
|
|
504
503
|
return void 0;
|
|
505
504
|
}
|
|
505
|
+
function collectShadowControls(root, visited = /* @__PURE__ */ new Set()) {
|
|
506
|
+
if (visited.has(root))
|
|
507
|
+
return [];
|
|
508
|
+
visited.add(root);
|
|
509
|
+
const results = [];
|
|
510
|
+
for (const el of Array.from(root.querySelectorAll("*"))) {
|
|
511
|
+
if (el.shadowRoot) {
|
|
512
|
+
results.push(
|
|
513
|
+
...Array.from(
|
|
514
|
+
el.shadowRoot.querySelectorAll(
|
|
515
|
+
"input, textarea, select"
|
|
516
|
+
)
|
|
517
|
+
),
|
|
518
|
+
...collectShadowControls(el.shadowRoot, visited)
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return results;
|
|
523
|
+
}
|
|
506
524
|
function buildSchema(form) {
|
|
507
525
|
const properties = {};
|
|
508
526
|
const required = [];
|
|
509
527
|
const fieldElements = /* @__PURE__ */ new Map();
|
|
510
528
|
const processedRadioGroups = /* @__PURE__ */ new Set();
|
|
511
529
|
const processedCheckboxGroups = /* @__PURE__ */ new Set();
|
|
512
|
-
const controls =
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
530
|
+
const controls = [
|
|
531
|
+
...Array.from(
|
|
532
|
+
form.querySelectorAll(
|
|
533
|
+
"input, textarea, select"
|
|
534
|
+
)
|
|
535
|
+
),
|
|
536
|
+
...collectShadowControls(form)
|
|
537
|
+
];
|
|
517
538
|
for (const control of controls) {
|
|
518
539
|
const name = control.name;
|
|
519
540
|
const fieldKey = name || resolveNativeControlFallbackKey(control);
|
|
@@ -691,9 +712,6 @@ function resolveAriaFieldKey(el) {
|
|
|
691
712
|
return null;
|
|
692
713
|
}
|
|
693
714
|
function inferAriaFieldTitle(el) {
|
|
694
|
-
const nativeTitle = el.getAttribute("toolparamtitle");
|
|
695
|
-
if (nativeTitle?.trim())
|
|
696
|
-
return nativeTitle.trim();
|
|
697
715
|
const htmlEl = el;
|
|
698
716
|
if (htmlEl.dataset?.["webmcpTitle"])
|
|
699
717
|
return htmlEl.dataset["webmcpTitle"];
|
|
@@ -732,9 +750,6 @@ function inferAriaFieldDescription(el) {
|
|
|
732
750
|
return "";
|
|
733
751
|
}
|
|
734
752
|
function inferFieldTitle(control) {
|
|
735
|
-
const nativeTitle = control.getAttribute("toolparamtitle");
|
|
736
|
-
if (nativeTitle?.trim())
|
|
737
|
-
return nativeTitle.trim();
|
|
738
753
|
if ("dataset" in control && control.dataset["webmcpTitle"]) {
|
|
739
754
|
return control.dataset["webmcpTitle"];
|
|
740
755
|
}
|
|
@@ -944,6 +959,9 @@ function buildExecuteHandler(form, config, toolName, metadata) {
|
|
|
944
959
|
return async (params) => {
|
|
945
960
|
pendingFillWarnings.set(form, []);
|
|
946
961
|
fillFormFields(form, params);
|
|
962
|
+
const missingNow = getMissingRequired(metadata, params);
|
|
963
|
+
if (missingNow.length > 0)
|
|
964
|
+
pendingWarnings.set(form, missingNow);
|
|
947
965
|
window.dispatchEvent(new CustomEvent("toolactivated", { detail: { toolName } }));
|
|
948
966
|
return new Promise((resolve, reject) => {
|
|
949
967
|
pendingExecutions.set(form, { resolve, reject });
|
|
@@ -970,9 +988,10 @@ function buildExecuteHandler(form, config, toolName, metadata) {
|
|
|
970
988
|
attachSubmitInterceptor(submitForm, toolName);
|
|
971
989
|
}
|
|
972
990
|
}
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
pendingWarnings.
|
|
991
|
+
if (submitForm !== form && pendingWarnings.has(form)) {
|
|
992
|
+
pendingWarnings.set(submitForm, pendingWarnings.get(form));
|
|
993
|
+
pendingWarnings.delete(form);
|
|
994
|
+
}
|
|
976
995
|
submitForm.requestSubmit();
|
|
977
996
|
} catch (err) {
|
|
978
997
|
reject(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -994,17 +1013,37 @@ function attachSubmitInterceptor(form, toolName) {
|
|
|
994
1013
|
pendingExecutions.delete(form);
|
|
995
1014
|
const formData = serializeFormData(form, lastParams.get(form), formFieldElements.get(form));
|
|
996
1015
|
lastFilledSnapshot.delete(form);
|
|
997
|
-
const
|
|
1016
|
+
const missingRequired = pendingWarnings.get(form) ?? [];
|
|
998
1017
|
pendingWarnings.delete(form);
|
|
999
1018
|
const fillWarnings = pendingFillWarnings.get(form) ?? [];
|
|
1000
1019
|
pendingFillWarnings.delete(form);
|
|
1001
|
-
const
|
|
1002
|
-
|
|
1003
|
-
|
|
1020
|
+
const skippedFields = fillWarnings.filter((w) => w.type === "not_filled").map((w) => w.field);
|
|
1021
|
+
const structured = {
|
|
1022
|
+
status: missingRequired.length > 0 || skippedFields.length > 0 ? "partial" : "success",
|
|
1023
|
+
filled_fields: formData,
|
|
1024
|
+
skipped_fields: skippedFields,
|
|
1025
|
+
missing_required: missingRequired,
|
|
1026
|
+
warnings: [
|
|
1027
|
+
...missingRequired.map((f) => ({
|
|
1028
|
+
field: f,
|
|
1029
|
+
type: "missing_required",
|
|
1030
|
+
message: `required field "${f}" was not provided`
|
|
1031
|
+
})),
|
|
1032
|
+
...fillWarnings
|
|
1033
|
+
]
|
|
1034
|
+
};
|
|
1035
|
+
const allWarnMessages = [
|
|
1036
|
+
...missingRequired.length ? [`required fields were not filled: ${missingRequired.join(", ")}`] : [],
|
|
1037
|
+
...fillWarnings.map((w) => w.message)
|
|
1004
1038
|
];
|
|
1005
|
-
const warningText =
|
|
1039
|
+
const warningText = allWarnMessages.length ? ` Note: ${allWarnMessages.join("; ")}.` : "";
|
|
1006
1040
|
const text = `Form submitted. Fields: ${JSON.stringify(formData)}${warningText}`;
|
|
1007
|
-
const result = {
|
|
1041
|
+
const result = {
|
|
1042
|
+
content: [
|
|
1043
|
+
{ type: "text", text },
|
|
1044
|
+
{ type: "text", text: JSON.stringify(structured) }
|
|
1045
|
+
]
|
|
1046
|
+
};
|
|
1008
1047
|
if (e.agentInvoked && typeof e.respondWith === "function") {
|
|
1009
1048
|
e.preventDefault();
|
|
1010
1049
|
e.respondWith(Promise.resolve(result));
|
|
@@ -1138,16 +1177,26 @@ function fillInput(input, form, key, value) {
|
|
|
1138
1177
|
const raw = String(value ?? "");
|
|
1139
1178
|
const num = Number(raw);
|
|
1140
1179
|
if (raw === "" || isNaN(num)) {
|
|
1141
|
-
pendingFillWarnings.get(form)?.push(
|
|
1180
|
+
pendingFillWarnings.get(form)?.push({
|
|
1181
|
+
field: key,
|
|
1182
|
+
type: "type_mismatch",
|
|
1183
|
+
message: `"${key}" expects a number, got: ${JSON.stringify(value)}`,
|
|
1184
|
+
original: value
|
|
1185
|
+
});
|
|
1142
1186
|
return;
|
|
1143
1187
|
}
|
|
1144
1188
|
const min = input.min !== "" ? parseFloat(input.min) : -Infinity;
|
|
1145
1189
|
const max = input.max !== "" ? parseFloat(input.max) : Infinity;
|
|
1146
1190
|
if (num < min || num > max) {
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1191
|
+
const clamped = Math.min(Math.max(num, min), max);
|
|
1192
|
+
pendingFillWarnings.get(form)?.push({
|
|
1193
|
+
field: key,
|
|
1194
|
+
type: "clamped",
|
|
1195
|
+
message: `"${key}" value ${num} is outside allowed range [${input.min || "?"}, ${input.max || "?"}], clamped to ${clamped}`,
|
|
1196
|
+
original: num,
|
|
1197
|
+
actual: clamped
|
|
1198
|
+
});
|
|
1199
|
+
input.value = String(clamped);
|
|
1151
1200
|
} else {
|
|
1152
1201
|
input.value = String(num);
|
|
1153
1202
|
}
|
|
@@ -1334,74 +1383,6 @@ function getMissingRequired(metadata, params) {
|
|
|
1334
1383
|
return metadata.inputSchema.required.filter((fieldKey) => !(fieldKey in params));
|
|
1335
1384
|
}
|
|
1336
1385
|
|
|
1337
|
-
// src/enhancer.ts
|
|
1338
|
-
async function enrichMetadata(metadata, enhancer) {
|
|
1339
|
-
try {
|
|
1340
|
-
const enriched = await callLLM(metadata, enhancer);
|
|
1341
|
-
return { ...metadata, description: enriched };
|
|
1342
|
-
} catch (err) {
|
|
1343
|
-
console.warn("[auto-webmcp] Enrichment failed, using heuristic description:", err);
|
|
1344
|
-
return metadata;
|
|
1345
|
-
}
|
|
1346
|
-
}
|
|
1347
|
-
async function callLLM(metadata, config) {
|
|
1348
|
-
const prompt = buildPrompt(metadata);
|
|
1349
|
-
if (config.provider === "claude") {
|
|
1350
|
-
return callClaude(prompt, config);
|
|
1351
|
-
} else {
|
|
1352
|
-
return callGemini(prompt, config);
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
function buildPrompt(metadata) {
|
|
1356
|
-
const fields = Object.entries(metadata.inputSchema.properties).map(([name, prop]) => `- ${prop.title ?? name} (${prop.type}): ${prop.description ?? ""}`).join("\n");
|
|
1357
|
-
return `You are helping describe a web form as an AI tool. Given this form information:
|
|
1358
|
-
|
|
1359
|
-
Name: ${metadata.name}
|
|
1360
|
-
Current description: ${metadata.description}
|
|
1361
|
-
Fields:
|
|
1362
|
-
${fields}
|
|
1363
|
-
|
|
1364
|
-
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.`;
|
|
1365
|
-
}
|
|
1366
|
-
async function callClaude(prompt, config) {
|
|
1367
|
-
const model = config.model ?? "claude-haiku-4-5-20251001";
|
|
1368
|
-
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
1369
|
-
method: "POST",
|
|
1370
|
-
headers: {
|
|
1371
|
-
"x-api-key": config.apiKey,
|
|
1372
|
-
"anthropic-version": "2023-06-01",
|
|
1373
|
-
"content-type": "application/json"
|
|
1374
|
-
},
|
|
1375
|
-
body: JSON.stringify({
|
|
1376
|
-
model,
|
|
1377
|
-
max_tokens: 150,
|
|
1378
|
-
messages: [{ role: "user", content: prompt }]
|
|
1379
|
-
})
|
|
1380
|
-
});
|
|
1381
|
-
if (!response.ok) {
|
|
1382
|
-
throw new Error(`Claude API error: ${response.status}`);
|
|
1383
|
-
}
|
|
1384
|
-
const data = await response.json();
|
|
1385
|
-
return data.content.filter((block) => block.type === "text").map((block) => block.text).join("").trim();
|
|
1386
|
-
}
|
|
1387
|
-
async function callGemini(prompt, config) {
|
|
1388
|
-
const model = config.model ?? "gemini-1.5-flash";
|
|
1389
|
-
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${config.apiKey}`;
|
|
1390
|
-
const response = await fetch(url, {
|
|
1391
|
-
method: "POST",
|
|
1392
|
-
headers: { "content-type": "application/json" },
|
|
1393
|
-
body: JSON.stringify({
|
|
1394
|
-
contents: [{ parts: [{ text: prompt }] }],
|
|
1395
|
-
generationConfig: { maxOutputTokens: 150, temperature: 0.2 }
|
|
1396
|
-
})
|
|
1397
|
-
});
|
|
1398
|
-
if (!response.ok) {
|
|
1399
|
-
throw new Error(`Gemini API error: ${response.status}`);
|
|
1400
|
-
}
|
|
1401
|
-
const data = await response.json();
|
|
1402
|
-
return data.candidates[0]?.content.parts.map((p) => p.text).join("").trim() ?? "";
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
1386
|
// src/discovery.ts
|
|
1406
1387
|
function emit(type, form, toolName) {
|
|
1407
1388
|
window.dispatchEvent(
|
|
@@ -1433,12 +1414,7 @@ async function registerForm(form, config) {
|
|
|
1433
1414
|
} catch {
|
|
1434
1415
|
}
|
|
1435
1416
|
}
|
|
1436
|
-
|
|
1437
|
-
if (config.enhance) {
|
|
1438
|
-
if (config.debug)
|
|
1439
|
-
console.debug(`[auto-webmcp] Enriching: ${metadata.name}\u2026`);
|
|
1440
|
-
metadata = await enrichMetadata(metadata, config.enhance);
|
|
1441
|
-
}
|
|
1417
|
+
const metadata = analyzeForm(form, override);
|
|
1442
1418
|
if (config.debug) {
|
|
1443
1419
|
warnToolQuality(metadata.name, metadata.description);
|
|
1444
1420
|
}
|