@eventcatalog/core 5.9.0 → 5.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.
Files changed (51) hide show
  1. package/dist/analytics/analytics.cjs +1 -1
  2. package/dist/analytics/analytics.js +2 -2
  3. package/dist/analytics/log-build.cjs +1 -1
  4. package/dist/analytics/log-build.js +3 -3
  5. package/dist/{chunk-WN6ZFSXF.js → chunk-5QOG7UYZ.js} +1 -1
  6. package/dist/{chunk-C4L6B5KR.js → chunk-EETSYUKN.js} +1 -1
  7. package/dist/{chunk-UIJUSHOT.js → chunk-EGOTM5ZG.js} +3 -2
  8. package/dist/{chunk-26C26LFE.js → chunk-EXBUNFIM.js} +1 -1
  9. package/dist/{chunk-SZU7I2PD.js → chunk-ZE2RPUNY.js} +1 -1
  10. package/dist/constants.cjs +1 -1
  11. package/dist/constants.js +1 -1
  12. package/dist/docs/development/00-why-eventcatalog.md +3 -0
  13. package/dist/docs/development/ask-your-architecture/01-intro.md +17 -4
  14. package/dist/docs/development/ask-your-architecture/06-sync-docs-with-your-code.md +8 -1
  15. package/dist/docs/development/ask-your-architecture/07-catch-breaking-changes.md +8 -1
  16. package/dist/docs/development/ask-your-architecture/08-check-code-against-adrs.md +169 -0
  17. package/dist/docs/development/guides/resources/adrs/01-introduction.md +1 -0
  18. package/dist/docs/development/guides/resources/entities/05-entity-maps.md +5 -0
  19. package/dist/docs/development/guides/resources/entities/06-global-entity-map.md +101 -0
  20. package/dist/docs/development/guides/resources/entities/{06-reference.md → 07-reference.md} +2 -1
  21. package/dist/eventcatalog.cjs +6 -5
  22. package/dist/eventcatalog.js +5 -5
  23. package/dist/generate.cjs +3 -2
  24. package/dist/generate.js +3 -3
  25. package/dist/utils/cli-logger.cjs +1 -1
  26. package/dist/utils/cli-logger.js +2 -2
  27. package/eventcatalog/auth.config.ts +7 -9
  28. package/eventcatalog/src/components/MDX/EntityMap/CatalogEntityMap.tsx +301 -0
  29. package/eventcatalog/src/features/ai/chat-api.ts +2 -4
  30. package/eventcatalog/src/features/auth/login.astro +2 -4
  31. package/eventcatalog/src/features/auth/middleware/middleware.ts +2 -3
  32. package/eventcatalog/src/features/mcp/apps/generated/canvas.html +61 -61
  33. package/eventcatalog/src/features/mcp/apps/generated/viewer.html +72 -72
  34. package/eventcatalog/src/features/mcp/mcp-server.ts +2 -1
  35. package/eventcatalog/src/features/studio/components/ConnectAgentDialog.tsx +1 -1
  36. package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +2 -2
  37. package/eventcatalog/src/features/studio/pages/webmcp-relay.ts +1 -1
  38. package/eventcatalog/src/features/studio/webmcp-relay/README.md +1 -1
  39. package/eventcatalog/src/features/studio/webmcp-relay/embed.js +1 -2
  40. package/eventcatalog/src/features/studio/webmcp-relay/widget.html +2 -1
  41. package/eventcatalog/src/features/studio/webmcp-relay/widget.js +2 -1
  42. package/eventcatalog/src/pages/visualiser/entity-map/index.astro +47 -0
  43. package/eventcatalog/src/pages/visualiser/graph/index.astro +2 -2
  44. package/eventcatalog/src/stores/sidebar-store/state.ts +10 -1
  45. package/eventcatalog/src/utils/catalog-modules.ts +16 -0
  46. package/eventcatalog/src/utils/node-graphs/catalog-entity-map-view.ts +82 -0
  47. package/eventcatalog/src/utils/node-graphs/catalog-entity-map.ts +113 -0
  48. package/eventcatalog/src/utils/node-graphs/domain-entity-map.ts +17 -61
  49. package/eventcatalog/src/utils/node-graphs/entity-layout.ts +53 -0
  50. package/eventcatalog/src/utils/node-graphs/flow-lanes.ts +2 -2
  51. package/package.json +6 -6
@@ -3,6 +3,7 @@ import { Hono, type Context } from 'hono';
3
3
  import { McpServer, WebStandardStreamableHTTPServerTransport, type StandardSchemaWithJSON } from '@modelcontextprotocol/server';
4
4
  import { z } from 'zod';
5
5
  import { join } from 'node:path';
6
+ import { importCatalogModule } from '@utils/catalog-modules';
6
7
  import * as catalogTools from '@features/tools/catalog-tools';
7
8
  import { getCollection } from 'astro:content';
8
9
  import { createMcpAuthErrorResponse, validateMcpRequest } from './mcp-auth';
@@ -78,7 +79,7 @@ function createToolHandler<T>(fn: (params: T) => Promise<any>, errorMessage: str
78
79
  let extendedTools: Record<string, any> = {};
79
80
 
80
81
  try {
81
- const providerConfiguration = await import(/* @vite-ignore */ join(catalogDirectory, 'eventcatalog.chat.js'));
82
+ const providerConfiguration = await importCatalogModule('eventcatalog.chat.js');
82
83
 
83
84
  if (providerConfiguration.tools) {
84
85
  extendedTools = providerConfiguration.tools;
@@ -5,7 +5,7 @@ import { STATUS, ClaudeCodeLogo, ClaudeLogo, CursorLogo, OpenAILogo } from '@eve
5
5
 
6
6
  type CanvasLink = { canvasId: string; title?: string; canvasUrl: string; mcpUrl: string };
7
7
 
8
- export const RELAY_COMMAND = 'npx -y @mcp-b/webmcp-local-relay@5.1.0';
8
+ export const RELAY_COMMAND = 'npx -y @mcp-b/webmcp-local-relay@6.0.3';
9
9
  const RELAY_KEY = 'eventcatalog-webmcp-relay';
10
10
  const WEBMCP_KEY = 'eventcatalog-webmcp';
11
11
 
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useRef, useState } from 'react';
2
2
  import { z } from 'zod';
3
3
  import type * as Y from 'yjs';
4
- import { initializeWebMCPPolyfill } from '@mcp-b/webmcp-polyfill';
4
+ import { installWebMCP } from '@mcp-b/webmcp-polyfill';
5
5
  import {
6
6
  replyToThread,
7
7
  setThreadResolved,
@@ -129,7 +129,7 @@ export function useCanvasWebMcp(options: CanvasWebMcpOptions) {
129
129
  if (!options.doc) return;
130
130
  if (!getModelContext()) {
131
131
  if (!options.allowPolyfill) return;
132
- initializeWebMCPPolyfill();
132
+ installWebMCP();
133
133
  usingPolyfill = true;
134
134
  }
135
135
  const modelContext = getModelContext();
@@ -6,7 +6,7 @@ import widgetJs from '../webmcp-relay/widget.js?raw';
6
6
  /**
7
7
  * Serves the WebMCP local relay's browser files (embed.js loads widget.html next to it), so the canvas page
8
8
  * can hand its WebMCP tools to desktop MCP clients (Claude Code, Cursor...) running
9
- * `npx -y @mcp-b/webmcp-local-relay@5.1.0`. The files are copies of that version's (see webmcp-relay/README.md),
9
+ * `npx -y @mcp-b/webmcp-local-relay@6.0.3`. The files are copies of that version's (see webmcp-relay/README.md),
10
10
  * bundled with the page, so the version is pinned.
11
11
  */
12
12
  const FILES: Record<string, { body: string; contentType: string }> = {
@@ -1,5 +1,5 @@
1
1
  # WebMCP local relay (browser files)
2
2
 
3
- `embed.js`, `widget.html` and `widget.js` are the browser files of [`@mcp-b/webmcp-local-relay`](https://github.com/WebMCP-org/npm-packages/tree/main/packages/webmcp-local-relay) **5.1.0** (MIT, see `LICENSE`), copied as published. Studio serves them at `/webmcp-relay/*` so a canvas can hand its WebMCP tools to desktop MCP clients running `npx -y @mcp-b/webmcp-local-relay@5.1.0`.
3
+ `embed.js`, `widget.html` and `widget.js` are the browser files of [`@mcp-b/webmcp-local-relay`](https://github.com/WebMCP-org/npm-packages/tree/main/packages/webmcp-local-relay) **6.0.3** (MIT, see `LICENSE`), copied as published. Studio serves them at `/webmcp-relay/*` so a canvas can hand its WebMCP tools to desktop MCP clients running `npx -y @mcp-b/webmcp-local-relay@6.0.3`.
4
4
 
5
5
  They're copied rather than installed because the package's Node dependencies (which EventCatalog doesn't run) pin vulnerable versions of `@modelcontextprotocol/core` and `zod`. When moving to a new relay version, copy its `dist/browser` files here and update the version in `ConnectAgentDialog.tsx`.
@@ -1,2 +1 @@
1
- (function(){function e(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function t(t){if(!e(t))return!1;let n=Object.getPrototypeOf(t);return n===Object.prototype||n===null}function n(e,r=new WeakSet){if(e===null||typeof e==`string`||typeof e==`boolean`)return!0;if(typeof e==`number`)return Number.isFinite(e);if(typeof e!=`object`||r.has(e))return!1;r.add(e);try{return(Array.isArray(e)?e:t(e)?Object.values(e):null)?.every(e=>n(e,r))??!1}catch{return!1}finally{r.delete(e)}}function r(e){return n(e)?e:void 0}function i(t){return e(t)&&Array.isArray(t.content)}function a(e){if(typeof e==`string`)return e;try{return JSON.stringify(e)??String(e)}catch{return String(e)}}function o(e){if(i(e))return e;let t=r(e);return{content:[{type:`text`,text:a(e)}],...t===void 0?{}:{structuredContent:t},isError:!1}}function s(e){return!!e&&typeof e==`object`&&!Array.isArray(e)}function c(){return crypto.randomUUID()}let l=`[data-webmcp-relay]`,u=`__webmcp_relay_tab_id`,d=null,f;function p(){return document.currentScript instanceof HTMLScriptElement?document.currentScript:null}let m=p(),h=m?m.hasAttribute(`data-debug`):!1;function g(...e){h&&console.warn(`[webmcp-relay-embed]`,...e)}function _(){try{let e=sessionStorage.getItem(u);if(e)return e}catch(e){g(`sessionStorage read failed, tab ID will not persist:`,e)}let e=c();try{sessionStorage.setItem(u,e)}catch(e){g(`sessionStorage write failed:`,e)}return e}function v(e){if(!e?.src)throw Error(`The relay embed script must be loaded from a URL`);return new URL(`widget.html`,e.src).href}function y(e){let t=v(e),n=e?.getAttribute(`data-relay-id`)||void 0,r=e?.getAttribute(`data-relay-workspace`)||void 0,i=e?.getAttribute(`data-request-timeout`)||void 0;return{autoConnect:e?.getAttribute(`data-auto-connect`)!==`false`,relayHost:e?.getAttribute(`data-relay-host`)||`127.0.0.1`,relayPort:e?.getAttribute(`data-relay-port`)||`9333`,...n?{relayId:n}:{},...r?{relayWorkspace:r}:{},...i?{requestTimeout:i}:{},tabId:_(),widgetUrl:t,widgetOrigin:new URL(t).origin}}function b(e){return s(e)?e:(e!=null&&g(`Tool invocation args must be an object, got`,typeof e),{})}function x(e){let t;if(e.inputSchema!==void 0)if(typeof e.inputSchema==`string`)try{t=JSON.parse(e.inputSchema)}catch{return console.warn(`[webmcp-relay-embed] Tool "${e.name}" was not relayed because its input schema is malformed.`),null}else t=e.inputSchema;return{name:e.name,...e.title===void 0?{}:{title:e.title},description:e.description,...t===void 0?{}:{inputSchema:t},...e.annotations===void 0?{}:{annotations:e.annotations}}}function S(e){if(e===null)return{isError:!0,content:[{type:`text`,text:`Tool execution interrupted by navigation`}]};let t;try{t=JSON.parse(e)}catch{t=e}return s(t)&&t.resultType===`input_required`?{isError:!0,content:[{type:`text`,text:`The WebMCP local relay cannot forward MCP input_required results. Multi-round tool flows require direct McpServer registration.`}]}:o(t)}function C(e){return!!(e&&`executeTool`in e&&typeof e.executeTool==`function`)}function w(){let e=document.modelContext;return C(e)?e:void 0}async function T(){let e=w();return e?(await e.getTools()).map(x).filter(e=>e!==null):[]}async function E(e,t){let n=w();if(!n)throw Error(`No executable WebMCP runtime found on this page`);let r=(await n.getTools()).find(t=>t.name===e);if(!r)throw Error(`Tool not found: ${e}`);return S(await n.executeTool(r,JSON.stringify(t)))}let D=!1,O=0,k=null,A=``;function j(e){if(typeof e!=`object`||!e)return JSON.stringify(e)??`undefined`;if(Array.isArray(e))return`[${e.map(j).join(`,`)}]`;let t=e;return`{${Object.keys(t).sort().map(e=>`${JSON.stringify(e)}:${j(t[e])}`).join(`,`)}}`}function M(e){return e.map(j).sort().join(`
2
- `)}function N(){D=!1;let e=O;T().then(t=>{if(e!==O)return;let n=M(t);n===A||!d||(A=n,d.postMessage({type:`webmcp.tools.changed`,tools:t},f.widgetOrigin))}).catch(e=>{g(`Failed to sync tool changes:`,e)})}function P(){O++,!D&&(D=!0,setTimeout(N,0))}function F(){k||(k=setInterval(P,2e3))}function I(){try{let e=document.modelContext;return e?(e.addEventListener(`toolchange`,P),!0):!1}catch(e){return g(`addEventListener on modelContext threw:`,e),!1}}function L(){if(F(),P(),I())return;let e=0,t=100,n=()=>{setTimeout(()=>{if(e++,!I()){if(e>=40){g(`Could not subscribe to tool changes after 40 retries. Dynamic tool updates will rely on polling.`);return}t=Math.min(Math.round(t*1.5),1e3),n()}},t)};n()}function R(e,t,n){!e||typeof e!=`object`||!(`postMessage`in e)||e.postMessage(n,t)}function z(e){return!s(e)||typeof e.requestId!=`string`||typeof e.type!=`string`?null:{requestId:e.requestId,type:e.type,toolName:e.toolName,args:e.args}}function B(e,t){T().then(n=>{R(t.source,t.origin,{type:`webmcp.tools.list.response`,requestId:e.requestId,tools:n})}).catch(n=>{g(`Failed to list tools:`,n),R(t.source,t.origin,{type:`webmcp.tools.list.response`,requestId:e.requestId,tools:[],error:`Failed to list tools: ${n instanceof Error?n.message:String(n)}`})})}function V(e,t){E(String(e.toolName??``),b(e.args)).then(n=>{R(t.source,t.origin,{type:`webmcp.tools.invoke.response`,requestId:e.requestId,result:s(n)?n:{}})}).catch(n=>{R(t.source,t.origin,{type:`webmcp.tools.invoke.error`,requestId:e.requestId,error:String(n instanceof Error?n.message:n)})})}async function H(e){if(document.querySelector(l))return;let t=new URLSearchParams;t.set(`tabId`,e.tabId),t.set(`hostOrigin`,window.location.origin);let n=new URL(window.location.href);n.search=``,n.hash=``,t.set(`hostUrl`,n.href),t.set(`hostTitle`,document.title||``),t.set(`relayHost`,e.relayHost),t.set(`relayPort`,e.relayPort),t.set(`autoConnect`,e.autoConnect?`true`:`false`),e.relayId&&t.set(`relayId`,e.relayId),e.relayWorkspace&&t.set(`relayWorkspace`,e.relayWorkspace),e.requestTimeout&&t.set(`requestTimeout`,e.requestTimeout);let r=await fetch(e.widgetUrl);if(!r.ok)throw Error(`Widget HTML request failed with status ${String(r.status)}`);let i=await r.text(),a=`<script>window.__WEBMCP_RELAY_CONFIG=${JSON.stringify(Object.fromEntries(t))};<\/script>`,o=URL.createObjectURL(new Blob([i.replace(`</head>`,`${a}</head>`)],{type:`text/html`}));e.widgetOrigin=window.location.origin;let s=document.createElement(`iframe`);s.src=o,s.style.display=`none`,s.setAttribute(`aria-hidden`,`true`),s.setAttribute(`data-webmcp-relay`,`1`),s.setAttribute(`allow`,`loopback-network; local-network; local-network-access`),document.body.appendChild(s),d=s.contentWindow,s.addEventListener(`load`,()=>{d=s.contentWindow,URL.revokeObjectURL(o)}),s.addEventListener(`error`,()=>{console.error(`[webmcp-relay-embed] Failed to load relay widget iframe from:`,s.src,`-- WebMCP tools will NOT be relayed. Check network connectivity and widget URL.`),URL.revokeObjectURL(o)})}if(!document.querySelector(l)){try{f=y(m)}catch(e){throw console.error(`[webmcp-relay-embed] Failed to initialize relay configuration:`,e),e}window.addEventListener(`message`,e=>{if(e.origin!==f.widgetOrigin||!d||e.source!==d)return;let t=e.data;if(s(t)&&t.type===`webmcp.reload`){window.location.reload();return}let n=z(e.data);if(n){if(n.type===`webmcp.tools.list.request`){B(n,e);return}n.type===`webmcp.tools.invoke.request`&&V(n,e)}});let e=()=>{H(f).catch(e=>{console.error(`[webmcp-relay-embed] Failed to inject relay widget:`,e)})};document.body?e():document.addEventListener(`DOMContentLoaded`,e,{once:!0}),L(),document.addEventListener(`visibilitychange`,()=>{document.visibilityState===`visible`&&d&&d.postMessage({type:`webmcp.connect`},f.widgetOrigin)})}})();
1
+ (function(){function e(e){return!!e&&typeof e==`object`&&!Array.isArray(e)}let t=`[data-webmcp-relay]`,n=`__webmcp_relay_tab_id`,r=null,i=document.currentScript instanceof HTMLScriptElement?document.currentScript:null,a=i?i.hasAttribute(`data-debug`):!1;function o(...e){a&&console.warn(`[webmcp-relay-embed]`,...e)}function s(){try{let e=sessionStorage.getItem(n);if(e)return e}catch(e){o(`sessionStorage read failed, tab ID will not persist:`,e)}let e=crypto.randomUUID();try{sessionStorage.setItem(n,e)}catch(e){o(`sessionStorage write failed:`,e)}return e}function c(e){if(!e?.src)throw Error(`The relay embed script must be loaded from a URL`);return{autoConnect:String(e.getAttribute(`data-auto-connect`)!==`false`),relayHost:e.getAttribute(`data-relay-host`)||`127.0.0.1`,relayPort:e.getAttribute(`data-relay-port`)||`9333`,relayId:e.getAttribute(`data-relay-id`)||void 0,relayWorkspace:e.getAttribute(`data-relay-workspace`)||void 0,requestTimeout:e.getAttribute(`data-request-timeout`)||void 0,tabId:s(),widgetUrl:new URL(`widget.html`,e.src).href}}async function l({widgetUrl:e,...n}){if(document.querySelector(t))return;let i=new URL(window.location.href);i.search=``,i.hash=``;let a=JSON.stringify({...n,hostOrigin:window.location.origin,hostUrl:i.href,hostTitle:document.title}).replace(/</g,`\\u003c`),o=await fetch(e);if(!o.ok)throw Error(`Widget HTML request failed with status ${String(o.status)}`);let s=await o.text(),c=`<script>window.__WEBMCP_RELAY_CONFIG=${a};<\/script>`,l=URL.createObjectURL(new Blob([s.replace(`</head>`,()=>`${c}</head>`)],{type:`text/html`})),u=document.createElement(`iframe`);u.src=l,u.style.display=`none`,u.setAttribute(`aria-hidden`,`true`),u.setAttribute(`data-webmcp-relay`,`1`),u.setAttribute(`allow`,`loopback-network; local-network; local-network-access`),document.body.appendChild(u),r=u.contentWindow,u.addEventListener(`load`,()=>{r=u.contentWindow,URL.revokeObjectURL(l)}),u.addEventListener(`error`,()=>{console.error(`[webmcp-relay-embed] Failed to load relay widget iframe from:`,u.src,`-- WebMCP tools will NOT be relayed. Check network connectivity and widget URL.`),URL.revokeObjectURL(l)})}if(!document.querySelector(t)){let t;try{t=c(i)}catch(e){throw console.error(`[webmcp-relay-embed] Failed to initialize relay configuration:`,e),e}window.addEventListener(`message`,t=>{t.origin!==window.location.origin||!r||t.source!==r||e(t.data)&&t.data.type===`webmcp.reload`&&window.location.reload()});let n=()=>{l(t).catch(e=>{console.error(`[webmcp-relay-embed] Failed to inject relay widget:`,e)})};document.body?n():document.addEventListener(`DOMContentLoaded`,n,{once:!0}),document.addEventListener(`visibilitychange`,()=>{document.visibilityState===`visible`&&r&&r.postMessage({type:`webmcp.connect`},window.location.origin)})}})();
@@ -5,6 +5,7 @@
5
5
  <title>WebMCP Relay Widget</title>
6
6
  </head>
7
7
  <body>
8
- <script>(function(){let e=new Set([`127.0.0.1`,`localhost`,`::1`,`[::1]`]),t=9333;function n(e){return!!e&&typeof e==`object`&&!Array.isArray(e)}function r(t){return e.has(t)}function i(){return crypto.randomUUID()}function a(e){return String(e).replace(/[\r\n]/g,``)}function o(e,t){try{e.readyState===WebSocket.OPEN&&e.send(t)}catch(e){console.warn(`[webmcp-relay] Failed to send message:`,e)}}function s(e){return`__webmcp_relay_endpoint:${[e.hostOrigin,e.relayId??``,e.workspace??``].map(e=>encodeURIComponent(e)).join(`:`)}`}let c=[1e4,2e4,3e4];function l(e=window.location.search){let n=new URLSearchParams(e),a=globalThis.__WEBMCP_RELAY_CONFIG;function o(e){return n.get(e)??a?.[e]??null}let s=o(`hostOrigin`);if(!s)return null;let c=o(`hostUrl`)||s,l=o(`hostTitle`)||``,u=o(`tabId`)||i(),d=o(`relayHost`)||`127.0.0.1`,f=o(`relayPort`),p=f&&f.length>0?Number(f):t,m=o(`autoConnect`)!==`false`,h=o(`relayId`)||void 0,g=o(`relayWorkspace`)||void 0,_=o(`requestTimeout`),v=_&&_.length>0?Number(_):6e4;return r(d)?!Number.isInteger(p)||p<1||p>65535?(console.error(`[webmcp-relay-widget] relayPort must be an integer between 1 and 65535, got:`,f),null):!Number.isInteger(v)||v<1?(console.error(`[webmcp-relay-widget] requestTimeout must be a positive integer (ms), got:`,_),null):{autoConnect:m,hostOrigin:s,hostTitle:l,hostUrl:c,relayHostHint:d,relayPortHint:p,...h?{relayId:h}:{},...g?{relayWorkspace:g}:{},requestTimeoutMs:v,tabId:u}:(console.error(`[webmcp-relay-widget] relayHost must be a loopback address, got:`,d),null)}function u(e){return!n(e)||typeof e.requestId!=`string`||typeof e.type!=`string`?null:{requestId:e.requestId,type:e.type,tools:e.tools,result:e.result,error:e.error}}function d(){let e=l();if(!e){console.warn(`[webmcp-relay-widget] Missing required hostOrigin parameter. Widget will not start.`);return}b(e)}function f(e){return!n(e)||e.type!==`server-hello`||e.service!==`webmcp-local-relay`||e.version!==1||typeof e.host!=`string`||typeof e.instanceId!=`string`||typeof e.port!=`number`?null:{type:`server-hello`,service:`webmcp-local-relay`,version:1,host:e.host,instanceId:e.instanceId,port:e.port,...typeof e.label==`string`?{label:e.label}:{},...typeof e.relayId==`string`?{relayId:e.relayId}:{},...typeof e.workspace==`string`?{workspace:e.workspace}:{}}}function p(e){return!n(e)||e.type!==`hello/rejected`||typeof e.message!=`string`||typeof e.reason!=`string`?null:{type:`hello/rejected`,message:e.message,reason:e.reason}}function m(e){return s({hostOrigin:e.hostOrigin,...e.relayId?{relayId:e.relayId}:{},...e.relayWorkspace?{workspace:e.relayWorkspace}:{}})}function h(e){if(typeof sessionStorage>`u`)return null;try{let t=sessionStorage.getItem(m(e));if(!t)return null;let n=JSON.parse(t);return typeof n.host!=`string`||n.host.length===0||typeof n.port!=`number`||!Number.isInteger(n.port)||n.port<1||n.port>65535?null:{host:n.host,port:n.port}}catch{return null}}function g(e,t){if(!(typeof sessionStorage>`u`))try{sessionStorage.setItem(m(e),JSON.stringify({host:t.host,port:t.port}))}catch{}}function _(e){if(!(typeof sessionStorage>`u`))try{sessionStorage.removeItem(m(e))}catch{}}function v(e){let n=h(e),r=new Set,i=[],a=(e,t)=>{if(!Number.isInteger(t)||t<1||t>65535)return;let n=`${e}:${String(t)}`;r.has(n)||(r.add(n),i.push({host:e,port:t}))};e.relayPortHint!==void 0&&a(e.relayHostHint,e.relayPortHint),n&&a(n.host,n.port);for(let e of[`127.0.0.1`,`[::1]`])for(let n=t;n<=9348;n+=1)a(e,n);return i}async function y(e){let t=`ws://${e.host.includes(`:`)&&!e.host.startsWith(`[`)?`[${e.host}]`:e.host}:${String(e.port)}`;return new Promise(n=>{let r=!1,i=new WebSocket(t,[`webmcp-discovery.v1`,`webmcp.v1`]),a=e=>{if(!r){if(r=!0,clearTimeout(l),i.removeEventListener(`close`,s),i.removeEventListener(`error`,o),i.removeEventListener(`message`,c),e===null)try{i.close()}catch{}n(e)}},o=()=>{a(null)},s=()=>{a(null)},c=t=>{let n;try{n=JSON.parse(String(t.data))}catch{return}let r=f(n);r&&a({endpoint:{hello:r,host:e.host,port:e.port},socket:i})},l=setTimeout(()=>{a(null)},1200);i.addEventListener(`close`,s,{once:!0}),i.addEventListener(`error`,o,{once:!0}),i.addEventListener(`message`,c)})}function b(e){let t=new Map,r=null,s=null,l=!1,d=null,m=null,b=`idle`,x=0,S=null,C=e=>{for(let[n,r]of t)clearTimeout(r.timeoutId),t.delete(n),r.reject(Error(e))};function w(n,r){let a=i();return new Promise((i,o)=>{let s=setTimeout(()=>{t.delete(a),o(Error(`Host response timeout: ${n}`))},e.requestTimeoutMs);t.set(a,{resolve:i,reject:o,timeoutId:s,responseType:`${n}.response`,errorType:`${n}.error`}),window.parent.postMessage({type:`${n}.request`,requestId:a,...r},e.hostOrigin)})}let T=(t,i)=>{let c=[],u=()=>{d&&(clearTimeout(d),d=null)},h=()=>{o(t,JSON.stringify({type:`tools/list`,tools:c}))};m&&(clearTimeout(m),m=null),r=i,s=t,x=0,l=!1,b=`idle`,t.addEventListener(`message`,s=>{let c;try{c=JSON.parse(String(s.data))}catch(e){console.warn(`[webmcp-relay-widget] Failed to parse relay message:`,e);return}let d=f(c);if(d){r={hello:d,host:i.host,port:i.port};return}if(n(c)&&c.type===`hello/accepted`){u(),l=!0,g(e,i),h();return}let m=p(c);if(m){u(),l=!1,_(e),window.parent.postMessage({type:`webmcp.relay.rejected`,host:i.host,port:i.port,message:m.message,reason:m.reason},e.hostOrigin),console.error(`[webmcp-relay-widget] Relay rejected browser hello:`,m.reason,m.message);try{t.close(1008,m.message)}catch{}return}if(!n(c)||typeof c.type!=`string`)return;let v=c;if(v.type===`ping`){o(t,JSON.stringify({type:`pong`}));return}if(v.type===`reload`){window.parent.postMessage({type:`webmcp.reload`},e.hostOrigin);return}if(v.type!==`invoke`){console.debug(`[webmcp-relay-widget] Ignoring unrecognized message type:`,a(v.type));return}w(`webmcp.tools.invoke`,{toolName:v.toolName,args:n(v.args)?v.args:{}}).then(e=>{o(t,JSON.stringify({type:`result`,callId:v.callId,result:e.result}))}).catch(e=>{o(t,JSON.stringify({type:`result`,callId:v.callId,result:{isError:!0,content:[{type:`text`,text:String(e instanceof Error?e.message:e)}]}}))})}),t.addEventListener(`close`,()=>{s===t&&(l=!1,s=null,u(),C(`WebSocket connection lost`),O())},{once:!0}),t.addEventListener(`error`,e=>{console.warn(`[webmcp-relay-widget] WebSocket error:`,e);try{t.close()}catch(e){console.warn(`[webmcp-relay-widget] Error closing socket after error:`,e)}}),w(`webmcp.tools.list`,{}).then(n=>{c=Array.isArray(n.tools)?n.tools:[],o(t,JSON.stringify({type:`hello`,tabId:e.tabId,origin:e.hostOrigin,title:e.hostTitle||document.referrer||`Unknown page`,url:e.hostUrl})),d=setTimeout(()=>{d=null,!(s!==t||t.readyState!==WebSocket.OPEN||l)&&(console.warn(`[webmcp-relay-widget] Relay did not acknowledge browser hello`),t.close(4e3,`Browser hello was not acknowledged`))},1e3)}).catch(e=>{console.warn(`[webmcp-relay-widget] Hello handshake failed:`,e);try{t.close()}catch{}})},E=async t=>{let n=await y({host:t.host,port:t.port});if(!n)return!1;let r=n.endpoint;return e.relayId&&r.hello.relayId!==e.relayId||e.relayWorkspace&&r.hello.workspace!==e.relayWorkspace?(n.socket.close(),!1):(T(n.socket,r),!0)},D=async()=>{b=`discovering`;try{for(let t of v(e))if(await E(t))return!0;return!1}finally{b===`discovering`&&(b=`idle`)}},O=()=>{if(!r||m)return;let e={host:r.host,port:r.port},t=Math.round(500*(.85+Math.random()*.3));m=setTimeout(()=>{m=null,E(e).then(e=>{e||k()})},t)},k=()=>{if(m)return;let e=c[x];if(e===void 0){M();return}m=setTimeout(()=>{m=null,D().then(e=>{e||(x+=1,k())})},e)},A=()=>{document.visibilityState===`visible`&&b===`dormant`&&P()},j=()=>{document.removeEventListener(`visibilitychange`,A),S!==null&&(clearInterval(S),S=null)},M=()=>{b!==`dormant`&&(b=`dormant`,m&&(clearTimeout(m),m=null),document.addEventListener(`visibilitychange`,A),S=setInterval(()=>{N()},12e4))},N=async()=>{if(b!==`dormant`)return;let t=new Set,n=[],r=(e,r)=>{let i=`${e}:${String(r)}`;t.has(i)||(t.add(i),n.push({host:e,port:r}))};e.relayPortHint!==void 0&&r(e.relayHostHint,e.relayPortHint);let i=h(e);if(i&&r(i.host,i.port),n.length!==0){b=`discovering`,j();for(let e of n)if(await E(e)){x=0;return}M()}},P=()=>{j(),x=0,D().then(e=>{e||M()})};window.addEventListener(`message`,r=>{if(r.source!==window.parent||r.origin!==e.hostOrigin)return;let i=r.data;if(n(i)&&i.type===`webmcp.tools.changed`){s&&l&&o(s,JSON.stringify({type:`tools/changed`,tools:Array.isArray(i.tools)?i.tools:[]}));return}if(n(i)&&i.type===`webmcp.connect`){b===`dormant`?P():!s&&b!==`discovering`&&D();return}let a=u(i);if(!a)return;let c=t.get(a.requestId);if(c){if(a.type===c.responseType){clearTimeout(c.timeoutId),t.delete(a.requestId),c.resolve(a);return}a.type===c.errorType&&(clearTimeout(c.timeoutId),t.delete(a.requestId),c.reject(Error(String(a.error||`Unknown host error`))))}}),e.autoConnect&&D().then(e=>{e||k()})}d()})();</script>
8
+ <script>(function(){function e(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function t(t){if(!e(t))return!1;let n=Object.getPrototypeOf(t);return n===Object.prototype||n===null}function n(e,r=new WeakSet){if(e===null||typeof e==`string`||typeof e==`boolean`)return!0;if(typeof e==`number`)return Number.isFinite(e);if(typeof e!=`object`||r.has(e))return!1;r.add(e);try{return(Array.isArray(e)?e:t(e)?Object.values(e):null)?.every(e=>n(e,r))??!1}catch{return!1}finally{r.delete(e)}}function r(t){if(!e(t))return!1;switch(t.type){case`text`:return typeof t.text==`string`;case`image`:case`audio`:return typeof t.data==`string`&&typeof t.mimeType==`string`;case`resource_link`:return typeof t.uri==`string`&&typeof t.name==`string`;case`resource`:{let n=t.resource;return e(n)&&typeof n.uri==`string`&&(typeof n.text==`string`||typeof n.blob==`string`)}default:return!1}}function i(t){return e(t)&&n(t)&&Array.isArray(t.content)&&t.content.every(r)&&(t.isError===void 0||typeof t.isError==`boolean`)&&(t._meta===void 0||e(t._meta))}function a(e){if(typeof e==`string`)return e;try{return JSON.stringify(e)??String(e)}catch{return String(e)}}function o(e){if(i(e))return e;let t={content:[{type:`text`,text:a(e)}],isError:!1};return n(e)&&(t.structuredContent=e),t}let s=new Set([`127.0.0.1`,`localhost`,`::1`,`[::1]`]),c=9333,l=new Set;function u(e){return!!e&&typeof e==`object`&&!Array.isArray(e)}function d(e){let t;try{t=JSON.parse(e)}catch{}return u(t)?t.resultType===`input_required`?{isError:!0,content:[{type:`text`,text:`The WebMCP local relay cannot forward MCP input_required results. Multi-round tool flows require direct McpServer registration.`}]}:o(t):o(typeof t==`string`?t:e)}function f(e,t){let n=new Map;for(let r of e){let e=n.get(r.name);e&&(l.has(r.name)||(l.add(r.name),console.warn(`[webmcp-relay-widget] More than one frame registered a tool named "${r.name}". Only one is relayed, preferring the host page's own tool.`)),e.window===t||r.window!==t)||n.set(r.name,r)}return[...n.values()]}function p(e){return s.has(e)}function m(e){return e.replace(/[\r\n]/g,``)}function h(e,t){try{e.readyState===WebSocket.OPEN&&e.send(t)}catch(e){console.warn(`[webmcp-relay] Failed to send message:`,e)}}function g(e){return`__webmcp_relay_endpoint:${[e.hostOrigin,e.relayId??``,e.workspace??``].map(e=>encodeURIComponent(e)).join(`:`)}`}let _=[1e4,2e4,3e4];function v(e=window.location.search){let t=new URLSearchParams(e),n=globalThis.__WEBMCP_RELAY_CONFIG;function r(e){return t.get(e)??n?.[e]??null}let i=r(`hostOrigin`);if(!i)return null;let a=r(`relayHost`)||`127.0.0.1`;if(!p(a))return console.error(`[webmcp-relay-widget] relayHost must be a loopback address, got:`,a),null;let o=r(`relayPort`),s=o?Number(o):c;if(!Number.isInteger(s)||s<1||s>65535)return console.error(`[webmcp-relay-widget] relayPort must be an integer between 1 and 65535, got:`,o),null;let l=r(`requestTimeout`),u=l?Number(l):6e4;return!Number.isInteger(u)||u<1?(console.error(`[webmcp-relay-widget] requestTimeout must be a positive integer (ms), got:`,l),null):{autoConnect:r(`autoConnect`)!==`false`,hostOrigin:i,hostTitle:r(`hostTitle`)||``,hostUrl:r(`hostUrl`)||i,relayHostHint:a,relayId:r(`relayId`)||void 0,relayPortHint:s,relayWorkspace:r(`relayWorkspace`)||void 0,requestTimeoutMs:u,tabId:r(`tabId`)||crypto.randomUUID()}}function y(){let e=v();if(!e){console.warn(`[webmcp-relay-widget] Missing required hostOrigin parameter. Widget will not start.`);return}j(e)}function b(e){if(!u(e)||e.type!==`server-hello`||e.service!==`webmcp-local-relay`||e.version!==1||typeof e.host!=`string`||typeof e.instanceId!=`string`||typeof e.port!=`number`)return null;let t={type:`server-hello`,service:`webmcp-local-relay`,version:1,host:e.host,instanceId:e.instanceId,port:e.port};return typeof e.label==`string`&&(t.label=e.label),typeof e.relayId==`string`&&(t.relayId=e.relayId),typeof e.workspace==`string`&&(t.workspace=e.workspace),t}function x(e){return!u(e)||e.type!==`hello/rejected`||typeof e.message!=`string`||typeof e.reason!=`string`?null:{type:`hello/rejected`,message:e.message,reason:e.reason}}function S(e){return g({hostOrigin:e.hostOrigin,relayId:e.relayId??null,workspace:e.relayWorkspace??null})}function C(e){try{let t=sessionStorage.getItem(S(e));if(!t)return null;let n=JSON.parse(t);return!u(n)||typeof n.host!=`string`||n.host.length===0||typeof n.port!=`number`||!Number.isInteger(n.port)||n.port<1||n.port>65535?null:{host:n.host,port:n.port}}catch{return null}}function w(e,t){try{sessionStorage.setItem(S(e),JSON.stringify({host:t.host,port:t.port}))}catch{}}function T(e){try{sessionStorage.removeItem(S(e))}catch{}}function E(e){let t=C(e),n=new Set,r=[],i=(e,t)=>{let i=`${e}:${String(t)}`;n.has(i)||(n.add(i),r.push({host:e,port:t}))};i(e.relayHostHint,e.relayPortHint),t&&i(t.host,t.port);for(let e of[`127.0.0.1`,`[::1]`])for(let t=c;t<=9348;t+=1)i(e,t);return r}async function D(e){let t=`ws://${e.host.includes(`:`)&&!e.host.startsWith(`[`)?`[${e.host}]`:e.host}:${String(e.port)}`;return new Promise(n=>{let r=!1,i=new WebSocket(t,[`webmcp-discovery.v1`,`webmcp.v1`]),a=e=>{if(!r){if(r=!0,clearTimeout(l),i.removeEventListener(`close`,s),i.removeEventListener(`error`,o),i.removeEventListener(`message`,c),e===null)try{i.close()}catch{}n(e)}},o=()=>{a(null)},s=()=>{a(null)},c=t=>{let n;try{n=JSON.parse(String(t.data))}catch{return}let r=b(n);r&&a({endpoint:{hello:r,host:e.host,port:e.port},socket:i})},l=setTimeout(()=>{a(null)},1e4);i.addEventListener(`open`,()=>{clearTimeout(l),l=setTimeout(()=>{a(null)},1200)},{once:!0}),i.addEventListener(`close`,s,{once:!0}),i.addEventListener(`error`,o,{once:!0}),i.addEventListener(`message`,c)})}function O(e){if(typeof e.inputSchema==`string`)return console.warn(`[webmcp-relay-widget] Tool "${e.name}" was not relayed because this browser's WebMCP predates Chrome 155. Update Chrome, or turn off the WebMCP testing flag to use the polyfill.`),null;if(e.inputSchema!==void 0&&!u(e.inputSchema))return console.warn(`[webmcp-relay-widget] Tool "${e.name}" was not relayed because its input schema is malformed.`),null;let t={name:e.name,title:e.title,description:e.description};return e.inputSchema!==void 0&&(t.inputSchema=e.inputSchema),e.annotations!==void 0&&(t.annotations=e.annotations),t}function k(){return window.parent.document.modelContext}async function A(){let e=k();return e?f(await e.getTools(),window.parent).flatMap(e=>{let t=O(e);return t?[{registered:e,descriptor:t}]:[]}):[]}function j(e){let t=[],n=[],r=0,i=``,a=null,o=null,s=!1,c=!1,l=null,f=null,p=`idle`,g=0,v=null,y=async()=>{for(;;){let e=r,a=await A();if(e!==r)continue;let l=a.map(({descriptor:e})=>e),u=l.map(e=>JSON.stringify(e)).sort().join(`
9
+ `);if(t=a,n=l,u===i)return;i=u,o&&s&&c&&h(o,JSON.stringify({type:`tools/changed`,tools:n}));return}},S=()=>{r++,y().catch(e=>{console.warn(`[webmcp-relay-widget] Failed to refresh WebMCP tools:`,e)})},O=k();typeof O?.addEventListener==`function`&&O.addEventListener(`toolchange`,S),y().catch(e=>{console.warn(`[webmcp-relay-widget] Failed to read WebMCP tools:`,e)});let j=(r,i)=>{let _=()=>{l&&(clearTimeout(l),l=null)},v=()=>{c=!0,h(r,JSON.stringify({type:`tools/list`,tools:n}))};f&&(clearTimeout(f),f=null);let S=new Map;a=i,o=r,s=!1;let C=!1;c=!1,p=`idle`,r.addEventListener(`message`,n=>{let o;try{o=JSON.parse(String(n.data))}catch(e){console.warn(`[webmcp-relay-widget] Failed to parse relay message:`,e);return}let c=b(o);if(c){a={hello:c,host:i.host,port:i.port};return}if(u(o)&&o.type===`hello/accepted`){_(),s=!0,g=0,w(e,i),y().then(v).catch(e=>{console.warn(`[webmcp-relay-widget] Failed to refresh WebMCP tools:`,e),v()});return}let l=x(o);if(l){_(),s=!1,C=!0,T(e),console.error(`[webmcp-relay-widget] Relay rejected browser hello:`,l.reason,l.message);try{r.close(1008,l.message)}catch{}return}if(!u(o)||typeof o.type!=`string`)return;let f=o;if(f.type===`ping`){h(r,JSON.stringify({type:`pong`}));return}if(f.type===`reload`){window.parent.postMessage({type:`webmcp.reload`},e.hostOrigin);return}if(f.type===`cancel`){if(typeof f.callId!=`string`)return;S.get(f.callId)?.abort(Error(`Tool execution cancelled`)),S.delete(f.callId);return}if(f.type!==`invoke`){console.debug(`[webmcp-relay-widget] Ignoring unrecognized message type:`,m(o.type));return}let{callId:p}=f;if(typeof p!=`string`||S.has(p))return;let E=String(f.toolName??``),D=new AbortController;S.set(p,D),(async()=>{await y(),D.signal.throwIfAborted();let n=t.find(({registered:e})=>e.name===E),i=k();if(!n||!i)throw Error(`Tool not found: ${E}`);let a;try{let t=new Promise((e,t)=>{D.signal.addEventListener(`abort`,()=>t(D.signal.reason),{once:!0})});a=setTimeout(()=>{D.abort(Error(`Tool execution timed out`))},e.requestTimeoutMs);let o=i.executeTool(n.registered,u(f.args)?f.args:{},{signal:D.signal}),s=await Promise.race([o,t]);h(r,JSON.stringify({type:`result`,callId:p,result:d(s)}))}finally{clearTimeout(a)}})().catch(e=>{S.get(p)===D&&h(r,JSON.stringify({type:`result`,callId:p,result:{isError:!0,content:[{type:`text`,text:String(e instanceof Error?e.message:e)}]}}))}).finally(()=>{S.get(p)===D&&S.delete(p)})}),r.addEventListener(`close`,()=>{for(let e of S.values())e.abort(Error(`Relay connection closed`));S.clear(),o===r&&(s=!1,o=null,_(),C?I():F())},{once:!0}),r.addEventListener(`error`,e=>{console.warn(`[webmcp-relay-widget] WebSocket error:`,e);try{r.close()}catch(e){console.warn(`[webmcp-relay-widget] Error closing socket after error:`,e)}}),y().catch(e=>{console.warn(`[webmcp-relay-widget] Failed to read WebMCP tools:`,e)}).then(()=>{h(r,JSON.stringify({type:`hello`,tabId:e.tabId,origin:e.hostOrigin,title:e.hostTitle||document.referrer||`Unknown page`,url:e.hostUrl})),l=setTimeout(()=>{l=null,!(o!==r||r.readyState!==WebSocket.OPEN||s)&&(console.warn(`[webmcp-relay-widget] Relay did not acknowledge browser hello`),C=!0,r.close(4e3,`Browser hello was not acknowledged`))},1e3)}).catch(e=>{console.warn(`[webmcp-relay-widget] Hello handshake failed:`,e);try{r.close()}catch{}})},M=t=>(!e.relayId||t.hello.relayId===e.relayId)&&(!e.relayWorkspace||t.hello.workspace===e.relayWorkspace),N=async e=>{let t=await D({host:e.host,port:e.port});return t?M(t.endpoint)?(j(t.socket,t.endpoint),!0):(t.socket.close(),!1):!1},P=async()=>{p=`discovering`;try{let[t,...n]=E(e);if(t&&await N(t))return!0;let r=await Promise.all(n.map(e=>D(e))),i=r.find(e=>e!==null&&M(e.endpoint));for(let e of r)e&&e!==i&&e.socket.close();return i?(j(i.socket,i.endpoint),!0):!1}finally{p===`discovering`&&(p=`idle`)}},F=()=>{if(!a||f)return;let e={host:a.host,port:a.port},t=Math.round(500*(.85+Math.random()*.3));f=setTimeout(()=>{f=null,N(e).then(e=>{e||I()})},t)},I=()=>{if(f)return;let e=_[g];if(e===void 0){z();return}f=setTimeout(()=>{f=null,g+=1,P().then(e=>{e||I()})},e)},L=()=>{document.visibilityState===`visible`&&p===`dormant`&&V()},R=()=>{document.removeEventListener(`visibilitychange`,L),v!==null&&(clearInterval(v),v=null)},z=()=>{p!==`dormant`&&(p=`dormant`,f&&(clearTimeout(f),f=null),document.addEventListener(`visibilitychange`,L),v=setInterval(()=>{B()},12e4))},B=async()=>{if(p!==`dormant`)return;let t=[{host:e.relayHostHint,port:e.relayPortHint}],n=C(e);n&&(n.host!==e.relayHostHint||n.port!==e.relayPortHint)&&t.push(n),p=`discovering`,R();for(let e of t)if(await N(e))return;z()},V=()=>{R(),g=0,P().then(e=>{e||z()})};window.addEventListener(`message`,t=>{if(t.source!==window.parent||t.origin!==e.hostOrigin)return;let n=t.data;u(n)&&n.type===`webmcp.connect`&&(p===`dormant`?V():!o&&p!==`discovering`&&P())}),e.autoConnect&&P().then(e=>{e||I()})}y()})();</script>
9
10
  </body>
10
11
  </html>
@@ -1 +1,2 @@
1
- (function(){let e=new Set([`127.0.0.1`,`localhost`,`::1`,`[::1]`]),t=9333;function n(e){return!!e&&typeof e==`object`&&!Array.isArray(e)}function r(t){return e.has(t)}function i(){return crypto.randomUUID()}function a(e){return String(e).replace(/[\r\n]/g,``)}function o(e,t){try{e.readyState===WebSocket.OPEN&&e.send(t)}catch(e){console.warn(`[webmcp-relay] Failed to send message:`,e)}}function s(e){return`__webmcp_relay_endpoint:${[e.hostOrigin,e.relayId??``,e.workspace??``].map(e=>encodeURIComponent(e)).join(`:`)}`}let c=[1e4,2e4,3e4];function l(e=window.location.search){let n=new URLSearchParams(e),a=globalThis.__WEBMCP_RELAY_CONFIG;function o(e){return n.get(e)??a?.[e]??null}let s=o(`hostOrigin`);if(!s)return null;let c=o(`hostUrl`)||s,l=o(`hostTitle`)||``,u=o(`tabId`)||i(),d=o(`relayHost`)||`127.0.0.1`,f=o(`relayPort`),p=f&&f.length>0?Number(f):t,m=o(`autoConnect`)!==`false`,h=o(`relayId`)||void 0,g=o(`relayWorkspace`)||void 0,_=o(`requestTimeout`),v=_&&_.length>0?Number(_):6e4;return r(d)?!Number.isInteger(p)||p<1||p>65535?(console.error(`[webmcp-relay-widget] relayPort must be an integer between 1 and 65535, got:`,f),null):!Number.isInteger(v)||v<1?(console.error(`[webmcp-relay-widget] requestTimeout must be a positive integer (ms), got:`,_),null):{autoConnect:m,hostOrigin:s,hostTitle:l,hostUrl:c,relayHostHint:d,relayPortHint:p,...h?{relayId:h}:{},...g?{relayWorkspace:g}:{},requestTimeoutMs:v,tabId:u}:(console.error(`[webmcp-relay-widget] relayHost must be a loopback address, got:`,d),null)}function u(e){return!n(e)||typeof e.requestId!=`string`||typeof e.type!=`string`?null:{requestId:e.requestId,type:e.type,tools:e.tools,result:e.result,error:e.error}}function d(){let e=l();if(!e){console.warn(`[webmcp-relay-widget] Missing required hostOrigin parameter. Widget will not start.`);return}b(e)}function f(e){return!n(e)||e.type!==`server-hello`||e.service!==`webmcp-local-relay`||e.version!==1||typeof e.host!=`string`||typeof e.instanceId!=`string`||typeof e.port!=`number`?null:{type:`server-hello`,service:`webmcp-local-relay`,version:1,host:e.host,instanceId:e.instanceId,port:e.port,...typeof e.label==`string`?{label:e.label}:{},...typeof e.relayId==`string`?{relayId:e.relayId}:{},...typeof e.workspace==`string`?{workspace:e.workspace}:{}}}function p(e){return!n(e)||e.type!==`hello/rejected`||typeof e.message!=`string`||typeof e.reason!=`string`?null:{type:`hello/rejected`,message:e.message,reason:e.reason}}function m(e){return s({hostOrigin:e.hostOrigin,...e.relayId?{relayId:e.relayId}:{},...e.relayWorkspace?{workspace:e.relayWorkspace}:{}})}function h(e){if(typeof sessionStorage>`u`)return null;try{let t=sessionStorage.getItem(m(e));if(!t)return null;let n=JSON.parse(t);return typeof n.host!=`string`||n.host.length===0||typeof n.port!=`number`||!Number.isInteger(n.port)||n.port<1||n.port>65535?null:{host:n.host,port:n.port}}catch{return null}}function g(e,t){if(!(typeof sessionStorage>`u`))try{sessionStorage.setItem(m(e),JSON.stringify({host:t.host,port:t.port}))}catch{}}function _(e){if(!(typeof sessionStorage>`u`))try{sessionStorage.removeItem(m(e))}catch{}}function v(e){let n=h(e),r=new Set,i=[],a=(e,t)=>{if(!Number.isInteger(t)||t<1||t>65535)return;let n=`${e}:${String(t)}`;r.has(n)||(r.add(n),i.push({host:e,port:t}))};e.relayPortHint!==void 0&&a(e.relayHostHint,e.relayPortHint),n&&a(n.host,n.port);for(let e of[`127.0.0.1`,`[::1]`])for(let n=t;n<=9348;n+=1)a(e,n);return i}async function y(e){let t=`ws://${e.host.includes(`:`)&&!e.host.startsWith(`[`)?`[${e.host}]`:e.host}:${String(e.port)}`;return new Promise(n=>{let r=!1,i=new WebSocket(t,[`webmcp-discovery.v1`,`webmcp.v1`]),a=e=>{if(!r){if(r=!0,clearTimeout(l),i.removeEventListener(`close`,s),i.removeEventListener(`error`,o),i.removeEventListener(`message`,c),e===null)try{i.close()}catch{}n(e)}},o=()=>{a(null)},s=()=>{a(null)},c=t=>{let n;try{n=JSON.parse(String(t.data))}catch{return}let r=f(n);r&&a({endpoint:{hello:r,host:e.host,port:e.port},socket:i})},l=setTimeout(()=>{a(null)},1200);i.addEventListener(`close`,s,{once:!0}),i.addEventListener(`error`,o,{once:!0}),i.addEventListener(`message`,c)})}function b(e){let t=new Map,r=null,s=null,l=!1,d=null,m=null,b=`idle`,x=0,S=null,C=e=>{for(let[n,r]of t)clearTimeout(r.timeoutId),t.delete(n),r.reject(Error(e))};function w(n,r){let a=i();return new Promise((i,o)=>{let s=setTimeout(()=>{t.delete(a),o(Error(`Host response timeout: ${n}`))},e.requestTimeoutMs);t.set(a,{resolve:i,reject:o,timeoutId:s,responseType:`${n}.response`,errorType:`${n}.error`}),window.parent.postMessage({type:`${n}.request`,requestId:a,...r},e.hostOrigin)})}let T=(t,i)=>{let c=[],u=()=>{d&&(clearTimeout(d),d=null)},h=()=>{o(t,JSON.stringify({type:`tools/list`,tools:c}))};m&&(clearTimeout(m),m=null),r=i,s=t,x=0,l=!1,b=`idle`,t.addEventListener(`message`,s=>{let c;try{c=JSON.parse(String(s.data))}catch(e){console.warn(`[webmcp-relay-widget] Failed to parse relay message:`,e);return}let d=f(c);if(d){r={hello:d,host:i.host,port:i.port};return}if(n(c)&&c.type===`hello/accepted`){u(),l=!0,g(e,i),h();return}let m=p(c);if(m){u(),l=!1,_(e),window.parent.postMessage({type:`webmcp.relay.rejected`,host:i.host,port:i.port,message:m.message,reason:m.reason},e.hostOrigin),console.error(`[webmcp-relay-widget] Relay rejected browser hello:`,m.reason,m.message);try{t.close(1008,m.message)}catch{}return}if(!n(c)||typeof c.type!=`string`)return;let v=c;if(v.type===`ping`){o(t,JSON.stringify({type:`pong`}));return}if(v.type===`reload`){window.parent.postMessage({type:`webmcp.reload`},e.hostOrigin);return}if(v.type!==`invoke`){console.debug(`[webmcp-relay-widget] Ignoring unrecognized message type:`,a(v.type));return}w(`webmcp.tools.invoke`,{toolName:v.toolName,args:n(v.args)?v.args:{}}).then(e=>{o(t,JSON.stringify({type:`result`,callId:v.callId,result:e.result}))}).catch(e=>{o(t,JSON.stringify({type:`result`,callId:v.callId,result:{isError:!0,content:[{type:`text`,text:String(e instanceof Error?e.message:e)}]}}))})}),t.addEventListener(`close`,()=>{s===t&&(l=!1,s=null,u(),C(`WebSocket connection lost`),O())},{once:!0}),t.addEventListener(`error`,e=>{console.warn(`[webmcp-relay-widget] WebSocket error:`,e);try{t.close()}catch(e){console.warn(`[webmcp-relay-widget] Error closing socket after error:`,e)}}),w(`webmcp.tools.list`,{}).then(n=>{c=Array.isArray(n.tools)?n.tools:[],o(t,JSON.stringify({type:`hello`,tabId:e.tabId,origin:e.hostOrigin,title:e.hostTitle||document.referrer||`Unknown page`,url:e.hostUrl})),d=setTimeout(()=>{d=null,!(s!==t||t.readyState!==WebSocket.OPEN||l)&&(console.warn(`[webmcp-relay-widget] Relay did not acknowledge browser hello`),t.close(4e3,`Browser hello was not acknowledged`))},1e3)}).catch(e=>{console.warn(`[webmcp-relay-widget] Hello handshake failed:`,e);try{t.close()}catch{}})},E=async t=>{let n=await y({host:t.host,port:t.port});if(!n)return!1;let r=n.endpoint;return e.relayId&&r.hello.relayId!==e.relayId||e.relayWorkspace&&r.hello.workspace!==e.relayWorkspace?(n.socket.close(),!1):(T(n.socket,r),!0)},D=async()=>{b=`discovering`;try{for(let t of v(e))if(await E(t))return!0;return!1}finally{b===`discovering`&&(b=`idle`)}},O=()=>{if(!r||m)return;let e={host:r.host,port:r.port},t=Math.round(500*(.85+Math.random()*.3));m=setTimeout(()=>{m=null,E(e).then(e=>{e||k()})},t)},k=()=>{if(m)return;let e=c[x];if(e===void 0){M();return}m=setTimeout(()=>{m=null,D().then(e=>{e||(x+=1,k())})},e)},A=()=>{document.visibilityState===`visible`&&b===`dormant`&&P()},j=()=>{document.removeEventListener(`visibilitychange`,A),S!==null&&(clearInterval(S),S=null)},M=()=>{b!==`dormant`&&(b=`dormant`,m&&(clearTimeout(m),m=null),document.addEventListener(`visibilitychange`,A),S=setInterval(()=>{N()},12e4))},N=async()=>{if(b!==`dormant`)return;let t=new Set,n=[],r=(e,r)=>{let i=`${e}:${String(r)}`;t.has(i)||(t.add(i),n.push({host:e,port:r}))};e.relayPortHint!==void 0&&r(e.relayHostHint,e.relayPortHint);let i=h(e);if(i&&r(i.host,i.port),n.length!==0){b=`discovering`,j();for(let e of n)if(await E(e)){x=0;return}M()}},P=()=>{j(),x=0,D().then(e=>{e||M()})};window.addEventListener(`message`,r=>{if(r.source!==window.parent||r.origin!==e.hostOrigin)return;let i=r.data;if(n(i)&&i.type===`webmcp.tools.changed`){s&&l&&o(s,JSON.stringify({type:`tools/changed`,tools:Array.isArray(i.tools)?i.tools:[]}));return}if(n(i)&&i.type===`webmcp.connect`){b===`dormant`?P():!s&&b!==`discovering`&&D();return}let a=u(i);if(!a)return;let c=t.get(a.requestId);if(c){if(a.type===c.responseType){clearTimeout(c.timeoutId),t.delete(a.requestId),c.resolve(a);return}a.type===c.errorType&&(clearTimeout(c.timeoutId),t.delete(a.requestId),c.reject(Error(String(a.error||`Unknown host error`))))}}),e.autoConnect&&D().then(e=>{e||k()})}d()})();
1
+ (function(){function e(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function t(t){if(!e(t))return!1;let n=Object.getPrototypeOf(t);return n===Object.prototype||n===null}function n(e,r=new WeakSet){if(e===null||typeof e==`string`||typeof e==`boolean`)return!0;if(typeof e==`number`)return Number.isFinite(e);if(typeof e!=`object`||r.has(e))return!1;r.add(e);try{return(Array.isArray(e)?e:t(e)?Object.values(e):null)?.every(e=>n(e,r))??!1}catch{return!1}finally{r.delete(e)}}function r(t){if(!e(t))return!1;switch(t.type){case`text`:return typeof t.text==`string`;case`image`:case`audio`:return typeof t.data==`string`&&typeof t.mimeType==`string`;case`resource_link`:return typeof t.uri==`string`&&typeof t.name==`string`;case`resource`:{let n=t.resource;return e(n)&&typeof n.uri==`string`&&(typeof n.text==`string`||typeof n.blob==`string`)}default:return!1}}function i(t){return e(t)&&n(t)&&Array.isArray(t.content)&&t.content.every(r)&&(t.isError===void 0||typeof t.isError==`boolean`)&&(t._meta===void 0||e(t._meta))}function a(e){if(typeof e==`string`)return e;try{return JSON.stringify(e)??String(e)}catch{return String(e)}}function o(e){if(i(e))return e;let t={content:[{type:`text`,text:a(e)}],isError:!1};return n(e)&&(t.structuredContent=e),t}let s=new Set([`127.0.0.1`,`localhost`,`::1`,`[::1]`]),c=9333,l=new Set;function u(e){return!!e&&typeof e==`object`&&!Array.isArray(e)}function d(e){let t;try{t=JSON.parse(e)}catch{}return u(t)?t.resultType===`input_required`?{isError:!0,content:[{type:`text`,text:`The WebMCP local relay cannot forward MCP input_required results. Multi-round tool flows require direct McpServer registration.`}]}:o(t):o(typeof t==`string`?t:e)}function f(e,t){let n=new Map;for(let r of e){let e=n.get(r.name);e&&(l.has(r.name)||(l.add(r.name),console.warn(`[webmcp-relay-widget] More than one frame registered a tool named "${r.name}". Only one is relayed, preferring the host page's own tool.`)),e.window===t||r.window!==t)||n.set(r.name,r)}return[...n.values()]}function p(e){return s.has(e)}function m(e){return e.replace(/[\r\n]/g,``)}function h(e,t){try{e.readyState===WebSocket.OPEN&&e.send(t)}catch(e){console.warn(`[webmcp-relay] Failed to send message:`,e)}}function g(e){return`__webmcp_relay_endpoint:${[e.hostOrigin,e.relayId??``,e.workspace??``].map(e=>encodeURIComponent(e)).join(`:`)}`}let _=[1e4,2e4,3e4];function v(e=window.location.search){let t=new URLSearchParams(e),n=globalThis.__WEBMCP_RELAY_CONFIG;function r(e){return t.get(e)??n?.[e]??null}let i=r(`hostOrigin`);if(!i)return null;let a=r(`relayHost`)||`127.0.0.1`;if(!p(a))return console.error(`[webmcp-relay-widget] relayHost must be a loopback address, got:`,a),null;let o=r(`relayPort`),s=o?Number(o):c;if(!Number.isInteger(s)||s<1||s>65535)return console.error(`[webmcp-relay-widget] relayPort must be an integer between 1 and 65535, got:`,o),null;let l=r(`requestTimeout`),u=l?Number(l):6e4;return!Number.isInteger(u)||u<1?(console.error(`[webmcp-relay-widget] requestTimeout must be a positive integer (ms), got:`,l),null):{autoConnect:r(`autoConnect`)!==`false`,hostOrigin:i,hostTitle:r(`hostTitle`)||``,hostUrl:r(`hostUrl`)||i,relayHostHint:a,relayId:r(`relayId`)||void 0,relayPortHint:s,relayWorkspace:r(`relayWorkspace`)||void 0,requestTimeoutMs:u,tabId:r(`tabId`)||crypto.randomUUID()}}function y(){let e=v();if(!e){console.warn(`[webmcp-relay-widget] Missing required hostOrigin parameter. Widget will not start.`);return}j(e)}function b(e){if(!u(e)||e.type!==`server-hello`||e.service!==`webmcp-local-relay`||e.version!==1||typeof e.host!=`string`||typeof e.instanceId!=`string`||typeof e.port!=`number`)return null;let t={type:`server-hello`,service:`webmcp-local-relay`,version:1,host:e.host,instanceId:e.instanceId,port:e.port};return typeof e.label==`string`&&(t.label=e.label),typeof e.relayId==`string`&&(t.relayId=e.relayId),typeof e.workspace==`string`&&(t.workspace=e.workspace),t}function x(e){return!u(e)||e.type!==`hello/rejected`||typeof e.message!=`string`||typeof e.reason!=`string`?null:{type:`hello/rejected`,message:e.message,reason:e.reason}}function S(e){return g({hostOrigin:e.hostOrigin,relayId:e.relayId??null,workspace:e.relayWorkspace??null})}function C(e){try{let t=sessionStorage.getItem(S(e));if(!t)return null;let n=JSON.parse(t);return!u(n)||typeof n.host!=`string`||n.host.length===0||typeof n.port!=`number`||!Number.isInteger(n.port)||n.port<1||n.port>65535?null:{host:n.host,port:n.port}}catch{return null}}function w(e,t){try{sessionStorage.setItem(S(e),JSON.stringify({host:t.host,port:t.port}))}catch{}}function T(e){try{sessionStorage.removeItem(S(e))}catch{}}function E(e){let t=C(e),n=new Set,r=[],i=(e,t)=>{let i=`${e}:${String(t)}`;n.has(i)||(n.add(i),r.push({host:e,port:t}))};i(e.relayHostHint,e.relayPortHint),t&&i(t.host,t.port);for(let e of[`127.0.0.1`,`[::1]`])for(let t=c;t<=9348;t+=1)i(e,t);return r}async function D(e){let t=`ws://${e.host.includes(`:`)&&!e.host.startsWith(`[`)?`[${e.host}]`:e.host}:${String(e.port)}`;return new Promise(n=>{let r=!1,i=new WebSocket(t,[`webmcp-discovery.v1`,`webmcp.v1`]),a=e=>{if(!r){if(r=!0,clearTimeout(l),i.removeEventListener(`close`,s),i.removeEventListener(`error`,o),i.removeEventListener(`message`,c),e===null)try{i.close()}catch{}n(e)}},o=()=>{a(null)},s=()=>{a(null)},c=t=>{let n;try{n=JSON.parse(String(t.data))}catch{return}let r=b(n);r&&a({endpoint:{hello:r,host:e.host,port:e.port},socket:i})},l=setTimeout(()=>{a(null)},1e4);i.addEventListener(`open`,()=>{clearTimeout(l),l=setTimeout(()=>{a(null)},1200)},{once:!0}),i.addEventListener(`close`,s,{once:!0}),i.addEventListener(`error`,o,{once:!0}),i.addEventListener(`message`,c)})}function O(e){if(typeof e.inputSchema==`string`)return console.warn(`[webmcp-relay-widget] Tool "${e.name}" was not relayed because this browser's WebMCP predates Chrome 155. Update Chrome, or turn off the WebMCP testing flag to use the polyfill.`),null;if(e.inputSchema!==void 0&&!u(e.inputSchema))return console.warn(`[webmcp-relay-widget] Tool "${e.name}" was not relayed because its input schema is malformed.`),null;let t={name:e.name,title:e.title,description:e.description};return e.inputSchema!==void 0&&(t.inputSchema=e.inputSchema),e.annotations!==void 0&&(t.annotations=e.annotations),t}function k(){return window.parent.document.modelContext}async function A(){let e=k();return e?f(await e.getTools(),window.parent).flatMap(e=>{let t=O(e);return t?[{registered:e,descriptor:t}]:[]}):[]}function j(e){let t=[],n=[],r=0,i=``,a=null,o=null,s=!1,c=!1,l=null,f=null,p=`idle`,g=0,v=null,y=async()=>{for(;;){let e=r,a=await A();if(e!==r)continue;let l=a.map(({descriptor:e})=>e),u=l.map(e=>JSON.stringify(e)).sort().join(`
2
+ `);if(t=a,n=l,u===i)return;i=u,o&&s&&c&&h(o,JSON.stringify({type:`tools/changed`,tools:n}));return}},S=()=>{r++,y().catch(e=>{console.warn(`[webmcp-relay-widget] Failed to refresh WebMCP tools:`,e)})},O=k();typeof O?.addEventListener==`function`&&O.addEventListener(`toolchange`,S),y().catch(e=>{console.warn(`[webmcp-relay-widget] Failed to read WebMCP tools:`,e)});let j=(r,i)=>{let _=()=>{l&&(clearTimeout(l),l=null)},v=()=>{c=!0,h(r,JSON.stringify({type:`tools/list`,tools:n}))};f&&(clearTimeout(f),f=null);let S=new Map;a=i,o=r,s=!1;let C=!1;c=!1,p=`idle`,r.addEventListener(`message`,n=>{let o;try{o=JSON.parse(String(n.data))}catch(e){console.warn(`[webmcp-relay-widget] Failed to parse relay message:`,e);return}let c=b(o);if(c){a={hello:c,host:i.host,port:i.port};return}if(u(o)&&o.type===`hello/accepted`){_(),s=!0,g=0,w(e,i),y().then(v).catch(e=>{console.warn(`[webmcp-relay-widget] Failed to refresh WebMCP tools:`,e),v()});return}let l=x(o);if(l){_(),s=!1,C=!0,T(e),console.error(`[webmcp-relay-widget] Relay rejected browser hello:`,l.reason,l.message);try{r.close(1008,l.message)}catch{}return}if(!u(o)||typeof o.type!=`string`)return;let f=o;if(f.type===`ping`){h(r,JSON.stringify({type:`pong`}));return}if(f.type===`reload`){window.parent.postMessage({type:`webmcp.reload`},e.hostOrigin);return}if(f.type===`cancel`){if(typeof f.callId!=`string`)return;S.get(f.callId)?.abort(Error(`Tool execution cancelled`)),S.delete(f.callId);return}if(f.type!==`invoke`){console.debug(`[webmcp-relay-widget] Ignoring unrecognized message type:`,m(o.type));return}let{callId:p}=f;if(typeof p!=`string`||S.has(p))return;let E=String(f.toolName??``),D=new AbortController;S.set(p,D),(async()=>{await y(),D.signal.throwIfAborted();let n=t.find(({registered:e})=>e.name===E),i=k();if(!n||!i)throw Error(`Tool not found: ${E}`);let a;try{let t=new Promise((e,t)=>{D.signal.addEventListener(`abort`,()=>t(D.signal.reason),{once:!0})});a=setTimeout(()=>{D.abort(Error(`Tool execution timed out`))},e.requestTimeoutMs);let o=i.executeTool(n.registered,u(f.args)?f.args:{},{signal:D.signal}),s=await Promise.race([o,t]);h(r,JSON.stringify({type:`result`,callId:p,result:d(s)}))}finally{clearTimeout(a)}})().catch(e=>{S.get(p)===D&&h(r,JSON.stringify({type:`result`,callId:p,result:{isError:!0,content:[{type:`text`,text:String(e instanceof Error?e.message:e)}]}}))}).finally(()=>{S.get(p)===D&&S.delete(p)})}),r.addEventListener(`close`,()=>{for(let e of S.values())e.abort(Error(`Relay connection closed`));S.clear(),o===r&&(s=!1,o=null,_(),C?I():F())},{once:!0}),r.addEventListener(`error`,e=>{console.warn(`[webmcp-relay-widget] WebSocket error:`,e);try{r.close()}catch(e){console.warn(`[webmcp-relay-widget] Error closing socket after error:`,e)}}),y().catch(e=>{console.warn(`[webmcp-relay-widget] Failed to read WebMCP tools:`,e)}).then(()=>{h(r,JSON.stringify({type:`hello`,tabId:e.tabId,origin:e.hostOrigin,title:e.hostTitle||document.referrer||`Unknown page`,url:e.hostUrl})),l=setTimeout(()=>{l=null,!(o!==r||r.readyState!==WebSocket.OPEN||s)&&(console.warn(`[webmcp-relay-widget] Relay did not acknowledge browser hello`),C=!0,r.close(4e3,`Browser hello was not acknowledged`))},1e3)}).catch(e=>{console.warn(`[webmcp-relay-widget] Hello handshake failed:`,e);try{r.close()}catch{}})},M=t=>(!e.relayId||t.hello.relayId===e.relayId)&&(!e.relayWorkspace||t.hello.workspace===e.relayWorkspace),N=async e=>{let t=await D({host:e.host,port:e.port});return t?M(t.endpoint)?(j(t.socket,t.endpoint),!0):(t.socket.close(),!1):!1},P=async()=>{p=`discovering`;try{let[t,...n]=E(e);if(t&&await N(t))return!0;let r=await Promise.all(n.map(e=>D(e))),i=r.find(e=>e!==null&&M(e.endpoint));for(let e of r)e&&e!==i&&e.socket.close();return i?(j(i.socket,i.endpoint),!0):!1}finally{p===`discovering`&&(p=`idle`)}},F=()=>{if(!a||f)return;let e={host:a.host,port:a.port},t=Math.round(500*(.85+Math.random()*.3));f=setTimeout(()=>{f=null,N(e).then(e=>{e||I()})},t)},I=()=>{if(f)return;let e=_[g];if(e===void 0){z();return}f=setTimeout(()=>{f=null,g+=1,P().then(e=>{e||I()})},e)},L=()=>{document.visibilityState===`visible`&&p===`dormant`&&V()},R=()=>{document.removeEventListener(`visibilitychange`,L),v!==null&&(clearInterval(v),v=null)},z=()=>{p!==`dormant`&&(p=`dormant`,f&&(clearTimeout(f),f=null),document.addEventListener(`visibilitychange`,L),v=setInterval(()=>{B()},12e4))},B=async()=>{if(p!==`dormant`)return;let t=[{host:e.relayHostHint,port:e.relayPortHint}],n=C(e);n&&(n.host!==e.relayHostHint||n.port!==e.relayPortHint)&&t.push(n),p=`discovering`,R();for(let e of t)if(await N(e))return;z()},V=()=>{R(),g=0,P().then(e=>{e||z()})};window.addEventListener(`message`,t=>{if(t.source!==window.parent||t.origin!==e.hostOrigin)return;let n=t.data;u(n)&&n.type===`webmcp.connect`&&(p===`dormant`?V():!o&&p!==`discovering`&&P())}),e.autoConnect&&P().then(e=>{e||I()})}y()})();
@@ -0,0 +1,47 @@
1
+ ---
2
+ import VisualiserLayout from '@layouts/VisualiserLayout.astro';
3
+ import { ClientRouter } from 'astro:transitions';
4
+ // Server side, so the visualiser's styles ship in the page head (see NodeGraph.astro)
5
+ import '@eventcatalog/visualiser/styles-core.css';
6
+ import CatalogEntityMap from '@components/MDX/EntityMap/CatalogEntityMap';
7
+ import { getNodesAndEdges, SHARED_DOMAIN_LANE } from '@utils/node-graphs/catalog-entity-map';
8
+ import { compactVisualiserGraph } from '@utils/node-graphs/compact-visualiser-graph';
9
+ import { isEventCatalogChatVisible, isDevMode, isVisualiserEnabled } from '@utils/feature';
10
+
11
+ if (!isVisualiserEnabled()) return Astro.rewrite('/404');
12
+
13
+ const id = 'entity-map';
14
+ const graph = await getNodesAndEdges();
15
+ const { nodes, edges } = compactVisualiserGraph(graph.nodes as any[], graph.edges as any[]);
16
+ ---
17
+
18
+ <VisualiserLayout
19
+ title="Visualiser | Global Entity Map"
20
+ description="Every entity in the catalog and how they relate across domains"
21
+ >
22
+ <div id="entity-map-page" class="p-2 flex gap-2 h-[calc(100vh-60px)]">
23
+ <CatalogEntityMap
24
+ client:only="react"
25
+ id={id}
26
+ nodes={nodes}
27
+ edges={edges}
28
+ sharedLaneId={SHARED_DOMAIN_LANE.id}
29
+ title="Global Entity Map"
30
+ mode="full"
31
+ linkTo="docs"
32
+ swimlanes="domain"
33
+ preferenceScope="entity-map"
34
+ isChatEnabled={isEventCatalogChatVisible()}
35
+ isDevMode={isDevMode()}
36
+ syncRenderModeWithUrl
37
+ />
38
+ <div class="flex-1 min-w-0 relative border border-[rgb(var(--ec-page-border))] rounded-md" id={`${id}-portal`}></div>
39
+ </div>
40
+ <ClientRouter />
41
+ </VisualiserLayout>
42
+
43
+ <style is:global>
44
+ .react-flow__attribution {
45
+ display: none;
46
+ }
47
+ </style>
@@ -16,10 +16,10 @@ const { nodes: wireNodes, links: wireLinks, linkLabels } = toCatalogForceGraphWi
16
16
  ---
17
17
 
18
18
  <VisualiserLayout title="Visualiser | Architecture Graph" description="Force-directed graph of every resource in the catalog">
19
- <div id="architecture-graph-page" class="m-4">
19
+ <div id="architecture-graph-page" class="p-4">
20
20
  <div
21
21
  id="architecture-graph-container"
22
- class="h-[calc(100vh-130px)] w-full relative border border-[rgb(var(--ec-page-border))] rounded-md overflow-hidden"
22
+ class="h-[calc(100vh-60px-2rem)] w-full relative border border-[rgb(var(--ec-page-border))] rounded-md overflow-hidden"
23
23
  >
24
24
  <CatalogForceGraph nodes={wireNodes} links={wireLinks} linkLabels={linkLabels} client:only="react" />
25
25
  </div>
@@ -872,7 +872,7 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
872
872
  // for any catalog — the group renders when either item has something to show
873
873
  const architectureGraphEnabled = isArchitectureGraphEnabled();
874
874
  const topLevelDiagramsNode =
875
- visualiserEnabled && (systems.length > 0 || architectureGraphEnabled)
875
+ visualiserEnabled && (systems.length > 0 || architectureGraphEnabled || entities.length > 0)
876
876
  ? {
877
877
  type: 'group' as const,
878
878
  title: 'Top level diagrams',
@@ -896,6 +896,15 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
896
896
  },
897
897
  ]
898
898
  : []),
899
+ ...(entities.length > 0
900
+ ? [
901
+ {
902
+ type: 'item' as const,
903
+ title: 'Global Entity Map',
904
+ href: buildUrl('/visualiser/entity-map'),
905
+ },
906
+ ]
907
+ : []),
899
908
  ],
900
909
  }
901
910
  : undefined;
@@ -0,0 +1,16 @@
1
+ import { join } from 'node:path';
2
+ import { pathToFileURL } from 'node:url';
3
+
4
+ /**
5
+ * The file:// URL of a file in the catalog. Node's ES module loader only takes URLs: on Windows an absolute path
6
+ * (C:\catalog\eventcatalog.auth.js) reads as a URL with the scheme "c:", and the import fails.
7
+ */
8
+ export const catalogModuleUrl = (file: string, directory = process.env.PROJECT_DIR || process.cwd()) =>
9
+ pathToFileURL(join(directory, file)).href;
10
+
11
+ /**
12
+ * Imports a module from the catalog (eventcatalog.auth.js, eventcatalog.chat.js, middleware.ts), at runtime rather
13
+ * than bundled into the build, so the catalog's own file is the one that runs.
14
+ */
15
+ export const importCatalogModule = (file: string, directory?: string) =>
16
+ import(/* @vite-ignore */ catalogModuleUrl(file, directory));
@@ -0,0 +1,82 @@
1
+ import type { Edge, Node } from '@xyflow/react';
2
+
3
+ /**
4
+ * What the catalog's entity map shows, from the whole map and the viewer's filters. Pure, so it runs in
5
+ * the browser (the page's filters) and in tests.
6
+ */
7
+
8
+ export type Lane = { id: string; name: string };
9
+
10
+ /** Where entities nothing lists are counted, in the list of domains */
11
+ export const NO_DOMAIN: Lane = { id: '__none', name: 'No domain' };
12
+
13
+ export const domainOf = (node: Node): Lane =>
14
+ (node.data as { lanes?: { domain?: Lane } } | undefined)?.lanes?.domain ?? NO_DOMAIN;
15
+
16
+ /**
17
+ * References between entities in different domains. A shared entity (more than one domain has it) is
18
+ * across domains whatever it references. An entity with no domain isn't across anything: it has no
19
+ * domain to cross from.
20
+ */
21
+ export const getCrossDomainEdges = (nodes: Node[], edges: Edge[], sharedLaneId: string): Edge[] => {
22
+ const domains = new Map(nodes.map((node) => [node.id, domainOf(node)]));
23
+ return edges.filter((edge) => {
24
+ const source = domains.get(edge.source);
25
+ const target = domains.get(edge.target);
26
+ if (!source || !target || source.id === NO_DOMAIN.id || target.id === NO_DOMAIN.id) return false;
27
+ return source.id !== target.id || source.id === sharedLaneId;
28
+ });
29
+ };
30
+
31
+ export type DomainSummary = { lane: Lane; count: number; crossDomain: number };
32
+
33
+ /**
34
+ * Each domain with how many entities it has and how many references across domains they're at either
35
+ * end of. Sorted by name, with the shared entities and those with no domain last.
36
+ */
37
+ export const getDomainSummaries = (nodes: Node[], crossDomainEdges: Edge[], sharedLaneId: string): DomainSummary[] => {
38
+ const domains = new Map(nodes.map((node) => [node.id, domainOf(node)]));
39
+ const byId = new Map<string, DomainSummary>();
40
+ nodes.forEach((node) => {
41
+ const lane = domainOf(node);
42
+ const entry = byId.get(lane.id) ?? { lane, count: 0, crossDomain: 0 };
43
+ entry.count += 1;
44
+ byId.set(lane.id, entry);
45
+ });
46
+ crossDomainEdges.forEach((edge) => {
47
+ [edge.source, edge.target].forEach((id) => {
48
+ const entry = byId.get(domains.get(id)?.id ?? '');
49
+ if (entry) entry.crossDomain += 1;
50
+ });
51
+ });
52
+ const last = (id: string) => (id === sharedLaneId ? 1 : id === NO_DOMAIN.id ? 2 : 0);
53
+ return [...byId.values()].sort((a, b) => last(a.lane.id) - last(b.lane.id) || a.lane.name.localeCompare(b.lane.name));
54
+ };
55
+
56
+ /**
57
+ * The entities of the domains not hidden (only those at an end of a reference across domains, when
58
+ * asked) and the references between them. `isEverything` when nothing is filtered out, so the map's
59
+ * own layout can be used.
60
+ */
61
+ export const getShownGraph = ({
62
+ nodes,
63
+ edges,
64
+ crossDomainEdges,
65
+ hidden,
66
+ crossDomainOnly,
67
+ }: {
68
+ nodes: Node[];
69
+ edges: Edge[];
70
+ crossDomainEdges: Edge[];
71
+ hidden: Set<string>;
72
+ crossDomainOnly: boolean;
73
+ }) => {
74
+ const onCrossDomainEdge = new Set(crossDomainEdges.flatMap((edge) => [edge.source, edge.target]));
75
+ const visible = nodes.filter((node) => !hidden.has(domainOf(node).id) && (!crossDomainOnly || onCrossDomainEdge.has(node.id)));
76
+ const ids = new Set(visible.map((node) => node.id));
77
+ return {
78
+ nodes: visible,
79
+ edges: (crossDomainOnly ? crossDomainEdges : edges).filter((edge) => ids.has(edge.source) && ids.has(edge.target)),
80
+ isEverything: visible.length === nodes.length && !crossDomainOnly,
81
+ };
82
+ };
@@ -0,0 +1,113 @@
1
+ import { getCollection } from 'astro:content';
2
+ import { getEntities, type Entity } from '@utils/collections/entities';
3
+ import { createVersionedMap, satisfies } from '@utils/collections/util';
4
+ import { generateIdForNode } from './utils/utils';
5
+ import { createLaneIndex, isLatest, toLane, type Lane, type StepLanes } from './flow-lanes';
6
+ import {
7
+ ENTITY_TARGET_HANDLE_ID,
8
+ createEntityReferenceEdges,
9
+ getReferencedEntityId,
10
+ getReferencePropertyNames,
11
+ } from './domain-entity-map';
12
+ import { layoutEntityNodes } from './entity-layout';
13
+
14
+ /** The domain lane of an entity more than one domain has: grouped by domain, it gets a box of its own */
15
+ export const SHARED_DOMAIN_LANE: Lane = { id: '__shared', name: 'Shared across domains' };
16
+
17
+ /**
18
+ * Whether a domain, system or service lists this version of the entity (the latest, which the map shows):
19
+ * a pointer without a version, or to `latest`, or one whose version or range it satisfies. A resource that
20
+ * pins an older version doesn't own the latest one (as `getEntities` matches them)
21
+ */
22
+ const listsEntity = (resource: { data: { entities?: { id: string; version?: string }[] } }, entity: Entity) =>
23
+ (resource.data.entities || []).some((pointer) => {
24
+ if (pointer.id !== entity.data.id) return false;
25
+ if (!pointer.version || pointer.version === 'latest') return true;
26
+ return satisfies(entity.data.version, pointer.version);
27
+ });
28
+ const uniqueLanes = (lanes: (Lane | undefined)[]) => [
29
+ ...new Map(lanes.filter((lane): lane is Lane => !!lane).map((lane) => [lane.id, lane])).values(),
30
+ ];
31
+
32
+ /**
33
+ * Every entity in the catalog (latest versions) and the references between them, across domains.
34
+ *
35
+ * Entities can be listed by domains, systems and services, so each node says which domain, system and
36
+ * team it belongs to (`data.lanes`), for the visualiser to group them by:
37
+ * - domain: the domain that lists it, or the domain of the system or service that does. An entity more
38
+ * than one domain has is in the "Shared across domains" lane.
39
+ * - system: the system that lists it, or the system of the service that does
40
+ * - team: the entity's owner, or else the owner of what lists it
41
+ *
42
+ * Entities nothing lists have no lanes (`{}`): they stay out of every group rather than taking a
43
+ * neighbour's.
44
+ */
45
+ export const getNodesAndEdges = async () => {
46
+ const [entities, allEntities, domains, systems, services, teams, users] = await Promise.all([
47
+ getEntities({ getAllVersions: false }),
48
+ getCollection('entities'),
49
+ getCollection('domains'),
50
+ getCollection('systems'),
51
+ getCollection('services'),
52
+ getCollection('teams'),
53
+ getCollection('users'),
54
+ ]);
55
+ const entityMap = createVersionedMap(allEntities as Entity[]);
56
+ const laneIndex = createLaneIndex({ domains, systems, teams, users });
57
+ const latestDomains = domains.filter(isLatest);
58
+ const latestSystems = systems.filter(isLatest);
59
+ const latestServices = services.filter(isLatest);
60
+
61
+ const lanesForEntity = (entity: Entity): StepLanes => {
62
+ const owningDomains = latestDomains.filter((domain) => listsEntity(domain, entity));
63
+ const owningSystems = latestSystems.filter((system) => listsEntity(system, entity));
64
+ const owningServices = latestServices.filter((service) => listsEntity(service, entity));
65
+
66
+ const ownerLanes = [
67
+ ...owningSystems.map((system) => laneIndex.lanesForSystem(system as any)),
68
+ ...owningServices.map((service) => laneIndex.lanesFor(service as any)),
69
+ ];
70
+ // A domain listing the entity itself wins over the domains of its systems and services
71
+ const domainLanes = owningDomains.length
72
+ ? uniqueLanes(owningDomains.map(toLane))
73
+ : uniqueLanes(ownerLanes.map((lanes) => lanes?.domain));
74
+ const domain = domainLanes.length > 1 ? SHARED_DOMAIN_LANE : domainLanes[0];
75
+ const system = uniqueLanes(ownerLanes.map((lanes) => lanes?.system))[0];
76
+ const team =
77
+ laneIndex.lanesFor(entity as any)?.team ??
78
+ [...owningDomains, ...owningSystems, ...owningServices]
79
+ .map((owner) => laneIndex.lanesFor(owner as any)?.team)
80
+ .find(Boolean);
81
+
82
+ return {
83
+ ...(domain && { domain }),
84
+ ...(system && { system }),
85
+ ...(team && { team }),
86
+ };
87
+ };
88
+
89
+ const nodes = entities.map((entity) => {
90
+ const lanes = lanesForEntity(entity);
91
+ const domain = lanes.domain && lanes.domain.id !== SHARED_DOMAIN_LANE.id ? lanes.domain : undefined;
92
+ return {
93
+ id: generateIdForNode(entity),
94
+ type: 'entities',
95
+ position: { x: 0, y: 0 },
96
+ data: {
97
+ label: entity.data.name,
98
+ entity,
99
+ ...(domain && { domainName: domain.name, domainId: domain.id }),
100
+ entityTargetHandle: ENTITY_TARGET_HANDLE_ID,
101
+ referencePropertyNames: getReferencePropertyNames(entity, entityMap),
102
+ lanes,
103
+ },
104
+ };
105
+ });
106
+
107
+ const entitiesWithReferences = entities.filter((entity) =>
108
+ entity.data.properties?.some((property: any) => getReferencedEntityId(property, entityMap))
109
+ );
110
+ const edges = createEntityReferenceEdges(entitiesWithReferences, entities, entityMap);
111
+
112
+ return { nodes: await layoutEntityNodes(nodes, edges), edges };
113
+ };