auto-webmcp 0.1.0
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/LICENSE +21 -0
- package/README.md +193 -0
- package/dist/analyzer.d.ts +15 -0
- package/dist/analyzer.d.ts.map +1 -0
- package/dist/auto-webmcp.cjs.js +703 -0
- package/dist/auto-webmcp.cjs.js.map +7 -0
- package/dist/auto-webmcp.esm.js +687 -0
- package/dist/auto-webmcp.esm.js.map +7 -0
- package/dist/auto-webmcp.iife.js +10 -0
- package/dist/auto-webmcp.iife.js.map +7 -0
- package/dist/config.d.ts +44 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/discovery.d.ts +11 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/enhancer.d.ts +10 -0
- package/dist/enhancer.d.ts.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/interceptor.d.ts +33 -0
- package/dist/interceptor.d.ts.map +1 -0
- package/dist/registry.d.ts +40 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/schema.d.ts +25 -0
- package/dist/schema.d.ts.map +1 -0
- package/package.json +46 -0
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __esm = (fn, res) => function __init() {
|
|
7
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
8
|
+
};
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
22
|
+
|
|
23
|
+
// src/registry.ts
|
|
24
|
+
var registry_exports = {};
|
|
25
|
+
__export(registry_exports, {
|
|
26
|
+
getAllRegisteredTools: () => getAllRegisteredTools,
|
|
27
|
+
getRegisteredToolName: () => getRegisteredToolName,
|
|
28
|
+
isWebMCPSupported: () => isWebMCPSupported,
|
|
29
|
+
registerFormTool: () => registerFormTool,
|
|
30
|
+
unregisterAll: () => unregisterAll,
|
|
31
|
+
unregisterFormTool: () => unregisterFormTool
|
|
32
|
+
});
|
|
33
|
+
function isWebMCPSupported() {
|
|
34
|
+
return typeof navigator !== "undefined" && typeof navigator.modelContext !== "undefined";
|
|
35
|
+
}
|
|
36
|
+
async function registerFormTool(form, metadata, execute) {
|
|
37
|
+
if (!isWebMCPSupported())
|
|
38
|
+
return;
|
|
39
|
+
const existing = registeredTools.get(form);
|
|
40
|
+
if (existing) {
|
|
41
|
+
await unregisterFormTool(form);
|
|
42
|
+
}
|
|
43
|
+
await navigator.modelContext.registerTool({
|
|
44
|
+
name: metadata.name,
|
|
45
|
+
description: metadata.description,
|
|
46
|
+
inputSchema: metadata.inputSchema,
|
|
47
|
+
execute
|
|
48
|
+
});
|
|
49
|
+
registeredTools.set(form, metadata.name);
|
|
50
|
+
}
|
|
51
|
+
async function unregisterFormTool(form) {
|
|
52
|
+
if (!isWebMCPSupported())
|
|
53
|
+
return;
|
|
54
|
+
const name = registeredTools.get(form);
|
|
55
|
+
if (!name)
|
|
56
|
+
return;
|
|
57
|
+
try {
|
|
58
|
+
await navigator.modelContext.unregisterTool(name);
|
|
59
|
+
} catch {
|
|
60
|
+
}
|
|
61
|
+
registeredTools.delete(form);
|
|
62
|
+
}
|
|
63
|
+
function getRegisteredToolName(form) {
|
|
64
|
+
return registeredTools.get(form);
|
|
65
|
+
}
|
|
66
|
+
function getAllRegisteredTools() {
|
|
67
|
+
return Array.from(registeredTools.entries()).map(([form, name]) => ({ form, name }));
|
|
68
|
+
}
|
|
69
|
+
async function unregisterAll() {
|
|
70
|
+
const entries = Array.from(registeredTools.entries());
|
|
71
|
+
await Promise.all(entries.map(([form]) => unregisterFormTool(form)));
|
|
72
|
+
}
|
|
73
|
+
var registeredTools;
|
|
74
|
+
var init_registry = __esm({
|
|
75
|
+
"src/registry.ts"() {
|
|
76
|
+
"use strict";
|
|
77
|
+
registeredTools = /* @__PURE__ */ new Map();
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// src/index.ts
|
|
82
|
+
var src_exports = {};
|
|
83
|
+
__export(src_exports, {
|
|
84
|
+
autoWebMCP: () => autoWebMCP
|
|
85
|
+
});
|
|
86
|
+
module.exports = __toCommonJS(src_exports);
|
|
87
|
+
|
|
88
|
+
// src/config.ts
|
|
89
|
+
function resolveConfig(userConfig) {
|
|
90
|
+
return {
|
|
91
|
+
exclude: userConfig?.exclude ?? [],
|
|
92
|
+
autoSubmit: userConfig?.autoSubmit ?? false,
|
|
93
|
+
enhance: userConfig?.enhance ?? null,
|
|
94
|
+
overrides: userConfig?.overrides ?? {},
|
|
95
|
+
debug: userConfig?.debug ?? false
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/schema.ts
|
|
100
|
+
function inputTypeToSchema(input) {
|
|
101
|
+
if (input instanceof HTMLInputElement) {
|
|
102
|
+
return mapInputElement(input);
|
|
103
|
+
}
|
|
104
|
+
if (input instanceof HTMLTextAreaElement) {
|
|
105
|
+
return { type: "string" };
|
|
106
|
+
}
|
|
107
|
+
if (input instanceof HTMLSelectElement) {
|
|
108
|
+
return mapSelectElement(input);
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
function mapInputElement(input) {
|
|
113
|
+
const type = input.type.toLowerCase();
|
|
114
|
+
switch (type) {
|
|
115
|
+
case "text":
|
|
116
|
+
case "search":
|
|
117
|
+
case "tel":
|
|
118
|
+
return buildStringSchema(input);
|
|
119
|
+
case "email":
|
|
120
|
+
return { ...buildStringSchema(input), format: "email" };
|
|
121
|
+
case "url":
|
|
122
|
+
return { ...buildStringSchema(input), format: "uri" };
|
|
123
|
+
case "number":
|
|
124
|
+
case "range": {
|
|
125
|
+
const prop = { type: "number" };
|
|
126
|
+
if (input.min !== "")
|
|
127
|
+
prop.minimum = parseFloat(input.min);
|
|
128
|
+
if (input.max !== "")
|
|
129
|
+
prop.maximum = parseFloat(input.max);
|
|
130
|
+
return prop;
|
|
131
|
+
}
|
|
132
|
+
case "date":
|
|
133
|
+
return { type: "string", format: "date" };
|
|
134
|
+
case "datetime-local":
|
|
135
|
+
return { type: "string", format: "date-time" };
|
|
136
|
+
case "time":
|
|
137
|
+
return { type: "string", format: "time" };
|
|
138
|
+
case "month":
|
|
139
|
+
return { type: "string", pattern: "^\\d{4}-\\d{2}$" };
|
|
140
|
+
case "week":
|
|
141
|
+
return { type: "string", pattern: "^\\d{4}-W\\d{2}$" };
|
|
142
|
+
case "color":
|
|
143
|
+
return { type: "string", pattern: "^#[0-9a-fA-F]{6}$" };
|
|
144
|
+
case "checkbox":
|
|
145
|
+
return { type: "boolean" };
|
|
146
|
+
case "radio":
|
|
147
|
+
return { type: "string" };
|
|
148
|
+
case "file":
|
|
149
|
+
case "hidden":
|
|
150
|
+
case "submit":
|
|
151
|
+
case "reset":
|
|
152
|
+
case "button":
|
|
153
|
+
case "image":
|
|
154
|
+
return null;
|
|
155
|
+
case "password":
|
|
156
|
+
return null;
|
|
157
|
+
default:
|
|
158
|
+
return { type: "string" };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function buildStringSchema(input) {
|
|
162
|
+
const prop = { type: "string" };
|
|
163
|
+
if (input.minLength > 0)
|
|
164
|
+
prop.minLength = input.minLength;
|
|
165
|
+
if (input.maxLength > 0 && input.maxLength !== 524288)
|
|
166
|
+
prop.maxLength = input.maxLength;
|
|
167
|
+
if (input.pattern)
|
|
168
|
+
prop.pattern = input.pattern;
|
|
169
|
+
return prop;
|
|
170
|
+
}
|
|
171
|
+
function mapSelectElement(select) {
|
|
172
|
+
const options = Array.from(select.options).filter((o) => o.value !== "").map((o) => o.value);
|
|
173
|
+
if (options.length === 0) {
|
|
174
|
+
return { type: "string" };
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
type: "string",
|
|
178
|
+
enum: options
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function collectRadioEnum(form, name) {
|
|
182
|
+
const radios = Array.from(
|
|
183
|
+
form.querySelectorAll(`input[type="radio"][name="${CSS.escape(name)}"]`)
|
|
184
|
+
);
|
|
185
|
+
return radios.map((r) => r.value).filter((v) => v !== "");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/analyzer.ts
|
|
189
|
+
var formIndex = 0;
|
|
190
|
+
function analyzeForm(form, override) {
|
|
191
|
+
const name = override?.name ?? inferToolName(form);
|
|
192
|
+
const description = override?.description ?? inferToolDescription(form);
|
|
193
|
+
const inputSchema = buildSchema(form);
|
|
194
|
+
return { name, description, inputSchema };
|
|
195
|
+
}
|
|
196
|
+
function inferToolName(form) {
|
|
197
|
+
const explicit = form.dataset["webmcpName"];
|
|
198
|
+
if (explicit)
|
|
199
|
+
return sanitizeName(explicit);
|
|
200
|
+
const submitText = getSubmitButtonText(form);
|
|
201
|
+
if (submitText)
|
|
202
|
+
return sanitizeName(submitText);
|
|
203
|
+
const heading = getNearestHeadingText(form);
|
|
204
|
+
if (heading)
|
|
205
|
+
return sanitizeName(heading);
|
|
206
|
+
if (form.id)
|
|
207
|
+
return sanitizeName(form.id);
|
|
208
|
+
if (form.name)
|
|
209
|
+
return sanitizeName(form.name);
|
|
210
|
+
if (form.action) {
|
|
211
|
+
const segment = getLastPathSegment(form.action);
|
|
212
|
+
if (segment)
|
|
213
|
+
return sanitizeName(segment);
|
|
214
|
+
}
|
|
215
|
+
return `form_${++formIndex}`;
|
|
216
|
+
}
|
|
217
|
+
function sanitizeName(raw) {
|
|
218
|
+
return raw.toLowerCase().trim().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 64) || "form";
|
|
219
|
+
}
|
|
220
|
+
function getSubmitButtonText(form) {
|
|
221
|
+
const buttons = [
|
|
222
|
+
...Array.from(form.querySelectorAll('button[type="submit"], button:not([type])')),
|
|
223
|
+
...Array.from(form.querySelectorAll('input[type="submit"]'))
|
|
224
|
+
];
|
|
225
|
+
for (const btn of buttons) {
|
|
226
|
+
const text = btn instanceof HTMLInputElement ? btn.value.trim() : btn.textContent?.trim() ?? "";
|
|
227
|
+
if (text && text.length > 0 && text.length < 80)
|
|
228
|
+
return text;
|
|
229
|
+
}
|
|
230
|
+
return "";
|
|
231
|
+
}
|
|
232
|
+
function getNearestHeadingText(form) {
|
|
233
|
+
let node = form;
|
|
234
|
+
while (node) {
|
|
235
|
+
let sibling = node.previousElementSibling;
|
|
236
|
+
while (sibling) {
|
|
237
|
+
if (/^H[1-3]$/i.test(sibling.tagName)) {
|
|
238
|
+
const text = sibling.textContent?.trim() ?? "";
|
|
239
|
+
if (text)
|
|
240
|
+
return text;
|
|
241
|
+
}
|
|
242
|
+
sibling = sibling.previousElementSibling;
|
|
243
|
+
}
|
|
244
|
+
node = node.parentElement;
|
|
245
|
+
if (!node || node === document.body)
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
return "";
|
|
249
|
+
}
|
|
250
|
+
function getLastPathSegment(url) {
|
|
251
|
+
try {
|
|
252
|
+
const parsed = new URL(url, window.location.href);
|
|
253
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
254
|
+
return segments[segments.length - 1] ?? "";
|
|
255
|
+
} catch {
|
|
256
|
+
return "";
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function inferToolDescription(form) {
|
|
260
|
+
const explicit = form.dataset["webmcpDescription"];
|
|
261
|
+
if (explicit)
|
|
262
|
+
return explicit.trim();
|
|
263
|
+
const legend = form.querySelector("legend");
|
|
264
|
+
if (legend?.textContent?.trim())
|
|
265
|
+
return legend.textContent.trim();
|
|
266
|
+
const ariaLabel = form.getAttribute("aria-label");
|
|
267
|
+
if (ariaLabel?.trim())
|
|
268
|
+
return ariaLabel.trim();
|
|
269
|
+
const describedById = form.getAttribute("aria-describedby");
|
|
270
|
+
if (describedById) {
|
|
271
|
+
const descEl = document.getElementById(describedById);
|
|
272
|
+
if (descEl?.textContent?.trim())
|
|
273
|
+
return descEl.textContent.trim();
|
|
274
|
+
}
|
|
275
|
+
const heading = getNearestHeadingText(form);
|
|
276
|
+
const pageTitle = document.title?.trim();
|
|
277
|
+
if (heading && pageTitle)
|
|
278
|
+
return `${heading} \u2014 ${pageTitle}`;
|
|
279
|
+
if (heading)
|
|
280
|
+
return heading;
|
|
281
|
+
if (pageTitle)
|
|
282
|
+
return pageTitle;
|
|
283
|
+
return "Submit form";
|
|
284
|
+
}
|
|
285
|
+
function buildSchema(form) {
|
|
286
|
+
const properties = {};
|
|
287
|
+
const required = [];
|
|
288
|
+
const processedRadioGroups = /* @__PURE__ */ new Set();
|
|
289
|
+
const controls = Array.from(
|
|
290
|
+
form.querySelectorAll(
|
|
291
|
+
"input, textarea, select"
|
|
292
|
+
)
|
|
293
|
+
);
|
|
294
|
+
for (const control of controls) {
|
|
295
|
+
const name = control.name;
|
|
296
|
+
if (!name)
|
|
297
|
+
continue;
|
|
298
|
+
if (control instanceof HTMLInputElement && control.type === "radio") {
|
|
299
|
+
if (processedRadioGroups.has(name))
|
|
300
|
+
continue;
|
|
301
|
+
processedRadioGroups.add(name);
|
|
302
|
+
}
|
|
303
|
+
const schemaProp = inputTypeToSchema(control);
|
|
304
|
+
if (!schemaProp)
|
|
305
|
+
continue;
|
|
306
|
+
schemaProp.title = inferFieldTitle(control);
|
|
307
|
+
const desc = inferFieldDescription(control);
|
|
308
|
+
if (desc)
|
|
309
|
+
schemaProp.description = desc;
|
|
310
|
+
if (control instanceof HTMLInputElement && control.type === "radio") {
|
|
311
|
+
schemaProp.enum = collectRadioEnum(form, name);
|
|
312
|
+
}
|
|
313
|
+
properties[name] = schemaProp;
|
|
314
|
+
if (control.required) {
|
|
315
|
+
required.push(name);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return { type: "object", properties, required };
|
|
319
|
+
}
|
|
320
|
+
function inferFieldTitle(control) {
|
|
321
|
+
if ("dataset" in control && control.dataset["webmcpTitle"]) {
|
|
322
|
+
return control.dataset["webmcpTitle"];
|
|
323
|
+
}
|
|
324
|
+
const labelText = getAssociatedLabelText(control);
|
|
325
|
+
if (labelText)
|
|
326
|
+
return labelText;
|
|
327
|
+
if (control.name)
|
|
328
|
+
return humanizeName(control.name);
|
|
329
|
+
if (control.id)
|
|
330
|
+
return humanizeName(control.id);
|
|
331
|
+
return "";
|
|
332
|
+
}
|
|
333
|
+
function inferFieldDescription(control) {
|
|
334
|
+
const el = control;
|
|
335
|
+
if (el.dataset["webmcpDescription"])
|
|
336
|
+
return el.dataset["webmcpDescription"];
|
|
337
|
+
const ariaDesc = control.getAttribute("aria-description");
|
|
338
|
+
if (ariaDesc)
|
|
339
|
+
return ariaDesc;
|
|
340
|
+
const describedById = control.getAttribute("aria-describedby");
|
|
341
|
+
if (describedById) {
|
|
342
|
+
const descEl = document.getElementById(describedById);
|
|
343
|
+
if (descEl?.textContent?.trim())
|
|
344
|
+
return descEl.textContent.trim();
|
|
345
|
+
}
|
|
346
|
+
if (control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement) {
|
|
347
|
+
const ph = control.placeholder?.trim();
|
|
348
|
+
if (ph && ph.length > 0)
|
|
349
|
+
return ph;
|
|
350
|
+
}
|
|
351
|
+
return "";
|
|
352
|
+
}
|
|
353
|
+
function getAssociatedLabelText(control) {
|
|
354
|
+
if (control.id) {
|
|
355
|
+
const label = document.querySelector(`label[for="${CSS.escape(control.id)}"]`);
|
|
356
|
+
if (label) {
|
|
357
|
+
const text = labelTextWithoutNested(label);
|
|
358
|
+
if (text)
|
|
359
|
+
return text;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
const parent = control.closest("label");
|
|
363
|
+
if (parent) {
|
|
364
|
+
const text = labelTextWithoutNested(parent);
|
|
365
|
+
if (text)
|
|
366
|
+
return text;
|
|
367
|
+
}
|
|
368
|
+
return "";
|
|
369
|
+
}
|
|
370
|
+
function labelTextWithoutNested(label) {
|
|
371
|
+
const clone = label.cloneNode(true);
|
|
372
|
+
clone.querySelectorAll("input, select, textarea, button").forEach((el) => el.remove());
|
|
373
|
+
return clone.textContent?.trim() ?? "";
|
|
374
|
+
}
|
|
375
|
+
function humanizeName(raw) {
|
|
376
|
+
return raw.replace(/[-_]/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").trim().replace(/\b\w/g, (c) => c.toUpperCase());
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// src/discovery.ts
|
|
380
|
+
init_registry();
|
|
381
|
+
|
|
382
|
+
// src/interceptor.ts
|
|
383
|
+
var pendingExecutions = /* @__PURE__ */ new WeakMap();
|
|
384
|
+
function buildExecuteHandler(form, config) {
|
|
385
|
+
attachSubmitInterceptor(form);
|
|
386
|
+
return async (params) => {
|
|
387
|
+
fillFormFields(form, params);
|
|
388
|
+
return new Promise((resolve, reject) => {
|
|
389
|
+
pendingExecutions.set(form, { resolve, reject });
|
|
390
|
+
if (config.autoSubmit || form.dataset["webmcpAutosubmit"] !== void 0) {
|
|
391
|
+
form.requestSubmit();
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
function attachSubmitInterceptor(form) {
|
|
397
|
+
if (form["__awmcp_intercepted"])
|
|
398
|
+
return;
|
|
399
|
+
form["__awmcp_intercepted"] = true;
|
|
400
|
+
form.addEventListener("submit", (e) => {
|
|
401
|
+
const pending = pendingExecutions.get(form);
|
|
402
|
+
if (!pending)
|
|
403
|
+
return;
|
|
404
|
+
const { resolve } = pending;
|
|
405
|
+
pendingExecutions.delete(form);
|
|
406
|
+
const formData = serializeFormData(form);
|
|
407
|
+
if (e.agentInvoked && typeof e.respondWith === "function") {
|
|
408
|
+
e.preventDefault();
|
|
409
|
+
e.respondWith(
|
|
410
|
+
Promise.resolve({
|
|
411
|
+
success: true,
|
|
412
|
+
data: formData
|
|
413
|
+
})
|
|
414
|
+
);
|
|
415
|
+
resolve({ success: true, data: formData });
|
|
416
|
+
} else {
|
|
417
|
+
const targetUrl = resolveFormAction(form);
|
|
418
|
+
resolve({ success: true, data: formData, url: targetUrl });
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
function fillFormFields(form, params) {
|
|
423
|
+
for (const [name, value] of Object.entries(params)) {
|
|
424
|
+
const escapedName = CSS.escape(name);
|
|
425
|
+
const input = form.querySelector(
|
|
426
|
+
`[name="${escapedName}"]`
|
|
427
|
+
);
|
|
428
|
+
if (!input)
|
|
429
|
+
continue;
|
|
430
|
+
if (input instanceof HTMLInputElement) {
|
|
431
|
+
fillInput(input, form, name, value);
|
|
432
|
+
} else if (input instanceof HTMLTextAreaElement) {
|
|
433
|
+
input.value = String(value ?? "");
|
|
434
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
435
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
436
|
+
} else if (input instanceof HTMLSelectElement) {
|
|
437
|
+
input.value = String(value ?? "");
|
|
438
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
function fillInput(input, form, name, value) {
|
|
443
|
+
const type = input.type.toLowerCase();
|
|
444
|
+
if (type === "checkbox") {
|
|
445
|
+
input.checked = Boolean(value);
|
|
446
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (type === "radio") {
|
|
450
|
+
const escapedName = CSS.escape(name);
|
|
451
|
+
const radios = form.querySelectorAll(
|
|
452
|
+
`input[type="radio"][name="${escapedName}"]`
|
|
453
|
+
);
|
|
454
|
+
for (const radio of radios) {
|
|
455
|
+
if (radio.value === String(value)) {
|
|
456
|
+
radio.checked = true;
|
|
457
|
+
radio.dispatchEvent(new Event("change", { bubbles: true }));
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
input.value = String(value ?? "");
|
|
464
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
465
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
466
|
+
}
|
|
467
|
+
function serializeFormData(form) {
|
|
468
|
+
const result = {};
|
|
469
|
+
const data = new FormData(form);
|
|
470
|
+
for (const [key, val] of data.entries()) {
|
|
471
|
+
if (result[key] !== void 0) {
|
|
472
|
+
const existing = result[key];
|
|
473
|
+
if (Array.isArray(existing)) {
|
|
474
|
+
existing.push(val);
|
|
475
|
+
} else {
|
|
476
|
+
result[key] = [existing, val];
|
|
477
|
+
}
|
|
478
|
+
} else {
|
|
479
|
+
result[key] = val;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return result;
|
|
483
|
+
}
|
|
484
|
+
function resolveFormAction(form) {
|
|
485
|
+
if (form.action) {
|
|
486
|
+
try {
|
|
487
|
+
return new URL(form.action, window.location.href).href;
|
|
488
|
+
} catch {
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return window.location.href;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// src/enhancer.ts
|
|
495
|
+
async function enrichMetadata(metadata, enhancer) {
|
|
496
|
+
try {
|
|
497
|
+
const enriched = await callLLM(metadata, enhancer);
|
|
498
|
+
return { ...metadata, description: enriched };
|
|
499
|
+
} catch (err) {
|
|
500
|
+
console.warn("[auto-webmcp] Enrichment failed, using heuristic description:", err);
|
|
501
|
+
return metadata;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
async function callLLM(metadata, config) {
|
|
505
|
+
const prompt = buildPrompt(metadata);
|
|
506
|
+
if (config.provider === "claude") {
|
|
507
|
+
return callClaude(prompt, config);
|
|
508
|
+
} else {
|
|
509
|
+
return callGemini(prompt, config);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
function buildPrompt(metadata) {
|
|
513
|
+
const fields = Object.entries(metadata.inputSchema.properties).map(([name, prop]) => `- ${prop.title ?? name} (${prop.type}): ${prop.description ?? ""}`).join("\n");
|
|
514
|
+
return `You are helping describe a web form as an AI tool. Given this form information:
|
|
515
|
+
|
|
516
|
+
Name: ${metadata.name}
|
|
517
|
+
Current description: ${metadata.description}
|
|
518
|
+
Fields:
|
|
519
|
+
${fields}
|
|
520
|
+
|
|
521
|
+
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.`;
|
|
522
|
+
}
|
|
523
|
+
async function callClaude(prompt, config) {
|
|
524
|
+
const model = config.model ?? "claude-haiku-4-5-20251001";
|
|
525
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
526
|
+
method: "POST",
|
|
527
|
+
headers: {
|
|
528
|
+
"x-api-key": config.apiKey,
|
|
529
|
+
"anthropic-version": "2023-06-01",
|
|
530
|
+
"content-type": "application/json"
|
|
531
|
+
},
|
|
532
|
+
body: JSON.stringify({
|
|
533
|
+
model,
|
|
534
|
+
max_tokens: 150,
|
|
535
|
+
messages: [{ role: "user", content: prompt }]
|
|
536
|
+
})
|
|
537
|
+
});
|
|
538
|
+
if (!response.ok) {
|
|
539
|
+
throw new Error(`Claude API error: ${response.status}`);
|
|
540
|
+
}
|
|
541
|
+
const data = await response.json();
|
|
542
|
+
return data.content.filter((block) => block.type === "text").map((block) => block.text).join("").trim();
|
|
543
|
+
}
|
|
544
|
+
async function callGemini(prompt, config) {
|
|
545
|
+
const model = config.model ?? "gemini-1.5-flash";
|
|
546
|
+
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${config.apiKey}`;
|
|
547
|
+
const response = await fetch(url, {
|
|
548
|
+
method: "POST",
|
|
549
|
+
headers: { "content-type": "application/json" },
|
|
550
|
+
body: JSON.stringify({
|
|
551
|
+
contents: [{ parts: [{ text: prompt }] }],
|
|
552
|
+
generationConfig: { maxOutputTokens: 150, temperature: 0.2 }
|
|
553
|
+
})
|
|
554
|
+
});
|
|
555
|
+
if (!response.ok) {
|
|
556
|
+
throw new Error(`Gemini API error: ${response.status}`);
|
|
557
|
+
}
|
|
558
|
+
const data = await response.json();
|
|
559
|
+
return data.candidates[0]?.content.parts.map((p) => p.text).join("").trim() ?? "";
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// src/discovery.ts
|
|
563
|
+
function emit(type, form, toolName) {
|
|
564
|
+
window.dispatchEvent(
|
|
565
|
+
new CustomEvent(type, { detail: { form, toolName } })
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
function isExcluded(form, config) {
|
|
569
|
+
if (form.hasAttribute("toolname"))
|
|
570
|
+
return true;
|
|
571
|
+
if (form.dataset["noWebmcp"] !== void 0)
|
|
572
|
+
return true;
|
|
573
|
+
for (const selector of config.exclude) {
|
|
574
|
+
try {
|
|
575
|
+
if (form.matches(selector))
|
|
576
|
+
return true;
|
|
577
|
+
} catch {
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
async function registerForm(form, config) {
|
|
583
|
+
if (isExcluded(form, config))
|
|
584
|
+
return;
|
|
585
|
+
let override;
|
|
586
|
+
for (const [selector, ovr] of Object.entries(config.overrides)) {
|
|
587
|
+
try {
|
|
588
|
+
if (form.matches(selector)) {
|
|
589
|
+
override = ovr;
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
592
|
+
} catch {
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
let metadata = analyzeForm(form, override);
|
|
596
|
+
if (config.enhance) {
|
|
597
|
+
if (config.debug)
|
|
598
|
+
console.debug(`[auto-webmcp] Enriching: ${metadata.name}\u2026`);
|
|
599
|
+
metadata = await enrichMetadata(metadata, config.enhance);
|
|
600
|
+
}
|
|
601
|
+
const execute = buildExecuteHandler(form, config);
|
|
602
|
+
await registerFormTool(form, metadata, execute);
|
|
603
|
+
if (config.debug) {
|
|
604
|
+
console.debug(`[auto-webmcp] Registered: ${metadata.name}`, metadata);
|
|
605
|
+
}
|
|
606
|
+
emit("form:registered", form, metadata.name);
|
|
607
|
+
}
|
|
608
|
+
async function unregisterForm(form, config) {
|
|
609
|
+
const { getRegisteredToolName: getRegisteredToolName2 } = await Promise.resolve().then(() => (init_registry(), registry_exports));
|
|
610
|
+
const name = getRegisteredToolName2(form);
|
|
611
|
+
if (!name)
|
|
612
|
+
return;
|
|
613
|
+
await unregisterFormTool(form);
|
|
614
|
+
if (config.debug) {
|
|
615
|
+
console.debug(`[auto-webmcp] Unregistered: ${name}`);
|
|
616
|
+
}
|
|
617
|
+
emit("form:unregistered", form, name);
|
|
618
|
+
}
|
|
619
|
+
var observer = null;
|
|
620
|
+
function startObserver(config) {
|
|
621
|
+
if (observer)
|
|
622
|
+
return;
|
|
623
|
+
observer = new MutationObserver((mutations) => {
|
|
624
|
+
for (const mutation of mutations) {
|
|
625
|
+
for (const node of mutation.addedNodes) {
|
|
626
|
+
if (!(node instanceof Element))
|
|
627
|
+
continue;
|
|
628
|
+
const forms = node instanceof HTMLFormElement ? [node] : Array.from(node.querySelectorAll("form"));
|
|
629
|
+
for (const form of forms) {
|
|
630
|
+
void registerForm(form, config);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
for (const node of mutation.removedNodes) {
|
|
634
|
+
if (!(node instanceof Element))
|
|
635
|
+
continue;
|
|
636
|
+
const forms = node instanceof HTMLFormElement ? [node] : Array.from(node.querySelectorAll("form"));
|
|
637
|
+
for (const form of forms) {
|
|
638
|
+
void unregisterForm(form, config);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
644
|
+
}
|
|
645
|
+
function listenForRouteChanges(config) {
|
|
646
|
+
window.addEventListener("hashchange", () => scanForms(config));
|
|
647
|
+
const original = {
|
|
648
|
+
pushState: history.pushState.bind(history),
|
|
649
|
+
replaceState: history.replaceState.bind(history)
|
|
650
|
+
};
|
|
651
|
+
history.pushState = function(...args) {
|
|
652
|
+
original.pushState(...args);
|
|
653
|
+
scanForms(config);
|
|
654
|
+
};
|
|
655
|
+
history.replaceState = function(...args) {
|
|
656
|
+
original.replaceState(...args);
|
|
657
|
+
scanForms(config);
|
|
658
|
+
};
|
|
659
|
+
window.addEventListener("popstate", () => scanForms(config));
|
|
660
|
+
}
|
|
661
|
+
async function scanForms(config) {
|
|
662
|
+
const forms = Array.from(document.querySelectorAll("form"));
|
|
663
|
+
await Promise.all(forms.map((form) => registerForm(form, config)));
|
|
664
|
+
}
|
|
665
|
+
async function startDiscovery(config) {
|
|
666
|
+
if (document.readyState === "loading") {
|
|
667
|
+
await new Promise(
|
|
668
|
+
(resolve) => document.addEventListener("DOMContentLoaded", () => resolve(), { once: true })
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
await scanForms(config);
|
|
672
|
+
startObserver(config);
|
|
673
|
+
listenForRouteChanges(config);
|
|
674
|
+
}
|
|
675
|
+
function stopDiscovery() {
|
|
676
|
+
observer?.disconnect();
|
|
677
|
+
observer = null;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// src/index.ts
|
|
681
|
+
init_registry();
|
|
682
|
+
async function autoWebMCP(config) {
|
|
683
|
+
const resolved = resolveConfig(config);
|
|
684
|
+
if (resolved.debug) {
|
|
685
|
+
console.debug("[auto-webmcp] Initializing", {
|
|
686
|
+
webmcpSupported: isWebMCPSupported(),
|
|
687
|
+
config: resolved
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
await startDiscovery(resolved);
|
|
691
|
+
return {
|
|
692
|
+
destroy: async () => {
|
|
693
|
+
stopDiscovery();
|
|
694
|
+
await unregisterAll();
|
|
695
|
+
},
|
|
696
|
+
getTools: getAllRegisteredTools,
|
|
697
|
+
isSupported: isWebMCPSupported()
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
if (typeof window !== "undefined" && !window["__AUTO_WEBMCP_NO_AUTOINIT"]) {
|
|
701
|
+
void autoWebMCP();
|
|
702
|
+
}
|
|
703
|
+
//# sourceMappingURL=auto-webmcp.cjs.js.map
|