@aibranch/llm-meta-widget 0.8.6 → 0.9.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.
@@ -483,6 +483,17 @@ async function readMcpResource({ endpoint, uri, signal }) {
483
483
  label: `readMcpResource(${uri})`
484
484
  });
485
485
  }
486
+ async function getPagePrompt({ name, args, prompts }) {
487
+ const handler = prompts && prompts[name];
488
+ if (typeof handler !== "function") {
489
+ throw new Error('the page declares prompt "' + name + '" but no handler answers to that name');
490
+ }
491
+ const result = await handler(args || {});
492
+ if (typeof result === "string") {
493
+ return { messages: [{ role: "user", content: { type: "text", text: result } }] };
494
+ }
495
+ return result || { messages: [] };
496
+ }
486
497
  function promptMessagesToText(result) {
487
498
  return (result?.messages || []).map((m) => {
488
499
  const c = m.content;
@@ -549,8 +560,40 @@ function parseSseFrame(raw) {
549
560
  return { name, data };
550
561
  }
551
562
  var STATIC_PRIMITIVES_META = "io.modelcontextprotocol/static-primitives";
563
+ function readStaticPrimitivesCard(text, warn = console.warn) {
564
+ const empty = { tools: [], prompts: [], resources: [] };
565
+ if (typeof text !== "string" || text.trim() === "") return empty;
566
+ let doc;
567
+ try {
568
+ doc = JSON.parse(text);
569
+ } catch (e) {
570
+ warn("[llm-meta-widget] page card is not valid JSON; ignoring it:", e.message);
571
+ return empty;
572
+ }
573
+ const ext = doc && doc._meta && doc._meta[STATIC_PRIMITIVES_META] || null;
574
+ if (!ext) {
575
+ warn(`[llm-meta-widget] page card has no _meta["${STATIC_PRIMITIVES_META}"]; ignoring it`);
576
+ return empty;
577
+ }
578
+ const named = (arr, kind) => (Array.isArray(arr) ? arr : []).filter((e) => {
579
+ const ok = e && typeof e.name === "string" && e.name.trim() !== "";
580
+ if (!ok) warn(`[llm-meta-widget] page card: dropping a ${kind} with no name`);
581
+ return ok;
582
+ });
583
+ return {
584
+ tools: named(ext.tools, "tool"),
585
+ prompts: named(ext.prompts, "prompt"),
586
+ // Resources are addressed by uri, not name — resources/read takes the uri.
587
+ resources: (Array.isArray(ext.resources) ? ext.resources : []).filter((e) => {
588
+ const ok = e && typeof e.uri === "string" && e.uri.trim() !== "";
589
+ if (!ok) warn("[llm-meta-widget] page card: dropping a resource with no uri");
590
+ return ok;
591
+ })
592
+ };
593
+ }
552
594
  var VOLATILITIES = ["stable", "volatile"];
553
595
  var usableSize = (n) => typeof n === "number" && isFinite(n) ? n : null;
596
+ var pickDeclared = (own, fromMeta) => own === void 0 ? fromMeta : own;
554
597
  function resourceHints(entry) {
555
598
  const meta = entry && entry._meta && entry._meta[STATIC_PRIMITIVES_META] || {};
556
599
  return {
@@ -566,8 +609,13 @@ function resourceHints(entry) {
566
609
  get sizeBytes() {
567
610
  return this.size;
568
611
  },
569
- volatility: VOLATILITIES.indexOf(meta.volatility) === -1 ? "stable" : meta.volatility,
570
- autoAttach: typeof meta.autoAttach === "boolean" ? meta.autoAttach : true
612
+ // Same rule as `size`: the entry's own field wins, `_meta` is the fallback.
613
+ // SEP-0000 defines these ON the Resource object; a server puts them in
614
+ // `_meta` only because the extension is not merged yet. A PAGE card's
615
+ // entries already sit inside the extension block, so for them the fields
616
+ // are simply top-level — reading both is what lets one planner serve both.
617
+ volatility: VOLATILITIES.indexOf(pickDeclared(entry && entry.volatility, meta.volatility)) === -1 ? "stable" : pickDeclared(entry && entry.volatility, meta.volatility),
618
+ autoAttach: typeof pickDeclared(entry && entry.autoAttach, meta.autoAttach) === "boolean" ? pickDeclared(entry && entry.autoAttach, meta.autoAttach) : true
571
619
  };
572
620
  }
573
621
  function planResourceAttachment(entry, budgetBytes) {
@@ -611,6 +659,45 @@ async function readResourceContext({ endpoint, uri, name, read, budgetBytes }) {
611
659
  return null;
612
660
  }
613
661
  }
662
+ function planPageResources({ entries, budgetBytes, onSkip }) {
663
+ return (entries || []).map((entry) => {
664
+ const plan = planResourceAttachment(entry, budgetBytes);
665
+ plan.name = entry.name || entry.uri;
666
+ plan.description = entry.description || "";
667
+ if (!plan.fetch && onSkip) onSkip(plan);
668
+ return plan;
669
+ }).filter((plan) => plan.fetch);
670
+ }
671
+ function pageResourceLine(plan, text) {
672
+ return "- " + plan.name + (plan.description ? " (" + plan.description + ")" : "") + ": " + text;
673
+ }
674
+ async function pageResourceLinesForTurn({ plans, resources, budgetBytes, cache = {} }) {
675
+ const lines = [];
676
+ for (const plan of plans || []) {
677
+ let text = plan.volatility === "volatile" ? null : cache[plan.uri];
678
+ if (text === null || text === void 0) {
679
+ text = await readPageResource({ uri: plan.uri, resources, budgetBytes });
680
+ if (plan.volatility !== "volatile" && text !== null) cache[plan.uri] = text;
681
+ }
682
+ if (text !== null) lines.push(pageResourceLine(plan, text));
683
+ }
684
+ return { lines, cache };
685
+ }
686
+ async function readPageResource({ uri, resources, budgetBytes }) {
687
+ const handler = resources && resources[uri];
688
+ if (typeof handler !== "function") {
689
+ console.warn('[llm-meta-widget] the page declares resource "' + uri + '" but no handler answers to that uri');
690
+ return null;
691
+ }
692
+ try {
693
+ const value = await handler();
694
+ const text = typeof value === "string" ? value : JSON.stringify(value);
695
+ return trimResourceText(text === void 0 ? "" : text, budgetBytes);
696
+ } catch (e) {
697
+ console.warn('[llm-meta-widget] reading "' + uri + '" failed:', e && e.message);
698
+ return null;
699
+ }
700
+ }
614
701
  async function loadHostResource({ endpoint, budgetBytes, list: list2, read, onSkip }) {
615
702
  const resources = await list2({ endpoint });
616
703
  const candidate = (resources || []).filter((r) => (r.mimeType || "") === "application/json")[0];
@@ -2904,8 +2991,11 @@ var lexer = _Lexer.lex;
2904
2991
  var DEFAULTS = Object.freeze({
2905
2992
  API_KEY_UUID: "ollama-local",
2906
2993
  ACTIONS_SCHEMA_ID: "ai-actions",
2994
+ PAGE_CARD_ID: "ai-page",
2907
2995
  STATE_GLOBAL: "aiState",
2908
2996
  ACTIONS_GLOBAL: "aiActions",
2997
+ PROMPTS_GLOBAL: "aiPrompts",
2998
+ RESOURCES_GLOBAL: "aiResources",
2909
2999
  REMOTE_TOOLS_SCHEMA_ID: "remote-mcp-tools",
2910
3000
  LLM_PROVIDER: "llm_meta_server",
2911
3001
  MAX_ROUNDS: 3
@@ -2939,9 +3029,12 @@ function readConfig(el) {
2939
3029
  API_KEY_UUID: str("api-key-uuid", DEFAULTS.API_KEY_UUID),
2940
3030
  MODEL: str("model", null),
2941
3031
  ACTIONS_SCHEMA_ID: str("actions-schema-id", DEFAULTS.ACTIONS_SCHEMA_ID),
3032
+ PAGE_CARD_ID: str("page-card-id", DEFAULTS.PAGE_CARD_ID),
2942
3033
  GREETING: str("greeting", null),
2943
3034
  STATE_GLOBAL: str("state-global", DEFAULTS.STATE_GLOBAL),
2944
3035
  ACTIONS_GLOBAL: str("actions-global", DEFAULTS.ACTIONS_GLOBAL),
3036
+ PROMPTS_GLOBAL: str("prompts-global", DEFAULTS.PROMPTS_GLOBAL),
3037
+ RESOURCES_GLOBAL: str("resources-global", DEFAULTS.RESOURCES_GLOBAL),
2945
3038
  REMOTE_TOOLS_SCHEMA_ID: str("remote-tools-schema-id", DEFAULTS.REMOTE_TOOLS_SCHEMA_ID),
2946
3039
  MAX_ROUNDS: int("max-rounds", DEFAULTS.MAX_ROUNDS),
2947
3040
  WELL_KNOWN_URLS: wellKnown === null ? null : wellKnown.trim() === "" ? [] : splitList(wellKnown),
@@ -3658,9 +3751,12 @@ function boot(cfg) {
3658
3751
  var API_KEY_UUID = cfg.API_KEY_UUID;
3659
3752
  var MODEL = cfg.MODEL;
3660
3753
  var ACTIONS_SCHEMA_ID = cfg.ACTIONS_SCHEMA_ID;
3754
+ var PAGE_CARD_ID = cfg.PAGE_CARD_ID;
3661
3755
  var GREETING = cfg.GREETING;
3662
3756
  var STATE_GLOBAL = cfg.STATE_GLOBAL;
3663
3757
  var ACTIONS_GLOBAL = cfg.ACTIONS_GLOBAL;
3758
+ var PROMPTS_GLOBAL = cfg.PROMPTS_GLOBAL;
3759
+ var RESOURCES_GLOBAL = cfg.RESOURCES_GLOBAL;
3664
3760
  var REMOTE_TOOLS_SCHEMA_ID = cfg.REMOTE_TOOLS_SCHEMA_ID;
3665
3761
  var MAX_ROUNDS = cfg.MAX_ROUNDS;
3666
3762
  var WELL_KNOWN_URLS = cfg.WELL_KNOWN_URLS;
@@ -3791,8 +3887,10 @@ function boot(cfg) {
3791
3887
  model: MODEL
3792
3888
  });
3793
3889
  }
3890
+ var pageCardEl = document.getElementById(PAGE_CARD_ID);
3891
+ var pageCard = readStaticPrimitivesCard(pageCardEl ? pageCardEl.textContent : null);
3794
3892
  var actionsSchemaEl = document.getElementById(ACTIONS_SCHEMA_ID);
3795
- var localTools = actionsSchemaEl ? JSON.parse(actionsSchemaEl.textContent) : [];
3893
+ var localTools = pageCard.tools.length ? pageCard.tools : actionsSchemaEl ? JSON.parse(actionsSchemaEl.textContent) : [];
3796
3894
  var remoteToolsEl = document.getElementById(REMOTE_TOOLS_SCHEMA_ID);
3797
3895
  var baselineRemoteTools = remoteToolsEl ? JSON.parse(remoteToolsEl.textContent) : [];
3798
3896
  var remoteTools = baselineRemoteTools.slice();
@@ -4015,11 +4113,22 @@ function boot(cfg) {
4015
4113
  }
4016
4114
  }
4017
4115
  var hostWideTools = [];
4018
- var hostWidePrompts = [];
4116
+ var hostWidePrompts = pageCard.prompts.map(function(p) {
4117
+ return Object.assign({}, p, { source: "page" });
4118
+ });
4019
4119
  var resourceContext = null;
4020
4120
  var resourcePlan = null;
4021
4121
  var RESOURCE_BUDGET_BYTES = 8e3;
4122
+ var pageResourcePlans = planPageResources({
4123
+ entries: pageCard.resources,
4124
+ budgetBytes: RESOURCE_BUDGET_BYTES,
4125
+ onSkip: function(plan) {
4126
+ console.info("[llm_meta_widget] not attaching " + plan.uri + " (" + plan.reason + ", size=" + plan.size + ")");
4127
+ }
4128
+ });
4129
+ var pageResourceCache = {};
4022
4130
  restoreConversation();
4131
+ var promptsEl = root.querySelector(".lmw-prompts");
4023
4132
  var wellKnownReady = (async function() {
4024
4133
  var urls = WELL_KNOWN_URLS === null ? [window.location.origin + "/.well-known/mcp.json"] : WELL_KNOWN_URLS;
4025
4134
  for (var i = 0; i < urls.length; i++) {
@@ -4053,7 +4162,6 @@ function boot(cfg) {
4053
4162
  renderPromptButtons();
4054
4163
  renderWelcome();
4055
4164
  })();
4056
- var promptsEl = root.querySelector(".lmw-prompts");
4057
4165
  function renderPromptButtons() {
4058
4166
  if (!promptsEl) return;
4059
4167
  promptsEl.textContent = "";
@@ -4150,7 +4258,7 @@ function boot(cfg) {
4150
4258
  }
4151
4259
  button.disabled = true;
4152
4260
  try {
4153
- var result = await getMcpPrompt({ endpoint: prompt.endpoint, name: prompt.name, args });
4261
+ var result = prompt.source === "page" ? await getPagePrompt({ name: prompt.name, args, prompts: window[PROMPTS_GLOBAL] }) : await getMcpPrompt({ endpoint: prompt.endpoint, name: prompt.name, args });
4154
4262
  var text = promptMessagesToText(result);
4155
4263
  if (!text) {
4156
4264
  appendTurn("error", "The server returned an empty prompt.");
@@ -4364,7 +4472,14 @@ function boot(cfg) {
4364
4472
  budgetBytes: RESOURCE_BUDGET_BYTES
4365
4473
  });
4366
4474
  resourceContext = turn.cached;
4367
- return turn.lines;
4475
+ var page = await pageResourceLinesForTurn({
4476
+ plans: pageResourcePlans,
4477
+ resources: window[RESOURCES_GLOBAL],
4478
+ budgetBytes: RESOURCE_BUDGET_BYTES,
4479
+ cache: pageResourceCache
4480
+ });
4481
+ pageResourceCache = page.cache;
4482
+ return turn.lines.concat(page.lines);
4368
4483
  }
4369
4484
  var currentAbort = null;
4370
4485
  clearBtn.addEventListener("click", function() {
@@ -730,6 +730,28 @@ export async function readMcpResource({ endpoint, uri, signal }) {
730
730
  // The spec allows `content` to be an object or an array of content blocks;
731
731
  // anything non-text is dropped, which is worth knowing — a prompt carrying an
732
732
  // image would silently lose it on this path.
733
+ // A prompt the PAGE declared, resolved against its handler map. This is
734
+ // prompts/get for a surface with no transport: the card carries the metadata
735
+ // (name, description, arguments — what prompts/list returns) and the handler
736
+ // returns the filled messages, exactly as a server's prompts/get would.
737
+ //
738
+ // Keyed by `name` because that is what prompts/get takes. Accepts a plain
739
+ // string as well as a messages array: a page prompt is usually one block of
740
+ // text, and making every host write the envelope by hand would be a tax with
741
+ // no benefit — promptMessagesToText flattens both to the same thing.
742
+ export async function getPagePrompt({ name, args, prompts }) {
743
+ const handler = prompts && prompts[name]
744
+ if (typeof handler !== "function") {
745
+ throw new Error('the page declares prompt "' + name + '" but no handler answers to that name')
746
+ }
747
+
748
+ const result = await handler(args || {})
749
+ if (typeof result === "string") {
750
+ return { messages: [ { role: "user", content: { type: "text", text: result } } ] }
751
+ }
752
+ return result || { messages: [] }
753
+ }
754
+
733
755
  export function promptMessagesToText(result) {
734
756
  return (result?.messages || [])
735
757
  .map((m) => {
@@ -840,9 +862,63 @@ function parseSseFrame(raw) {
840
862
  // the first turn only.
841
863
  export const STATIC_PRIMITIVES_META = "io.modelcontextprotocol/static-primitives"
842
864
 
865
+ // ---- page card --------------------------------------------------------
866
+ //
867
+ // The same `io.modelcontextprotocol/static-primitives` envelope a server puts
868
+ // in its Server Card, delivered in the DOCUMENT instead: one <script> of JSON
869
+ // declaring the page's own tools, prompts and resources. SEP-2575 already says
870
+ // the Server Card and server/discover are two deliveries of one content; a page
871
+ // is a third, with no transport at all.
872
+ //
873
+ // Parsing only. What a declaration MEANS is resolved elsewhere: tool handlers
874
+ // live in window.aiActions keyed by `name`, and reads will be keyed by the
875
+ // protocol's own identifier — `name` for prompts, `uri` for resources, because
876
+ // that is what prompts/get and resources/read take.
877
+ //
878
+ // Malformed input must never take the page down: a card is host-authored and a
879
+ // typo in it should cost that one entry, not the assistant. Everything
880
+ // unparseable returns empty arrays and says so on the console, which is how
881
+ // aiState already treats a reader missing its description.
882
+ export function readStaticPrimitivesCard(text, warn = console.warn) {
883
+ const empty = { tools: [], prompts: [], resources: [] }
884
+ if (typeof text !== "string" || text.trim() === "") return empty
885
+
886
+ let doc
887
+ try {
888
+ doc = JSON.parse(text)
889
+ } catch (e) {
890
+ warn("[llm-meta-widget] page card is not valid JSON; ignoring it:", e.message)
891
+ return empty
892
+ }
893
+
894
+ const ext = (doc && doc._meta && doc._meta[STATIC_PRIMITIVES_META]) || null
895
+ if (!ext) {
896
+ warn(`[llm-meta-widget] page card has no _meta["${STATIC_PRIMITIVES_META}"]; ignoring it`)
897
+ return empty
898
+ }
899
+
900
+ const named = (arr, kind) => (Array.isArray(arr) ? arr : []).filter((e) => {
901
+ const ok = e && typeof e.name === "string" && e.name.trim() !== ""
902
+ if (!ok) warn(`[llm-meta-widget] page card: dropping a ${kind} with no name`)
903
+ return ok
904
+ })
905
+
906
+ return {
907
+ tools: named(ext.tools, "tool"),
908
+ prompts: named(ext.prompts, "prompt"),
909
+ // Resources are addressed by uri, not name — resources/read takes the uri.
910
+ resources: (Array.isArray(ext.resources) ? ext.resources : []).filter((e) => {
911
+ const ok = e && typeof e.uri === "string" && e.uri.trim() !== ""
912
+ if (!ok) warn("[llm-meta-widget] page card: dropping a resource with no uri")
913
+ return ok
914
+ })
915
+ }
916
+ }
917
+
843
918
  const VOLATILITIES = [ "stable", "volatile" ]
844
919
 
845
920
  const usableSize = (n) => (typeof n === "number" && isFinite(n) ? n : null)
921
+ const pickDeclared = (own, fromMeta) => (own === undefined ? fromMeta : own)
846
922
 
847
923
  export function resourceHints(entry) {
848
924
  const meta = (entry && entry._meta && entry._meta[STATIC_PRIMITIVES_META]) || {}
@@ -857,8 +933,17 @@ export function resourceHints(entry) {
857
933
  // it returns is public API. Dropped at 0.9.0 together with the
858
934
  // `_meta.sizeBytes` fallback above — one deprecation, one bump.
859
935
  get sizeBytes() { return this.size },
860
- volatility: VOLATILITIES.indexOf(meta.volatility) === -1 ? "stable" : meta.volatility,
861
- autoAttach: typeof meta.autoAttach === "boolean" ? meta.autoAttach : true
936
+ // Same rule as `size`: the entry's own field wins, `_meta` is the fallback.
937
+ // SEP-0000 defines these ON the Resource object; a server puts them in
938
+ // `_meta` only because the extension is not merged yet. A PAGE card's
939
+ // entries already sit inside the extension block, so for them the fields
940
+ // are simply top-level — reading both is what lets one planner serve both.
941
+ volatility: VOLATILITIES.indexOf(pickDeclared(entry && entry.volatility, meta.volatility)) === -1
942
+ ? "stable"
943
+ : pickDeclared(entry && entry.volatility, meta.volatility),
944
+ autoAttach: typeof pickDeclared(entry && entry.autoAttach, meta.autoAttach) === "boolean"
945
+ ? pickDeclared(entry && entry.autoAttach, meta.autoAttach)
946
+ : true
862
947
  }
863
948
  }
864
949
 
@@ -920,6 +1005,65 @@ async function readResourceContext({ endpoint, uri, name, read, budgetBytes }) {
920
1005
  }
921
1006
  }
922
1007
 
1008
+ // ---- page resources ----------------------------------------------------
1009
+ //
1010
+ // resources/read for a surface with no transport. The card lists them, a
1011
+ // handler map keyed by URI reads them — URI because that is what
1012
+ // resources/read takes, and because the spec defines it as the unique
1013
+ // identifier while `name` is only required, never unique.
1014
+ //
1015
+ // Unlike a server endpoint, a page declares SEVERAL: PubDictionaries has six
1016
+ // readers, each with its own description. So these are planned and rendered as
1017
+ // a list, not as the single reference resource loadHostResource assumes.
1018
+ export function planPageResources({ entries, budgetBytes, onSkip }) {
1019
+ return (entries || []).map((entry) => {
1020
+ const plan = planResourceAttachment(entry, budgetBytes)
1021
+ plan.name = entry.name || entry.uri
1022
+ plan.description = entry.description || ""
1023
+ if (!plan.fetch && onSkip) onSkip(plan)
1024
+ return plan
1025
+ }).filter((plan) => plan.fetch)
1026
+ }
1027
+
1028
+ // One compact line per resource — the same shape the page-state annex uses,
1029
+ // NOT the block form a hub resource gets. Six block-form resources would cost
1030
+ // three lines each and repeat a header the model does not need; how to render
1031
+ // attached context is the client's decision, which the spec leaves open.
1032
+ export function pageResourceLine(plan, text) {
1033
+ return "- " + plan.name + (plan.description ? " (" + plan.description + ")" : "") + ": " + text
1034
+ }
1035
+
1036
+ export async function pageResourceLinesForTurn({ plans, resources, budgetBytes, cache = {} }) {
1037
+ const lines = []
1038
+ for (const plan of plans || []) {
1039
+ let text = plan.volatility === "volatile" ? null : cache[plan.uri]
1040
+ if (text === null || text === undefined) {
1041
+ text = await readPageResource({ uri: plan.uri, resources, budgetBytes })
1042
+ if (plan.volatility !== "volatile" && text !== null) cache[plan.uri] = text
1043
+ }
1044
+ if (text !== null) lines.push(pageResourceLine(plan, text))
1045
+ }
1046
+ return { lines, cache }
1047
+ }
1048
+
1049
+ async function readPageResource({ uri, resources, budgetBytes }) {
1050
+ const handler = resources && resources[uri]
1051
+ if (typeof handler !== "function") {
1052
+ console.warn('[llm-meta-widget] the page declares resource "' + uri + '" but no handler answers to that uri')
1053
+ return null
1054
+ }
1055
+ try {
1056
+ const value = await handler()
1057
+ const text = typeof value === "string" ? value : JSON.stringify(value)
1058
+ return trimResourceText(text === undefined ? "" : text, budgetBytes)
1059
+ } catch (e) {
1060
+ // Optional context — one broken reader must not cost the turn, exactly as
1061
+ // a failed hub read does not.
1062
+ console.warn('[llm-meta-widget] reading "' + uri + '" failed:', e && e.message)
1063
+ return null
1064
+ }
1065
+ }
1066
+
923
1067
  // The discovery sequence for one endpoint's reference resource, kept here
924
1068
  // rather than in the panel so the decision AND the call site are testable
925
1069
  // together: a gate nothing consults is exactly the bug this shape prevents.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aibranch/llm-meta-widget",
3
- "version": "0.8.6",
3
+ "version": "0.9.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": [