@aibranch/llm-meta-widget 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -216,6 +216,7 @@ easy to get wrong:
|
|
|
216
216
|
| `llm-provider` | `llm_meta_server` (default) or `ollama` |
|
|
217
217
|
| `api-key-uuid`, `greeting`, `max-rounds` | as the helper options |
|
|
218
218
|
| `actions-schema-id`, `state-global`, `actions-global`, `remote-tools-schema-id` | as the helper options |
|
|
219
|
+
| `page-card-id`, `prompts-global`, `resources-global` | as the helper options |
|
|
219
220
|
| `models`, `hub-tools` | comma-separated. Omitted **or empty** means no allowlist — an allowlist permitting nothing is never what anyone meant |
|
|
220
221
|
| `enable-model-picker`, `enable-tool-picker` | **value attributes, not boolean attributes.** They default to true, so presence cannot mean true. Disable with `enable-tool-picker="false"`; any other value is true |
|
|
221
222
|
| `well-known-urls` | **Three different states**, because an attribute cannot tell "not set" from "set to empty": omitted = auto-discover same-origin `/.well-known/mcp.json`; `""` = discovery off; `"a,b"` = fetch those |
|
|
@@ -401,6 +402,9 @@ translate directly.
|
|
|
401
402
|
| `api_key_uuid:` | `"ollama-local"` | Hub API-key uuid to invoke |
|
|
402
403
|
| `element_path:` | `"/llm_meta_widget_assets/llm-meta-widget.js"` | The bundled element the page loads. Served by the gem's engine; override to load it from elsewhere |
|
|
403
404
|
| `actions_schema_id:` | `"ai-actions"` | DOM id of the Class-3 schema block |
|
|
405
|
+
| `page_card_id:` | `"ai-page"` | DOM id of the page card (see "The page card"). Its tools replace the `actions_schema_id` block |
|
|
406
|
+
| `prompts_global:` | `"aiPrompts"` | Global object holding page prompt handlers, keyed by prompt name |
|
|
407
|
+
| `resources_global:` | `"aiResources"` | Global object holding page resource readers, keyed by resource URI |
|
|
404
408
|
| `state_global:` | `"aiState"` | Global window object holding Class-3 state readers, each `{ description, read }` (see the breaking change in 0.8.0) |
|
|
405
409
|
| `actions_global:` | `"aiActions"` | Global window object holding Class-3 implementations |
|
|
406
410
|
| `remote_tools_schema_id:` | `"remote-mcp-tools"` | DOM id of a JSON block listing Class-1 tools (`{id, name, description, input_schema}`) to start with. They seed the picker rather than bypassing it — shown ticked, and the visitor may untick them. Read once, at boot |
|
|
@@ -412,6 +416,74 @@ translate directly.
|
|
|
412
416
|
| `models:` | `nil` | Model-name allowlist; `nil` = all anon-available |
|
|
413
417
|
| `hub_tools:` | `nil` | MCP-server-name allowlist; `nil` = all anon-public |
|
|
414
418
|
|
|
419
|
+
## The page card
|
|
420
|
+
|
|
421
|
+
Since 0.9.0 a page can declare what it offers in one block of JSON, instead of
|
|
422
|
+
using a separate channel for each kind of thing. The block uses the same envelope
|
|
423
|
+
an MCP server puts in its Server Card, so the page and the server describe
|
|
424
|
+
themselves the same way.
|
|
425
|
+
|
|
426
|
+
```html
|
|
427
|
+
<script type="application/mcp-static-primitives+json">
|
|
428
|
+
{ "_meta": { "io.modelcontextprotocol/static-primitives": {
|
|
429
|
+
"tools": [ { "name": "set_text", "description": "…", "inputSchema": { } } ],
|
|
430
|
+
"prompts": [ { "name": "annotate", "description": "Annotate this page" } ],
|
|
431
|
+
"resources": [ { "uri": "page:text", "name": "Text", "description": "the text to annotate",
|
|
432
|
+
"volatility": "volatile" } ]
|
|
433
|
+
} } }
|
|
434
|
+
</script>
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
The card says what exists. Three global objects say what it does, and each one is
|
|
438
|
+
keyed by the identifier that the matching MCP operation takes:
|
|
439
|
+
|
|
440
|
+
| declared in the card | implemented in | keyed by |
|
|
441
|
+
|---|---|---|
|
|
442
|
+
| `tools` | `window.aiActions` | tool `name`, as `tools/call` takes |
|
|
443
|
+
| `prompts` | `window.aiPrompts` | prompt `name`, as `prompts/get` takes |
|
|
444
|
+
| `resources` | `window.aiResources` | resource `uri`, as `resources/read` takes |
|
|
445
|
+
|
|
446
|
+
```js
|
|
447
|
+
window.aiActions = { set_text: (args) => { /* change the page */ } }
|
|
448
|
+
window.aiPrompts = { annotate: (args) => "Annotate this: " + args.text }
|
|
449
|
+
window.aiResources = { "page:text": () => document.querySelector("#text").value }
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
A prompt handler may return a plain string or the full `messages` array that
|
|
453
|
+
`prompts/get` returns. A resource reader may return a string or any value that
|
|
454
|
+
can be turned into JSON.
|
|
455
|
+
|
|
456
|
+
**Resources carry two optional hints.** `volatility` is `"volatile"` when the
|
|
457
|
+
value changes between turns, so the widget reads it again every turn; the default
|
|
458
|
+
is `"stable"`, which is read once and reused. `autoAttach: false` keeps a resource
|
|
459
|
+
out of the turn unless something asks for it. A `size` in bytes lets the widget
|
|
460
|
+
decide whether the value fits its budget before reading it.
|
|
461
|
+
|
|
462
|
+
**The `page:` scheme is experimental.** It is not a registered URI scheme, and
|
|
463
|
+
nothing outside this widget gives it meaning. It is used because the protocol
|
|
464
|
+
asks for a URI, and because `https://` is reserved for resources a client can
|
|
465
|
+
fetch from the web by itself — which page resources are not. The form is
|
|
466
|
+
`page:<name>` with no path, because a page has only one namespace. Expect this to
|
|
467
|
+
change.
|
|
468
|
+
|
|
469
|
+
**The card is found by its media type.** A client matches
|
|
470
|
+
`script[type="application/mcp-static-primitives+json"]`, so the element needs no
|
|
471
|
+
`id`. This is how a client that was never told about your page can still read
|
|
472
|
+
it, and it follows SEP-0000's "HTML-delivery mode". The `page_card_id:` option
|
|
473
|
+
is kept only for a host that cannot set the type; it is read when no block with
|
|
474
|
+
the media type is present.
|
|
475
|
+
|
|
476
|
+
**A document may hold several blocks.** A page built from components can let
|
|
477
|
+
each component declare its own primitives, and the arrays join by primitive
|
|
478
|
+
type. Declaring the same tool or prompt `name`, or the same resource `uri`,
|
|
479
|
+
twice is an error: every copy is dropped and a message is written to the
|
|
480
|
+
console. The first one does not win, because which block comes first depends on
|
|
481
|
+
the order the page's parts are rendered in, and that is not something the author
|
|
482
|
+
chose.
|
|
483
|
+
|
|
484
|
+
**The card is optional.** A page that declares nothing keeps working exactly as
|
|
485
|
+
before, and a page that declares no `tools` still uses its `ai-actions` block.
|
|
486
|
+
|
|
415
487
|
## Surviving navigation
|
|
416
488
|
|
|
417
489
|
A page action that navigates — submitting a form, following a link — no longer
|
|
@@ -560,6 +560,25 @@ function parseSseFrame(raw) {
|
|
|
560
560
|
return { name, data };
|
|
561
561
|
}
|
|
562
562
|
var STATIC_PRIMITIVES_META = "io.modelcontextprotocol/static-primitives";
|
|
563
|
+
var PAGE_CARD_MEDIA_TYPE = "application/mcp-static-primitives+json";
|
|
564
|
+
function mergeStaticPrimitivesCards(cards, warn = console.warn) {
|
|
565
|
+
const merged = { tools: [], prompts: [], resources: [] };
|
|
566
|
+
const keyOf = { tools: "name", prompts: "name", resources: "uri" };
|
|
567
|
+
for (const kind of ["tools", "prompts", "resources"]) {
|
|
568
|
+
const key = keyOf[kind];
|
|
569
|
+
const all = [];
|
|
570
|
+
for (const card of cards || []) all.push(...card && card[kind] || []);
|
|
571
|
+
const counts = /* @__PURE__ */ new Map();
|
|
572
|
+
for (const entry of all) counts.set(entry[key], (counts.get(entry[key]) || 0) + 1);
|
|
573
|
+
for (const [id, n] of counts) {
|
|
574
|
+
if (n > 1) {
|
|
575
|
+
warn(`[llm-meta-widget] the page declares ${kind} "${id}" ${n} times; dropping every copy \u2014 remove the duplicate to say which one you mean`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
merged[kind] = all.filter((entry) => counts.get(entry[key]) === 1);
|
|
579
|
+
}
|
|
580
|
+
return merged;
|
|
581
|
+
}
|
|
563
582
|
function readStaticPrimitivesCard(text, warn = console.warn) {
|
|
564
583
|
const empty = { tools: [], prompts: [], resources: [] };
|
|
565
584
|
if (typeof text !== "string" || text.trim() === "") return empty;
|
|
@@ -737,7 +756,20 @@ async function resourceLinesForTurn({ plan, cached, endpoint, read, budgetBytes
|
|
|
737
756
|
}
|
|
738
757
|
return { lines: resourceContextLines(cached), cached };
|
|
739
758
|
}
|
|
759
|
+
var PAGE_URI_PREFIX = "page:";
|
|
740
760
|
var PROMPT_ARG_ALIASES = { dictionaries: "selected_dictionaries" };
|
|
761
|
+
function promptArgState({ entries = [], resources = {}, state = {} } = {}) {
|
|
762
|
+
const merged = Object.assign({}, state);
|
|
763
|
+
for (const entry of entries) {
|
|
764
|
+
const uri = entry && entry.uri;
|
|
765
|
+
if (!uri || uri.indexOf(PAGE_URI_PREFIX) !== 0) continue;
|
|
766
|
+
const name = uri.slice(PAGE_URI_PREFIX.length);
|
|
767
|
+
const reader = resources[uri];
|
|
768
|
+
if (!name || merged[name] || typeof reader !== "function") continue;
|
|
769
|
+
merged[name] = { description: entry.description || "", read: reader };
|
|
770
|
+
}
|
|
771
|
+
return merged;
|
|
772
|
+
}
|
|
741
773
|
function promptArgFromState(argName, state) {
|
|
742
774
|
const entry = state && state[argName] || state && state[PROMPT_ARG_ALIASES[argName]];
|
|
743
775
|
const reader = entry && typeof entry.read === "function" ? entry.read : null;
|
|
@@ -3887,8 +3919,18 @@ function boot(cfg) {
|
|
|
3887
3919
|
model: MODEL
|
|
3888
3920
|
});
|
|
3889
3921
|
}
|
|
3890
|
-
var
|
|
3891
|
-
var pageCard
|
|
3922
|
+
var typedCards = document.querySelectorAll('script[type="' + PAGE_CARD_MEDIA_TYPE + '"]');
|
|
3923
|
+
var pageCard;
|
|
3924
|
+
if (typedCards.length) {
|
|
3925
|
+
pageCard = mergeStaticPrimitivesCards(
|
|
3926
|
+
Array.prototype.map.call(typedCards, function(el) {
|
|
3927
|
+
return readStaticPrimitivesCard(el.textContent);
|
|
3928
|
+
})
|
|
3929
|
+
);
|
|
3930
|
+
} else {
|
|
3931
|
+
var pageCardEl = document.getElementById(PAGE_CARD_ID);
|
|
3932
|
+
pageCard = readStaticPrimitivesCard(pageCardEl ? pageCardEl.textContent : null);
|
|
3933
|
+
}
|
|
3892
3934
|
var actionsSchemaEl = document.getElementById(ACTIONS_SCHEMA_ID);
|
|
3893
3935
|
var localTools = pageCard.tools.length ? pageCard.tools : actionsSchemaEl ? JSON.parse(actionsSchemaEl.textContent) : [];
|
|
3894
3936
|
var remoteToolsEl = document.getElementById(REMOTE_TOOLS_SCHEMA_ID);
|
|
@@ -4162,6 +4204,13 @@ function boot(cfg) {
|
|
|
4162
4204
|
renderPromptButtons();
|
|
4163
4205
|
renderWelcome();
|
|
4164
4206
|
})();
|
|
4207
|
+
function argState() {
|
|
4208
|
+
return promptArgState({
|
|
4209
|
+
entries: pageCard.resources,
|
|
4210
|
+
resources: window[RESOURCES_GLOBAL] || {},
|
|
4211
|
+
state: window[STATE_GLOBAL] || {}
|
|
4212
|
+
});
|
|
4213
|
+
}
|
|
4165
4214
|
function renderPromptButtons() {
|
|
4166
4215
|
if (!promptsEl) return;
|
|
4167
4216
|
promptsEl.textContent = "";
|
|
@@ -4213,7 +4262,7 @@ function boot(cfg) {
|
|
|
4213
4262
|
hello.className = "lmw-welcome-hello";
|
|
4214
4263
|
hello.textContent = GREETING || "Hi \u2014 I can work this page for you. Tell me what you need in your own words" + (hostWidePrompts.length ? ", or start with one of these:" : ".");
|
|
4215
4264
|
box.appendChild(hello);
|
|
4216
|
-
var state =
|
|
4265
|
+
var state = argState();
|
|
4217
4266
|
hostWidePrompts.forEach(function(prompt) {
|
|
4218
4267
|
var props = promptButtonProps(prompt);
|
|
4219
4268
|
var card = document.createElement("div");
|
|
@@ -4249,7 +4298,7 @@ function boot(cfg) {
|
|
|
4249
4298
|
historyEl.appendChild(box);
|
|
4250
4299
|
}
|
|
4251
4300
|
async function runPromptTemplate(prompt, button) {
|
|
4252
|
-
var resolved = resolvePromptArguments(prompt,
|
|
4301
|
+
var resolved = resolvePromptArguments(prompt, argState());
|
|
4253
4302
|
var args = resolved.args;
|
|
4254
4303
|
var missing = resolved.missing;
|
|
4255
4304
|
if (missing.length) {
|
|
@@ -879,6 +879,43 @@ export const STATIC_PRIMITIVES_META = "io.modelcontextprotocol/static-primitives
|
|
|
879
879
|
// typo in it should cost that one entry, not the assistant. Everything
|
|
880
880
|
// unparseable returns empty arrays and says so on the console, which is how
|
|
881
881
|
// aiState already treats a reader missing its description.
|
|
882
|
+
// The media type that locates a card inside an HTML document. It is the
|
|
883
|
+
// locator, so it is NOT configurable: a client that has to be told where to
|
|
884
|
+
// look cannot discover anything. SEP-0000, "HTML-delivery mode".
|
|
885
|
+
export const PAGE_CARD_MEDIA_TYPE = "application/mcp-static-primitives+json"
|
|
886
|
+
|
|
887
|
+
// Several blocks may appear in one document, so that a page assembled from
|
|
888
|
+
// components can let each component declare its own primitives. The arrays
|
|
889
|
+
// concatenate by primitive type.
|
|
890
|
+
//
|
|
891
|
+
// A duplicate identifier across blocks is an ERROR, not a precedence puzzle:
|
|
892
|
+
// the spec says a client should report it and should not silently pick one. So
|
|
893
|
+
// every copy of a duplicated identifier is dropped and the author is told.
|
|
894
|
+
// Picking the first would make which component wins depend on DOM order, which
|
|
895
|
+
// is not something a page author controls when the components are partials.
|
|
896
|
+
export function mergeStaticPrimitivesCards(cards, warn = console.warn) {
|
|
897
|
+
const merged = { tools: [], prompts: [], resources: [] }
|
|
898
|
+
const keyOf = { tools: "name", prompts: "name", resources: "uri" }
|
|
899
|
+
|
|
900
|
+
for (const kind of [ "tools", "prompts", "resources" ]) {
|
|
901
|
+
const key = keyOf[kind]
|
|
902
|
+
const all = []
|
|
903
|
+
for (const card of cards || []) all.push(...((card && card[kind]) || []))
|
|
904
|
+
|
|
905
|
+
const counts = new Map()
|
|
906
|
+
for (const entry of all) counts.set(entry[key], (counts.get(entry[key]) || 0) + 1)
|
|
907
|
+
|
|
908
|
+
for (const [ id, n ] of counts) {
|
|
909
|
+
if (n > 1) {
|
|
910
|
+
warn(`[llm-meta-widget] the page declares ${kind} "${id}" ${n} times; ` +
|
|
911
|
+
"dropping every copy — remove the duplicate to say which one you mean")
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
merged[kind] = all.filter((entry) => counts.get(entry[key]) === 1)
|
|
915
|
+
}
|
|
916
|
+
return merged
|
|
917
|
+
}
|
|
918
|
+
|
|
882
919
|
export function readStaticPrimitivesCard(text, warn = console.warn) {
|
|
883
920
|
const empty = { tools: [], prompts: [], resources: [] }
|
|
884
921
|
if (typeof text !== "string" || text.trim() === "") return empty
|
|
@@ -1129,8 +1166,35 @@ export async function resourceLinesForTurn({ plan, cached, endpoint, read, budge
|
|
|
1129
1166
|
// (window.aiState) by name. `dictionaries` is read from a
|
|
1130
1167
|
// `selected_dictionaries` reader: the page selects a LIST, and the prompt
|
|
1131
1168
|
// declares the CSV shape the text_annotation tool already takes.
|
|
1169
|
+
// Page resources are addressed `page:<name>`: a custom scheme because the
|
|
1170
|
+
// protocol asks for a URI and `https:` is reserved for what a client can fetch
|
|
1171
|
+
// itself, and with no path because a page has one namespace.
|
|
1172
|
+
const PAGE_URI_PREFIX = "page:"
|
|
1173
|
+
|
|
1132
1174
|
const PROMPT_ARG_ALIASES = { dictionaries: "selected_dictionaries" }
|
|
1133
1175
|
|
|
1176
|
+
// Prompt arguments are declared by plain name (`text`); page resources are
|
|
1177
|
+
// addressed by URI (`page:text`). So on a page that declares its state as
|
|
1178
|
+
// resources and keeps no `aiState`, the resolver below finds nothing under the
|
|
1179
|
+
// names it looks up and every argument arrives empty — the visitor is asked to
|
|
1180
|
+
// retype what is already on screen. This presents those resources in the shape
|
|
1181
|
+
// the resolver consumes, keyed by the name inside the URI.
|
|
1182
|
+
//
|
|
1183
|
+
// An explicit `aiState` entry wins: a page running both channels has said what
|
|
1184
|
+
// it wants under that name.
|
|
1185
|
+
export function promptArgState({ entries = [], resources = {}, state = {} } = {}) {
|
|
1186
|
+
const merged = Object.assign({}, state)
|
|
1187
|
+
for (const entry of entries) {
|
|
1188
|
+
const uri = entry && entry.uri
|
|
1189
|
+
if (!uri || uri.indexOf(PAGE_URI_PREFIX) !== 0) continue
|
|
1190
|
+
const name = uri.slice(PAGE_URI_PREFIX.length)
|
|
1191
|
+
const reader = resources[uri]
|
|
1192
|
+
if (!name || merged[name] || typeof reader !== "function") continue
|
|
1193
|
+
merged[name] = { description: entry.description || "", read: reader }
|
|
1194
|
+
}
|
|
1195
|
+
return merged
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1134
1198
|
export function promptArgFromState(argName, state) {
|
|
1135
1199
|
// Same object shape the system-prompt annex consumes: { description, read }.
|
|
1136
1200
|
// The bare-function form was removed in 0.8.0, so an entry without `read` is
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aibranch/llm-meta-widget",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"//version": "Must stay in step with lib/llm_meta_widget/version.rb. Two independently versioned copies of the same client is the drift docs/custom-element-distribution.md warns about: bump both, release both.",
|
|
5
5
|
"description": "Embeddable browser chat widget: a custom element that discovers tools at page, origin and hub scope and dispatches each tool call to the right one. Two lines of HTML on any host, Rails or not.",
|
|
6
6
|
"keywords": [
|