@crestapps/webmcp 2.0.0-preview.250 → 2.0.0-preview.257
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/dist/webmcp-page-tools.js +190 -11
- package/dist/webmcp-page-tools.js.map +1 -1
- package/dist/webmcp-page-tools.min.js +2 -1
- package/dist/webmcp-page-tools.min.js.map +1 -1
- package/dist/webmcp.js +45 -4
- package/dist/webmcp.js.map +1 -1
- package/dist/webmcp.min.js +1 -1
- package/dist/webmcp.min.js.map +1 -1
- package/package.json +1 -1
|
@@ -10,9 +10,15 @@
|
|
|
10
10
|
var MAX_LINKS = 200;
|
|
11
11
|
var MAX_FIELDS = 200;
|
|
12
12
|
var MAX_VALUE_LENGTH = 2e3;
|
|
13
|
+
var MAX_CONTENT_LENGTH = 8e3;
|
|
14
|
+
var MAX_TABLES = 10;
|
|
15
|
+
var MAX_TABLE_ROWS = 50;
|
|
16
|
+
var MAX_CELL_LENGTH = 300;
|
|
13
17
|
var selectors = {
|
|
14
18
|
description: "meta[name=\"description\"]",
|
|
15
19
|
headings: "h1, h2",
|
|
20
|
+
main: "main, [role=\"main\"]",
|
|
21
|
+
ignored: "script, style, noscript, template, svg, nav, aside, form, dialog:not([open]), [hidden], [aria-hidden=\"true\"], [role=\"navigation\"], [data-webmcp-ignore], [class*=\"ai-chat-widget\"]",
|
|
16
22
|
links: "nav a[href], header a[href], aside a[href], [role=\"navigation\"] a[href]",
|
|
17
23
|
fields: "form input, form select, form textarea"
|
|
18
24
|
};
|
|
@@ -29,6 +35,112 @@
|
|
|
29
35
|
function cleanText(value) {
|
|
30
36
|
return (value == null ? "" : String(value)).replace(/\s+/g, " ").trim();
|
|
31
37
|
}
|
|
38
|
+
var ABSOLUTE_ADDRESS = /^[A-Za-z][A-Za-z0-9+.-]*:/;
|
|
39
|
+
var registeredTools = /* @__PURE__ */ new WeakMap();
|
|
40
|
+
var BLOCK_TAGS = [
|
|
41
|
+
"ADDRESS",
|
|
42
|
+
"ARTICLE",
|
|
43
|
+
"BLOCKQUOTE",
|
|
44
|
+
"BR",
|
|
45
|
+
"DD",
|
|
46
|
+
"DETAILS",
|
|
47
|
+
"DIV",
|
|
48
|
+
"DL",
|
|
49
|
+
"DT",
|
|
50
|
+
"FIELDSET",
|
|
51
|
+
"FIGCAPTION",
|
|
52
|
+
"FIGURE",
|
|
53
|
+
"FOOTER",
|
|
54
|
+
"H1",
|
|
55
|
+
"H2",
|
|
56
|
+
"H3",
|
|
57
|
+
"H4",
|
|
58
|
+
"H5",
|
|
59
|
+
"H6",
|
|
60
|
+
"HEADER",
|
|
61
|
+
"HR",
|
|
62
|
+
"LI",
|
|
63
|
+
"OL",
|
|
64
|
+
"P",
|
|
65
|
+
"PRE",
|
|
66
|
+
"SECTION",
|
|
67
|
+
"SUMMARY",
|
|
68
|
+
"UL"
|
|
69
|
+
];
|
|
70
|
+
function isIgnored(node) {
|
|
71
|
+
if (node.hidden || typeof node.matches === "function" && node.matches(selectors.ignored)) return true;
|
|
72
|
+
return typeof node.checkVisibility === "function" && !node.checkVisibility();
|
|
73
|
+
}
|
|
74
|
+
function cellText(cell) {
|
|
75
|
+
var text = cleanText(cell.textContent);
|
|
76
|
+
return text.length > MAX_CELL_LENGTH ? text.slice(0, MAX_CELL_LENGTH) + "…" : text;
|
|
77
|
+
}
|
|
78
|
+
function readTable(table) {
|
|
79
|
+
var rows = Array.prototype.slice.call(table.rows || []);
|
|
80
|
+
var headers = [];
|
|
81
|
+
if (rows.length && Array.prototype.every.call(rows[0].cells || [], function(cell) {
|
|
82
|
+
return (cell.tagName || "").toUpperCase() === "TH";
|
|
83
|
+
})) headers = Array.prototype.map.call(rows.shift().cells, cellText);
|
|
84
|
+
return {
|
|
85
|
+
caption: (table.caption ? cleanText(table.caption.textContent) : "") || null,
|
|
86
|
+
headers,
|
|
87
|
+
rows: rows.slice(0, MAX_TABLE_ROWS).map(function(row) {
|
|
88
|
+
return Array.prototype.map.call(row.cells || [], cellText);
|
|
89
|
+
}),
|
|
90
|
+
totalRows: rows.length
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Reads the text and the tables of the page's main region, the way a person reads it: menus, forms, hidden parts
|
|
95
|
+
* and the chat are left out, and each table is read once, as rows, where the text says [table N].
|
|
96
|
+
*/
|
|
97
|
+
function readContent(doc) {
|
|
98
|
+
var root = doc.querySelector(selectors.main) || doc.body;
|
|
99
|
+
var lines = [];
|
|
100
|
+
var line = [];
|
|
101
|
+
var tables = [];
|
|
102
|
+
var tableCount = 0;
|
|
103
|
+
function endLine() {
|
|
104
|
+
var text = cleanText(line.join(" "));
|
|
105
|
+
if (text) lines.push(text);
|
|
106
|
+
line = [];
|
|
107
|
+
}
|
|
108
|
+
function walk(node) {
|
|
109
|
+
if (node.nodeType === 3) {
|
|
110
|
+
line.push(node.nodeValue);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (node.nodeType !== 1 || isIgnored(node)) return;
|
|
114
|
+
var tag = (node.tagName || "").toUpperCase();
|
|
115
|
+
if (tag === "TABLE") {
|
|
116
|
+
endLine();
|
|
117
|
+
tableCount++;
|
|
118
|
+
if (tables.length < MAX_TABLES) {
|
|
119
|
+
tables.push(readTable(node));
|
|
120
|
+
lines.push("[table " + tables.length + "]");
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
var isBlock = BLOCK_TAGS.indexOf(tag) >= 0;
|
|
125
|
+
if (isBlock) endLine();
|
|
126
|
+
var children = node.childNodes || [];
|
|
127
|
+
for (var i = 0; i < children.length; i++) walk(children[i]);
|
|
128
|
+
if (isBlock) endLine();
|
|
129
|
+
}
|
|
130
|
+
if (root) {
|
|
131
|
+
walk(root);
|
|
132
|
+
endLine();
|
|
133
|
+
}
|
|
134
|
+
var text = lines.join("\n");
|
|
135
|
+
var truncated = text.length > MAX_CONTENT_LENGTH || tableCount > tables.length || tables.some(function(table) {
|
|
136
|
+
return table.totalRows > table.rows.length;
|
|
137
|
+
});
|
|
138
|
+
return {
|
|
139
|
+
text: text.length > MAX_CONTENT_LENGTH ? text.slice(0, MAX_CONTENT_LENGTH) + "…" : text,
|
|
140
|
+
tables,
|
|
141
|
+
truncated
|
|
142
|
+
};
|
|
143
|
+
}
|
|
32
144
|
function resolveSameOrigin(href, loc) {
|
|
33
145
|
if (!href) return null;
|
|
34
146
|
try {
|
|
@@ -97,22 +209,45 @@
|
|
|
97
209
|
for (var m = 0; m < matchers.length; m++) for (var i = 0; i < fields.length; i++) if (matchers[m](fields[i])) return fields[i];
|
|
98
210
|
return null;
|
|
99
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* Asks the site whether a page exists, with a HEAD request that downloads nothing. Only a 404 or 410 answer means it
|
|
214
|
+
* does not: a page that refuses HEAD (405), sends to the login page or cannot be reached is opened as asked.
|
|
215
|
+
*
|
|
216
|
+
* @param {Function | undefined} fetchImpl The fetch function, or nothing to skip the check.
|
|
217
|
+
* @param {string} href The address.
|
|
218
|
+
*/
|
|
219
|
+
async function pageIsMissing(fetchImpl, href) {
|
|
220
|
+
if (typeof fetchImpl !== "function") return false;
|
|
221
|
+
try {
|
|
222
|
+
var response = await fetchImpl(href, {
|
|
223
|
+
method: "HEAD",
|
|
224
|
+
credentials: "same-origin",
|
|
225
|
+
redirect: "follow"
|
|
226
|
+
});
|
|
227
|
+
return response.status === 404 || response.status === 410;
|
|
228
|
+
} catch (error) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
100
232
|
function raise(field, type) {
|
|
101
233
|
field.dispatchEvent(new Event(type, { bubbles: true }));
|
|
102
234
|
}
|
|
103
235
|
/**
|
|
104
236
|
* Creates the page tools for a document.
|
|
105
237
|
*
|
|
106
|
-
* @param {{ doc: any, loc: any, isAdmin?: boolean }} options The document,
|
|
238
|
+
* @param {{ doc: any, loc: any, isAdmin?: boolean, fetch?: Function, navigateDelayMs?: number }} options The document,
|
|
239
|
+
* its location, whether it is an admin page, the fetch function page_navigate checks that a page exists with (no
|
|
240
|
+
* check without it), and how long page_navigate waits after answering before it opens the page.
|
|
107
241
|
*/
|
|
108
242
|
function createPageTools(options) {
|
|
109
243
|
var doc = options.doc;
|
|
110
244
|
var loc = options.loc;
|
|
245
|
+
var navigateDelayMs = typeof options.navigateDelayMs === "number" ? options.navigateDelayMs : 400;
|
|
111
246
|
return [
|
|
112
247
|
{
|
|
113
248
|
name: "page_get_info",
|
|
114
249
|
title: "Get page info",
|
|
115
|
-
description: "Describes the page the user has open: its title, address, description and main headings, and whether it is an admin page.",
|
|
250
|
+
description: "Describes the page the user has open: its title, address, description and main headings, and whether it is an admin page. To read what the page shows, use page_read_content.",
|
|
116
251
|
inputSchema: {
|
|
117
252
|
type: "object",
|
|
118
253
|
properties: {}
|
|
@@ -138,6 +273,19 @@
|
|
|
138
273
|
};
|
|
139
274
|
}
|
|
140
275
|
},
|
|
276
|
+
{
|
|
277
|
+
name: "page_read_content",
|
|
278
|
+
title: "Read page content",
|
|
279
|
+
description: "Reads what the page shows in its main area: its text, and its tables as headers and rows (for example the records a list page shows). Menus, forms and hidden parts are left out; use page_list_links and page_list_form_fields for those.",
|
|
280
|
+
inputSchema: {
|
|
281
|
+
type: "object",
|
|
282
|
+
properties: {}
|
|
283
|
+
},
|
|
284
|
+
annotations: { readOnlyHint: true },
|
|
285
|
+
execute: function() {
|
|
286
|
+
return readContent(doc);
|
|
287
|
+
}
|
|
288
|
+
},
|
|
141
289
|
{
|
|
142
290
|
name: "page_list_links",
|
|
143
291
|
title: "List page links",
|
|
@@ -171,19 +319,25 @@
|
|
|
171
319
|
{
|
|
172
320
|
name: "page_navigate",
|
|
173
321
|
title: "Open page",
|
|
174
|
-
description: "Opens another page of this site in the current tab. Only addresses on this site are allowed
|
|
322
|
+
description: "Opens another page of this site in the current tab. Only addresses on this site are allowed; take them from page_list_links, and start a site path with /. Unsaved changes on the current page are lost.",
|
|
175
323
|
inputSchema: {
|
|
176
324
|
type: "object",
|
|
177
325
|
properties: { url: {
|
|
178
326
|
type: "string",
|
|
179
|
-
description: "The address to open
|
|
327
|
+
description: "The address to open: a path that starts with \"/\", or a full address on this site, as page_list_links lists them."
|
|
180
328
|
} },
|
|
181
329
|
required: ["url"]
|
|
182
330
|
},
|
|
183
|
-
execute: function(input) {
|
|
184
|
-
var
|
|
185
|
-
if (!
|
|
186
|
-
|
|
331
|
+
execute: async function(input) {
|
|
332
|
+
var address = cleanText(input && input.url);
|
|
333
|
+
if (!address) throw new Error("Give the address of the page to open. Call page_list_links to find it.");
|
|
334
|
+
if (address.charAt(0) !== "/" && !ABSOLUTE_ADDRESS.test(address)) throw new Error("\"" + address + "\" is not an address on this site. Give a path that starts with \"/\" or a full address on this site, as page_list_links lists them. Call page_list_links to find the page to open.");
|
|
335
|
+
var url = resolveSameOrigin(address, loc);
|
|
336
|
+
if (!url) throw new Error("Only addresses on this site can be opened. Call page_list_links to find the page to open.");
|
|
337
|
+
if (await pageIsMissing(options.fetch, url.href)) throw new Error("The site has no page at \"" + url.pathname + url.search + "\" (404). Do not guess addresses: call page_list_links to find the page to open.");
|
|
338
|
+
setTimeout(function() {
|
|
339
|
+
loc.assign(url.href);
|
|
340
|
+
}, navigateDelayMs);
|
|
187
341
|
return { navigatedTo: url.href };
|
|
188
342
|
}
|
|
189
343
|
},
|
|
@@ -240,26 +394,51 @@
|
|
|
240
394
|
* Registers the page tools with the page's ModelContext.
|
|
241
395
|
*
|
|
242
396
|
* @param {any} modelContext The page's ModelContext (document.modelContext).
|
|
243
|
-
* @param {{ doc: any, loc: any, isAdmin?: boolean }} options The document, its location,
|
|
397
|
+
* @param {{ doc: any, loc: any, isAdmin?: boolean, fetch?: Function }} options The document, its location, whether it
|
|
398
|
+
* is an admin page, and the fetch function page_navigate checks that a page exists with (no check without it).
|
|
244
399
|
* @param {AbortSignal} [signal] Unregisters the tools when aborted.
|
|
245
400
|
*/
|
|
246
401
|
function register(modelContext, options, signal) {
|
|
402
|
+
var tools = registeredTools.get(modelContext);
|
|
403
|
+
if (!tools) {
|
|
404
|
+
tools = /* @__PURE__ */ new Map();
|
|
405
|
+
registeredTools.set(modelContext, tools);
|
|
406
|
+
}
|
|
247
407
|
return Promise.all(createPageTools(options).map(function(tool) {
|
|
248
|
-
return modelContext.registerTool(tool, signal ? { signal } : {}).
|
|
408
|
+
return modelContext.registerTool(tool, signal ? { signal } : {}).then(function() {
|
|
409
|
+
if (signal && signal.aborted) return;
|
|
410
|
+
tools.set(tool.name, tool);
|
|
411
|
+
if (signal) signal.addEventListener("abort", function() {
|
|
412
|
+
if (tools.get(tool.name) === tool) tools.delete(tool.name);
|
|
413
|
+
}, { once: true });
|
|
414
|
+
}, function(error) {
|
|
249
415
|
console.warn("The page tool \"" + tool.name + "\" could not be registered.", error);
|
|
250
416
|
});
|
|
251
417
|
}));
|
|
252
418
|
}
|
|
419
|
+
/**
|
|
420
|
+
* Answers the page tool register() registered with a ModelContext under a name, with its execute callback, or null
|
|
421
|
+
* when it registered none: another script may have registered a tool of that name first.
|
|
422
|
+
*
|
|
423
|
+
* @param {any} modelContext The ModelContext the tool was registered with.
|
|
424
|
+
* @param {string} name The tool's name.
|
|
425
|
+
*/
|
|
426
|
+
function getRegisteredTool(modelContext, name) {
|
|
427
|
+
var tools = modelContext ? registeredTools.get(modelContext) : null;
|
|
428
|
+
return tools && tools.get(name) || null;
|
|
429
|
+
}
|
|
253
430
|
window.CrestAppsWebMcpPageTools = {
|
|
254
431
|
selectors,
|
|
255
432
|
createPageTools,
|
|
256
|
-
register
|
|
433
|
+
register,
|
|
434
|
+
getRegisteredTool
|
|
257
435
|
};
|
|
258
436
|
if (window.document && window.document.modelContext) {
|
|
259
437
|
var config = window.crestAppsWebMcp && window.crestAppsWebMcp.config;
|
|
260
438
|
register(window.document.modelContext, {
|
|
261
439
|
doc: window.document,
|
|
262
440
|
loc: window.location,
|
|
441
|
+
fetch: typeof window.fetch === "function" ? window.fetch.bind(window) : void 0,
|
|
263
442
|
isAdmin: !!(config && config.isAdmin)
|
|
264
443
|
});
|
|
265
444
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webmcp-page-tools.js","sources":["../../Assets/js/webmcp-page-tools.js"],"sourcesContent":["// @ts-check\n/*\n * WebMCP page tools.\n *\n * Tools that work on the page the user sees: what the page is, where its menus lead, opening another page of the same\n * site, and reading and filling its forms. They never submit a form and never leave the site, so the user always\n * reviews and saves the changes an agent makes.\n *\n * The tools are registered with the standard `document.modelContext.registerTool`, so any WebMCP agent can use them:\n * the browser's own assistant, or the CrestApps AI chat. As WebMCP defines, each answers a JSON-serializable value and\n * reports a failure by rejecting. Load webmcp-polyfill.js first in browsers without native WebMCP.\n */\n\n(function () {\n 'use strict';\n\n var MAX_HEADINGS = 20;\n var MAX_LINKS = 200;\n var MAX_FIELDS = 200;\n var MAX_VALUE_LENGTH = 2000;\n\n // The selectors the tools read the page with.\n var selectors = {\n description: 'meta[name=\"description\"]',\n headings: 'h1, h2',\n links: 'nav a[href], header a[href], aside a[href], [role=\"navigation\"] a[href]',\n fields: 'form input, form select, form textarea',\n };\n\n // Field types an agent never reads or fills.\n var SKIPPED_TYPES = ['hidden', 'password', 'file', 'submit', 'button', 'reset', 'image'];\n var SKIPPED_NAMES = ['__RequestVerificationToken'];\n\n function cleanText(value) {\n return (value == null ? '' : String(value)).replace(/\\s+/g, ' ').trim();\n }\n\n\n function resolveSameOrigin(href, loc) {\n if (!href) {\n return null;\n }\n\n try {\n var url = new URL(href, loc.href);\n\n return url.origin === loc.origin ? url : null;\n } catch (error) {\n return null;\n }\n }\n\n function fieldType(field) {\n var tag = (field.tagName || '').toLowerCase();\n\n if (tag === 'select' || tag === 'textarea') {\n return tag;\n }\n\n return (field.type || 'text').toLowerCase();\n }\n\n function fieldLabel(field) {\n if (field.labels && field.labels.length > 0) {\n var label = cleanText(field.labels[0].textContent);\n\n if (label) {\n return label;\n }\n }\n\n return cleanText(field.getAttribute('aria-label')) || cleanText(field.getAttribute('placeholder'));\n }\n\n function isListed(field) {\n return !field.disabled &&\n SKIPPED_TYPES.indexOf(fieldType(field)) < 0 &&\n SKIPPED_NAMES.indexOf(field.name) < 0;\n }\n\n function listFields(doc) {\n var fields = [];\n var all = doc.querySelectorAll(selectors.fields);\n\n for (var i = 0; i < all.length && fields.length < MAX_FIELDS; i++) {\n if (isListed(all[i])) {\n fields.push(all[i]);\n }\n }\n\n return fields;\n }\n\n function describeField(field) {\n var type = fieldType(field);\n var description = {\n key: field.name || field.id,\n name: field.name,\n id: field.id,\n label: fieldLabel(field),\n type: type,\n value: type === 'checkbox' || type === 'radio'\n ? !!field.checked\n : String(field.value == null ? '' : field.value).substring(0, MAX_VALUE_LENGTH),\n required: !!field.required,\n };\n\n if (type === 'select') {\n description.options = Array.prototype.map.call(field.options || [], function (option) {\n return { value: option.value, text: cleanText(option.text), selected: !!option.selected };\n });\n }\n\n return description;\n }\n\n function findField(doc, key) {\n var wanted = cleanText(key).toLowerCase();\n\n if (!wanted) {\n return null;\n }\n\n var fields = listFields(doc);\n var matchers = [\n function (field) {\n return (field.name || '').toLowerCase() === wanted;\n },\n function (field) {\n return (field.id || '').toLowerCase() === wanted;\n },\n function (field) {\n return fieldLabel(field).toLowerCase() === wanted;\n },\n ];\n\n for (var m = 0; m < matchers.length; m++) {\n for (var i = 0; i < fields.length; i++) {\n if (matchers[m](fields[i])) {\n return fields[i];\n }\n }\n }\n\n return null;\n }\n\n function raise(field, type) {\n field.dispatchEvent(new Event(type, { bubbles: true }));\n }\n\n /**\n * Creates the page tools for a document.\n *\n * @param {{ doc: any, loc: any, isAdmin?: boolean }} options The document, its location, and whether it is an admin page.\n */\n function createPageTools(options) {\n var doc = options.doc;\n var loc = options.loc;\n\n return [\n {\n name: 'page_get_info',\n title: 'Get page info',\n description: 'Describes the page the user has open: its title, address, description and main headings, and whether it is an admin page.',\n inputSchema: { type: 'object', properties: {} },\n annotations: { readOnlyHint: true },\n execute: function () {\n var meta = doc.querySelector(selectors.description);\n var headings = [];\n var all = doc.querySelectorAll(selectors.headings);\n\n for (var i = 0; i < all.length && headings.length < MAX_HEADINGS; i++) {\n var text = cleanText(all[i].textContent);\n\n if (text) {\n headings.push({ level: (all[i].tagName || '').toUpperCase() === 'H1' ? 1 : 2, text: text });\n }\n }\n\n return ({\n title: doc.title,\n url: loc.href,\n description: meta ? meta.getAttribute('content') : null,\n headings: headings,\n isAdmin: !!options.isAdmin,\n });\n },\n },\n {\n name: 'page_list_links',\n title: 'List page links',\n description: 'Lists the links in the page\\'s menus and navigation that stay on this site, with their text. Use it to find the page to open for a task.',\n inputSchema: {\n type: 'object',\n properties: {\n filter: { type: 'string', description: 'Only list links whose text contains these words.' },\n },\n },\n annotations: { readOnlyHint: true },\n execute: function (input) {\n var filter = cleanText(input && input.filter).toLowerCase();\n var seen = new Set();\n var links = [];\n var all = doc.querySelectorAll(selectors.links);\n\n for (var i = 0; i < all.length && links.length < MAX_LINKS; i++) {\n var url = resolveSameOrigin(all[i].getAttribute('href'), loc);\n var text = cleanText(all[i].textContent);\n\n if (!url || !text || seen.has(url.href) || (filter && text.toLowerCase().indexOf(filter) < 0)) {\n continue;\n }\n\n seen.add(url.href);\n links.push({ text: text, url: url.href });\n }\n\n return ({ links: links });\n },\n },\n {\n name: 'page_navigate',\n title: 'Open page',\n description: 'Opens another page of this site in the current tab. Only addresses on this site are allowed. Unsaved changes on the current page are lost.',\n inputSchema: {\n type: 'object',\n properties: {\n url: { type: 'string', description: 'The address to open, relative to the site or absolute on the same site.' },\n },\n required: ['url'],\n },\n execute: function (input) {\n var url = resolveSameOrigin(input && input.url, loc);\n\n if (!url) {\n throw new Error('Only addresses on this site can be opened.');\n }\n\n loc.assign(url.href);\n\n return ({ navigatedTo: url.href });\n },\n },\n {\n name: 'page_list_form_fields',\n title: 'List form fields',\n description: 'Lists the fields of the forms on the page: name, label, type, current value, whether it is required and, for lists, the choices. Hidden, password and file fields are not listed.',\n inputSchema: { type: 'object', properties: {} },\n annotations: { readOnlyHint: true },\n execute: function () {\n return ({ fields: listFields(doc).map(describeField) });\n },\n },\n {\n name: 'page_set_form_field',\n title: 'Set form field',\n description: 'Puts a value into one form field on the page, found by its name, id or label. It does not save or submit the form: the user reviews the change and saves it.',\n inputSchema: {\n type: 'object',\n properties: {\n field: { type: 'string', description: 'The field\\'s name, id or label, as page_list_form_fields lists it.' },\n value: { description: 'The value. For a check box, true or false. For a list, a choice\\'s value or text.' },\n },\n required: ['field', 'value'],\n },\n execute: function (input) {\n var field = findField(doc, input && input.field);\n\n if (!field) {\n throw new Error('No editable field on the page is named \"' + cleanText(input && input.field) + '\".');\n }\n\n var type = fieldType(field);\n var value = input.value;\n\n if (type === 'checkbox' || type === 'radio') {\n field.checked = value === true || String(value).toLowerCase() === 'true';\n } else if (type === 'select') {\n var wanted = cleanText(value).toLowerCase();\n var option = Array.prototype.find.call(field.options || [], function (candidate) {\n return String(candidate.value).toLowerCase() === wanted || cleanText(candidate.text).toLowerCase() === wanted;\n });\n\n if (!option) {\n throw new Error('The list \"' + fieldLabel(field) + '\" has no choice \"' + cleanText(value) + '\".');\n }\n\n field.value = option.value;\n } else {\n field.value = value == null ? '' : String(value);\n }\n\n raise(field, 'input');\n raise(field, 'change');\n\n return ({ field: describeField(field) });\n },\n },\n ];\n }\n\n /**\n * Registers the page tools with the page's ModelContext.\n *\n * @param {any} modelContext The page's ModelContext (document.modelContext).\n * @param {{ doc: any, loc: any, isAdmin?: boolean }} options The document, its location, and whether it is an admin page.\n * @param {AbortSignal} [signal] Unregisters the tools when aborted.\n */\n function register(modelContext, options, signal) {\n return Promise.all(createPageTools(options).map(function (tool) {\n return modelContext.registerTool(tool, signal ? { signal: signal } : {}).catch(function (error) {\n console.warn('The page tool \"' + tool.name + '\" could not be registered.', error);\n });\n }));\n }\n\n window.CrestAppsWebMcpPageTools = {\n selectors: selectors,\n createPageTools: createPageTools,\n register: register,\n };\n\n if (window.document && window.document.modelContext) {\n var config = window.crestAppsWebMcp && window.crestAppsWebMcp.config;\n\n register(window.document.modelContext, {\n doc: window.document,\n loc: window.location,\n isAdmin: !!(config && config.isAdmin),\n });\n }\n})();\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"webmcp-page-tools.js","sources":["../../Assets/js/webmcp-page-tools.js"],"sourcesContent":["// @ts-check\n/*\n * WebMCP page tools.\n *\n * Tools that work on the page the user sees: what the page is, what it shows, where its menus lead, opening another\n * page of the same site, and reading and filling its forms. They never submit a form and never leave the site, so the user always\n * reviews and saves the changes an agent makes.\n *\n * The tools are registered with the standard `document.modelContext.registerTool`, so any WebMCP agent can use them:\n * the browser's own assistant, or the CrestApps AI chat. As WebMCP defines, each answers a JSON-serializable value and\n * reports a failure by rejecting. Load webmcp-polyfill.js first in browsers without native WebMCP.\n */\n\n(function () {\n 'use strict';\n\n var MAX_HEADINGS = 20;\n var MAX_LINKS = 200;\n var MAX_FIELDS = 200;\n var MAX_VALUE_LENGTH = 2000;\n var MAX_CONTENT_LENGTH = 8000;\n var MAX_TABLES = 10;\n var MAX_TABLE_ROWS = 50;\n var MAX_CELL_LENGTH = 300;\n\n // The selectors the tools read the page with.\n var selectors = {\n description: 'meta[name=\"description\"]',\n headings: 'h1, h2',\n main: 'main, [role=\"main\"]',\n // What page_read_content leaves out of the page's text: scripts, menus, forms (page_list_form_fields reads them),\n // hidden parts, the chat itself, and anything a page marks with data-webmcp-ignore.\n ignored: 'script, style, noscript, template, svg, nav, aside, form, dialog:not([open]), [hidden], [aria-hidden=\"true\"], [role=\"navigation\"], [data-webmcp-ignore], [class*=\"ai-chat-widget\"]',\n links: 'nav a[href], header a[href], aside a[href], [role=\"navigation\"] a[href]',\n fields: 'form input, form select, form textarea',\n };\n\n // Field types an agent never reads or fills.\n var SKIPPED_TYPES = ['hidden', 'password', 'file', 'submit', 'button', 'reset', 'image'];\n var SKIPPED_NAMES = ['__RequestVerificationToken'];\n\n function cleanText(value) {\n return (value == null ? '' : String(value)).replace(/\\s+/g, ' ').trim();\n }\n\n\n // An address that starts with a scheme, such as \"https:\" or \"javascript:\".\n var ABSOLUTE_ADDRESS = /^[A-Za-z][A-Za-z0-9+.-]*:/;\n\n // The tools register() registered, by ModelContext and name, so the CrestApps chat can run them through their own\n // execute and tell the model why one failed (executeTool reports every failure as \"Tool execution failed\").\n var registeredTools = new WeakMap();\n\n // Elements that start a new line in the text page_read_content reads.\n var BLOCK_TAGS = ['ADDRESS', 'ARTICLE', 'BLOCKQUOTE', 'BR', 'DD', 'DETAILS', 'DIV', 'DL', 'DT', 'FIELDSET', 'FIGCAPTION', 'FIGURE', 'FOOTER', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'HEADER', 'HR', 'LI', 'OL', 'P', 'PRE', 'SECTION', 'SUMMARY', 'UL'];\n\n function isIgnored(node) {\n if (node.hidden || (typeof node.matches === 'function' && node.matches(selectors.ignored))) {\n return true;\n }\n\n // Parts the page hides with CSS, such as closed menus and modals.\n return typeof node.checkVisibility === 'function' && !node.checkVisibility();\n }\n\n function cellText(cell) {\n var text = cleanText(cell.textContent);\n\n return text.length > MAX_CELL_LENGTH ? text.slice(0, MAX_CELL_LENGTH) + '…' : text;\n }\n\n function readTable(table) {\n var rows = Array.prototype.slice.call(table.rows || []);\n var headers = [];\n\n if (rows.length && Array.prototype.every.call(rows[0].cells || [], function (cell) { return (cell.tagName || '').toUpperCase() === 'TH'; })) {\n headers = Array.prototype.map.call(rows.shift().cells, cellText);\n }\n\n var caption = table.caption ? cleanText(table.caption.textContent) : '';\n\n return {\n caption: caption || null,\n headers: headers,\n rows: rows.slice(0, MAX_TABLE_ROWS).map(function (row) {\n return Array.prototype.map.call(row.cells || [], cellText);\n }),\n totalRows: rows.length,\n };\n }\n\n /**\n * Reads the text and the tables of the page's main region, the way a person reads it: menus, forms, hidden parts\n * and the chat are left out, and each table is read once, as rows, where the text says [table N].\n */\n function readContent(doc) {\n var root = doc.querySelector(selectors.main) || doc.body;\n var lines = [];\n var line = [];\n var tables = [];\n var tableCount = 0;\n\n function endLine() {\n var text = cleanText(line.join(' '));\n\n if (text) {\n lines.push(text);\n }\n\n line = [];\n }\n\n function walk(node) {\n if (node.nodeType === 3) {\n line.push(node.nodeValue);\n\n return;\n }\n\n if (node.nodeType !== 1 || isIgnored(node)) {\n return;\n }\n\n var tag = (node.tagName || '').toUpperCase();\n\n if (tag === 'TABLE') {\n endLine();\n tableCount++;\n\n if (tables.length < MAX_TABLES) {\n tables.push(readTable(node));\n lines.push('[table ' + tables.length + ']');\n }\n\n return;\n }\n\n var isBlock = BLOCK_TAGS.indexOf(tag) >= 0;\n\n if (isBlock) {\n endLine();\n }\n\n var children = node.childNodes || [];\n\n for (var i = 0; i < children.length; i++) {\n walk(children[i]);\n }\n\n if (isBlock) {\n endLine();\n }\n }\n\n if (root) {\n walk(root);\n endLine();\n }\n\n var text = lines.join('\\n');\n var truncated = text.length > MAX_CONTENT_LENGTH || tableCount > tables.length ||\n tables.some(function (table) { return table.totalRows > table.rows.length; });\n\n return {\n text: text.length > MAX_CONTENT_LENGTH ? text.slice(0, MAX_CONTENT_LENGTH) + '…' : text,\n tables: tables,\n truncated: truncated,\n };\n }\n\n function resolveSameOrigin(href, loc) {\n if (!href) {\n return null;\n }\n\n try {\n var url = new URL(href, loc.href);\n\n return url.origin === loc.origin ? url : null;\n } catch (error) {\n return null;\n }\n }\n\n function fieldType(field) {\n var tag = (field.tagName || '').toLowerCase();\n\n if (tag === 'select' || tag === 'textarea') {\n return tag;\n }\n\n return (field.type || 'text').toLowerCase();\n }\n\n function fieldLabel(field) {\n if (field.labels && field.labels.length > 0) {\n var label = cleanText(field.labels[0].textContent);\n\n if (label) {\n return label;\n }\n }\n\n return cleanText(field.getAttribute('aria-label')) || cleanText(field.getAttribute('placeholder'));\n }\n\n function isListed(field) {\n return !field.disabled &&\n SKIPPED_TYPES.indexOf(fieldType(field)) < 0 &&\n SKIPPED_NAMES.indexOf(field.name) < 0;\n }\n\n function listFields(doc) {\n var fields = [];\n var all = doc.querySelectorAll(selectors.fields);\n\n for (var i = 0; i < all.length && fields.length < MAX_FIELDS; i++) {\n if (isListed(all[i])) {\n fields.push(all[i]);\n }\n }\n\n return fields;\n }\n\n function describeField(field) {\n var type = fieldType(field);\n var description = {\n key: field.name || field.id,\n name: field.name,\n id: field.id,\n label: fieldLabel(field),\n type: type,\n value: type === 'checkbox' || type === 'radio'\n ? !!field.checked\n : String(field.value == null ? '' : field.value).substring(0, MAX_VALUE_LENGTH),\n required: !!field.required,\n };\n\n if (type === 'select') {\n description.options = Array.prototype.map.call(field.options || [], function (option) {\n return { value: option.value, text: cleanText(option.text), selected: !!option.selected };\n });\n }\n\n return description;\n }\n\n function findField(doc, key) {\n var wanted = cleanText(key).toLowerCase();\n\n if (!wanted) {\n return null;\n }\n\n var fields = listFields(doc);\n var matchers = [\n function (field) {\n return (field.name || '').toLowerCase() === wanted;\n },\n function (field) {\n return (field.id || '').toLowerCase() === wanted;\n },\n function (field) {\n return fieldLabel(field).toLowerCase() === wanted;\n },\n ];\n\n for (var m = 0; m < matchers.length; m++) {\n for (var i = 0; i < fields.length; i++) {\n if (matchers[m](fields[i])) {\n return fields[i];\n }\n }\n }\n\n return null;\n }\n\n /**\n * Asks the site whether a page exists, with a HEAD request that downloads nothing. Only a 404 or 410 answer means it\n * does not: a page that refuses HEAD (405), sends to the login page or cannot be reached is opened as asked.\n *\n * @param {Function | undefined} fetchImpl The fetch function, or nothing to skip the check.\n * @param {string} href The address.\n */\n async function pageIsMissing(fetchImpl, href) {\n if (typeof fetchImpl !== 'function') {\n return false;\n }\n\n try {\n var response = await fetchImpl(href, { method: 'HEAD', credentials: 'same-origin', redirect: 'follow' });\n\n return response.status === 404 || response.status === 410;\n } catch (error) {\n return false;\n }\n }\n\n function raise(field, type) {\n field.dispatchEvent(new Event(type, { bubbles: true }));\n }\n\n /**\n * Creates the page tools for a document.\n *\n * @param {{ doc: any, loc: any, isAdmin?: boolean, fetch?: Function, navigateDelayMs?: number }} options The document,\n * its location, whether it is an admin page, the fetch function page_navigate checks that a page exists with (no\n * check without it), and how long page_navigate waits after answering before it opens the page.\n */\n function createPageTools(options) {\n var doc = options.doc;\n var loc = options.loc;\n var navigateDelayMs = typeof options.navigateDelayMs === 'number' ? options.navigateDelayMs : 400;\n\n return [\n {\n name: 'page_get_info',\n title: 'Get page info',\n description: 'Describes the page the user has open: its title, address, description and main headings, and whether it is an admin page. To read what the page shows, use page_read_content.',\n inputSchema: { type: 'object', properties: {} },\n annotations: { readOnlyHint: true },\n execute: function () {\n var meta = doc.querySelector(selectors.description);\n var headings = [];\n var all = doc.querySelectorAll(selectors.headings);\n\n for (var i = 0; i < all.length && headings.length < MAX_HEADINGS; i++) {\n var text = cleanText(all[i].textContent);\n\n if (text) {\n headings.push({ level: (all[i].tagName || '').toUpperCase() === 'H1' ? 1 : 2, text: text });\n }\n }\n\n return ({\n title: doc.title,\n url: loc.href,\n description: meta ? meta.getAttribute('content') : null,\n headings: headings,\n isAdmin: !!options.isAdmin,\n });\n },\n },\n {\n name: 'page_read_content',\n title: 'Read page content',\n description: 'Reads what the page shows in its main area: its text, and its tables as headers and rows (for example the records a list page shows). Menus, forms and hidden parts are left out; use page_list_links and page_list_form_fields for those.',\n inputSchema: { type: 'object', properties: {} },\n annotations: { readOnlyHint: true },\n execute: function () {\n return readContent(doc);\n },\n },\n {\n name: 'page_list_links',\n title: 'List page links',\n description: 'Lists the links in the page\\'s menus and navigation that stay on this site, with their text. Use it to find the page to open for a task.',\n inputSchema: {\n type: 'object',\n properties: {\n filter: { type: 'string', description: 'Only list links whose text contains these words.' },\n },\n },\n annotations: { readOnlyHint: true },\n execute: function (input) {\n var filter = cleanText(input && input.filter).toLowerCase();\n var seen = new Set();\n var links = [];\n var all = doc.querySelectorAll(selectors.links);\n\n for (var i = 0; i < all.length && links.length < MAX_LINKS; i++) {\n var url = resolveSameOrigin(all[i].getAttribute('href'), loc);\n var text = cleanText(all[i].textContent);\n\n if (!url || !text || seen.has(url.href) || (filter && text.toLowerCase().indexOf(filter) < 0)) {\n continue;\n }\n\n seen.add(url.href);\n links.push({ text: text, url: url.href });\n }\n\n return ({ links: links });\n },\n },\n {\n name: 'page_navigate',\n title: 'Open page',\n description: 'Opens another page of this site in the current tab. Only addresses on this site are allowed; take them from page_list_links, and start a site path with /. Unsaved changes on the current page are lost.',\n inputSchema: {\n type: 'object',\n properties: {\n url: { type: 'string', description: 'The address to open: a path that starts with \"/\", or a full address on this site, as page_list_links lists them.' },\n },\n required: ['url'],\n },\n execute: async function (input) {\n var address = cleanText(input && input.url);\n\n if (!address) {\n throw new Error('Give the address of the page to open. Call page_list_links to find it.');\n }\n\n // A bare relative path, such as \"ArticlesAdmin\", resolves against the current page and usually names a page\n // that does not exist: it is a guess, not an address the site gave.\n if (address.charAt(0) !== '/' && !ABSOLUTE_ADDRESS.test(address)) {\n throw new Error('\"' + address + '\" is not an address on this site. Give a path that starts with \"/\" or a full address on this site, as page_list_links lists them. Call page_list_links to find the page to open.');\n }\n\n var url = resolveSameOrigin(address, loc);\n\n if (!url) {\n throw new Error('Only addresses on this site can be opened. Call page_list_links to find the page to open.');\n }\n\n if (await pageIsMissing(options.fetch, url.href)) {\n throw new Error('The site has no page at \"' + url.pathname + url.search + '\" (404). Do not guess addresses: call page_list_links to find the page to open.');\n }\n\n // Answer first and open the page a moment later, so the agent gets the result before this page (and\n // the chat connection on it) goes away. The chat finishes the reply on the server and shows it on the\n // next page.\n setTimeout(function () {\n loc.assign(url.href);\n }, navigateDelayMs);\n\n return ({ navigatedTo: url.href });\n },\n },\n {\n name: 'page_list_form_fields',\n title: 'List form fields',\n description: 'Lists the fields of the forms on the page: name, label, type, current value, whether it is required and, for lists, the choices. Hidden, password and file fields are not listed.',\n inputSchema: { type: 'object', properties: {} },\n annotations: { readOnlyHint: true },\n execute: function () {\n return ({ fields: listFields(doc).map(describeField) });\n },\n },\n {\n name: 'page_set_form_field',\n title: 'Set form field',\n description: 'Puts a value into one form field on the page, found by its name, id or label. It does not save or submit the form: the user reviews the change and saves it.',\n inputSchema: {\n type: 'object',\n properties: {\n field: { type: 'string', description: 'The field\\'s name, id or label, as page_list_form_fields lists it.' },\n value: { description: 'The value. For a check box, true or false. For a list, a choice\\'s value or text.' },\n },\n required: ['field', 'value'],\n },\n execute: function (input) {\n var field = findField(doc, input && input.field);\n\n if (!field) {\n throw new Error('No editable field on the page is named \"' + cleanText(input && input.field) + '\".');\n }\n\n var type = fieldType(field);\n var value = input.value;\n\n if (type === 'checkbox' || type === 'radio') {\n field.checked = value === true || String(value).toLowerCase() === 'true';\n } else if (type === 'select') {\n var wanted = cleanText(value).toLowerCase();\n var option = Array.prototype.find.call(field.options || [], function (candidate) {\n return String(candidate.value).toLowerCase() === wanted || cleanText(candidate.text).toLowerCase() === wanted;\n });\n\n if (!option) {\n throw new Error('The list \"' + fieldLabel(field) + '\" has no choice \"' + cleanText(value) + '\".');\n }\n\n field.value = option.value;\n } else {\n field.value = value == null ? '' : String(value);\n }\n\n raise(field, 'input');\n raise(field, 'change');\n\n return ({ field: describeField(field) });\n },\n },\n ];\n }\n\n /**\n * Registers the page tools with the page's ModelContext.\n *\n * @param {any} modelContext The page's ModelContext (document.modelContext).\n * @param {{ doc: any, loc: any, isAdmin?: boolean, fetch?: Function }} options The document, its location, whether it\n * is an admin page, and the fetch function page_navigate checks that a page exists with (no check without it).\n * @param {AbortSignal} [signal] Unregisters the tools when aborted.\n */\n function register(modelContext, options, signal) {\n var tools = registeredTools.get(modelContext);\n\n if (!tools) {\n tools = new Map();\n registeredTools.set(modelContext, tools);\n }\n\n return Promise.all(createPageTools(options).map(function (tool) {\n return modelContext.registerTool(tool, signal ? { signal: signal } : {}).then(function () {\n if (signal && signal.aborted) {\n return;\n }\n\n tools.set(tool.name, tool);\n\n if (signal) {\n signal.addEventListener('abort', function () {\n if (tools.get(tool.name) === tool) {\n tools.delete(tool.name);\n }\n }, { once: true });\n }\n }, function (error) {\n console.warn('The page tool \"' + tool.name + '\" could not be registered.', error);\n });\n }));\n }\n\n /**\n * Answers the page tool register() registered with a ModelContext under a name, with its execute callback, or null\n * when it registered none: another script may have registered a tool of that name first.\n *\n * @param {any} modelContext The ModelContext the tool was registered with.\n * @param {string} name The tool's name.\n */\n function getRegisteredTool(modelContext, name) {\n var tools = modelContext ? registeredTools.get(modelContext) : null;\n\n return (tools && tools.get(name)) || null;\n }\n\n window.CrestAppsWebMcpPageTools = {\n selectors: selectors,\n createPageTools: createPageTools,\n register: register,\n getRegisteredTool: getRegisteredTool,\n };\n\n if (window.document && window.document.modelContext) {\n var config = window.crestAppsWebMcp && window.crestAppsWebMcp.config;\n\n register(window.document.modelContext, {\n doc: window.document,\n loc: window.location,\n fetch: typeof window.fetch === 'function' ? window.fetch.bind(window) : undefined,\n isAdmin: !!(config && config.isAdmin),\n });\n }\n})();\n"],"names":[],"mappings":";;;;;;CAaA,CAAC,WAAY;EACX;EAEA,IAAI,eAAe;EACnB,IAAI,YAAY;EAChB,IAAI,aAAa;EACjB,IAAI,mBAAmB;EACvB,IAAI,qBAAqB;EACzB,IAAI,aAAa;EACjB,IAAI,iBAAiB;EACrB,IAAI,kBAAkB;EAGtB,IAAI,YAAY;GACd,aAAa;GACb,UAAU;GACV,MAAM;GAGN,SAAS;GACT,OAAO;GACP,QAAQ;EACV;EAGA,IAAI,gBAAgB;GAAC;GAAU;GAAY;GAAQ;GAAU;GAAU;GAAS;EAAO;EACvF,IAAI,gBAAgB,CAAC,4BAA4B;EAEjD,SAAS,UAAU,OAAO;GACxB,QAAQ,SAAS,OAAO,KAAK,OAAO,KAAK,EAAA,CAAG,QAAQ,QAAQ,GAAG,CAAC,CAAC,KAAK;EACxE;EAIA,IAAI,mBAAmB;EAIvB,IAAI,kCAAkB,IAAI,QAAQ;EAGlC,IAAI,aAAa;GAAC;GAAW;GAAW;GAAc;GAAM;GAAM;GAAW;GAAO;GAAM;GAAM;GAAY;GAAc;GAAU;GAAU;GAAM;GAAM;GAAM;GAAM;GAAM;GAAM;GAAU;GAAM;GAAM;GAAM;GAAK;GAAO;GAAW;GAAW;EAAI;EAEpP,SAAS,UAAU,MAAM;GACvB,IAAI,KAAK,UAAW,OAAO,KAAK,YAAY,cAAc,KAAK,QAAQ,UAAU,OAAO,GACtF,OAAO;GAIT,OAAO,OAAO,KAAK,oBAAoB,cAAc,CAAC,KAAK,gBAAgB;EAC7E;EAEA,SAAS,SAAS,MAAM;GACtB,IAAI,OAAO,UAAU,KAAK,WAAW;GAErC,OAAO,KAAK,SAAS,kBAAkB,KAAK,MAAM,GAAG,eAAe,IAAI,MAAM;EAChF;EAEA,SAAS,UAAU,OAAO;GACxB,IAAI,OAAO,MAAM,UAAU,MAAM,KAAK,MAAM,QAAQ,CAAC,CAAC;GACtD,IAAI,UAAU,CAAC;GAEf,IAAI,KAAK,UAAU,MAAM,UAAU,MAAM,KAAK,KAAK,EAAE,CAAC,SAAS,CAAC,GAAG,SAAU,MAAM;IAAE,QAAQ,KAAK,WAAW,GAAA,CAAI,YAAY,MAAM;GAAM,CAAC,GACxI,UAAU,MAAM,UAAU,IAAI,KAAK,KAAK,MAAM,CAAC,CAAC,OAAO,QAAQ;GAKjE,OAAO;IACL,UAHY,MAAM,UAAU,UAAU,MAAM,QAAQ,WAAW,IAAI,OAG/C;IACX;IACT,MAAM,KAAK,MAAM,GAAG,cAAc,CAAC,CAAC,IAAI,SAAU,KAAK;KACrD,OAAO,MAAM,UAAU,IAAI,KAAK,IAAI,SAAS,CAAC,GAAG,QAAQ;IAC3D,CAAC;IACD,WAAW,KAAK;GAClB;EACF;;;;;EAMA,SAAS,YAAY,KAAK;GACxB,IAAI,OAAO,IAAI,cAAc,UAAU,IAAI,KAAK,IAAI;GACpD,IAAI,QAAQ,CAAC;GACb,IAAI,OAAO,CAAC;GACZ,IAAI,SAAS,CAAC;GACd,IAAI,aAAa;GAEjB,SAAS,UAAU;IACjB,IAAI,OAAO,UAAU,KAAK,KAAK,GAAG,CAAC;IAEnC,IAAI,MACF,MAAM,KAAK,IAAI;IAGjB,OAAO,CAAC;GACV;GAEA,SAAS,KAAK,MAAM;IAClB,IAAI,KAAK,aAAa,GAAG;KACvB,KAAK,KAAK,KAAK,SAAS;KAExB;IACF;IAEA,IAAI,KAAK,aAAa,KAAK,UAAU,IAAI,GACvC;IAGF,IAAI,OAAO,KAAK,WAAW,GAAA,CAAI,YAAY;IAE3C,IAAI,QAAQ,SAAS;KACnB,QAAQ;KACR;KAEA,IAAI,OAAO,SAAS,YAAY;MAC9B,OAAO,KAAK,UAAU,IAAI,CAAC;MAC3B,MAAM,KAAK,YAAY,OAAO,SAAS,GAAG;KAC5C;KAEA;IACF;IAEA,IAAI,UAAU,WAAW,QAAQ,GAAG,KAAK;IAEzC,IAAI,SACF,QAAQ;IAGV,IAAI,WAAW,KAAK,cAAc,CAAC;IAEnC,KAAK,IAAI,IAAI,GAAG,IAAI,SAAS,QAAQ,KACnC,KAAK,SAAS,EAAE;IAGlB,IAAI,SACF,QAAQ;GAEZ;GAEA,IAAI,MAAM;IACR,KAAK,IAAI;IACT,QAAQ;GACV;GAEA,IAAI,OAAO,MAAM,KAAK,IAAI;GAC1B,IAAI,YAAY,KAAK,SAAS,sBAAsB,aAAa,OAAO,UACtE,OAAO,KAAK,SAAU,OAAO;IAAE,OAAO,MAAM,YAAY,MAAM,KAAK;GAAQ,CAAC;GAE9E,OAAO;IACL,MAAM,KAAK,SAAS,qBAAqB,KAAK,MAAM,GAAG,kBAAkB,IAAI,MAAM;IAC3E;IACG;GACb;EACF;EAEA,SAAS,kBAAkB,MAAM,KAAK;GACpC,IAAI,CAAC,MACH,OAAO;GAGT,IAAI;IACF,IAAI,MAAM,IAAI,IAAI,MAAM,IAAI,IAAI;IAEhC,OAAO,IAAI,WAAW,IAAI,SAAS,MAAM;GAC3C,SAAS,OAAO;IACd,OAAO;GACT;EACF;EAEA,SAAS,UAAU,OAAO;GACxB,IAAI,OAAO,MAAM,WAAW,GAAA,CAAI,YAAY;GAE5C,IAAI,QAAQ,YAAY,QAAQ,YAC9B,OAAO;GAGT,QAAQ,MAAM,QAAQ,OAAA,CAAQ,YAAY;EAC5C;EAEA,SAAS,WAAW,OAAO;GACzB,IAAI,MAAM,UAAU,MAAM,OAAO,SAAS,GAAG;IAC3C,IAAI,QAAQ,UAAU,MAAM,OAAO,EAAE,CAAC,WAAW;IAEjD,IAAI,OACF,OAAO;GAEX;GAEA,OAAO,UAAU,MAAM,aAAa,YAAY,CAAC,KAAK,UAAU,MAAM,aAAa,aAAa,CAAC;EACnG;EAEA,SAAS,SAAS,OAAO;GACvB,OAAO,CAAC,MAAM,YACZ,cAAc,QAAQ,UAAU,KAAK,CAAC,IAAI,KAC1C,cAAc,QAAQ,MAAM,IAAI,IAAI;EACxC;EAEA,SAAS,WAAW,KAAK;GACvB,IAAI,SAAS,CAAC;GACd,IAAI,MAAM,IAAI,iBAAiB,UAAU,MAAM;GAE/C,KAAK,IAAI,IAAI,GAAG,IAAI,IAAI,UAAU,OAAO,SAAS,YAAY,KAC5D,IAAI,SAAS,IAAI,EAAE,GACjB,OAAO,KAAK,IAAI,EAAE;GAItB,OAAO;EACT;EAEA,SAAS,cAAc,OAAO;GAC5B,IAAI,OAAO,UAAU,KAAK;GAC1B,IAAI,cAAc;IAChB,KAAK,MAAM,QAAQ,MAAM;IACzB,MAAM,MAAM;IACZ,IAAI,MAAM;IACV,OAAO,WAAW,KAAK;IACjB;IACN,OAAO,SAAS,cAAc,SAAS,UACnC,CAAC,CAAC,MAAM,UACR,OAAO,MAAM,SAAS,OAAO,KAAK,MAAM,KAAK,CAAC,CAAC,UAAU,GAAG,gBAAgB;IAChF,UAAU,CAAC,CAAC,MAAM;GACpB;GAEA,IAAI,SAAS,UACX,YAAY,UAAU,MAAM,UAAU,IAAI,KAAK,MAAM,WAAW,CAAC,GAAG,SAAU,QAAQ;IACpF,OAAO;KAAE,OAAO,OAAO;KAAO,MAAM,UAAU,OAAO,IAAI;KAAG,UAAU,CAAC,CAAC,OAAO;IAAS;GAC1F,CAAC;GAGH,OAAO;EACT;EAEA,SAAS,UAAU,KAAK,KAAK;GAC3B,IAAI,SAAS,UAAU,GAAG,CAAC,CAAC,YAAY;GAExC,IAAI,CAAC,QACH,OAAO;GAGT,IAAI,SAAS,WAAW,GAAG;GAC3B,IAAI,WAAW;IACb,SAAU,OAAO;KACf,QAAQ,MAAM,QAAQ,GAAA,CAAI,YAAY,MAAM;IAC9C;IACA,SAAU,OAAO;KACf,QAAQ,MAAM,MAAM,GAAA,CAAI,YAAY,MAAM;IAC5C;IACA,SAAU,OAAO;KACf,OAAO,WAAW,KAAK,CAAC,CAAC,YAAY,MAAM;IAC7C;GACF;GAEA,KAAK,IAAI,IAAI,GAAG,IAAI,SAAS,QAAQ,KACnC,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,QAAQ,KACjC,IAAI,SAAS,EAAE,CAAC,OAAO,EAAE,GACvB,OAAO,OAAO;GAKpB,OAAO;EACT;;;;;;;;EASA,eAAe,cAAc,WAAW,MAAM;GAC5C,IAAI,OAAO,cAAc,YACvB,OAAO;GAGT,IAAI;IACF,IAAI,WAAW,MAAM,UAAU,MAAM;KAAE,QAAQ;KAAQ,aAAa;KAAe,UAAU;IAAS,CAAC;IAEvG,OAAO,SAAS,WAAW,OAAO,SAAS,WAAW;GACxD,SAAS,OAAO;IACd,OAAO;GACT;EACF;EAEA,SAAS,MAAM,OAAO,MAAM;GAC1B,MAAM,cAAc,IAAI,MAAM,MAAM,EAAE,SAAS,KAAK,CAAC,CAAC;EACxD;;;;;;;;EASA,SAAS,gBAAgB,SAAS;GAChC,IAAI,MAAM,QAAQ;GAClB,IAAI,MAAM,QAAQ;GAClB,IAAI,kBAAkB,OAAO,QAAQ,oBAAoB,WAAW,QAAQ,kBAAkB;GAE9F,OAAO;IACL;KACE,MAAM;KACN,OAAO;KACP,aAAa;KACb,aAAa;MAAE,MAAM;MAAU,YAAY,CAAC;KAAE;KAC9C,aAAa,EAAE,cAAc,KAAK;KAClC,SAAS,WAAY;MACnB,IAAI,OAAO,IAAI,cAAc,UAAU,WAAW;MAClD,IAAI,WAAW,CAAC;MAChB,IAAI,MAAM,IAAI,iBAAiB,UAAU,QAAQ;MAEjD,KAAK,IAAI,IAAI,GAAG,IAAI,IAAI,UAAU,SAAS,SAAS,cAAc,KAAK;OACrE,IAAI,OAAO,UAAU,IAAI,EAAE,CAAC,WAAW;OAEvC,IAAI,MACF,SAAS,KAAK;QAAE,QAAQ,IAAI,EAAE,CAAC,WAAW,GAAA,CAAI,YAAY,MAAM,OAAO,IAAI;QAAS;OAAK,CAAC;MAE9F;MAEA,OAAQ;OACN,OAAO,IAAI;OACX,KAAK,IAAI;OACT,aAAa,OAAO,KAAK,aAAa,SAAS,IAAI;OACzC;OACV,SAAS,CAAC,CAAC,QAAQ;MACrB;KACF;IACF;IACA;KACE,MAAM;KACN,OAAO;KACP,aAAa;KACb,aAAa;MAAE,MAAM;MAAU,YAAY,CAAC;KAAE;KAC9C,aAAa,EAAE,cAAc,KAAK;KAClC,SAAS,WAAY;MACnB,OAAO,YAAY,GAAG;KACxB;IACF;IACA;KACE,MAAM;KACN,OAAO;KACP,aAAa;KACb,aAAa;MACX,MAAM;MACN,YAAY,EACV,QAAQ;OAAE,MAAM;OAAU,aAAa;MAAmD,EAC5F;KACF;KACA,aAAa,EAAE,cAAc,KAAK;KAClC,SAAS,SAAU,OAAO;MACxB,IAAI,SAAS,UAAU,SAAS,MAAM,MAAM,CAAC,CAAC,YAAY;MAC1D,IAAI,uBAAO,IAAI,IAAI;MACnB,IAAI,QAAQ,CAAC;MACb,IAAI,MAAM,IAAI,iBAAiB,UAAU,KAAK;MAE9C,KAAK,IAAI,IAAI,GAAG,IAAI,IAAI,UAAU,MAAM,SAAS,WAAW,KAAK;OAC/D,IAAI,MAAM,kBAAkB,IAAI,EAAE,CAAC,aAAa,MAAM,GAAG,GAAG;OAC5D,IAAI,OAAO,UAAU,IAAI,EAAE,CAAC,WAAW;OAEvC,IAAI,CAAC,OAAO,CAAC,QAAQ,KAAK,IAAI,IAAI,IAAI,KAAM,UAAU,KAAK,YAAY,CAAC,CAAC,QAAQ,MAAM,IAAI,GACzF;OAGF,KAAK,IAAI,IAAI,IAAI;OACjB,MAAM,KAAK;QAAQ;QAAM,KAAK,IAAI;OAAK,CAAC;MAC1C;MAEA,OAAQ,EAAS,MAAM;KACzB;IACF;IACA;KACE,MAAM;KACN,OAAO;KACP,aAAa;KACb,aAAa;MACX,MAAM;MACN,YAAY,EACV,KAAK;OAAE,MAAM;OAAU,aAAa;MAAmH,EACzJ;MACA,UAAU,CAAC,KAAK;KAClB;KACA,SAAS,eAAgB,OAAO;MAC9B,IAAI,UAAU,UAAU,SAAS,MAAM,GAAG;MAE1C,IAAI,CAAC,SACH,MAAM,IAAI,MAAM,wEAAwE;MAK1F,IAAI,QAAQ,OAAO,CAAC,MAAM,OAAO,CAAC,iBAAiB,KAAK,OAAO,GAC7D,MAAM,IAAI,MAAM,OAAM,UAAU,qLAAkL;MAGpN,IAAI,MAAM,kBAAkB,SAAS,GAAG;MAExC,IAAI,CAAC,KACH,MAAM,IAAI,MAAM,2FAA2F;MAG7G,IAAI,MAAM,cAAc,QAAQ,OAAO,IAAI,IAAI,GAC7C,MAAM,IAAI,MAAM,+BAA8B,IAAI,WAAW,IAAI,SAAS,kFAAiF;MAM7J,WAAW,WAAY;OACrB,IAAI,OAAO,IAAI,IAAI;MACrB,GAAG,eAAe;MAElB,OAAQ,EAAE,aAAa,IAAI,KAAK;KAClC;IACF;IACA;KACE,MAAM;KACN,OAAO;KACP,aAAa;KACb,aAAa;MAAE,MAAM;MAAU,YAAY,CAAC;KAAE;KAC9C,aAAa,EAAE,cAAc,KAAK;KAClC,SAAS,WAAY;MACnB,OAAQ,EAAE,QAAQ,WAAW,GAAG,CAAC,CAAC,IAAI,aAAa,EAAE;KACvD;IACF;IACA;KACE,MAAM;KACN,OAAO;KACP,aAAa;KACb,aAAa;MACX,MAAM;MACN,YAAY;OACV,OAAO;QAAE,MAAM;QAAU,aAAa;OAAqE;OAC3G,OAAO,EAAE,aAAa,mFAAoF;MAC5G;MACA,UAAU,CAAC,SAAS,OAAO;KAC7B;KACA,SAAS,SAAU,OAAO;MACxB,IAAI,QAAQ,UAAU,KAAK,SAAS,MAAM,KAAK;MAE/C,IAAI,CAAC,OACH,MAAM,IAAI,MAAM,8CAA6C,UAAU,SAAS,MAAM,KAAK,IAAI,KAAI;MAGrG,IAAI,OAAO,UAAU,KAAK;MAC1B,IAAI,QAAQ,MAAM;MAElB,IAAI,SAAS,cAAc,SAAS,SAClC,MAAM,UAAU,UAAU,QAAQ,OAAO,KAAK,CAAC,CAAC,YAAY,MAAM;WAC7D,IAAI,SAAS,UAAU;OAC5B,IAAI,SAAS,UAAU,KAAK,CAAC,CAAC,YAAY;OAC1C,IAAI,SAAS,MAAM,UAAU,KAAK,KAAK,MAAM,WAAW,CAAC,GAAG,SAAU,WAAW;QAC/E,OAAO,OAAO,UAAU,KAAK,CAAC,CAAC,YAAY,MAAM,UAAU,UAAU,UAAU,IAAI,CAAC,CAAC,YAAY,MAAM;OACzG,CAAC;OAED,IAAI,CAAC,QACH,MAAM,IAAI,MAAM,gBAAe,WAAW,KAAK,IAAI,wBAAsB,UAAU,KAAK,IAAI,KAAI;OAGlG,MAAM,QAAQ,OAAO;MACvB,OACE,MAAM,QAAQ,SAAS,OAAO,KAAK,OAAO,KAAK;MAGjD,MAAM,OAAO,OAAO;MACpB,MAAM,OAAO,QAAQ;MAErB,OAAQ,EAAE,OAAO,cAAc,KAAK,EAAE;KACxC;IACF;GACF;EACF;;;;;;;;;EAUA,SAAS,SAAS,cAAc,SAAS,QAAQ;GAC/C,IAAI,QAAQ,gBAAgB,IAAI,YAAY;GAE5C,IAAI,CAAC,OAAO;IACV,wBAAQ,IAAI,IAAI;IAChB,gBAAgB,IAAI,cAAc,KAAK;GACzC;GAEA,OAAO,QAAQ,IAAI,gBAAgB,OAAO,CAAC,CAAC,IAAI,SAAU,MAAM;IAC9D,OAAO,aAAa,aAAa,MAAM,SAAS,EAAU,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,WAAY;KACxF,IAAI,UAAU,OAAO,SACnB;KAGF,MAAM,IAAI,KAAK,MAAM,IAAI;KAEzB,IAAI,QACF,OAAO,iBAAiB,SAAS,WAAY;MAC3C,IAAI,MAAM,IAAI,KAAK,IAAI,MAAM,MAC3B,MAAM,OAAO,KAAK,IAAI;KAE1B,GAAG,EAAE,MAAM,KAAK,CAAC;IAErB,GAAG,SAAU,OAAO;KAClB,QAAQ,KAAK,qBAAoB,KAAK,OAAO,+BAA8B,KAAK;IAClF,CAAC;GACH,CAAC,CAAC;EACJ;;;;;;;;EASA,SAAS,kBAAkB,cAAc,MAAM;GAC7C,IAAI,QAAQ,eAAe,gBAAgB,IAAI,YAAY,IAAI;GAE/D,OAAQ,SAAS,MAAM,IAAI,IAAI,KAAM;EACvC;EAEA,OAAO,2BAA2B;GACrB;GACM;GACP;GACS;EACrB;EAEA,IAAI,OAAO,YAAY,OAAO,SAAS,cAAc;GACnD,IAAI,SAAS,OAAO,mBAAmB,OAAO,gBAAgB;GAE9D,SAAS,OAAO,SAAS,cAAc;IACrC,KAAK,OAAO;IACZ,KAAK,OAAO;IACZ,OAAO,OAAO,OAAO,UAAU,aAAa,OAAO,MAAM,KAAK,MAAM,IAAI;IACxE,SAAS,CAAC,EAAE,UAAU,OAAO;GAC/B,CAAC;EACH;CACF,EAAA,CAAG"}
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
(function(){(function(){"use strict";var e={description:`meta[name="description"]`,headings:`h1, h2`,links:`nav a[href], header a[href], aside a[href], [role="navigation"] a[href]`,fields:`form input, form select, form textarea`},
|
|
1
|
+
(function(){(function(){"use strict";var e=8e3,t={description:`meta[name="description"]`,headings:`h1, h2`,main:`main, [role="main"]`,ignored:`script, style, noscript, template, svg, nav, aside, form, dialog:not([open]), [hidden], [aria-hidden="true"], [role="navigation"], [data-webmcp-ignore], [class*="ai-chat-widget"]`,links:`nav a[href], header a[href], aside a[href], [role="navigation"] a[href]`,fields:`form input, form select, form textarea`},n=[`hidden`,`password`,`file`,`submit`,`button`,`reset`,`image`],r=[`__RequestVerificationToken`];function i(e){return(e==null?``:String(e)).replace(/\s+/g,` `).trim()}var a=/^[A-Za-z][A-Za-z0-9+.-]*:/,o=new WeakMap,s=`ADDRESS.ARTICLE.BLOCKQUOTE.BR.DD.DETAILS.DIV.DL.DT.FIELDSET.FIGCAPTION.FIGURE.FOOTER.H1.H2.H3.H4.H5.H6.HEADER.HR.LI.OL.P.PRE.SECTION.SUMMARY.UL`.split(`.`);function c(e){return e.hidden||typeof e.matches==`function`&&e.matches(t.ignored)?!0:typeof e.checkVisibility==`function`&&!e.checkVisibility()}function l(e){var t=i(e.textContent);return t.length>300?t.slice(0,300)+`…`:t}function u(e){var t=Array.prototype.slice.call(e.rows||[]),n=[];return t.length&&Array.prototype.every.call(t[0].cells||[],function(e){return(e.tagName||``).toUpperCase()===`TH`})&&(n=Array.prototype.map.call(t.shift().cells,l)),{caption:(e.caption?i(e.caption.textContent):``)||null,headers:n,rows:t.slice(0,50).map(function(e){return Array.prototype.map.call(e.cells||[],l)}),totalRows:t.length}}function d(n){var r=n.querySelector(t.main)||n.body,a=[],o=[],l=[],d=0;function f(){var e=i(o.join(` `));e&&a.push(e),o=[]}function p(e){if(e.nodeType===3)o.push(e.nodeValue);else if(!(e.nodeType!==1||c(e))){var t=(e.tagName||``).toUpperCase();if(t===`TABLE`)f(),d++,l.length<10&&(l.push(u(e)),a.push(`[table `+l.length+`]`));else{var n=s.indexOf(t)>=0;n&&f();for(var r=e.childNodes||[],i=0;i<r.length;i++)p(r[i]);n&&f()}}}r&&(p(r),f());var m=a.join(`
|
|
2
|
+
`),h=m.length>e||d>l.length||l.some(function(e){return e.totalRows>e.rows.length});return{text:m.length>e?m.slice(0,e)+`…`:m,tables:l,truncated:h}}function f(e,t){if(!e)return null;try{var n=new URL(e,t.href);return n.origin===t.origin?n:null}catch{return null}}function p(e){var t=(e.tagName||``).toLowerCase();return t===`select`||t===`textarea`?t:(e.type||`text`).toLowerCase()}function m(e){if(e.labels&&e.labels.length>0){var t=i(e.labels[0].textContent);if(t)return t}return i(e.getAttribute(`aria-label`))||i(e.getAttribute(`placeholder`))}function h(e){return!e.disabled&&n.indexOf(p(e))<0&&r.indexOf(e.name)<0}function g(e){for(var n=[],r=e.querySelectorAll(t.fields),i=0;i<r.length&&n.length<200;i++)h(r[i])&&n.push(r[i]);return n}function _(e){var t=p(e),n={key:e.name||e.id,name:e.name,id:e.id,label:m(e),type:t,value:t===`checkbox`||t===`radio`?!!e.checked:String(e.value==null?``:e.value).substring(0,2e3),required:!!e.required};return t===`select`&&(n.options=Array.prototype.map.call(e.options||[],function(e){return{value:e.value,text:i(e.text),selected:!!e.selected}})),n}function v(e,t){var n=i(t).toLowerCase();if(!n)return null;for(var r=g(e),a=[function(e){return(e.name||``).toLowerCase()===n},function(e){return(e.id||``).toLowerCase()===n},function(e){return m(e).toLowerCase()===n}],o=0;o<a.length;o++)for(var s=0;s<r.length;s++)if(a[o](r[s]))return r[s];return null}async function y(e,t){if(typeof e!=`function`)return!1;try{var n=await e(t,{method:`HEAD`,credentials:`same-origin`,redirect:`follow`});return n.status===404||n.status===410}catch{return!1}}function b(e,t){e.dispatchEvent(new Event(t,{bubbles:!0}))}function x(e){var n=e.doc,r=e.loc,o=typeof e.navigateDelayMs==`number`?e.navigateDelayMs:400;return[{name:`page_get_info`,title:`Get page info`,description:`Describes the page the user has open: its title, address, description and main headings, and whether it is an admin page. To read what the page shows, use page_read_content.`,inputSchema:{type:`object`,properties:{}},annotations:{readOnlyHint:!0},execute:function(){for(var a=n.querySelector(t.description),o=[],s=n.querySelectorAll(t.headings),c=0;c<s.length&&o.length<20;c++){var l=i(s[c].textContent);l&&o.push({level:(s[c].tagName||``).toUpperCase()===`H1`?1:2,text:l})}return{title:n.title,url:r.href,description:a?a.getAttribute(`content`):null,headings:o,isAdmin:!!e.isAdmin}}},{name:`page_read_content`,title:`Read page content`,description:`Reads what the page shows in its main area: its text, and its tables as headers and rows (for example the records a list page shows). Menus, forms and hidden parts are left out; use page_list_links and page_list_form_fields for those.`,inputSchema:{type:`object`,properties:{}},annotations:{readOnlyHint:!0},execute:function(){return d(n)}},{name:`page_list_links`,title:`List page links`,description:`Lists the links in the page's menus and navigation that stay on this site, with their text. Use it to find the page to open for a task.`,inputSchema:{type:`object`,properties:{filter:{type:`string`,description:`Only list links whose text contains these words.`}}},annotations:{readOnlyHint:!0},execute:function(e){for(var a=i(e&&e.filter).toLowerCase(),o=new Set,s=[],c=n.querySelectorAll(t.links),l=0;l<c.length&&s.length<200;l++){var u=f(c[l].getAttribute(`href`),r),d=i(c[l].textContent);!u||!d||o.has(u.href)||a&&d.toLowerCase().indexOf(a)<0||(o.add(u.href),s.push({text:d,url:u.href}))}return{links:s}}},{name:`page_navigate`,title:`Open page`,description:`Opens another page of this site in the current tab. Only addresses on this site are allowed; take them from page_list_links, and start a site path with /. Unsaved changes on the current page are lost.`,inputSchema:{type:`object`,properties:{url:{type:`string`,description:`The address to open: a path that starts with "/", or a full address on this site, as page_list_links lists them.`}},required:[`url`]},execute:async function(t){var n=i(t&&t.url);if(!n)throw Error(`Give the address of the page to open. Call page_list_links to find it.`);if(n.charAt(0)!==`/`&&!a.test(n))throw Error(`"`+n+`" is not an address on this site. Give a path that starts with "/" or a full address on this site, as page_list_links lists them. Call page_list_links to find the page to open.`);var s=f(n,r);if(!s)throw Error(`Only addresses on this site can be opened. Call page_list_links to find the page to open.`);if(await y(e.fetch,s.href))throw Error(`The site has no page at "`+s.pathname+s.search+`" (404). Do not guess addresses: call page_list_links to find the page to open.`);return setTimeout(function(){r.assign(s.href)},o),{navigatedTo:s.href}}},{name:`page_list_form_fields`,title:`List form fields`,description:`Lists the fields of the forms on the page: name, label, type, current value, whether it is required and, for lists, the choices. Hidden, password and file fields are not listed.`,inputSchema:{type:`object`,properties:{}},annotations:{readOnlyHint:!0},execute:function(){return{fields:g(n).map(_)}}},{name:`page_set_form_field`,title:`Set form field`,description:`Puts a value into one form field on the page, found by its name, id or label. It does not save or submit the form: the user reviews the change and saves it.`,inputSchema:{type:`object`,properties:{field:{type:`string`,description:`The field's name, id or label, as page_list_form_fields lists it.`},value:{description:`The value. For a check box, true or false. For a list, a choice's value or text.`}},required:[`field`,`value`]},execute:function(e){var t=v(n,e&&e.field);if(!t)throw Error(`No editable field on the page is named "`+i(e&&e.field)+`".`);var r=p(t),a=e.value;if(r===`checkbox`||r===`radio`)t.checked=a===!0||String(a).toLowerCase()===`true`;else if(r===`select`){var o=i(a).toLowerCase(),s=Array.prototype.find.call(t.options||[],function(e){return String(e.value).toLowerCase()===o||i(e.text).toLowerCase()===o});if(!s)throw Error(`The list "`+m(t)+`" has no choice "`+i(a)+`".`);t.value=s.value}else t.value=a==null?``:String(a);return b(t,`input`),b(t,`change`),{field:_(t)}}}]}function S(e,t,n){var r=o.get(e);return r||(r=new Map,o.set(e,r)),Promise.all(x(t).map(function(t){return e.registerTool(t,n?{signal:n}:{}).then(function(){n&&n.aborted||(r.set(t.name,t),n&&n.addEventListener(`abort`,function(){r.get(t.name)===t&&r.delete(t.name)},{once:!0}))},function(e){console.warn(`The page tool "`+t.name+`" could not be registered.`,e)})}))}function C(e,t){var n=e?o.get(e):null;return n&&n.get(t)||null}if(window.CrestAppsWebMcpPageTools={selectors:t,createPageTools:x,register:S,getRegisteredTool:C},window.document&&window.document.modelContext){var w=window.crestAppsWebMcp&&window.crestAppsWebMcp.config;S(window.document.modelContext,{doc:window.document,loc:window.location,fetch:typeof window.fetch==`function`?window.fetch.bind(window):void 0,isAdmin:!!(w&&w.isAdmin)})}})()})();
|
|
2
3
|
//# sourceMappingURL=webmcp-page-tools.min.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webmcp-page-tools.min.js","sources":["../../Assets/js/webmcp-page-tools.js"],"sourcesContent":["// @ts-check\n/*\n * WebMCP page tools.\n *\n * Tools that work on the page the user sees: what the page is, where its menus lead, opening another page of the same\n * site, and reading and filling its forms. They never submit a form and never leave the site, so the user always\n * reviews and saves the changes an agent makes.\n *\n * The tools are registered with the standard `document.modelContext.registerTool`, so any WebMCP agent can use them:\n * the browser's own assistant, or the CrestApps AI chat. As WebMCP defines, each answers a JSON-serializable value and\n * reports a failure by rejecting. Load webmcp-polyfill.js first in browsers without native WebMCP.\n */\n\n(function () {\n 'use strict';\n\n var MAX_HEADINGS = 20;\n var MAX_LINKS = 200;\n var MAX_FIELDS = 200;\n var MAX_VALUE_LENGTH = 2000;\n\n // The selectors the tools read the page with.\n var selectors = {\n description: 'meta[name=\"description\"]',\n headings: 'h1, h2',\n links: 'nav a[href], header a[href], aside a[href], [role=\"navigation\"] a[href]',\n fields: 'form input, form select, form textarea',\n };\n\n // Field types an agent never reads or fills.\n var SKIPPED_TYPES = ['hidden', 'password', 'file', 'submit', 'button', 'reset', 'image'];\n var SKIPPED_NAMES = ['__RequestVerificationToken'];\n\n function cleanText(value) {\n return (value == null ? '' : String(value)).replace(/\\s+/g, ' ').trim();\n }\n\n\n function resolveSameOrigin(href, loc) {\n if (!href) {\n return null;\n }\n\n try {\n var url = new URL(href, loc.href);\n\n return url.origin === loc.origin ? url : null;\n } catch (error) {\n return null;\n }\n }\n\n function fieldType(field) {\n var tag = (field.tagName || '').toLowerCase();\n\n if (tag === 'select' || tag === 'textarea') {\n return tag;\n }\n\n return (field.type || 'text').toLowerCase();\n }\n\n function fieldLabel(field) {\n if (field.labels && field.labels.length > 0) {\n var label = cleanText(field.labels[0].textContent);\n\n if (label) {\n return label;\n }\n }\n\n return cleanText(field.getAttribute('aria-label')) || cleanText(field.getAttribute('placeholder'));\n }\n\n function isListed(field) {\n return !field.disabled &&\n SKIPPED_TYPES.indexOf(fieldType(field)) < 0 &&\n SKIPPED_NAMES.indexOf(field.name) < 0;\n }\n\n function listFields(doc) {\n var fields = [];\n var all = doc.querySelectorAll(selectors.fields);\n\n for (var i = 0; i < all.length && fields.length < MAX_FIELDS; i++) {\n if (isListed(all[i])) {\n fields.push(all[i]);\n }\n }\n\n return fields;\n }\n\n function describeField(field) {\n var type = fieldType(field);\n var description = {\n key: field.name || field.id,\n name: field.name,\n id: field.id,\n label: fieldLabel(field),\n type: type,\n value: type === 'checkbox' || type === 'radio'\n ? !!field.checked\n : String(field.value == null ? '' : field.value).substring(0, MAX_VALUE_LENGTH),\n required: !!field.required,\n };\n\n if (type === 'select') {\n description.options = Array.prototype.map.call(field.options || [], function (option) {\n return { value: option.value, text: cleanText(option.text), selected: !!option.selected };\n });\n }\n\n return description;\n }\n\n function findField(doc, key) {\n var wanted = cleanText(key).toLowerCase();\n\n if (!wanted) {\n return null;\n }\n\n var fields = listFields(doc);\n var matchers = [\n function (field) {\n return (field.name || '').toLowerCase() === wanted;\n },\n function (field) {\n return (field.id || '').toLowerCase() === wanted;\n },\n function (field) {\n return fieldLabel(field).toLowerCase() === wanted;\n },\n ];\n\n for (var m = 0; m < matchers.length; m++) {\n for (var i = 0; i < fields.length; i++) {\n if (matchers[m](fields[i])) {\n return fields[i];\n }\n }\n }\n\n return null;\n }\n\n function raise(field, type) {\n field.dispatchEvent(new Event(type, { bubbles: true }));\n }\n\n /**\n * Creates the page tools for a document.\n *\n * @param {{ doc: any, loc: any, isAdmin?: boolean }} options The document, its location, and whether it is an admin page.\n */\n function createPageTools(options) {\n var doc = options.doc;\n var loc = options.loc;\n\n return [\n {\n name: 'page_get_info',\n title: 'Get page info',\n description: 'Describes the page the user has open: its title, address, description and main headings, and whether it is an admin page.',\n inputSchema: { type: 'object', properties: {} },\n annotations: { readOnlyHint: true },\n execute: function () {\n var meta = doc.querySelector(selectors.description);\n var headings = [];\n var all = doc.querySelectorAll(selectors.headings);\n\n for (var i = 0; i < all.length && headings.length < MAX_HEADINGS; i++) {\n var text = cleanText(all[i].textContent);\n\n if (text) {\n headings.push({ level: (all[i].tagName || '').toUpperCase() === 'H1' ? 1 : 2, text: text });\n }\n }\n\n return ({\n title: doc.title,\n url: loc.href,\n description: meta ? meta.getAttribute('content') : null,\n headings: headings,\n isAdmin: !!options.isAdmin,\n });\n },\n },\n {\n name: 'page_list_links',\n title: 'List page links',\n description: 'Lists the links in the page\\'s menus and navigation that stay on this site, with their text. Use it to find the page to open for a task.',\n inputSchema: {\n type: 'object',\n properties: {\n filter: { type: 'string', description: 'Only list links whose text contains these words.' },\n },\n },\n annotations: { readOnlyHint: true },\n execute: function (input) {\n var filter = cleanText(input && input.filter).toLowerCase();\n var seen = new Set();\n var links = [];\n var all = doc.querySelectorAll(selectors.links);\n\n for (var i = 0; i < all.length && links.length < MAX_LINKS; i++) {\n var url = resolveSameOrigin(all[i].getAttribute('href'), loc);\n var text = cleanText(all[i].textContent);\n\n if (!url || !text || seen.has(url.href) || (filter && text.toLowerCase().indexOf(filter) < 0)) {\n continue;\n }\n\n seen.add(url.href);\n links.push({ text: text, url: url.href });\n }\n\n return ({ links: links });\n },\n },\n {\n name: 'page_navigate',\n title: 'Open page',\n description: 'Opens another page of this site in the current tab. Only addresses on this site are allowed. Unsaved changes on the current page are lost.',\n inputSchema: {\n type: 'object',\n properties: {\n url: { type: 'string', description: 'The address to open, relative to the site or absolute on the same site.' },\n },\n required: ['url'],\n },\n execute: function (input) {\n var url = resolveSameOrigin(input && input.url, loc);\n\n if (!url) {\n throw new Error('Only addresses on this site can be opened.');\n }\n\n loc.assign(url.href);\n\n return ({ navigatedTo: url.href });\n },\n },\n {\n name: 'page_list_form_fields',\n title: 'List form fields',\n description: 'Lists the fields of the forms on the page: name, label, type, current value, whether it is required and, for lists, the choices. Hidden, password and file fields are not listed.',\n inputSchema: { type: 'object', properties: {} },\n annotations: { readOnlyHint: true },\n execute: function () {\n return ({ fields: listFields(doc).map(describeField) });\n },\n },\n {\n name: 'page_set_form_field',\n title: 'Set form field',\n description: 'Puts a value into one form field on the page, found by its name, id or label. It does not save or submit the form: the user reviews the change and saves it.',\n inputSchema: {\n type: 'object',\n properties: {\n field: { type: 'string', description: 'The field\\'s name, id or label, as page_list_form_fields lists it.' },\n value: { description: 'The value. For a check box, true or false. For a list, a choice\\'s value or text.' },\n },\n required: ['field', 'value'],\n },\n execute: function (input) {\n var field = findField(doc, input && input.field);\n\n if (!field) {\n throw new Error('No editable field on the page is named \"' + cleanText(input && input.field) + '\".');\n }\n\n var type = fieldType(field);\n var value = input.value;\n\n if (type === 'checkbox' || type === 'radio') {\n field.checked = value === true || String(value).toLowerCase() === 'true';\n } else if (type === 'select') {\n var wanted = cleanText(value).toLowerCase();\n var option = Array.prototype.find.call(field.options || [], function (candidate) {\n return String(candidate.value).toLowerCase() === wanted || cleanText(candidate.text).toLowerCase() === wanted;\n });\n\n if (!option) {\n throw new Error('The list \"' + fieldLabel(field) + '\" has no choice \"' + cleanText(value) + '\".');\n }\n\n field.value = option.value;\n } else {\n field.value = value == null ? '' : String(value);\n }\n\n raise(field, 'input');\n raise(field, 'change');\n\n return ({ field: describeField(field) });\n },\n },\n ];\n }\n\n /**\n * Registers the page tools with the page's ModelContext.\n *\n * @param {any} modelContext The page's ModelContext (document.modelContext).\n * @param {{ doc: any, loc: any, isAdmin?: boolean }} options The document, its location, and whether it is an admin page.\n * @param {AbortSignal} [signal] Unregisters the tools when aborted.\n */\n function register(modelContext, options, signal) {\n return Promise.all(createPageTools(options).map(function (tool) {\n return modelContext.registerTool(tool, signal ? { signal: signal } : {}).catch(function (error) {\n console.warn('The page tool \"' + tool.name + '\" could not be registered.', error);\n });\n }));\n }\n\n window.CrestAppsWebMcpPageTools = {\n selectors: selectors,\n createPageTools: createPageTools,\n register: register,\n };\n\n if (window.document && window.document.modelContext) {\n var config = window.crestAppsWebMcp && window.crestAppsWebMcp.config;\n\n register(window.document.modelContext, {\n doc: window.document,\n loc: window.location,\n isAdmin: !!(config && config.isAdmin),\n });\n }\n})();\n"],"names":["selectors","SKIPPED_TYPES","SKIPPED_NAMES","cleanText","value","resolveSameOrigin","href","url","loc","fieldType","tag","field","fieldLabel","label","isListed","listFields","fields","all","doc","describeField","type","description","option","findField","wanted","key","matchers","m","i","raise","createPageTools","options","meta","headings","text","filter","input","seen","links","candidate","register","modelContext","tool","signal","error","config"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"webmcp-page-tools.min.js","sources":["../../Assets/js/webmcp-page-tools.js"],"sourcesContent":["// @ts-check\n/*\n * WebMCP page tools.\n *\n * Tools that work on the page the user sees: what the page is, what it shows, where its menus lead, opening another\n * page of the same site, and reading and filling its forms. They never submit a form and never leave the site, so the user always\n * reviews and saves the changes an agent makes.\n *\n * The tools are registered with the standard `document.modelContext.registerTool`, so any WebMCP agent can use them:\n * the browser's own assistant, or the CrestApps AI chat. As WebMCP defines, each answers a JSON-serializable value and\n * reports a failure by rejecting. Load webmcp-polyfill.js first in browsers without native WebMCP.\n */\n\n(function () {\n 'use strict';\n\n var MAX_HEADINGS = 20;\n var MAX_LINKS = 200;\n var MAX_FIELDS = 200;\n var MAX_VALUE_LENGTH = 2000;\n var MAX_CONTENT_LENGTH = 8000;\n var MAX_TABLES = 10;\n var MAX_TABLE_ROWS = 50;\n var MAX_CELL_LENGTH = 300;\n\n // The selectors the tools read the page with.\n var selectors = {\n description: 'meta[name=\"description\"]',\n headings: 'h1, h2',\n main: 'main, [role=\"main\"]',\n // What page_read_content leaves out of the page's text: scripts, menus, forms (page_list_form_fields reads them),\n // hidden parts, the chat itself, and anything a page marks with data-webmcp-ignore.\n ignored: 'script, style, noscript, template, svg, nav, aside, form, dialog:not([open]), [hidden], [aria-hidden=\"true\"], [role=\"navigation\"], [data-webmcp-ignore], [class*=\"ai-chat-widget\"]',\n links: 'nav a[href], header a[href], aside a[href], [role=\"navigation\"] a[href]',\n fields: 'form input, form select, form textarea',\n };\n\n // Field types an agent never reads or fills.\n var SKIPPED_TYPES = ['hidden', 'password', 'file', 'submit', 'button', 'reset', 'image'];\n var SKIPPED_NAMES = ['__RequestVerificationToken'];\n\n function cleanText(value) {\n return (value == null ? '' : String(value)).replace(/\\s+/g, ' ').trim();\n }\n\n\n // An address that starts with a scheme, such as \"https:\" or \"javascript:\".\n var ABSOLUTE_ADDRESS = /^[A-Za-z][A-Za-z0-9+.-]*:/;\n\n // The tools register() registered, by ModelContext and name, so the CrestApps chat can run them through their own\n // execute and tell the model why one failed (executeTool reports every failure as \"Tool execution failed\").\n var registeredTools = new WeakMap();\n\n // Elements that start a new line in the text page_read_content reads.\n var BLOCK_TAGS = ['ADDRESS', 'ARTICLE', 'BLOCKQUOTE', 'BR', 'DD', 'DETAILS', 'DIV', 'DL', 'DT', 'FIELDSET', 'FIGCAPTION', 'FIGURE', 'FOOTER', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'HEADER', 'HR', 'LI', 'OL', 'P', 'PRE', 'SECTION', 'SUMMARY', 'UL'];\n\n function isIgnored(node) {\n if (node.hidden || (typeof node.matches === 'function' && node.matches(selectors.ignored))) {\n return true;\n }\n\n // Parts the page hides with CSS, such as closed menus and modals.\n return typeof node.checkVisibility === 'function' && !node.checkVisibility();\n }\n\n function cellText(cell) {\n var text = cleanText(cell.textContent);\n\n return text.length > MAX_CELL_LENGTH ? text.slice(0, MAX_CELL_LENGTH) + '…' : text;\n }\n\n function readTable(table) {\n var rows = Array.prototype.slice.call(table.rows || []);\n var headers = [];\n\n if (rows.length && Array.prototype.every.call(rows[0].cells || [], function (cell) { return (cell.tagName || '').toUpperCase() === 'TH'; })) {\n headers = Array.prototype.map.call(rows.shift().cells, cellText);\n }\n\n var caption = table.caption ? cleanText(table.caption.textContent) : '';\n\n return {\n caption: caption || null,\n headers: headers,\n rows: rows.slice(0, MAX_TABLE_ROWS).map(function (row) {\n return Array.prototype.map.call(row.cells || [], cellText);\n }),\n totalRows: rows.length,\n };\n }\n\n /**\n * Reads the text and the tables of the page's main region, the way a person reads it: menus, forms, hidden parts\n * and the chat are left out, and each table is read once, as rows, where the text says [table N].\n */\n function readContent(doc) {\n var root = doc.querySelector(selectors.main) || doc.body;\n var lines = [];\n var line = [];\n var tables = [];\n var tableCount = 0;\n\n function endLine() {\n var text = cleanText(line.join(' '));\n\n if (text) {\n lines.push(text);\n }\n\n line = [];\n }\n\n function walk(node) {\n if (node.nodeType === 3) {\n line.push(node.nodeValue);\n\n return;\n }\n\n if (node.nodeType !== 1 || isIgnored(node)) {\n return;\n }\n\n var tag = (node.tagName || '').toUpperCase();\n\n if (tag === 'TABLE') {\n endLine();\n tableCount++;\n\n if (tables.length < MAX_TABLES) {\n tables.push(readTable(node));\n lines.push('[table ' + tables.length + ']');\n }\n\n return;\n }\n\n var isBlock = BLOCK_TAGS.indexOf(tag) >= 0;\n\n if (isBlock) {\n endLine();\n }\n\n var children = node.childNodes || [];\n\n for (var i = 0; i < children.length; i++) {\n walk(children[i]);\n }\n\n if (isBlock) {\n endLine();\n }\n }\n\n if (root) {\n walk(root);\n endLine();\n }\n\n var text = lines.join('\\n');\n var truncated = text.length > MAX_CONTENT_LENGTH || tableCount > tables.length ||\n tables.some(function (table) { return table.totalRows > table.rows.length; });\n\n return {\n text: text.length > MAX_CONTENT_LENGTH ? text.slice(0, MAX_CONTENT_LENGTH) + '…' : text,\n tables: tables,\n truncated: truncated,\n };\n }\n\n function resolveSameOrigin(href, loc) {\n if (!href) {\n return null;\n }\n\n try {\n var url = new URL(href, loc.href);\n\n return url.origin === loc.origin ? url : null;\n } catch (error) {\n return null;\n }\n }\n\n function fieldType(field) {\n var tag = (field.tagName || '').toLowerCase();\n\n if (tag === 'select' || tag === 'textarea') {\n return tag;\n }\n\n return (field.type || 'text').toLowerCase();\n }\n\n function fieldLabel(field) {\n if (field.labels && field.labels.length > 0) {\n var label = cleanText(field.labels[0].textContent);\n\n if (label) {\n return label;\n }\n }\n\n return cleanText(field.getAttribute('aria-label')) || cleanText(field.getAttribute('placeholder'));\n }\n\n function isListed(field) {\n return !field.disabled &&\n SKIPPED_TYPES.indexOf(fieldType(field)) < 0 &&\n SKIPPED_NAMES.indexOf(field.name) < 0;\n }\n\n function listFields(doc) {\n var fields = [];\n var all = doc.querySelectorAll(selectors.fields);\n\n for (var i = 0; i < all.length && fields.length < MAX_FIELDS; i++) {\n if (isListed(all[i])) {\n fields.push(all[i]);\n }\n }\n\n return fields;\n }\n\n function describeField(field) {\n var type = fieldType(field);\n var description = {\n key: field.name || field.id,\n name: field.name,\n id: field.id,\n label: fieldLabel(field),\n type: type,\n value: type === 'checkbox' || type === 'radio'\n ? !!field.checked\n : String(field.value == null ? '' : field.value).substring(0, MAX_VALUE_LENGTH),\n required: !!field.required,\n };\n\n if (type === 'select') {\n description.options = Array.prototype.map.call(field.options || [], function (option) {\n return { value: option.value, text: cleanText(option.text), selected: !!option.selected };\n });\n }\n\n return description;\n }\n\n function findField(doc, key) {\n var wanted = cleanText(key).toLowerCase();\n\n if (!wanted) {\n return null;\n }\n\n var fields = listFields(doc);\n var matchers = [\n function (field) {\n return (field.name || '').toLowerCase() === wanted;\n },\n function (field) {\n return (field.id || '').toLowerCase() === wanted;\n },\n function (field) {\n return fieldLabel(field).toLowerCase() === wanted;\n },\n ];\n\n for (var m = 0; m < matchers.length; m++) {\n for (var i = 0; i < fields.length; i++) {\n if (matchers[m](fields[i])) {\n return fields[i];\n }\n }\n }\n\n return null;\n }\n\n /**\n * Asks the site whether a page exists, with a HEAD request that downloads nothing. Only a 404 or 410 answer means it\n * does not: a page that refuses HEAD (405), sends to the login page or cannot be reached is opened as asked.\n *\n * @param {Function | undefined} fetchImpl The fetch function, or nothing to skip the check.\n * @param {string} href The address.\n */\n async function pageIsMissing(fetchImpl, href) {\n if (typeof fetchImpl !== 'function') {\n return false;\n }\n\n try {\n var response = await fetchImpl(href, { method: 'HEAD', credentials: 'same-origin', redirect: 'follow' });\n\n return response.status === 404 || response.status === 410;\n } catch (error) {\n return false;\n }\n }\n\n function raise(field, type) {\n field.dispatchEvent(new Event(type, { bubbles: true }));\n }\n\n /**\n * Creates the page tools for a document.\n *\n * @param {{ doc: any, loc: any, isAdmin?: boolean, fetch?: Function, navigateDelayMs?: number }} options The document,\n * its location, whether it is an admin page, the fetch function page_navigate checks that a page exists with (no\n * check without it), and how long page_navigate waits after answering before it opens the page.\n */\n function createPageTools(options) {\n var doc = options.doc;\n var loc = options.loc;\n var navigateDelayMs = typeof options.navigateDelayMs === 'number' ? options.navigateDelayMs : 400;\n\n return [\n {\n name: 'page_get_info',\n title: 'Get page info',\n description: 'Describes the page the user has open: its title, address, description and main headings, and whether it is an admin page. To read what the page shows, use page_read_content.',\n inputSchema: { type: 'object', properties: {} },\n annotations: { readOnlyHint: true },\n execute: function () {\n var meta = doc.querySelector(selectors.description);\n var headings = [];\n var all = doc.querySelectorAll(selectors.headings);\n\n for (var i = 0; i < all.length && headings.length < MAX_HEADINGS; i++) {\n var text = cleanText(all[i].textContent);\n\n if (text) {\n headings.push({ level: (all[i].tagName || '').toUpperCase() === 'H1' ? 1 : 2, text: text });\n }\n }\n\n return ({\n title: doc.title,\n url: loc.href,\n description: meta ? meta.getAttribute('content') : null,\n headings: headings,\n isAdmin: !!options.isAdmin,\n });\n },\n },\n {\n name: 'page_read_content',\n title: 'Read page content',\n description: 'Reads what the page shows in its main area: its text, and its tables as headers and rows (for example the records a list page shows). Menus, forms and hidden parts are left out; use page_list_links and page_list_form_fields for those.',\n inputSchema: { type: 'object', properties: {} },\n annotations: { readOnlyHint: true },\n execute: function () {\n return readContent(doc);\n },\n },\n {\n name: 'page_list_links',\n title: 'List page links',\n description: 'Lists the links in the page\\'s menus and navigation that stay on this site, with their text. Use it to find the page to open for a task.',\n inputSchema: {\n type: 'object',\n properties: {\n filter: { type: 'string', description: 'Only list links whose text contains these words.' },\n },\n },\n annotations: { readOnlyHint: true },\n execute: function (input) {\n var filter = cleanText(input && input.filter).toLowerCase();\n var seen = new Set();\n var links = [];\n var all = doc.querySelectorAll(selectors.links);\n\n for (var i = 0; i < all.length && links.length < MAX_LINKS; i++) {\n var url = resolveSameOrigin(all[i].getAttribute('href'), loc);\n var text = cleanText(all[i].textContent);\n\n if (!url || !text || seen.has(url.href) || (filter && text.toLowerCase().indexOf(filter) < 0)) {\n continue;\n }\n\n seen.add(url.href);\n links.push({ text: text, url: url.href });\n }\n\n return ({ links: links });\n },\n },\n {\n name: 'page_navigate',\n title: 'Open page',\n description: 'Opens another page of this site in the current tab. Only addresses on this site are allowed; take them from page_list_links, and start a site path with /. Unsaved changes on the current page are lost.',\n inputSchema: {\n type: 'object',\n properties: {\n url: { type: 'string', description: 'The address to open: a path that starts with \"/\", or a full address on this site, as page_list_links lists them.' },\n },\n required: ['url'],\n },\n execute: async function (input) {\n var address = cleanText(input && input.url);\n\n if (!address) {\n throw new Error('Give the address of the page to open. Call page_list_links to find it.');\n }\n\n // A bare relative path, such as \"ArticlesAdmin\", resolves against the current page and usually names a page\n // that does not exist: it is a guess, not an address the site gave.\n if (address.charAt(0) !== '/' && !ABSOLUTE_ADDRESS.test(address)) {\n throw new Error('\"' + address + '\" is not an address on this site. Give a path that starts with \"/\" or a full address on this site, as page_list_links lists them. Call page_list_links to find the page to open.');\n }\n\n var url = resolveSameOrigin(address, loc);\n\n if (!url) {\n throw new Error('Only addresses on this site can be opened. Call page_list_links to find the page to open.');\n }\n\n if (await pageIsMissing(options.fetch, url.href)) {\n throw new Error('The site has no page at \"' + url.pathname + url.search + '\" (404). Do not guess addresses: call page_list_links to find the page to open.');\n }\n\n // Answer first and open the page a moment later, so the agent gets the result before this page (and\n // the chat connection on it) goes away. The chat finishes the reply on the server and shows it on the\n // next page.\n setTimeout(function () {\n loc.assign(url.href);\n }, navigateDelayMs);\n\n return ({ navigatedTo: url.href });\n },\n },\n {\n name: 'page_list_form_fields',\n title: 'List form fields',\n description: 'Lists the fields of the forms on the page: name, label, type, current value, whether it is required and, for lists, the choices. Hidden, password and file fields are not listed.',\n inputSchema: { type: 'object', properties: {} },\n annotations: { readOnlyHint: true },\n execute: function () {\n return ({ fields: listFields(doc).map(describeField) });\n },\n },\n {\n name: 'page_set_form_field',\n title: 'Set form field',\n description: 'Puts a value into one form field on the page, found by its name, id or label. It does not save or submit the form: the user reviews the change and saves it.',\n inputSchema: {\n type: 'object',\n properties: {\n field: { type: 'string', description: 'The field\\'s name, id or label, as page_list_form_fields lists it.' },\n value: { description: 'The value. For a check box, true or false. For a list, a choice\\'s value or text.' },\n },\n required: ['field', 'value'],\n },\n execute: function (input) {\n var field = findField(doc, input && input.field);\n\n if (!field) {\n throw new Error('No editable field on the page is named \"' + cleanText(input && input.field) + '\".');\n }\n\n var type = fieldType(field);\n var value = input.value;\n\n if (type === 'checkbox' || type === 'radio') {\n field.checked = value === true || String(value).toLowerCase() === 'true';\n } else if (type === 'select') {\n var wanted = cleanText(value).toLowerCase();\n var option = Array.prototype.find.call(field.options || [], function (candidate) {\n return String(candidate.value).toLowerCase() === wanted || cleanText(candidate.text).toLowerCase() === wanted;\n });\n\n if (!option) {\n throw new Error('The list \"' + fieldLabel(field) + '\" has no choice \"' + cleanText(value) + '\".');\n }\n\n field.value = option.value;\n } else {\n field.value = value == null ? '' : String(value);\n }\n\n raise(field, 'input');\n raise(field, 'change');\n\n return ({ field: describeField(field) });\n },\n },\n ];\n }\n\n /**\n * Registers the page tools with the page's ModelContext.\n *\n * @param {any} modelContext The page's ModelContext (document.modelContext).\n * @param {{ doc: any, loc: any, isAdmin?: boolean, fetch?: Function }} options The document, its location, whether it\n * is an admin page, and the fetch function page_navigate checks that a page exists with (no check without it).\n * @param {AbortSignal} [signal] Unregisters the tools when aborted.\n */\n function register(modelContext, options, signal) {\n var tools = registeredTools.get(modelContext);\n\n if (!tools) {\n tools = new Map();\n registeredTools.set(modelContext, tools);\n }\n\n return Promise.all(createPageTools(options).map(function (tool) {\n return modelContext.registerTool(tool, signal ? { signal: signal } : {}).then(function () {\n if (signal && signal.aborted) {\n return;\n }\n\n tools.set(tool.name, tool);\n\n if (signal) {\n signal.addEventListener('abort', function () {\n if (tools.get(tool.name) === tool) {\n tools.delete(tool.name);\n }\n }, { once: true });\n }\n }, function (error) {\n console.warn('The page tool \"' + tool.name + '\" could not be registered.', error);\n });\n }));\n }\n\n /**\n * Answers the page tool register() registered with a ModelContext under a name, with its execute callback, or null\n * when it registered none: another script may have registered a tool of that name first.\n *\n * @param {any} modelContext The ModelContext the tool was registered with.\n * @param {string} name The tool's name.\n */\n function getRegisteredTool(modelContext, name) {\n var tools = modelContext ? registeredTools.get(modelContext) : null;\n\n return (tools && tools.get(name)) || null;\n }\n\n window.CrestAppsWebMcpPageTools = {\n selectors: selectors,\n createPageTools: createPageTools,\n register: register,\n getRegisteredTool: getRegisteredTool,\n };\n\n if (window.document && window.document.modelContext) {\n var config = window.crestAppsWebMcp && window.crestAppsWebMcp.config;\n\n register(window.document.modelContext, {\n doc: window.document,\n loc: window.location,\n fetch: typeof window.fetch === 'function' ? window.fetch.bind(window) : undefined,\n isAdmin: !!(config && config.isAdmin),\n });\n }\n})();\n"],"names":["MAX_CONTENT_LENGTH","selectors","SKIPPED_TYPES","SKIPPED_NAMES","cleanText","value","ABSOLUTE_ADDRESS","registeredTools","BLOCK_TAGS","isIgnored","node","cellText","text","cell","readTable","rows","table","headers","row","readContent","root","doc","lines","line","tables","tableCount","endLine","walk","tag","isBlock","children","truncated","resolveSameOrigin","href","url","loc","fieldType","field","fieldLabel","label","isListed","listFields","fields","all","describeField","type","description","option","findField","wanted","key","matchers","m","i","pageIsMissing","fetchImpl","response","raise","createPageTools","options","navigateDelayMs","meta","headings","filter","input","seen","links","address","candidate","register","tools","modelContext","tool","signal","error","getRegisteredTool","name","config"],"mappings":"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"}
|
package/dist/webmcp.js
CHANGED
|
@@ -131,12 +131,30 @@
|
|
|
131
131
|
return descriptors;
|
|
132
132
|
}
|
|
133
133
|
/**
|
|
134
|
-
* Runs
|
|
134
|
+
* Runs one of the page tools CrestApps registered in this page itself, as executeTool would: on a copy of the input,
|
|
135
|
+
* with a signal, answering the result as text. Unlike executeTool, which reports every failure as "Tool execution
|
|
136
|
+
* failed", it keeps the message the tool failed with, so the model learns why and can correct itself.
|
|
137
|
+
*
|
|
138
|
+
* @param {any} tool The tool as it was registered, with its execute callback.
|
|
139
|
+
* @param {any} input The tool's input.
|
|
140
|
+
*/
|
|
141
|
+
async function runOwnTool(tool, input) {
|
|
142
|
+
var value = await tool.execute(JSON.parse(JSON.stringify(input)), { signal: new AbortController().signal });
|
|
143
|
+
if (typeof value === "string") return value;
|
|
144
|
+
return value === void 0 ? "" : JSON.stringify(value);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Runs a page tool the chat asked for, and answers what the hub expects back: `{ text, isError }`.
|
|
148
|
+
*
|
|
149
|
+
* A tool another script registered runs through the standard executeTool. A page tool CrestApps registered in this
|
|
150
|
+
* page itself, which `findOwnTool` answers, runs through its own execute callback, so a failure reaches the model
|
|
151
|
+
* with its reason.
|
|
135
152
|
*
|
|
136
153
|
* @param {any} modelContext The page's ModelContext.
|
|
137
154
|
* @param {string} name The tool's name.
|
|
138
155
|
* @param {string} argumentsJson The tool's input as JSON.
|
|
139
|
-
* @param {{ fromOrigins?: string[], excludedNames?: Set<string
|
|
156
|
+
* @param {{ fromOrigins?: string[], excludedNames?: Set<string>, findOwnTool?: (registeredTool: any) => any }} [options]
|
|
157
|
+
* The frames to look in, the names to refuse, and how to find the tools CrestApps registered in this page.
|
|
140
158
|
*/
|
|
141
159
|
async function runPageTool(modelContext, name, argumentsJson, options) {
|
|
142
160
|
var input;
|
|
@@ -165,6 +183,11 @@
|
|
|
165
183
|
text: "The page has no tool named \"" + name + "\".",
|
|
166
184
|
isError: true
|
|
167
185
|
};
|
|
186
|
+
var ownTool = options && typeof options.findOwnTool === "function" ? options.findOwnTool(tool) : null;
|
|
187
|
+
if (ownTool && typeof ownTool.execute === "function") return {
|
|
188
|
+
text: await runOwnTool(ownTool, input),
|
|
189
|
+
isError: false
|
|
190
|
+
};
|
|
168
191
|
var serialized = await modelContext.executeTool(tool, input);
|
|
169
192
|
var text = serialized;
|
|
170
193
|
try {
|
|
@@ -188,7 +211,8 @@
|
|
|
188
211
|
*
|
|
189
212
|
* @param {any} modelContext The page's ModelContext, or null when the page has no WebMCP.
|
|
190
213
|
* @param {any} connection The SignalR hub connection of the chat.
|
|
191
|
-
* @param {{ fromOrigins?: string[], excludedNames?: Set<string
|
|
214
|
+
* @param {{ fromOrigins?: string[], excludedNames?: Set<string>, findOwnTool?: (registeredTool: any) => any }} [options]
|
|
215
|
+
* The frames to look in, the names to leave out, and how to find the page tools CrestApps registered in this page.
|
|
192
216
|
*/
|
|
193
217
|
function connectChat(modelContext, connection, options) {
|
|
194
218
|
if (!modelContext) return {
|
|
@@ -237,6 +261,19 @@
|
|
|
237
261
|
}
|
|
238
262
|
};
|
|
239
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* Finds the page tool webmcp-page-tools.js registered in this window for a tool getTools() listed. A tool of another
|
|
266
|
+
* frame, or one another script registered, is not one of them.
|
|
267
|
+
*
|
|
268
|
+
* @param {any} win The window.
|
|
269
|
+
* @param {any} modelContext The window's ModelContext.
|
|
270
|
+
* @param {any} registeredTool The RegisteredTool record getTools() answered.
|
|
271
|
+
*/
|
|
272
|
+
function findOwnPageTool(win, modelContext, registeredTool) {
|
|
273
|
+
var pageTools = win.CrestAppsWebMcpPageTools;
|
|
274
|
+
if (!registeredTool || registeredTool.window !== win || !pageTools || typeof pageTools.getRegisteredTool !== "function") return null;
|
|
275
|
+
return pageTools.getRegisteredTool(modelContext, registeredTool.name);
|
|
276
|
+
}
|
|
240
277
|
function init(win) {
|
|
241
278
|
if (win.crestAppsWebMcp) return win.crestAppsWebMcp;
|
|
242
279
|
var config = readConfig(win.document) || {};
|
|
@@ -257,7 +294,10 @@
|
|
|
257
294
|
connectChat: function(connection) {
|
|
258
295
|
return connectChat(modelContext, connection, {
|
|
259
296
|
fromOrigins: Array.isArray(config.pageToolOrigins) ? config.pageToolOrigins : void 0,
|
|
260
|
-
excludedNames: serverToolNames
|
|
297
|
+
excludedNames: serverToolNames,
|
|
298
|
+
findOwnTool: function(registeredTool) {
|
|
299
|
+
return findOwnPageTool(win, modelContext, registeredTool);
|
|
300
|
+
}
|
|
261
301
|
});
|
|
262
302
|
}
|
|
263
303
|
};
|
|
@@ -272,6 +312,7 @@
|
|
|
272
312
|
registerServerTools,
|
|
273
313
|
toPageToolDescriptors,
|
|
274
314
|
runPageTool,
|
|
315
|
+
findOwnPageTool,
|
|
275
316
|
connectChat,
|
|
276
317
|
init
|
|
277
318
|
};
|
package/dist/webmcp.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webmcp.js","sources":["../../Assets/js/webmcp.js"],"sourcesContent":["// @ts-check\n/*\n * WebMCP for CrestApps.\n *\n * WebMCP (https://webmachinelearning.github.io/webmcp/) lets a web page give tools to AI agents through\n * `document.modelContext`. This script uses only that standard API:\n *\n * - It registers the AI tools the site lets the signed-in user call, each as a standard tool whose `execute` posts to\n * the site with the user's antiforgery token. Any agent that reads the page's tools, such as the browser's own\n * assistant, can find and run them.\n * - It lets the CrestApps AI chat use every tool the page exposes, whoever registered it: `getTools()` lists them,\n * and `executeTool()` runs the one the model calls. Only the site tools it registered itself are left out, because\n * the chat reaches those on the server.\n *\n * Load webmcp-polyfill.js first, so browsers without native WebMCP get the reference implementation; a browser that\n * has WebMCP keeps its own. The page configuration is read from a\n * `<script type=\"application/json\" id=\"crestapps-webmcp-config\">` block: `toolsUrl` and `invokeUrlTemplate` (with a\n * `{name}` placeholder) for the site's tools, `antiforgeryHeaderName` and `antiforgeryToken` for the calls,\n * `pageToolOrigins` for the cross-origin frames whose tools the chat may use, and any setting other page scripts need.\n */\n(function () {\n 'use strict';\n\n var CONFIG_ELEMENT_ID = 'crestapps-webmcp-config';\n var MAX_PAGE_TOOLS = 64;\n var TOOL_NAME = /^[A-Za-z0-9_.-]{1,128}$/;\n\n /**\n * Finds the page's WebMCP ModelContext.\n *\n * @param {any} doc The document.\n */\n function getModelContext(doc) {\n return doc && doc.modelContext ? doc.modelContext : null;\n }\n\n /**\n * Reads the configuration block the site writes into the page.\n *\n * @param {any} doc The document.\n */\n function readConfig(doc) {\n var element = doc && doc.getElementById ? doc.getElementById(CONFIG_ELEMENT_ID) : null;\n\n if (!element) {\n return null;\n }\n\n try {\n return JSON.parse(element.textContent);\n } catch (error) {\n return null;\n }\n }\n\n /**\n * Creates the standard tool that calls one of the site's AI tools.\n *\n * @param {{ name: string, title?: string, description: string, inputSchema?: any }} descriptor The tool the site lists.\n * @param {{ invokeUrlTemplate?: string, antiforgeryHeaderName?: string, antiforgeryToken?: string }} config The page configuration.\n * @param {Function} fetchImpl The fetch function.\n */\n function createServerTool(descriptor, config, fetchImpl) {\n if (!descriptor || !descriptor.name || !config || !config.invokeUrlTemplate) {\n throw new TypeError('A site tool needs a name and an address to call.');\n }\n\n var url = config.invokeUrlTemplate.replace('{name}', encodeURIComponent(descriptor.name));\n /** @type {any} */\n var tool = {\n name: descriptor.name,\n description: descriptor.description,\n // The site's tools read data the page did not write, so agents are told to treat what they answer as data.\n annotations: { untrustedContentHint: true },\n execute: async function (input) {\n /** @type {Record<string, string>} */\n var headers = { 'Content-Type': 'application/json' };\n\n if (config.antiforgeryHeaderName && config.antiforgeryToken) {\n headers[config.antiforgeryHeaderName] = config.antiforgeryToken;\n }\n\n var response = await fetchImpl(url, {\n method: 'POST',\n credentials: 'same-origin',\n headers: headers,\n body: JSON.stringify(input || {}),\n });\n\n if (!response.ok) {\n throw new Error('The site refused the tool call (' + response.status + ').');\n }\n\n var data = await response.json();\n var text = (Array.isArray(data.content) ? data.content : [])\n .filter(function (part) {\n return part && part.type === 'text';\n })\n .map(function (part) {\n return part.text == null ? '' : String(part.text);\n })\n .join('\\n');\n\n if (data.isError) {\n // A rejected execute is how WebMCP reports a failed tool.\n throw new Error(text || 'The tool failed.');\n }\n\n return text;\n },\n };\n\n if (descriptor.title) {\n tool.title = descriptor.title;\n }\n\n if (descriptor.inputSchema) {\n tool.inputSchema = descriptor.inputSchema;\n }\n\n return tool;\n }\n\n /**\n * Fetches the AI tools the site lets this user call and registers each with the page's ModelContext.\n *\n * @param {any} modelContext The page's ModelContext.\n * @param {any} config The page configuration.\n * @param {Function} fetchImpl The fetch function.\n * @param {AbortSignal} [signal] Unregisters the tools when aborted.\n * @returns {Promise<string[]>} The names of the tools registered.\n */\n async function registerServerTools(modelContext, config, fetchImpl, signal) {\n if (!modelContext || !config || !config.toolsUrl || !config.invokeUrlTemplate) {\n return [];\n }\n\n var descriptors;\n\n try {\n var response = await fetchImpl(config.toolsUrl, { credentials: 'same-origin', headers: { Accept: 'application/json' } });\n\n if (!response.ok) {\n return [];\n }\n\n var data = await response.json();\n\n descriptors = data && Array.isArray(data.tools) ? data.tools : [];\n } catch (error) {\n console.warn('The site\\'s WebMCP tools could not be loaded.', error);\n\n return [];\n }\n\n /** @type {string[]} */\n var names = [];\n\n for (var i = 0; i < descriptors.length; i++) {\n try {\n await modelContext.registerTool(createServerTool(descriptors[i], config, fetchImpl), signal ? { signal: signal } : {});\n names.push(descriptors[i].name);\n } catch (error) {\n console.warn('A site WebMCP tool could not be registered.', error);\n }\n }\n\n return names;\n }\n\n /**\n * Turns the page's registered tools into what the chat hub accepts: tools with valid WebMCP names, at most 64, with\n * their schemas as JSON text and their annotations as flags.\n *\n * @param {Array<any>} tools The RegisteredTool records getTools() answered.\n * @param {Set<string>} [excludedNames] The names to leave out.\n */\n function toPageToolDescriptors(tools, excludedNames) {\n /** @type {Array<any>} */\n var descriptors = [];\n\n (tools || []).forEach(function (tool) {\n if (descriptors.length >= MAX_PAGE_TOOLS || !tool || !TOOL_NAME.test(tool.name || '') || (excludedNames && excludedNames.has(tool.name))) {\n return;\n }\n\n var annotations = tool.annotations || {};\n\n descriptors.push({\n name: tool.name,\n title: tool.title || null,\n description: tool.description,\n inputSchema: tool.inputSchema === undefined ? null : JSON.stringify(tool.inputSchema),\n readOnly: annotations.readOnlyHint === true,\n consequential: annotations.consequentialHint === true,\n untrustedContent: annotations.untrustedContentHint === true,\n });\n });\n\n return descriptors;\n }\n\n /**\n * Runs a page tool the chat asked for with executeTool, and answers what the hub expects back: `{ text, isError }`.\n *\n * @param {any} modelContext The page's ModelContext.\n * @param {string} name The tool's name.\n * @param {string} argumentsJson The tool's input as JSON.\n * @param {{ fromOrigins?: string[], excludedNames?: Set<string> }} [options] The frames to look in and the names to refuse.\n */\n async function runPageTool(modelContext, name, argumentsJson, options) {\n var input;\n\n try {\n input = argumentsJson ? JSON.parse(argumentsJson) : {};\n } catch (error) {\n return { text: 'The tool input is not valid JSON.', isError: true };\n }\n\n if (!input || typeof input !== 'object' || Array.isArray(input)) {\n return { text: 'The tool input must be a JSON object.', isError: true };\n }\n\n var excludedNames = options && options.excludedNames;\n\n if (excludedNames && excludedNames.has(name)) {\n return { text: 'The tool \"' + name + '\" is not a page tool.', isError: true };\n }\n\n try {\n var tools = await modelContext.getTools(options && options.fromOrigins ? { fromOrigins: options.fromOrigins } : {});\n var tool = tools.find(function (candidate) {\n return candidate.name === name;\n });\n\n if (!tool) {\n return { text: 'The page has no tool named \"' + name + '\".', isError: true };\n }\n\n var serialized = await modelContext.executeTool(tool, input);\n var text = serialized;\n\n try {\n var value = JSON.parse(serialized);\n\n if (typeof value === 'string') {\n text = value;\n }\n } catch (error) {\n // The result is not JSON; keep it as it is.\n }\n\n return { text: text == null ? '' : String(text), isError: false };\n } catch (error) {\n return { text: 'The tool failed: ' + (error && error.message ? error.message : String(error)), isError: true };\n }\n }\n\n /**\n * Connects the page's tools to a CrestApps chat hub connection. The hub calls `ExecutePageTool` on the connection\n * and waits for the result; `sync` sends the page's tools before a message, only when they changed.\n *\n * @param {any} modelContext The page's ModelContext, or null when the page has no WebMCP.\n * @param {any} connection The SignalR hub connection of the chat.\n * @param {{ fromOrigins?: string[], excludedNames?: Set<string> }} [options] The frames to look in and the names to leave out.\n */\n function connectChat(modelContext, connection, options) {\n if (!modelContext) {\n return {\n sync: function () {\n return Promise.resolve(0);\n },\n reset: function () { },\n };\n }\n\n var settings = options || {};\n /** @type {string | null} */\n var lastSignature = null;\n\n connection.on('ExecutePageTool', function (name, argumentsJson) {\n return runPageTool(modelContext, name, argumentsJson, settings);\n });\n\n if (typeof connection.onreconnected === 'function') {\n // A reconnected connection has a new identifier, and the hub forgot the tools of the old one.\n connection.onreconnected(function () {\n lastSignature = null;\n });\n }\n\n async function sync() {\n var tools;\n\n try {\n tools = await modelContext.getTools(settings.fromOrigins ? { fromOrigins: settings.fromOrigins } : {});\n } catch (error) {\n console.warn('The page\\'s WebMCP tools could not be read.', error);\n\n return 0;\n }\n\n var descriptors = toPageToolDescriptors(tools, settings.excludedNames);\n var signature = JSON.stringify(descriptors);\n\n if (signature === lastSignature) {\n return descriptors.length;\n }\n\n if (descriptors.length === 0 && lastSignature === null) {\n lastSignature = signature;\n\n return 0;\n }\n\n try {\n if (descriptors.length === 0) {\n await connection.invoke('ClearPageTools');\n } else {\n await connection.invoke('SetPageTools', descriptors);\n }\n\n lastSignature = signature;\n } catch (error) {\n console.warn('The page tools could not be sent to the chat.', error);\n }\n\n return descriptors.length;\n }\n\n return {\n sync: sync,\n reset: function () {\n lastSignature = null;\n },\n };\n }\n\n function init(win) {\n if (win.crestAppsWebMcp) {\n return win.crestAppsWebMcp;\n }\n\n var config = readConfig(win.document) || {};\n var modelContext = getModelContext(win.document);\n /** @type {Set<string>} */\n var serverToolNames = new Set();\n var ready = modelContext && config.toolsUrl && typeof win.fetch === 'function'\n ? registerServerTools(modelContext, config, win.fetch.bind(win)).then(function (names) {\n names.forEach(function (name) {\n serverToolNames.add(name);\n });\n\n return names;\n })\n : Promise.resolve([]);\n\n var api = {\n config: config,\n ready: ready,\n isSupported: function () {\n return !!modelContext;\n },\n connectChat: function (connection) {\n return connectChat(modelContext, connection, {\n fromOrigins: Array.isArray(config.pageToolOrigins) ? config.pageToolOrigins : undefined,\n excludedNames: serverToolNames,\n });\n },\n };\n\n win.crestAppsWebMcp = api;\n\n if (win.document && typeof win.document.dispatchEvent === 'function' && typeof win.CustomEvent === 'function') {\n win.document.dispatchEvent(new win.CustomEvent('crestapps-webmcp-ready', { detail: api }));\n }\n\n return api;\n }\n\n window.CrestAppsWebMcp = {\n getModelContext: getModelContext,\n readConfig: readConfig,\n createServerTool: createServerTool,\n registerServerTools: registerServerTools,\n toPageToolDescriptors: toPageToolDescriptors,\n runPageTool: runPageTool,\n connectChat: connectChat,\n init: init,\n };\n\n if (window.document) {\n init(window);\n }\n})();\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"webmcp.js","sources":["../../Assets/js/webmcp.js"],"sourcesContent":["// @ts-check\n/*\n * WebMCP for CrestApps.\n *\n * WebMCP (https://webmachinelearning.github.io/webmcp/) lets a web page give tools to AI agents through\n * `document.modelContext`. This script uses only that standard API:\n *\n * - It registers the AI tools the site lets the signed-in user call, each as a standard tool whose `execute` posts to\n * the site with the user's antiforgery token. Any agent that reads the page's tools, such as the browser's own\n * assistant, can find and run them.\n * - It lets the CrestApps AI chat use every tool the page exposes, whoever registered it: `getTools()` lists them,\n * and `executeTool()` runs the one the model calls. Only the site tools it registered itself are left out, because\n * the chat reaches those on the server. The page tools of webmcp-page-tools.js run through their own `execute`,\n * because `executeTool()` hides why a tool failed, and the model needs the reason to correct itself.\n *\n * Load webmcp-polyfill.js first, so browsers without native WebMCP get the reference implementation; a browser that\n * has WebMCP keeps its own. The page configuration is read from a\n * `<script type=\"application/json\" id=\"crestapps-webmcp-config\">` block: `toolsUrl` and `invokeUrlTemplate` (with a\n * `{name}` placeholder) for the site's tools, `antiforgeryHeaderName` and `antiforgeryToken` for the calls,\n * `pageToolOrigins` for the cross-origin frames whose tools the chat may use, and any setting other page scripts need.\n */\n(function () {\n 'use strict';\n\n var CONFIG_ELEMENT_ID = 'crestapps-webmcp-config';\n var MAX_PAGE_TOOLS = 64;\n var TOOL_NAME = /^[A-Za-z0-9_.-]{1,128}$/;\n\n /**\n * Finds the page's WebMCP ModelContext.\n *\n * @param {any} doc The document.\n */\n function getModelContext(doc) {\n return doc && doc.modelContext ? doc.modelContext : null;\n }\n\n /**\n * Reads the configuration block the site writes into the page.\n *\n * @param {any} doc The document.\n */\n function readConfig(doc) {\n var element = doc && doc.getElementById ? doc.getElementById(CONFIG_ELEMENT_ID) : null;\n\n if (!element) {\n return null;\n }\n\n try {\n return JSON.parse(element.textContent);\n } catch (error) {\n return null;\n }\n }\n\n /**\n * Creates the standard tool that calls one of the site's AI tools.\n *\n * @param {{ name: string, title?: string, description: string, inputSchema?: any }} descriptor The tool the site lists.\n * @param {{ invokeUrlTemplate?: string, antiforgeryHeaderName?: string, antiforgeryToken?: string }} config The page configuration.\n * @param {Function} fetchImpl The fetch function.\n */\n function createServerTool(descriptor, config, fetchImpl) {\n if (!descriptor || !descriptor.name || !config || !config.invokeUrlTemplate) {\n throw new TypeError('A site tool needs a name and an address to call.');\n }\n\n var url = config.invokeUrlTemplate.replace('{name}', encodeURIComponent(descriptor.name));\n /** @type {any} */\n var tool = {\n name: descriptor.name,\n description: descriptor.description,\n // The site's tools read data the page did not write, so agents are told to treat what they answer as data.\n annotations: { untrustedContentHint: true },\n execute: async function (input) {\n /** @type {Record<string, string>} */\n var headers = { 'Content-Type': 'application/json' };\n\n if (config.antiforgeryHeaderName && config.antiforgeryToken) {\n headers[config.antiforgeryHeaderName] = config.antiforgeryToken;\n }\n\n var response = await fetchImpl(url, {\n method: 'POST',\n credentials: 'same-origin',\n headers: headers,\n body: JSON.stringify(input || {}),\n });\n\n if (!response.ok) {\n throw new Error('The site refused the tool call (' + response.status + ').');\n }\n\n var data = await response.json();\n var text = (Array.isArray(data.content) ? data.content : [])\n .filter(function (part) {\n return part && part.type === 'text';\n })\n .map(function (part) {\n return part.text == null ? '' : String(part.text);\n })\n .join('\\n');\n\n if (data.isError) {\n // A rejected execute is how WebMCP reports a failed tool.\n throw new Error(text || 'The tool failed.');\n }\n\n return text;\n },\n };\n\n if (descriptor.title) {\n tool.title = descriptor.title;\n }\n\n if (descriptor.inputSchema) {\n tool.inputSchema = descriptor.inputSchema;\n }\n\n return tool;\n }\n\n /**\n * Fetches the AI tools the site lets this user call and registers each with the page's ModelContext.\n *\n * @param {any} modelContext The page's ModelContext.\n * @param {any} config The page configuration.\n * @param {Function} fetchImpl The fetch function.\n * @param {AbortSignal} [signal] Unregisters the tools when aborted.\n * @returns {Promise<string[]>} The names of the tools registered.\n */\n async function registerServerTools(modelContext, config, fetchImpl, signal) {\n if (!modelContext || !config || !config.toolsUrl || !config.invokeUrlTemplate) {\n return [];\n }\n\n var descriptors;\n\n try {\n var response = await fetchImpl(config.toolsUrl, { credentials: 'same-origin', headers: { Accept: 'application/json' } });\n\n if (!response.ok) {\n return [];\n }\n\n var data = await response.json();\n\n descriptors = data && Array.isArray(data.tools) ? data.tools : [];\n } catch (error) {\n console.warn('The site\\'s WebMCP tools could not be loaded.', error);\n\n return [];\n }\n\n /** @type {string[]} */\n var names = [];\n\n for (var i = 0; i < descriptors.length; i++) {\n try {\n await modelContext.registerTool(createServerTool(descriptors[i], config, fetchImpl), signal ? { signal: signal } : {});\n names.push(descriptors[i].name);\n } catch (error) {\n console.warn('A site WebMCP tool could not be registered.', error);\n }\n }\n\n return names;\n }\n\n /**\n * Turns the page's registered tools into what the chat hub accepts: tools with valid WebMCP names, at most 64, with\n * their schemas as JSON text and their annotations as flags.\n *\n * @param {Array<any>} tools The RegisteredTool records getTools() answered.\n * @param {Set<string>} [excludedNames] The names to leave out.\n */\n function toPageToolDescriptors(tools, excludedNames) {\n /** @type {Array<any>} */\n var descriptors = [];\n\n (tools || []).forEach(function (tool) {\n if (descriptors.length >= MAX_PAGE_TOOLS || !tool || !TOOL_NAME.test(tool.name || '') || (excludedNames && excludedNames.has(tool.name))) {\n return;\n }\n\n var annotations = tool.annotations || {};\n\n descriptors.push({\n name: tool.name,\n title: tool.title || null,\n description: tool.description,\n inputSchema: tool.inputSchema === undefined ? null : JSON.stringify(tool.inputSchema),\n readOnly: annotations.readOnlyHint === true,\n consequential: annotations.consequentialHint === true,\n untrustedContent: annotations.untrustedContentHint === true,\n });\n });\n\n return descriptors;\n }\n\n /**\n * Runs one of the page tools CrestApps registered in this page itself, as executeTool would: on a copy of the input,\n * with a signal, answering the result as text. Unlike executeTool, which reports every failure as \"Tool execution\n * failed\", it keeps the message the tool failed with, so the model learns why and can correct itself.\n *\n * @param {any} tool The tool as it was registered, with its execute callback.\n * @param {any} input The tool's input.\n */\n async function runOwnTool(tool, input) {\n var value = await tool.execute(JSON.parse(JSON.stringify(input)), { signal: new AbortController().signal });\n\n if (typeof value === 'string') {\n return value;\n }\n\n return value === undefined ? '' : JSON.stringify(value);\n }\n\n /**\n * Runs a page tool the chat asked for, and answers what the hub expects back: `{ text, isError }`.\n *\n * A tool another script registered runs through the standard executeTool. A page tool CrestApps registered in this\n * page itself, which `findOwnTool` answers, runs through its own execute callback, so a failure reaches the model\n * with its reason.\n *\n * @param {any} modelContext The page's ModelContext.\n * @param {string} name The tool's name.\n * @param {string} argumentsJson The tool's input as JSON.\n * @param {{ fromOrigins?: string[], excludedNames?: Set<string>, findOwnTool?: (registeredTool: any) => any }} [options]\n * The frames to look in, the names to refuse, and how to find the tools CrestApps registered in this page.\n */\n async function runPageTool(modelContext, name, argumentsJson, options) {\n var input;\n\n try {\n input = argumentsJson ? JSON.parse(argumentsJson) : {};\n } catch (error) {\n return { text: 'The tool input is not valid JSON.', isError: true };\n }\n\n if (!input || typeof input !== 'object' || Array.isArray(input)) {\n return { text: 'The tool input must be a JSON object.', isError: true };\n }\n\n var excludedNames = options && options.excludedNames;\n\n if (excludedNames && excludedNames.has(name)) {\n return { text: 'The tool \"' + name + '\" is not a page tool.', isError: true };\n }\n\n try {\n var tools = await modelContext.getTools(options && options.fromOrigins ? { fromOrigins: options.fromOrigins } : {});\n var tool = tools.find(function (candidate) {\n return candidate.name === name;\n });\n\n if (!tool) {\n return { text: 'The page has no tool named \"' + name + '\".', isError: true };\n }\n\n var ownTool = options && typeof options.findOwnTool === 'function' ? options.findOwnTool(tool) : null;\n\n if (ownTool && typeof ownTool.execute === 'function') {\n return { text: await runOwnTool(ownTool, input), isError: false };\n }\n\n var serialized = await modelContext.executeTool(tool, input);\n var text = serialized;\n\n try {\n var value = JSON.parse(serialized);\n\n if (typeof value === 'string') {\n text = value;\n }\n } catch (error) {\n // The result is not JSON; keep it as it is.\n }\n\n return { text: text == null ? '' : String(text), isError: false };\n } catch (error) {\n return { text: 'The tool failed: ' + (error && error.message ? error.message : String(error)), isError: true };\n }\n }\n\n /**\n * Connects the page's tools to a CrestApps chat hub connection. The hub calls `ExecutePageTool` on the connection\n * and waits for the result; `sync` sends the page's tools before a message, only when they changed.\n *\n * @param {any} modelContext The page's ModelContext, or null when the page has no WebMCP.\n * @param {any} connection The SignalR hub connection of the chat.\n * @param {{ fromOrigins?: string[], excludedNames?: Set<string>, findOwnTool?: (registeredTool: any) => any }} [options]\n * The frames to look in, the names to leave out, and how to find the page tools CrestApps registered in this page.\n */\n function connectChat(modelContext, connection, options) {\n if (!modelContext) {\n return {\n sync: function () {\n return Promise.resolve(0);\n },\n reset: function () { },\n };\n }\n\n var settings = options || {};\n /** @type {string | null} */\n var lastSignature = null;\n\n connection.on('ExecutePageTool', function (name, argumentsJson) {\n return runPageTool(modelContext, name, argumentsJson, settings);\n });\n\n if (typeof connection.onreconnected === 'function') {\n // A reconnected connection has a new identifier, and the hub forgot the tools of the old one.\n connection.onreconnected(function () {\n lastSignature = null;\n });\n }\n\n async function sync() {\n var tools;\n\n try {\n tools = await modelContext.getTools(settings.fromOrigins ? { fromOrigins: settings.fromOrigins } : {});\n } catch (error) {\n console.warn('The page\\'s WebMCP tools could not be read.', error);\n\n return 0;\n }\n\n var descriptors = toPageToolDescriptors(tools, settings.excludedNames);\n var signature = JSON.stringify(descriptors);\n\n if (signature === lastSignature) {\n return descriptors.length;\n }\n\n if (descriptors.length === 0 && lastSignature === null) {\n lastSignature = signature;\n\n return 0;\n }\n\n try {\n if (descriptors.length === 0) {\n await connection.invoke('ClearPageTools');\n } else {\n await connection.invoke('SetPageTools', descriptors);\n }\n\n lastSignature = signature;\n } catch (error) {\n console.warn('The page tools could not be sent to the chat.', error);\n }\n\n return descriptors.length;\n }\n\n return {\n sync: sync,\n reset: function () {\n lastSignature = null;\n },\n };\n }\n\n /**\n * Finds the page tool webmcp-page-tools.js registered in this window for a tool getTools() listed. A tool of another\n * frame, or one another script registered, is not one of them.\n *\n * @param {any} win The window.\n * @param {any} modelContext The window's ModelContext.\n * @param {any} registeredTool The RegisteredTool record getTools() answered.\n */\n function findOwnPageTool(win, modelContext, registeredTool) {\n var pageTools = win.CrestAppsWebMcpPageTools;\n\n if (!registeredTool || registeredTool.window !== win || !pageTools || typeof pageTools.getRegisteredTool !== 'function') {\n return null;\n }\n\n return pageTools.getRegisteredTool(modelContext, registeredTool.name);\n }\n\n function init(win) {\n if (win.crestAppsWebMcp) {\n return win.crestAppsWebMcp;\n }\n\n var config = readConfig(win.document) || {};\n var modelContext = getModelContext(win.document);\n /** @type {Set<string>} */\n var serverToolNames = new Set();\n var ready = modelContext && config.toolsUrl && typeof win.fetch === 'function'\n ? registerServerTools(modelContext, config, win.fetch.bind(win)).then(function (names) {\n names.forEach(function (name) {\n serverToolNames.add(name);\n });\n\n return names;\n })\n : Promise.resolve([]);\n\n var api = {\n config: config,\n ready: ready,\n isSupported: function () {\n return !!modelContext;\n },\n connectChat: function (connection) {\n return connectChat(modelContext, connection, {\n fromOrigins: Array.isArray(config.pageToolOrigins) ? config.pageToolOrigins : undefined,\n excludedNames: serverToolNames,\n findOwnTool: function (registeredTool) {\n return findOwnPageTool(win, modelContext, registeredTool);\n },\n });\n },\n };\n\n win.crestAppsWebMcp = api;\n\n if (win.document && typeof win.document.dispatchEvent === 'function' && typeof win.CustomEvent === 'function') {\n win.document.dispatchEvent(new win.CustomEvent('crestapps-webmcp-ready', { detail: api }));\n }\n\n return api;\n }\n\n window.CrestAppsWebMcp = {\n getModelContext: getModelContext,\n readConfig: readConfig,\n createServerTool: createServerTool,\n registerServerTools: registerServerTools,\n toPageToolDescriptors: toPageToolDescriptors,\n runPageTool: runPageTool,\n findOwnPageTool: findOwnPageTool,\n connectChat: connectChat,\n init: init,\n };\n\n if (window.document) {\n init(window);\n }\n})();\n"],"names":[],"mappings":"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"}
|
package/dist/webmcp.min.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
(function(){(function(){"use strict";var e=/^[A-Za-z0-9_.-]{1,128}$/;function t(e){return e&&e.modelContext?e.modelContext:null}function n(e){var t=e&&e.getElementById?e.getElementById(`crestapps-webmcp-config`):null;if(!t)return null;try{return JSON.parse(t.textContent)}catch{return null}}function r(e,t,n){if(!e||!e.name||!t||!t.invokeUrlTemplate)throw TypeError(`A site tool needs a name and an address to call.`);var r=t.invokeUrlTemplate.replace(`{name}`,encodeURIComponent(e.name)),i={name:e.name,description:e.description,annotations:{untrustedContentHint:!0},execute:async function(e){var i={"Content-Type":`application/json`};t.antiforgeryHeaderName&&t.antiforgeryToken&&(i[t.antiforgeryHeaderName]=t.antiforgeryToken);var a=await n(r,{method:`POST`,credentials:`same-origin`,headers:i,body:JSON.stringify(e||{})});if(!a.ok)throw Error(`The site refused the tool call (`+a.status+`).`);var o=await a.json(),s=(Array.isArray(o.content)?o.content:[]).filter(function(e){return e&&e.type===`text`}).map(function(e){return e.text==null?``:String(e.text)}).join(`
|
|
2
|
-
`);if(o.isError)throw Error(s||`The tool failed.`);return s}};return e.title&&(i.title=e.title),e.inputSchema&&(i.inputSchema=e.inputSchema),i}async function i(e,t,n,i){if(!e||!t||!t.toolsUrl||!t.invokeUrlTemplate)return[];var a;try{var o=await n(t.toolsUrl,{credentials:`same-origin`,headers:{Accept:`application/json`}});if(!o.ok)return[];var s=await o.json();a=s&&Array.isArray(s.tools)?s.tools:[]}catch(e){return console.warn(`The site's WebMCP tools could not be loaded.`,e),[]}for(var c=[],l=0;l<a.length;l++)try{await e.registerTool(r(a[l],t,n),i?{signal:i}:{}),c.push(a[l].name)}catch(e){console.warn(`A site WebMCP tool could not be registered.`,e)}return c}function a(t,n){var r=[];return(t||[]).forEach(function(t){if(!(r.length>=64||!t||!e.test(t.name||``)||n&&n.has(t.name))){var i=t.annotations||{};r.push({name:t.name,title:t.title||null,description:t.description,inputSchema:t.inputSchema===void 0?null:JSON.stringify(t.inputSchema),readOnly:i.readOnlyHint===!0,consequential:i.consequentialHint===!0,untrustedContent:i.untrustedContentHint===!0})}}),r}async function o(e,t,n,r){var i;try{i=n?JSON.parse(n):{}}catch{return{text:`The tool input is not valid JSON.`,isError:!0}}if(!i||typeof i!=`object`||Array.isArray(i))return{text:`The tool input must be a JSON object.`,isError:!0};var a=r&&r.excludedNames;if(a&&a.has(t))return{text:`The tool "`+t+`" is not a page tool.`,isError:!0};try{var
|
|
2
|
+
`);if(o.isError)throw Error(s||`The tool failed.`);return s}};return e.title&&(i.title=e.title),e.inputSchema&&(i.inputSchema=e.inputSchema),i}async function i(e,t,n,i){if(!e||!t||!t.toolsUrl||!t.invokeUrlTemplate)return[];var a;try{var o=await n(t.toolsUrl,{credentials:`same-origin`,headers:{Accept:`application/json`}});if(!o.ok)return[];var s=await o.json();a=s&&Array.isArray(s.tools)?s.tools:[]}catch(e){return console.warn(`The site's WebMCP tools could not be loaded.`,e),[]}for(var c=[],l=0;l<a.length;l++)try{await e.registerTool(r(a[l],t,n),i?{signal:i}:{}),c.push(a[l].name)}catch(e){console.warn(`A site WebMCP tool could not be registered.`,e)}return c}function a(t,n){var r=[];return(t||[]).forEach(function(t){if(!(r.length>=64||!t||!e.test(t.name||``)||n&&n.has(t.name))){var i=t.annotations||{};r.push({name:t.name,title:t.title||null,description:t.description,inputSchema:t.inputSchema===void 0?null:JSON.stringify(t.inputSchema),readOnly:i.readOnlyHint===!0,consequential:i.consequentialHint===!0,untrustedContent:i.untrustedContentHint===!0})}}),r}async function o(e,t){var n=await e.execute(JSON.parse(JSON.stringify(t)),{signal:new AbortController().signal});return typeof n==`string`?n:n===void 0?``:JSON.stringify(n)}async function s(e,t,n,r){var i;try{i=n?JSON.parse(n):{}}catch{return{text:`The tool input is not valid JSON.`,isError:!0}}if(!i||typeof i!=`object`||Array.isArray(i))return{text:`The tool input must be a JSON object.`,isError:!0};var a=r&&r.excludedNames;if(a&&a.has(t))return{text:`The tool "`+t+`" is not a page tool.`,isError:!0};try{var s=(await e.getTools(r&&r.fromOrigins?{fromOrigins:r.fromOrigins}:{})).find(function(e){return e.name===t});if(!s)return{text:`The page has no tool named "`+t+`".`,isError:!0};var c=r&&typeof r.findOwnTool==`function`?r.findOwnTool(s):null;if(c&&typeof c.execute==`function`)return{text:await o(c,i),isError:!1};var l=await e.executeTool(s,i),u=l;try{var d=JSON.parse(l);typeof d==`string`&&(u=d)}catch{}return{text:u==null?``:String(u),isError:!1}}catch(e){return{text:`The tool failed: `+(e&&e.message?e.message:String(e)),isError:!0}}}function c(e,t,n){if(!e)return{sync:function(){return Promise.resolve(0)},reset:function(){}};var r=n||{},i=null;t.on(`ExecutePageTool`,function(t,n){return s(e,t,n,r)}),typeof t.onreconnected==`function`&&t.onreconnected(function(){i=null});async function o(){var n;try{n=await e.getTools(r.fromOrigins?{fromOrigins:r.fromOrigins}:{})}catch(e){return console.warn(`The page's WebMCP tools could not be read.`,e),0}var o=a(n,r.excludedNames),s=JSON.stringify(o);if(s===i)return o.length;if(o.length===0&&i===null)return i=s,0;try{o.length===0?await t.invoke(`ClearPageTools`):await t.invoke(`SetPageTools`,o),i=s}catch(e){console.warn(`The page tools could not be sent to the chat.`,e)}return o.length}return{sync:o,reset:function(){i=null}}}function l(e,t,n){var r=e.CrestAppsWebMcpPageTools;return!n||n.window!==e||!r||typeof r.getRegisteredTool!=`function`?null:r.getRegisteredTool(t,n.name)}function u(e){if(e.crestAppsWebMcp)return e.crestAppsWebMcp;var r=n(e.document)||{},a=t(e.document),o=new Set,s={config:r,ready:a&&r.toolsUrl&&typeof e.fetch==`function`?i(a,r,e.fetch.bind(e)).then(function(e){return e.forEach(function(e){o.add(e)}),e}):Promise.resolve([]),isSupported:function(){return!!a},connectChat:function(t){return c(a,t,{fromOrigins:Array.isArray(r.pageToolOrigins)?r.pageToolOrigins:void 0,excludedNames:o,findOwnTool:function(t){return l(e,a,t)}})}};return e.crestAppsWebMcp=s,e.document&&typeof e.document.dispatchEvent==`function`&&typeof e.CustomEvent==`function`&&e.document.dispatchEvent(new e.CustomEvent(`crestapps-webmcp-ready`,{detail:s})),s}window.CrestAppsWebMcp={getModelContext:t,readConfig:n,createServerTool:r,registerServerTools:i,toPageToolDescriptors:a,runPageTool:s,findOwnPageTool:l,connectChat:c,init:u},window.document&&u(window)})()})();
|
|
3
3
|
//# sourceMappingURL=webmcp.min.js.map
|
package/dist/webmcp.min.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webmcp.min.js","sources":["../../Assets/js/webmcp.js"],"sourcesContent":["// @ts-check\n/*\n * WebMCP for CrestApps.\n *\n * WebMCP (https://webmachinelearning.github.io/webmcp/) lets a web page give tools to AI agents through\n * `document.modelContext`. This script uses only that standard API:\n *\n * - It registers the AI tools the site lets the signed-in user call, each as a standard tool whose `execute` posts to\n * the site with the user's antiforgery token. Any agent that reads the page's tools, such as the browser's own\n * assistant, can find and run them.\n * - It lets the CrestApps AI chat use every tool the page exposes, whoever registered it: `getTools()` lists them,\n * and `executeTool()` runs the one the model calls. Only the site tools it registered itself are left out, because\n * the chat reaches those on the server.\n *\n * Load webmcp-polyfill.js first, so browsers without native WebMCP get the reference implementation; a browser that\n * has WebMCP keeps its own. The page configuration is read from a\n * `<script type=\"application/json\" id=\"crestapps-webmcp-config\">` block: `toolsUrl` and `invokeUrlTemplate` (with a\n * `{name}` placeholder) for the site's tools, `antiforgeryHeaderName` and `antiforgeryToken` for the calls,\n * `pageToolOrigins` for the cross-origin frames whose tools the chat may use, and any setting other page scripts need.\n */\n(function () {\n 'use strict';\n\n var CONFIG_ELEMENT_ID = 'crestapps-webmcp-config';\n var MAX_PAGE_TOOLS = 64;\n var TOOL_NAME = /^[A-Za-z0-9_.-]{1,128}$/;\n\n /**\n * Finds the page's WebMCP ModelContext.\n *\n * @param {any} doc The document.\n */\n function getModelContext(doc) {\n return doc && doc.modelContext ? doc.modelContext : null;\n }\n\n /**\n * Reads the configuration block the site writes into the page.\n *\n * @param {any} doc The document.\n */\n function readConfig(doc) {\n var element = doc && doc.getElementById ? doc.getElementById(CONFIG_ELEMENT_ID) : null;\n\n if (!element) {\n return null;\n }\n\n try {\n return JSON.parse(element.textContent);\n } catch (error) {\n return null;\n }\n }\n\n /**\n * Creates the standard tool that calls one of the site's AI tools.\n *\n * @param {{ name: string, title?: string, description: string, inputSchema?: any }} descriptor The tool the site lists.\n * @param {{ invokeUrlTemplate?: string, antiforgeryHeaderName?: string, antiforgeryToken?: string }} config The page configuration.\n * @param {Function} fetchImpl The fetch function.\n */\n function createServerTool(descriptor, config, fetchImpl) {\n if (!descriptor || !descriptor.name || !config || !config.invokeUrlTemplate) {\n throw new TypeError('A site tool needs a name and an address to call.');\n }\n\n var url = config.invokeUrlTemplate.replace('{name}', encodeURIComponent(descriptor.name));\n /** @type {any} */\n var tool = {\n name: descriptor.name,\n description: descriptor.description,\n // The site's tools read data the page did not write, so agents are told to treat what they answer as data.\n annotations: { untrustedContentHint: true },\n execute: async function (input) {\n /** @type {Record<string, string>} */\n var headers = { 'Content-Type': 'application/json' };\n\n if (config.antiforgeryHeaderName && config.antiforgeryToken) {\n headers[config.antiforgeryHeaderName] = config.antiforgeryToken;\n }\n\n var response = await fetchImpl(url, {\n method: 'POST',\n credentials: 'same-origin',\n headers: headers,\n body: JSON.stringify(input || {}),\n });\n\n if (!response.ok) {\n throw new Error('The site refused the tool call (' + response.status + ').');\n }\n\n var data = await response.json();\n var text = (Array.isArray(data.content) ? data.content : [])\n .filter(function (part) {\n return part && part.type === 'text';\n })\n .map(function (part) {\n return part.text == null ? '' : String(part.text);\n })\n .join('\\n');\n\n if (data.isError) {\n // A rejected execute is how WebMCP reports a failed tool.\n throw new Error(text || 'The tool failed.');\n }\n\n return text;\n },\n };\n\n if (descriptor.title) {\n tool.title = descriptor.title;\n }\n\n if (descriptor.inputSchema) {\n tool.inputSchema = descriptor.inputSchema;\n }\n\n return tool;\n }\n\n /**\n * Fetches the AI tools the site lets this user call and registers each with the page's ModelContext.\n *\n * @param {any} modelContext The page's ModelContext.\n * @param {any} config The page configuration.\n * @param {Function} fetchImpl The fetch function.\n * @param {AbortSignal} [signal] Unregisters the tools when aborted.\n * @returns {Promise<string[]>} The names of the tools registered.\n */\n async function registerServerTools(modelContext, config, fetchImpl, signal) {\n if (!modelContext || !config || !config.toolsUrl || !config.invokeUrlTemplate) {\n return [];\n }\n\n var descriptors;\n\n try {\n var response = await fetchImpl(config.toolsUrl, { credentials: 'same-origin', headers: { Accept: 'application/json' } });\n\n if (!response.ok) {\n return [];\n }\n\n var data = await response.json();\n\n descriptors = data && Array.isArray(data.tools) ? data.tools : [];\n } catch (error) {\n console.warn('The site\\'s WebMCP tools could not be loaded.', error);\n\n return [];\n }\n\n /** @type {string[]} */\n var names = [];\n\n for (var i = 0; i < descriptors.length; i++) {\n try {\n await modelContext.registerTool(createServerTool(descriptors[i], config, fetchImpl), signal ? { signal: signal } : {});\n names.push(descriptors[i].name);\n } catch (error) {\n console.warn('A site WebMCP tool could not be registered.', error);\n }\n }\n\n return names;\n }\n\n /**\n * Turns the page's registered tools into what the chat hub accepts: tools with valid WebMCP names, at most 64, with\n * their schemas as JSON text and their annotations as flags.\n *\n * @param {Array<any>} tools The RegisteredTool records getTools() answered.\n * @param {Set<string>} [excludedNames] The names to leave out.\n */\n function toPageToolDescriptors(tools, excludedNames) {\n /** @type {Array<any>} */\n var descriptors = [];\n\n (tools || []).forEach(function (tool) {\n if (descriptors.length >= MAX_PAGE_TOOLS || !tool || !TOOL_NAME.test(tool.name || '') || (excludedNames && excludedNames.has(tool.name))) {\n return;\n }\n\n var annotations = tool.annotations || {};\n\n descriptors.push({\n name: tool.name,\n title: tool.title || null,\n description: tool.description,\n inputSchema: tool.inputSchema === undefined ? null : JSON.stringify(tool.inputSchema),\n readOnly: annotations.readOnlyHint === true,\n consequential: annotations.consequentialHint === true,\n untrustedContent: annotations.untrustedContentHint === true,\n });\n });\n\n return descriptors;\n }\n\n /**\n * Runs a page tool the chat asked for with executeTool, and answers what the hub expects back: `{ text, isError }`.\n *\n * @param {any} modelContext The page's ModelContext.\n * @param {string} name The tool's name.\n * @param {string} argumentsJson The tool's input as JSON.\n * @param {{ fromOrigins?: string[], excludedNames?: Set<string> }} [options] The frames to look in and the names to refuse.\n */\n async function runPageTool(modelContext, name, argumentsJson, options) {\n var input;\n\n try {\n input = argumentsJson ? JSON.parse(argumentsJson) : {};\n } catch (error) {\n return { text: 'The tool input is not valid JSON.', isError: true };\n }\n\n if (!input || typeof input !== 'object' || Array.isArray(input)) {\n return { text: 'The tool input must be a JSON object.', isError: true };\n }\n\n var excludedNames = options && options.excludedNames;\n\n if (excludedNames && excludedNames.has(name)) {\n return { text: 'The tool \"' + name + '\" is not a page tool.', isError: true };\n }\n\n try {\n var tools = await modelContext.getTools(options && options.fromOrigins ? { fromOrigins: options.fromOrigins } : {});\n var tool = tools.find(function (candidate) {\n return candidate.name === name;\n });\n\n if (!tool) {\n return { text: 'The page has no tool named \"' + name + '\".', isError: true };\n }\n\n var serialized = await modelContext.executeTool(tool, input);\n var text = serialized;\n\n try {\n var value = JSON.parse(serialized);\n\n if (typeof value === 'string') {\n text = value;\n }\n } catch (error) {\n // The result is not JSON; keep it as it is.\n }\n\n return { text: text == null ? '' : String(text), isError: false };\n } catch (error) {\n return { text: 'The tool failed: ' + (error && error.message ? error.message : String(error)), isError: true };\n }\n }\n\n /**\n * Connects the page's tools to a CrestApps chat hub connection. The hub calls `ExecutePageTool` on the connection\n * and waits for the result; `sync` sends the page's tools before a message, only when they changed.\n *\n * @param {any} modelContext The page's ModelContext, or null when the page has no WebMCP.\n * @param {any} connection The SignalR hub connection of the chat.\n * @param {{ fromOrigins?: string[], excludedNames?: Set<string> }} [options] The frames to look in and the names to leave out.\n */\n function connectChat(modelContext, connection, options) {\n if (!modelContext) {\n return {\n sync: function () {\n return Promise.resolve(0);\n },\n reset: function () { },\n };\n }\n\n var settings = options || {};\n /** @type {string | null} */\n var lastSignature = null;\n\n connection.on('ExecutePageTool', function (name, argumentsJson) {\n return runPageTool(modelContext, name, argumentsJson, settings);\n });\n\n if (typeof connection.onreconnected === 'function') {\n // A reconnected connection has a new identifier, and the hub forgot the tools of the old one.\n connection.onreconnected(function () {\n lastSignature = null;\n });\n }\n\n async function sync() {\n var tools;\n\n try {\n tools = await modelContext.getTools(settings.fromOrigins ? { fromOrigins: settings.fromOrigins } : {});\n } catch (error) {\n console.warn('The page\\'s WebMCP tools could not be read.', error);\n\n return 0;\n }\n\n var descriptors = toPageToolDescriptors(tools, settings.excludedNames);\n var signature = JSON.stringify(descriptors);\n\n if (signature === lastSignature) {\n return descriptors.length;\n }\n\n if (descriptors.length === 0 && lastSignature === null) {\n lastSignature = signature;\n\n return 0;\n }\n\n try {\n if (descriptors.length === 0) {\n await connection.invoke('ClearPageTools');\n } else {\n await connection.invoke('SetPageTools', descriptors);\n }\n\n lastSignature = signature;\n } catch (error) {\n console.warn('The page tools could not be sent to the chat.', error);\n }\n\n return descriptors.length;\n }\n\n return {\n sync: sync,\n reset: function () {\n lastSignature = null;\n },\n };\n }\n\n function init(win) {\n if (win.crestAppsWebMcp) {\n return win.crestAppsWebMcp;\n }\n\n var config = readConfig(win.document) || {};\n var modelContext = getModelContext(win.document);\n /** @type {Set<string>} */\n var serverToolNames = new Set();\n var ready = modelContext && config.toolsUrl && typeof win.fetch === 'function'\n ? registerServerTools(modelContext, config, win.fetch.bind(win)).then(function (names) {\n names.forEach(function (name) {\n serverToolNames.add(name);\n });\n\n return names;\n })\n : Promise.resolve([]);\n\n var api = {\n config: config,\n ready: ready,\n isSupported: function () {\n return !!modelContext;\n },\n connectChat: function (connection) {\n return connectChat(modelContext, connection, {\n fromOrigins: Array.isArray(config.pageToolOrigins) ? config.pageToolOrigins : undefined,\n excludedNames: serverToolNames,\n });\n },\n };\n\n win.crestAppsWebMcp = api;\n\n if (win.document && typeof win.document.dispatchEvent === 'function' && typeof win.CustomEvent === 'function') {\n win.document.dispatchEvent(new win.CustomEvent('crestapps-webmcp-ready', { detail: api }));\n }\n\n return api;\n }\n\n window.CrestAppsWebMcp = {\n getModelContext: getModelContext,\n readConfig: readConfig,\n createServerTool: createServerTool,\n registerServerTools: registerServerTools,\n toPageToolDescriptors: toPageToolDescriptors,\n runPageTool: runPageTool,\n connectChat: connectChat,\n init: init,\n };\n\n if (window.document) {\n init(window);\n }\n})();\n"],"names":["TOOL_NAME","getModelContext","doc","readConfig","element","createServerTool","descriptor","config","url","tool","headers","response","fetchImpl","input","data","text","part","registerServerTools","modelContext","descriptors","error","names","i","signal","toPageToolDescriptors","tools","excludedNames","annotations","runPageTool","argumentsJson","options","name","candidate","serialized","value","connectChat","settings","lastSignature","connection","sync","signature","init","win","serverToolNames","api"],"mappings":"C,YAoBC,UAAY,CACX,aAEA,IAEIA,EAAY,0BAOhB,SAASC,EAAgB,EAAK,CAC5B,OAAOC,GAAOA,EAAI,aAAeA,EAAI,aAAe,IACtD,CAOA,SAASC,EAAW,EAAK,CACvB,IAAIC,EAAUF,GAAOA,EAAI,eAAiBA,EAAI,eAAe,yBAAiB,EAAI,KAElF,GAAI,CAACE,EACH,OAAO,KAGT,GAAI,CACF,OAAO,KAAK,MAAMA,EAAQ,WAAW,CACvC,MAAgB,CACd,OAAO,IACT,CACF,CASA,SAASC,EAAiB,EAAY,EAAQ,EAAW,CACvD,GAAI,CAACC,GAAc,CAACA,EAAW,MAAQ,CAACC,GAAU,CAACA,EAAO,kBACxD,MAAU,UAAU,kDAAkD,EAGxE,IAAIC,EAAMD,EAAO,kBAAkB,QAAQ,SAAU,mBAAmBD,EAAW,IAAI,CAAC,EAEpFG,EAAO,CACT,KAAMH,EAAW,KACjB,YAAaA,EAAW,YAExB,YAAa,CAAE,qBAAsB,EAAK,EAC1C,QAAS,eAAgB,EAAO,CAE9B,IAAII,EAAU,CAAE,eAAgB,kBAAmB,EAE/CH,EAAO,uBAAyBA,EAAO,mBACzC,EAAQA,EAAO,uBAAyBA,EAAO,kBAGjD,IAAII,EAAW,MAAMC,EAAUJ,EAAK,CAClC,OAAQ,OACR,YAAa,cACJ,UACT,KAAM,KAAK,UAAUK,GAAS,CAAC,CAAC,CAClC,CAAC,EAED,GAAI,CAACF,EAAS,GACZ,MAAU,MAAM,mCAAqCA,EAAS,OAAS,IAAI,EAG7E,IAAIG,EAAO,MAAMH,EAAS,KAAK,EAC3BI,GAAQ,MAAM,QAAQD,EAAK,OAAO,EAAIA,EAAK,QAAU,CAAC,EAAA,CACvD,OAAO,SAAU,EAAM,CACtB,OAAOE,GAAQA,EAAK,OAAS,MAC/B,CAAC,CAAC,CACD,IAAI,SAAU,EAAM,CACnB,OAAOA,EAAK,MAAQ,KAAO,GAAK,OAAOA,EAAK,IAAI,CAClD,CAAC,CAAC,CACD,KAAK;CAAI,EAEZ,GAAIF,EAAK,QAEP,MAAU,MAAMC,GAAQ,kBAAkB,EAG5C,OAAOA,CACT,CACF,EAUA,OARIT,EAAW,QACb,EAAK,MAAQA,EAAW,OAGtBA,EAAW,cACb,EAAK,YAAcA,EAAW,aAGzBG,CACT,CAWA,eAAeQ,EAAoB,EAAc,EAAQ,EAAW,EAAQ,CAC1E,GAAI,CAACC,GAAgB,CAACX,GAAU,CAACA,EAAO,UAAY,CAACA,EAAO,kBAC1D,MAAO,CAAC,EAGV,IAAIY,EAEJ,GAAI,CACF,IAAIR,EAAW,MAAMC,EAAUL,EAAO,SAAU,CAAE,YAAa,cAAe,QAAS,CAAE,OAAQ,kBAAmB,CAAE,CAAC,EAEvH,GAAI,CAACI,EAAS,GACZ,MAAO,CAAC,EAGV,IAAIG,EAAO,MAAMH,EAAS,KAAK,EAE/B,EAAcG,GAAQ,MAAM,QAAQA,EAAK,KAAK,EAAIA,EAAK,MAAQ,CAAC,CAClE,OAASM,EAAO,CAGd,OAFA,QAAQ,KAAK,+CAAiDA,CAAK,EAE5D,CAAC,CACV,CAKA,IAAK,IAFDC,EAAQ,CAAC,EAEJC,EAAI,EAAGA,EAAIH,EAAY,OAAQ,IACtC,GAAI,CACF,MAAMD,EAAa,aAAab,EAAiBc,EAAYG,GAAIf,EAAQK,CAAS,EAAGW,EAAS,CAAU,QAAO,EAAI,CAAC,CAAC,EACrHF,EAAM,KAAKF,EAAYG,EAAE,CAAC,IAAI,CAChC,OAASF,EAAO,CACd,QAAQ,KAAK,8CAA+CA,CAAK,CACnE,CAGF,OAAOC,CACT,CASA,SAASG,EAAsB,EAAO,EAAe,CAEnD,IAAIL,EAAc,CAAC,EAoBnB,OAlBCM,GAAS,CAAC,EAAA,CAAG,QAAQ,SAAU,EAAM,CACpC,GAAI,IAAY,QAAU,IAAkB,CAAChB,GAAQ,CAACT,EAAU,KAAKS,EAAK,MAAQ,EAAE,GAAMiB,GAAiBA,EAAc,IAAIjB,EAAK,IAAI,GAItI,KAAIkB,EAAclB,EAAK,aAAe,CAAC,EAEvCU,EAAY,KAAK,CACf,KAAMV,EAAK,KACX,MAAOA,EAAK,OAAS,KACrB,YAAaA,EAAK,YAClB,YAAaA,EAAK,cAAgB,OAAY,KAAO,KAAK,UAAUA,EAAK,WAAW,EACpF,SAAUkB,EAAY,eAAiB,GACvC,cAAeA,EAAY,oBAAsB,GACjD,iBAAkBA,EAAY,uBAAyB,EACzD,CAAC,CAVsC,CAWzC,CAAC,EAEMR,CACT,CAUA,eAAeS,EAAY,EAAc,EAAM,EAAe,EAAS,CACrE,IAAIf,EAEJ,GAAI,CACF,EAAQgB,EAAgB,KAAK,MAAMA,CAAa,EAAI,CAAC,CACvD,MAAgB,CACd,MAAO,CAAE,KAAM,oCAAqC,QAAS,EAAK,CACpE,CAEA,GAAI,CAAChB,GAAS,OAAOA,GAAU,UAAY,MAAM,QAAQA,CAAK,EAC5D,MAAO,CAAE,KAAM,wCAAyC,QAAS,EAAK,EAGxE,IAAIa,EAAgBI,GAAWA,EAAQ,cAEvC,GAAIJ,GAAiBA,EAAc,IAAIK,CAAI,EACzC,MAAO,CAAE,KAAM,aAAeA,EAAO,wBAAyB,QAAS,EAAK,EAG9E,GAAI,CAEF,IAAItB,GAAO,MADOS,EAAa,SAASY,GAAWA,EAAQ,YAAc,CAAE,YAAaA,EAAQ,WAAY,EAAI,CAAC,CAAC,EACvG,CAAM,KAAK,SAAU,EAAW,CACzC,OAAOE,EAAU,OAASD,CAC5B,CAAC,EAED,GAAI,CAACtB,EACH,MAAO,CAAE,KAAM,+BAAiCsB,EAAO,KAAM,QAAS,EAAK,EAG7E,IAAIE,EAAa,MAAMf,EAAa,YAAYT,EAAMI,CAAK,EACvDE,EAAOkB,EAEX,GAAI,CACF,IAAIC,EAAQ,KAAK,MAAMD,CAAU,EAE7B,OAAOC,GAAU,WACnB,EAAOA,EAEX,MAAgB,CAEhB,CAEA,MAAO,CAAE,KAAMnB,GAAQ,KAAO,GAAK,OAAOA,CAAI,EAAG,QAAS,EAAM,CAClE,OAASK,EAAO,CACd,MAAO,CAAE,KAAM,qBAAuBA,GAASA,EAAM,QAAUA,EAAM,QAAU,OAAOA,CAAK,GAAI,QAAS,EAAK,CAC/G,CACF,CAUA,SAASe,EAAY,EAAc,EAAY,EAAS,CACtD,GAAI,CAACjB,EACH,MAAO,CACL,KAAM,UAAY,CAChB,OAAO,QAAQ,QAAQ,CAAC,CAC1B,EACA,MAAO,UAAY,CAAE,CACvB,EAGF,IAAIkB,EAAWN,GAAW,CAAC,EAEvBO,EAAgB,KAEpBC,EAAW,GAAG,kBAAmB,SAAU,EAAM,EAAe,CAC9D,OAAOV,EAAYV,EAAca,EAAMF,EAAeO,CAAQ,CAChE,CAAC,EAEG,OAAOE,EAAW,eAAkB,YAEtCA,EAAW,cAAc,UAAY,CACnC,EAAgB,IAClB,CAAC,EAGH,eAAeC,GAAO,CACpB,IAAId,EAEJ,GAAI,CACF,EAAQ,MAAMP,EAAa,SAASkB,EAAS,YAAc,CAAE,YAAaA,EAAS,WAAY,EAAI,CAAC,CAAC,CACvG,OAAShB,EAAO,CAGd,OAFA,QAAQ,KAAK,6CAA+CA,CAAK,EAE1D,CACT,CAEA,IAAID,EAAcK,EAAsBC,EAAOW,EAAS,aAAa,EACjEI,EAAY,KAAK,UAAUrB,CAAW,EAE1C,GAAIqB,IAAcH,EAChB,OAAOlB,EAAY,OAGrB,GAAIA,EAAY,SAAW,GAAKkB,IAAkB,KAGhD,MAFA,GAAgBG,EAET,EAGT,GAAI,CACErB,EAAY,SAAW,EACzB,MAAMmB,EAAW,OAAO,gBAAgB,EAExC,MAAMA,EAAW,OAAO,eAAgBnB,CAAW,EAGrD,EAAgBqB,CAClB,OAASpB,EAAO,CACd,QAAQ,KAAK,gDAAiDA,CAAK,CACrE,CAEA,OAAOD,EAAY,MACrB,CAEA,MAAO,CACC,OACN,MAAO,UAAY,CACjB,EAAgB,IAClB,CACF,CACF,CAEA,SAASsB,EAAK,EAAK,CACjB,GAAIC,EAAI,gBACN,OAAOA,EAAI,gBAGb,IAAInC,EAASJ,EAAWuC,EAAI,QAAQ,GAAK,CAAC,EACtCxB,EAAejB,EAAgByC,EAAI,QAAQ,EAE3CC,EAAkB,IAAI,IAWtBC,EAAM,CACA,SACD,MAZG1B,GAAgBX,EAAO,UAAY,OAAOmC,EAAI,OAAU,WAChEzB,EAAoBC,EAAcX,EAAQmC,EAAI,MAAM,KAAKA,CAAG,CAAC,CAAC,CAAC,KAAK,SAAU,EAAO,CAKrF,OAJArB,EAAM,QAAQ,SAAU,EAAM,CAC5BsB,EAAgB,IAAIZ,CAAI,CAC1B,CAAC,EAEMV,CACT,CAAC,EACC,QAAQ,QAAQ,CAAC,CAAC,EAKpB,YAAa,UAAY,CACvB,MAAO,CAAC,CAACH,CACX,EACA,YAAa,SAAU,EAAY,CACjC,OAAOiB,EAAYjB,EAAcoB,EAAY,CAC3C,YAAa,MAAM,QAAQ/B,EAAO,eAAe,EAAIA,EAAO,gBAAkB,OAC9E,cAAeoC,CACjB,CAAC,CACH,CACF,EAQA,MANA,GAAI,gBAAkBC,EAElBF,EAAI,UAAY,OAAOA,EAAI,SAAS,eAAkB,YAAc,OAAOA,EAAI,aAAgB,YACjGA,EAAI,SAAS,cAAc,IAAIA,EAAI,YAAY,yBAA0B,CAAE,OAAQE,CAAI,CAAC,CAAC,EAGpFA,CACT,CAEA,OAAO,gBAAkB,CACN,kBACL,aACM,mBACG,sBACE,wBACV,cACA,cACP,MACR,EAEI,OAAO,UACTH,EAAK,MAAM,CAEf,EAAA,CAAG,C"}
|
|
1
|
+
{"version":3,"file":"webmcp.min.js","sources":["../../Assets/js/webmcp.js"],"sourcesContent":["// @ts-check\n/*\n * WebMCP for CrestApps.\n *\n * WebMCP (https://webmachinelearning.github.io/webmcp/) lets a web page give tools to AI agents through\n * `document.modelContext`. This script uses only that standard API:\n *\n * - It registers the AI tools the site lets the signed-in user call, each as a standard tool whose `execute` posts to\n * the site with the user's antiforgery token. Any agent that reads the page's tools, such as the browser's own\n * assistant, can find and run them.\n * - It lets the CrestApps AI chat use every tool the page exposes, whoever registered it: `getTools()` lists them,\n * and `executeTool()` runs the one the model calls. Only the site tools it registered itself are left out, because\n * the chat reaches those on the server. The page tools of webmcp-page-tools.js run through their own `execute`,\n * because `executeTool()` hides why a tool failed, and the model needs the reason to correct itself.\n *\n * Load webmcp-polyfill.js first, so browsers without native WebMCP get the reference implementation; a browser that\n * has WebMCP keeps its own. The page configuration is read from a\n * `<script type=\"application/json\" id=\"crestapps-webmcp-config\">` block: `toolsUrl` and `invokeUrlTemplate` (with a\n * `{name}` placeholder) for the site's tools, `antiforgeryHeaderName` and `antiforgeryToken` for the calls,\n * `pageToolOrigins` for the cross-origin frames whose tools the chat may use, and any setting other page scripts need.\n */\n(function () {\n 'use strict';\n\n var CONFIG_ELEMENT_ID = 'crestapps-webmcp-config';\n var MAX_PAGE_TOOLS = 64;\n var TOOL_NAME = /^[A-Za-z0-9_.-]{1,128}$/;\n\n /**\n * Finds the page's WebMCP ModelContext.\n *\n * @param {any} doc The document.\n */\n function getModelContext(doc) {\n return doc && doc.modelContext ? doc.modelContext : null;\n }\n\n /**\n * Reads the configuration block the site writes into the page.\n *\n * @param {any} doc The document.\n */\n function readConfig(doc) {\n var element = doc && doc.getElementById ? doc.getElementById(CONFIG_ELEMENT_ID) : null;\n\n if (!element) {\n return null;\n }\n\n try {\n return JSON.parse(element.textContent);\n } catch (error) {\n return null;\n }\n }\n\n /**\n * Creates the standard tool that calls one of the site's AI tools.\n *\n * @param {{ name: string, title?: string, description: string, inputSchema?: any }} descriptor The tool the site lists.\n * @param {{ invokeUrlTemplate?: string, antiforgeryHeaderName?: string, antiforgeryToken?: string }} config The page configuration.\n * @param {Function} fetchImpl The fetch function.\n */\n function createServerTool(descriptor, config, fetchImpl) {\n if (!descriptor || !descriptor.name || !config || !config.invokeUrlTemplate) {\n throw new TypeError('A site tool needs a name and an address to call.');\n }\n\n var url = config.invokeUrlTemplate.replace('{name}', encodeURIComponent(descriptor.name));\n /** @type {any} */\n var tool = {\n name: descriptor.name,\n description: descriptor.description,\n // The site's tools read data the page did not write, so agents are told to treat what they answer as data.\n annotations: { untrustedContentHint: true },\n execute: async function (input) {\n /** @type {Record<string, string>} */\n var headers = { 'Content-Type': 'application/json' };\n\n if (config.antiforgeryHeaderName && config.antiforgeryToken) {\n headers[config.antiforgeryHeaderName] = config.antiforgeryToken;\n }\n\n var response = await fetchImpl(url, {\n method: 'POST',\n credentials: 'same-origin',\n headers: headers,\n body: JSON.stringify(input || {}),\n });\n\n if (!response.ok) {\n throw new Error('The site refused the tool call (' + response.status + ').');\n }\n\n var data = await response.json();\n var text = (Array.isArray(data.content) ? data.content : [])\n .filter(function (part) {\n return part && part.type === 'text';\n })\n .map(function (part) {\n return part.text == null ? '' : String(part.text);\n })\n .join('\\n');\n\n if (data.isError) {\n // A rejected execute is how WebMCP reports a failed tool.\n throw new Error(text || 'The tool failed.');\n }\n\n return text;\n },\n };\n\n if (descriptor.title) {\n tool.title = descriptor.title;\n }\n\n if (descriptor.inputSchema) {\n tool.inputSchema = descriptor.inputSchema;\n }\n\n return tool;\n }\n\n /**\n * Fetches the AI tools the site lets this user call and registers each with the page's ModelContext.\n *\n * @param {any} modelContext The page's ModelContext.\n * @param {any} config The page configuration.\n * @param {Function} fetchImpl The fetch function.\n * @param {AbortSignal} [signal] Unregisters the tools when aborted.\n * @returns {Promise<string[]>} The names of the tools registered.\n */\n async function registerServerTools(modelContext, config, fetchImpl, signal) {\n if (!modelContext || !config || !config.toolsUrl || !config.invokeUrlTemplate) {\n return [];\n }\n\n var descriptors;\n\n try {\n var response = await fetchImpl(config.toolsUrl, { credentials: 'same-origin', headers: { Accept: 'application/json' } });\n\n if (!response.ok) {\n return [];\n }\n\n var data = await response.json();\n\n descriptors = data && Array.isArray(data.tools) ? data.tools : [];\n } catch (error) {\n console.warn('The site\\'s WebMCP tools could not be loaded.', error);\n\n return [];\n }\n\n /** @type {string[]} */\n var names = [];\n\n for (var i = 0; i < descriptors.length; i++) {\n try {\n await modelContext.registerTool(createServerTool(descriptors[i], config, fetchImpl), signal ? { signal: signal } : {});\n names.push(descriptors[i].name);\n } catch (error) {\n console.warn('A site WebMCP tool could not be registered.', error);\n }\n }\n\n return names;\n }\n\n /**\n * Turns the page's registered tools into what the chat hub accepts: tools with valid WebMCP names, at most 64, with\n * their schemas as JSON text and their annotations as flags.\n *\n * @param {Array<any>} tools The RegisteredTool records getTools() answered.\n * @param {Set<string>} [excludedNames] The names to leave out.\n */\n function toPageToolDescriptors(tools, excludedNames) {\n /** @type {Array<any>} */\n var descriptors = [];\n\n (tools || []).forEach(function (tool) {\n if (descriptors.length >= MAX_PAGE_TOOLS || !tool || !TOOL_NAME.test(tool.name || '') || (excludedNames && excludedNames.has(tool.name))) {\n return;\n }\n\n var annotations = tool.annotations || {};\n\n descriptors.push({\n name: tool.name,\n title: tool.title || null,\n description: tool.description,\n inputSchema: tool.inputSchema === undefined ? null : JSON.stringify(tool.inputSchema),\n readOnly: annotations.readOnlyHint === true,\n consequential: annotations.consequentialHint === true,\n untrustedContent: annotations.untrustedContentHint === true,\n });\n });\n\n return descriptors;\n }\n\n /**\n * Runs one of the page tools CrestApps registered in this page itself, as executeTool would: on a copy of the input,\n * with a signal, answering the result as text. Unlike executeTool, which reports every failure as \"Tool execution\n * failed\", it keeps the message the tool failed with, so the model learns why and can correct itself.\n *\n * @param {any} tool The tool as it was registered, with its execute callback.\n * @param {any} input The tool's input.\n */\n async function runOwnTool(tool, input) {\n var value = await tool.execute(JSON.parse(JSON.stringify(input)), { signal: new AbortController().signal });\n\n if (typeof value === 'string') {\n return value;\n }\n\n return value === undefined ? '' : JSON.stringify(value);\n }\n\n /**\n * Runs a page tool the chat asked for, and answers what the hub expects back: `{ text, isError }`.\n *\n * A tool another script registered runs through the standard executeTool. A page tool CrestApps registered in this\n * page itself, which `findOwnTool` answers, runs through its own execute callback, so a failure reaches the model\n * with its reason.\n *\n * @param {any} modelContext The page's ModelContext.\n * @param {string} name The tool's name.\n * @param {string} argumentsJson The tool's input as JSON.\n * @param {{ fromOrigins?: string[], excludedNames?: Set<string>, findOwnTool?: (registeredTool: any) => any }} [options]\n * The frames to look in, the names to refuse, and how to find the tools CrestApps registered in this page.\n */\n async function runPageTool(modelContext, name, argumentsJson, options) {\n var input;\n\n try {\n input = argumentsJson ? JSON.parse(argumentsJson) : {};\n } catch (error) {\n return { text: 'The tool input is not valid JSON.', isError: true };\n }\n\n if (!input || typeof input !== 'object' || Array.isArray(input)) {\n return { text: 'The tool input must be a JSON object.', isError: true };\n }\n\n var excludedNames = options && options.excludedNames;\n\n if (excludedNames && excludedNames.has(name)) {\n return { text: 'The tool \"' + name + '\" is not a page tool.', isError: true };\n }\n\n try {\n var tools = await modelContext.getTools(options && options.fromOrigins ? { fromOrigins: options.fromOrigins } : {});\n var tool = tools.find(function (candidate) {\n return candidate.name === name;\n });\n\n if (!tool) {\n return { text: 'The page has no tool named \"' + name + '\".', isError: true };\n }\n\n var ownTool = options && typeof options.findOwnTool === 'function' ? options.findOwnTool(tool) : null;\n\n if (ownTool && typeof ownTool.execute === 'function') {\n return { text: await runOwnTool(ownTool, input), isError: false };\n }\n\n var serialized = await modelContext.executeTool(tool, input);\n var text = serialized;\n\n try {\n var value = JSON.parse(serialized);\n\n if (typeof value === 'string') {\n text = value;\n }\n } catch (error) {\n // The result is not JSON; keep it as it is.\n }\n\n return { text: text == null ? '' : String(text), isError: false };\n } catch (error) {\n return { text: 'The tool failed: ' + (error && error.message ? error.message : String(error)), isError: true };\n }\n }\n\n /**\n * Connects the page's tools to a CrestApps chat hub connection. The hub calls `ExecutePageTool` on the connection\n * and waits for the result; `sync` sends the page's tools before a message, only when they changed.\n *\n * @param {any} modelContext The page's ModelContext, or null when the page has no WebMCP.\n * @param {any} connection The SignalR hub connection of the chat.\n * @param {{ fromOrigins?: string[], excludedNames?: Set<string>, findOwnTool?: (registeredTool: any) => any }} [options]\n * The frames to look in, the names to leave out, and how to find the page tools CrestApps registered in this page.\n */\n function connectChat(modelContext, connection, options) {\n if (!modelContext) {\n return {\n sync: function () {\n return Promise.resolve(0);\n },\n reset: function () { },\n };\n }\n\n var settings = options || {};\n /** @type {string | null} */\n var lastSignature = null;\n\n connection.on('ExecutePageTool', function (name, argumentsJson) {\n return runPageTool(modelContext, name, argumentsJson, settings);\n });\n\n if (typeof connection.onreconnected === 'function') {\n // A reconnected connection has a new identifier, and the hub forgot the tools of the old one.\n connection.onreconnected(function () {\n lastSignature = null;\n });\n }\n\n async function sync() {\n var tools;\n\n try {\n tools = await modelContext.getTools(settings.fromOrigins ? { fromOrigins: settings.fromOrigins } : {});\n } catch (error) {\n console.warn('The page\\'s WebMCP tools could not be read.', error);\n\n return 0;\n }\n\n var descriptors = toPageToolDescriptors(tools, settings.excludedNames);\n var signature = JSON.stringify(descriptors);\n\n if (signature === lastSignature) {\n return descriptors.length;\n }\n\n if (descriptors.length === 0 && lastSignature === null) {\n lastSignature = signature;\n\n return 0;\n }\n\n try {\n if (descriptors.length === 0) {\n await connection.invoke('ClearPageTools');\n } else {\n await connection.invoke('SetPageTools', descriptors);\n }\n\n lastSignature = signature;\n } catch (error) {\n console.warn('The page tools could not be sent to the chat.', error);\n }\n\n return descriptors.length;\n }\n\n return {\n sync: sync,\n reset: function () {\n lastSignature = null;\n },\n };\n }\n\n /**\n * Finds the page tool webmcp-page-tools.js registered in this window for a tool getTools() listed. A tool of another\n * frame, or one another script registered, is not one of them.\n *\n * @param {any} win The window.\n * @param {any} modelContext The window's ModelContext.\n * @param {any} registeredTool The RegisteredTool record getTools() answered.\n */\n function findOwnPageTool(win, modelContext, registeredTool) {\n var pageTools = win.CrestAppsWebMcpPageTools;\n\n if (!registeredTool || registeredTool.window !== win || !pageTools || typeof pageTools.getRegisteredTool !== 'function') {\n return null;\n }\n\n return pageTools.getRegisteredTool(modelContext, registeredTool.name);\n }\n\n function init(win) {\n if (win.crestAppsWebMcp) {\n return win.crestAppsWebMcp;\n }\n\n var config = readConfig(win.document) || {};\n var modelContext = getModelContext(win.document);\n /** @type {Set<string>} */\n var serverToolNames = new Set();\n var ready = modelContext && config.toolsUrl && typeof win.fetch === 'function'\n ? registerServerTools(modelContext, config, win.fetch.bind(win)).then(function (names) {\n names.forEach(function (name) {\n serverToolNames.add(name);\n });\n\n return names;\n })\n : Promise.resolve([]);\n\n var api = {\n config: config,\n ready: ready,\n isSupported: function () {\n return !!modelContext;\n },\n connectChat: function (connection) {\n return connectChat(modelContext, connection, {\n fromOrigins: Array.isArray(config.pageToolOrigins) ? config.pageToolOrigins : undefined,\n excludedNames: serverToolNames,\n findOwnTool: function (registeredTool) {\n return findOwnPageTool(win, modelContext, registeredTool);\n },\n });\n },\n };\n\n win.crestAppsWebMcp = api;\n\n if (win.document && typeof win.document.dispatchEvent === 'function' && typeof win.CustomEvent === 'function') {\n win.document.dispatchEvent(new win.CustomEvent('crestapps-webmcp-ready', { detail: api }));\n }\n\n return api;\n }\n\n window.CrestAppsWebMcp = {\n getModelContext: getModelContext,\n readConfig: readConfig,\n createServerTool: createServerTool,\n registerServerTools: registerServerTools,\n toPageToolDescriptors: toPageToolDescriptors,\n runPageTool: runPageTool,\n findOwnPageTool: findOwnPageTool,\n connectChat: connectChat,\n init: init,\n };\n\n if (window.document) {\n init(window);\n }\n})();\n"],"names":["TOOL_NAME","getModelContext","doc","readConfig","element","createServerTool","descriptor","config","url","tool","headers","response","fetchImpl","input","data","text","part","registerServerTools","modelContext","descriptors","error","names","i","signal","toPageToolDescriptors","tools","excludedNames","annotations","runOwnTool","value","runPageTool","argumentsJson","options","name","candidate","ownTool","serialized","connectChat","settings","lastSignature","connection","sync","signature","findOwnPageTool","pageTools","win","registeredTool","init","serverToolNames","api"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crestapps/webmcp",
|
|
3
|
-
"version": "2.0.0-preview.
|
|
3
|
+
"version": "2.0.0-preview.257",
|
|
4
4
|
"description": "WebMCP for CrestApps: registers a site's AI tools and page tools with the standard document.modelContext API, and lets the CrestApps AI chat discover and run any WebMCP tool a page exposes. Ships the W3C Web Machine Learning Community Group polyfill for browsers without native WebMCP.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "CrestApps",
|