@reqvire-org/ads 0.3.3 → 0.3.5

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 (52) hide show
  1. package/README.md +12 -4
  2. package/dist/behaviors.mjs +157 -0
  3. package/dist/bootstrap-ds/ADS.md +7 -0
  4. package/dist/bootstrap-ds/design-system/controllers/.gitkeep +0 -0
  5. package/dist/check.mjs +7 -2
  6. package/dist/cli.mjs +17 -0
  7. package/dist/config.mjs +19 -3
  8. package/dist/css-values.mjs +26 -0
  9. package/dist/index.mjs +2 -0
  10. package/dist/init.mjs +1 -1
  11. package/dist/mcp/build-status.mjs +10 -3
  12. package/dist/mcp/catalog.mjs +11 -8
  13. package/dist/mcp/context.mjs +2 -2
  14. package/dist/mcp/mutations.mjs +10 -0
  15. package/dist/mcp/preview-owner-guard.mjs +29 -0
  16. package/dist/mcp/preview-watch.mjs +28 -4
  17. package/dist/mcp/preview-widget.bundle.js +2 -2
  18. package/dist/mcp/prompts.mjs +2 -2
  19. package/dist/mcp/server.mjs +56 -19
  20. package/dist/mcp/showcase-host.mjs +9 -5
  21. package/dist/public-api.mjs +39 -0
  22. package/dist/scenarios-compiler.mjs +17 -2
  23. package/dist/scenarios.mjs +27 -39
  24. package/dist/skills/README.md +3 -3
  25. package/dist/skills/ads-break-ui/SKILL.md +22 -0
  26. package/dist/skills/ads-designer/SKILL.md +23 -1
  27. package/dist/skills/ads-designer/assets/bootstrap-ds/ADS.md +7 -0
  28. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/controllers/.gitkeep +0 -0
  29. package/dist/skills/ads-designer/references/cli.md +1 -1
  30. package/dist/skills/ads-designer/references/design-craft.md +1 -1
  31. package/dist/skills/ads-import/SKILL.md +22 -0
  32. package/dist/skills/ads-import/references/cli.md +1 -1
  33. package/dist/skills/{ads-assimilate → ads-migrate}/SKILL.md +28 -6
  34. package/dist/skills/ads-migrate/agents/openai.yaml +4 -0
  35. package/dist/skills/{ads-assimilate → ads-migrate}/references/cli.md +1 -1
  36. package/dist/skills/{ads-assimilate → ads-migrate}/references/native-implementation.md +11 -0
  37. package/dist/skills/ads-motion/SKILL.md +22 -0
  38. package/dist/tokens.mjs +3 -2
  39. package/dist/units.mjs +20 -9
  40. package/dist/verification/browser.mjs +27 -0
  41. package/dist/verification/playwright-reporter.cjs +20 -0
  42. package/dist/verification.mjs +214 -0
  43. package/docs/ADS.md +27 -2
  44. package/docs/ARCHITECTURE.md +39 -1
  45. package/docs/BEHAVIORS.md +230 -0
  46. package/docs/CHANGELOG.md +36 -0
  47. package/docs/GUIDE.md +16 -1
  48. package/docs/MCP.md +46 -7
  49. package/package.json +4 -2
  50. package/dist/skills/ads-assimilate/agents/openai.yaml +0 -4
  51. /package/dist/skills/{ads-assimilate → ads-migrate}/references/lint-and-docs.md +0 -0
  52. /package/dist/skills/{ads-assimilate → ads-migrate}/references/testing.md +0 -0
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { statSync, mkdirSync, writeFileSync, renameSync, rmSync, realpathSync } from "node:fs";
4
4
  import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
5
6
  import { buildFailure, inspectBuildStatus } from "./build-status.mjs";
6
7
 
7
8
  function stamp(file) {
@@ -16,6 +17,7 @@ export function createPreviewWatch(root, config, { log = process.stderr, buildTi
16
17
  let session = randomUUID();
17
18
  let state = command ? "idle" : "disabled";
18
19
  let child;
20
+ let ownerGuard;
19
21
  let error;
20
22
  let closed;
21
23
  let childClosed = false;
@@ -23,6 +25,7 @@ export function createPreviewWatch(root, config, { log = process.stderr, buildTi
23
25
  let stopping;
24
26
  let restarting;
25
27
  let disposed = false;
28
+ let stalled = false;
26
29
  const baselines = new Map();
27
30
  const ready = new Set();
28
31
  const requests = new Set();
@@ -69,6 +72,23 @@ export function createPreviewWatch(root, config, { log = process.stderr, buildTi
69
72
  log.write(`ads mcp: ${error}\n`);
70
73
  return;
71
74
  }
75
+ if (child.pid) {
76
+ // A small independent guard owns a pipe from this MCP process. It can
77
+ // terminate the detached compiler group even if this process is SIGKILLed.
78
+ const guard = ownerGuard = spawn(process.execPath, [fileURLToPath(new URL("./preview-owner-guard.mjs", import.meta.url)), String(child.pid)], {
79
+ stdio: ["pipe", "ignore", "pipe"], detached: process.platform !== "win32", windowsHide: true,
80
+ });
81
+ guard.stdin.on("error", () => {}); // Owner shutdown can race guard exit.
82
+ guard.stderr.pipe(log, { end: false });
83
+ function guardFailed(failure) {
84
+ if (ownerGuard !== guard || state === "stopping" || childClosed) return;
85
+ error = `Preview ownership guard failed: ${failure.message ?? failure}. Invoke the preview again after fixing the launcher.`;
86
+ state = "failed";
87
+ kill("SIGKILL");
88
+ }
89
+ guard.once("error", guardFailed);
90
+ guard.once("exit", (code, signal) => guardFailed(signal ?? code));
91
+ }
72
92
  child.stdout.pipe(log, { end: false });
73
93
  child.stderr.pipe(log, { end: false });
74
94
  closed = new Promise((resolve) => child.once("close", (code, signal) => {
@@ -76,6 +96,8 @@ export function createPreviewWatch(root, config, { log = process.stderr, buildTi
76
96
  // Finish cleanup now, before discarding the owned process-group ID.
77
97
  kill("SIGKILL");
78
98
  childClosed = true;
99
+ const guard = ownerGuard; ownerGuard = undefined;
100
+ guard?.stdin.end("disarm\n");
79
101
  clearTimeout(killTimer);
80
102
  if (state !== "stopping") {
81
103
  state = "failed";
@@ -102,14 +124,14 @@ export function createPreviewWatch(root, config, { log = process.stderr, buildTi
102
124
  requestFile = path.join(root, ".ds", `preview-requests-${session}.json`);
103
125
  reportFile = path.join(root, ".ds", `build-status-${session}.json`);
104
126
  state = "idle"; error = undefined; child = closed = stopping = undefined; childClosed = false;
105
- ready.clear(); baselines.clear(); active = undefined;
127
+ ready.clear(); baselines.clear(); active = undefined; stalled = false;
106
128
  start();
107
129
  })().finally(() => { restarting = undefined; });
108
130
  }
109
131
 
110
132
  async function ensure(file, signal) {
111
133
  if (signal?.aborted) throw new Error("Preview request cancelled.");
112
- if (state === "failed" || restarting) await restart();
134
+ if (state === "failed" || restarting || stalled) await restart();
113
135
  if (disposed) throw new Error("Preview watcher is stopped.");
114
136
  start();
115
137
  request(file);
@@ -132,10 +154,12 @@ export function createPreviewWatch(root, config, { log = process.stderr, buildTi
132
154
  if (build?.state === "unreported" && current !== null && current !== baseline) { ready.add(file); return build; }
133
155
  await new Promise((resolve) => setTimeout(resolve, 50));
134
156
  }
135
- throw new Error("Preview build is still pending. Check the MCP build logs and invoke the preview again.");
157
+ stalled = true;
158
+ const report = buildStatus();
159
+ throw Object.assign(new Error(`Preview build is still pending${report.progress ? ` in ${report.progress.phase} for ${Math.round((Date.now() - Date.parse(report.progress.updatedAt)) / 1000)}s` : ""}. Inspect ads://project/build and builder logs. Invoke the preview again to restart the timed-out builder within this MCP session.`), { build: { ...report, watcher: status(), code: "ADS_PREVIEW_PENDING" } });
136
160
  }
137
161
 
138
- const status = () => ({ state, ...(child?.pid ? { pid: child.pid } : {}), ...(error ? { error } : {}) });
162
+ const status = () => ({ state, ...(stalled ? { stalled: true } : {}), ...(child?.pid ? { pid: child.pid } : {}), ...(ownerGuard?.pid ? { guardPid: ownerGuard.pid } : {}), ...(error ? { error } : {}) });
139
163
  const buildStatus = () => inspectBuildStatus(root, config, command ? { session, file: reportFile, processState: status() } : {});
140
164
 
141
165
  async function stopChild() {
@@ -100,9 +100,9 @@ Boolean requesting whether a visible border and background is provided by the ho
100
100
  - omitted: host decides border`)}),Bx=d.object({method:d.literal("ui/request-display-mode"),params:d.object({mode:da.describe("The display mode being requested.")})}),Dv=d.object({mode:da.describe("The display mode that was actually set. May differ from requested if not supported.")}).passthrough(),Vv=d.union([d.literal("model"),d.literal("app")]).describe("Tool visibility scope - who can access the tool."),Wx=d.object({resourceUri:d.string().optional(),visibility:d.array(Vv).optional().describe(`Who can access this tool. Default: ["model", "app"]
101
101
  - "model": Tool visible to and callable by the agent
102
102
  - "app": Tool callable by the app from this server only`),csp:d.never().optional(),permissions:d.never().optional()}),Gx=d.object({mimeTypes:d.array(d.string()).optional().describe('Array of supported MIME types for UI resources.\nMust include `"text/html;profile=mcp-app"` for MCP Apps support.')}),Kx=d.object({method:d.literal("ui/download-file"),params:d.object({contents:d.array(d.union([go,So])).describe("Resource contents to download \u2014 embedded (inline data) or linked (host fetches). Uses standard MCP resource types.")})}),Yx=d.object({method:d.literal("ui/message"),params:d.object({role:d.literal("user").describe('Message role, currently only "user" is supported.'),content:d.array(Fe).describe("Message content blocks (text, image, etc.).")})}),Qx=d.object({method:d.literal("ui/notifications/sandbox-resource-ready"),params:d.object({html:d.string().describe("HTML content to load into the inner iframe."),sandbox:d.string().optional().describe("Optional override for the inner iframe's sandbox attribute."),csp:_f.optional().describe("CSP configuration from resource metadata."),permissions:yf.optional().describe("Sandbox permissions from resource metadata.")})}),Hv=d.object({method:d.literal("ui/notifications/tool-result"),params:Ot.describe("Standard MCP tool execution result.")}),x_=d.object({toolInfo:d.object({id:Ne.optional().describe("JSON-RPC id of the tools/call request."),tool:It.describe("Tool definition including name, inputSchema, etc.")}).optional().describe("Metadata of the tool call that instantiated this App."),theme:Tv.optional().describe("Current color theme preference."),styles:Av.optional().describe("Style configuration for theming the app."),displayMode:da.optional().describe("How the UI is currently displayed."),availableDisplayModes:d.array(da).optional().describe("Display modes the host supports."),containerDimensions:d.union([d.object({height:d.number().describe("Fixed container height in pixels.")}),d.object({maxHeight:d.union([d.number(),d.undefined()]).optional().describe("Maximum container height in pixels.")})]).and(d.union([d.object({width:d.number().describe("Fixed container width in pixels.")}),d.object({maxWidth:d.union([d.number(),d.undefined()]).optional().describe("Maximum container width in pixels.")})])).optional().describe(`Container dimensions. Represents the dimensions of the iframe or other
103
- container holding the app. Specify either width or maxWidth, and either height or maxHeight.`),locale:d.string().optional().describe("User's language and region preference in BCP 47 format."),timeZone:d.string().optional().describe("User's timezone in IANA format."),userAgent:d.string().optional().describe("Host application identifier."),platform:d.union([d.literal("web"),d.literal("desktop"),d.literal("mobile")]).optional().describe("Platform type for responsive design decisions."),deviceCapabilities:d.object({touch:d.boolean().optional().describe("Whether the device supports touch input."),hover:d.boolean().optional().describe("Whether the device supports hover interactions.")}).optional().describe("Device input capabilities."),safeAreaInsets:d.object({top:d.number().describe("Top safe area inset in pixels."),right:d.number().describe("Right safe area inset in pixels."),bottom:d.number().describe("Bottom safe area inset in pixels."),left:d.number().describe("Left safe area inset in pixels.")}).optional().describe("Mobile safe area boundaries in pixels.")}).passthrough(),Fv=d.object({method:d.literal("ui/notifications/host-context-changed"),params:x_.describe("Partial context update containing only changed fields.")}),Xx=d.object({method:d.literal("ui/update-model-context"),params:d.object({content:d.array(Fe).optional().describe("Context content blocks (text, image, etc.)."),structuredContent:d.record(d.string(),d.unknown().describe("Structured content for machine-readable context data.")).optional().describe("Structured content for machine-readable context data.")})}),eP=d.object({method:d.literal("ui/initialize"),params:d.object({appInfo:Ve.describe("App identification (name and version)."),appCapabilities:Zv.describe("Features and capabilities this app provides."),protocolVersion:d.string().describe("Protocol version this app supports.")})}),Jv=d.object({protocolVersion:d.string().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:Ve.describe("Host application identification and version."),hostCapabilities:Uv.describe("Features and capabilities provided by the host."),hostContext:x_.describe("Rich context about the host environment.")}).passthrough(),Bv={target:"draft-2020-12"};async function R_(e,t){let o=e["~standard"];if(o.jsonSchema)return o.jsonSchema[t](Bv);throw Error(`Schema (vendor: ${o.vendor}) does not implement Standard JSON Schema (~standard.jsonSchema). Use a library that does (zod v4, ArkType, Valibot) or wrap your schema accordingly.`)}async function w_(e,t,o=""){let r=await e["~standard"].validate(t);if(r.issues){let n=r.issues.map(s=>{let i=s.path?.map(c=>typeof c=="object"?c.key:c).join(".");return i?`${i}: ${s.message}`:s.message}).join("; ");throw Error(o+n)}return r.value}function Wv(e,t){let o={...e,...t};return t.experimental!==void 0&&(o.experimental={...e.experimental,...t.experimental}),t.tools!==void 0&&(o.tools={...e.tools,...t.tools}),o}var Jl=class e extends mf{_appInfo;_appCapabilities;options;_hostCapabilities;_hostInfo;_hostContext;_registeredTools={};_initializedSent=!1;_registeredEvents=new Set;_events=new ff;_methods=new gf;setRequestHandler=(...t)=>{let[o,...r]=z_("request",t)??t;this._methods.claim(o,"setRequestHandler"),super.setRequestHandler.call(this,o,...r)};setNotificationHandler=(...t)=>{let[o,...r]=z_("notification",t)??t;this._methods.claim(o,"setNotificationHandler"),super.setNotificationHandler.call(this,o,...r)};removeRequestHandler=t=>{this._methods.release(t),super.removeRequestHandler(t)};removeNotificationHandler=t=>{this._methods.release(t),super.removeNotificationHandler(t)};replaceRequestHandler=(t,...o)=>{this._methods.replace(t),super.setRequestHandler.call(this,t,...o)};_assertInitialized(t){if(this._initializedSent)return;let o=`[ext-apps] App.${t}() called before connect() completed the ui/initialize handshake. Await app.connect() before calling this method, or move data loading to an ontoolresult handler.`;if(this.options?.strict)throw Error(o);console.warn(`${o}. This will throw in a future release.`)}eventSchemas={toolinput:Ov,toolinputpartial:Nv,toolresult:Hv,toolcancelled:Mv,hostcontextchanged:Fv};static ONE_SHOT_EVENTS=new Set(["toolinput","toolinputpartial","toolresult","toolcancelled"]);_everHadListener=new Set;_assertHandlerTiming(t){if(!e.ONE_SHOT_EVENTS.has(t)||this._everHadListener.has(t)||(this._everHadListener.add(t),!this._initializedSent))return;let o=`[ext-apps] "${String(t)}" handler registered after connect() completed the ui/initialize handshake. The host may have already sent this notification. Register handlers before calling app.connect().`;if(this.options?.strict)throw Error(o);console.warn(o)}setEventHandler(t,o){o&&this._assertHandlerTiming(t),this._ensureEventSlot(t),this._events.setHandler(t,o)}addEventListener(t,o){this._assertHandlerTiming(t),this._ensureEventSlot(t),this._events.addEventListener(t,o)}removeEventListener(t,o){this._events.removeEventListener(t,o)}_ensureEventSlot(t){if(this._registeredEvents.has(t))return;let o=this.eventSchemas[t];if(!o)throw Error(`Unknown event: ${String(t)}`);this.setNotificationHandler(o.shape.method.value,{params:o.shape.params},r=>{let n=r;t==="hostcontextchanged"&&(this._hostContext={...this._hostContext,...n}),this._events.dispatch(t,n)}),this._registeredEvents.add(t)}getEventHandler(t){return this._events.getHandler(t)}warnIfRequestHandlerReplaced(t,o,r){o&&r&&console.warn(`[MCP Apps] ${t} handler replaced. Previous handler will no longer be called.`)}constructor(t,o={},r={autoResize:!0}){super(r),this._appInfo=t,this._appCapabilities=o,this.options=r,this._methods.replace("notifications/cancelled"),this._methods.replace("notifications/progress"),!r.allowUnsafeEval&&d.config({jitless:!0}),this.replaceRequestHandler("ping",n=>(console.log("Received ping:",n.params),{})),this.setEventHandler("hostcontextchanged",void 0)}buildContext(t){return t}registerAppCapabilities(t){if(this.transport)throw Error("Cannot register capabilities after transport is established");this._appCapabilities=Wv(this._appCapabilities,t)}registerTool(t,o,r){if(this._registeredTools[t])throw Error(`Tool ${t} is already registered`);let n=this,s=()=>{n._initializedSent&&n._appCapabilities.tools?.listChanged&&n.sendToolListChanged()},i=o.inputSchema!==void 0,c={title:o.title,description:o.description,inputSchema:o.inputSchema,outputSchema:o.outputSchema,annotations:o.annotations,_meta:o._meta,enabled:!0,enable(){this.enabled=!0,s()},disable(){this.enabled=!1,s()},update(l){Object.assign(this,l),s()},remove(){n._registeredTools[t]===c&&(delete n._registeredTools[t],s())},handler:async(l,u)=>{if(!c.enabled)throw Error(`Tool ${t} is disabled`);let m;if(i){let S=c.inputSchema,_=S?await w_(S,l??{},`Invalid input for tool ${t}: `):l??{};m=await r(_,u)}else m=await r(u);return c.outputSchema&&!m.isError&&(m.structuredContent=await w_(c.outputSchema,m.structuredContent,`Invalid output for tool ${t}: `)),m}};return this._registeredTools[t]=c,!this._appCapabilities.tools&&!this.transport&&this.registerAppCapabilities({tools:{listChanged:!0}}),this.ensureToolHandlersInitialized(),s(),c}_toolHandlersInitialized=!1;ensureToolHandlersInitialized(){this._toolHandlersInitialized||(this._toolHandlersInitialized=!0,this.oncalltool=async(t,o)=>{let r=this._registeredTools[t.name];if(!r)throw Error(`Tool ${t.name} not found`);return r.handler(t.arguments,o)},this.onlisttools=async(t,o)=>({tools:await Promise.all(Object.entries(this._registeredTools).filter(([r,n])=>n.enabled).map(async([r,n])=>{let s={name:r,title:n.title,description:n.description,inputSchema:n.inputSchema?await R_(n.inputSchema,"input"):{type:"object",properties:{}}};return n.outputSchema&&(s.outputSchema=await R_(n.outputSchema,"output")),n.annotations&&(s.annotations=n.annotations),n._meta&&(s._meta=n._meta),s}))}))}async sendToolListChanged(t={}){this._assertInitialized("sendToolListChanged"),await this.notification({method:"notifications/tools/list_changed",params:t})}getHostCapabilities(){return this._hostCapabilities}getHostVersion(){return this._hostInfo}getHostContext(){return this._hostContext}get ontoolinput(){return this.getEventHandler("toolinput")}set ontoolinput(t){this.setEventHandler("toolinput",t)}get ontoolinputpartial(){return this.getEventHandler("toolinputpartial")}set ontoolinputpartial(t){this.setEventHandler("toolinputpartial",t)}get ontoolresult(){return this.getEventHandler("toolresult")}set ontoolresult(t){this.setEventHandler("toolresult",t)}get ontoolcancelled(){return this.getEventHandler("toolcancelled")}set ontoolcancelled(t){this.setEventHandler("toolcancelled",t)}get onhostcontextchanged(){return this.getEventHandler("hostcontextchanged")}set onhostcontextchanged(t){this.setEventHandler("hostcontextchanged",t)}_onteardown;get onteardown(){return this._onteardown}set onteardown(t){this.warnIfRequestHandlerReplaced("onteardown",this._onteardown,t),this._onteardown=t,this.replaceRequestHandler("ui/resource-teardown",{params:jv.shape.params,result:Lv},(o,r)=>{if(!this._onteardown)throw Error("No onteardown handler set");return this._onteardown(o,r)})}_oncalltool;get oncalltool(){return this._oncalltool}set oncalltool(t){this.warnIfRequestHandlerReplaced("oncalltool",this._oncalltool,t),this._oncalltool=t,this.replaceRequestHandler("tools/call",(o,r)=>{if(!this._oncalltool)throw Error("No oncalltool handler set");return this._oncalltool(o.params,r)})}_onlisttools;get onlisttools(){return this._onlisttools}set onlisttools(t){this.warnIfRequestHandlerReplaced("onlisttools",this._onlisttools,t),this._onlisttools=t,this.replaceRequestHandler("tools/list",(o,r)=>{if(!this._onlisttools)throw Error("No onlisttools handler set");return this._onlisttools(o.params,r)})}assertCapabilityForMethod(t){switch(t){case"sampling/createMessage":if(!this._hostCapabilities?.sampling)throw Error(`Host does not support sampling (required for ${t})`);break}}assertRequestHandlerCapability(t){switch(t){case"tools/call":case"tools/list":if(!this._appCapabilities.tools)throw Error(`Client does not support tool capability (required for ${t})`);return;default:return}}assertNotificationCapability(t){}async callServerTool(t,o){if(this._assertInitialized("callServerTool"),typeof t=="string")throw Error(`callServerTool() expects an object as its first argument, but received a string ("${t}"). Did you mean: callServerTool({ name: "${t}", arguments: { ... } })?`);return await this.request({method:"tools/call",params:t},{onprogress:()=>{},resetTimeoutOnProgress:!0,...o})}async readServerResource(t,o){return this._assertInitialized("readServerResource"),await this.request({method:"resources/read",params:t},o)}async listServerResources(t,o){return this._assertInitialized("listServerResources"),await this.request({method:"resources/list",params:t},o)}async createSamplingMessage(t,o){return this._assertInitialized("createSamplingMessage"),await this.request({method:"sampling/createMessage",params:t},o)}sendMessage(t,o){return this._assertInitialized("sendMessage"),this.request({method:"ui/message",params:t},Iv,o)}sendLog(t){return this.notification({method:"notifications/message",params:t})}updateModelContext(t,o){return this._assertInitialized("updateModelContext"),this.request({method:"ui/update-model-context",params:t},Ct,o)}openLink(t,o){return this._assertInitialized("openLink"),this.request({method:"ui/open-link",params:t},$v,o)}sendOpenLink=this.openLink;downloadFile(t,o){return this._assertInitialized("downloadFile"),this.request({method:"ui/download-file",params:t},Cv,o)}requestTeardown(t={}){return this.notification({method:"ui/notifications/request-teardown",params:t})}requestDisplayMode(t,o){return this._assertInitialized("requestDisplayMode"),this.request({method:"ui/request-display-mode",params:t},Dv,o)}sendSizeChanged(t){return this.notification({method:"ui/notifications/size-changed",params:t})}setupSizeChangedNotifications(){let t=!1,o=0,r=0,n=()=>{t||(t=!0,requestAnimationFrame(()=>{t=!1;let i=document.documentElement,c=i.style.height;i.style.height="max-content";let l=Math.ceil(i.getBoundingClientRect().height);i.style.height=c;let u=Math.ceil(window.innerWidth);(u!==o||l!==r)&&(o=u,r=l,this.sendSizeChanged({width:u,height:l}))}))};n();let s=new ResizeObserver(n);return s.observe(document.documentElement),s.observe(document.body),()=>s.disconnect()}async connect(t=new Sf(window.parent,window.parent),o){if(this.transport)throw Error("App is already connected. Call close() before connecting again.");this._initializedSent=!1;try{await super.connect(t);let r=await this.request({method:"ui/initialize",params:{appCapabilities:this._appCapabilities,appInfo:this._appInfo,protocolVersion:xv}},Jv,o);if(r===void 0)throw Error(`Server sent invalid initialize result: ${r}`);this._hostCapabilities=r.hostCapabilities,this._hostInfo=r.hostInfo,this._hostContext=r.hostContext,await this.notification({method:"ui/notifications/initialized"}),this._initializedSent=!0,this.options?.autoResize&&this.setupSizeChangedNotifications()}catch(r){throw this.close(),r}}};var Bl=class{constructor(t){this.read=t,this.records=new Map,this.bytes=new Map,this.urls=new Map,this.styles=new Map,this.styleReady=new Map,this.revisions=new Set,this.importing=Promise.resolve(),this.generation=0,this.closed=!1,globalThis.__dsPreviewImport=o=>this.importModule(o)}register(t){if(t.schemaVersion!==1||!Array.isArray(t.scenarios)||!t.assets||typeof t.revision!="string")throw new Error("Unsupported preview manifest. Rebuild previews with the installed ADS version.");if(this.revisions.add(t.revision),this.revisions.size>20)throw Object.assign(new Error("Reopen the preview to release modules retained across 20 build revisions."),{code:"ADS_PREVIEW_REOPEN"});for(let[o,r]of Object.entries(t.assets)){if(!/^ads:\/\/assets\/[a-f0-9]{64}\.[a-z0-9]+$/.test(o)||!/^sha256:[a-f0-9]{64}$/.test(r.digest)||!["imports","dynamicImports","styles","references"].every(n=>Array.isArray(r[n])&&r[n].every(s=>Object.hasOwn(t.assets,s))))throw new Error("Invalid preview dependency graph.");if(this.records.has(o)&&this.records.get(o).digest!==r.digest)throw new Error("An immutable preview resource changed its content.");this.records.set(o,r)}}async resource(t){if(!this.bytes.has(t)){let o=(async()=>{let r=this.records.get(t);if(!r)throw new Error(`Unknown preview dependency: ${t}`);let n=await this.read(t);if(this.closed)throw new Error("The preview App was closed.");let s=n.contents.find(u=>u.uri===t);if(!s)throw new Error(`Missing preview dependency: ${t}`);let i=typeof s.text=="string"?new TextEncoder().encode(s.text):Uint8Array.from(atob(s.blob),u=>u.charCodeAt(0)),c=await crypto.subtle.digest("SHA-256",i),l="sha256:"+[...new Uint8Array(c)].map(u=>u.toString(16).padStart(2,"0")).join("");if(i.length!==r.size||l!==r.digest)throw new Error("Preview resource integrity check failed. Rebuild and reopen the preview.");return i})();this.bytes.set(t,o),o.catch(()=>this.bytes.delete(t))}return this.bytes.get(t)}closure(t){let o=[],r=new Set,n=s=>{if(r.has(s))return;r.add(s);let i=this.records.get(s);if(!i)throw new Error(`Missing preview dependency metadata: ${s}`);for(let c of[...i.imports,...i.styles,...i.references])n(c);o.push(s)};return n(t),o}async materialize(t){let o=this.closure(t);await Promise.all(o.map(n=>this.resource(n)));let r={};for(let n of o){if(this.closed)throw new Error("The preview App was closed.");if(this.urls.has(n))continue;let s=this.records.get(n),i=await this.resource(n);if(s.kind==="module"||s.mimeType==="text/css"){let l=new TextDecoder().decode(i);for(let u of s.references){let m=this.urls.get(u);if(!m)throw new Error("Unsupported cyclic asset reference.");l=l.replaceAll(u,m)}i=l}let c=URL.createObjectURL(new Blob([i],{type:s.mimeType}));this.urls.set(n,c),s.kind==="module"&&(r[n]=c)}if(Object.keys(r).length){let n=document.createElement("script");n.type="importmap",n.textContent=JSON.stringify({imports:r}),document.head.append(n)}return o}async importModule(t){let o=this.importing.then(()=>this.materialize(t));this.importing=o.catch(()=>{});let r=await o;for(let n of r){if(this.records.get(n).mimeType!=="text/css")continue;if(this.styles.has(n)){await this.styleReady.get(n);continue}let i=document.createElement("link");i.rel="stylesheet",i.href=this.urls.get(n),this.styles.set(n,i);let c=new Promise((l,u)=>{i.onload=l,i.onerror=()=>u(new Error("The host CSP blocked a preview stylesheet."))});this.styleReady.set(n,c),document.head.append(i);try{await c}catch(l){throw i.remove(),this.styles.delete(n),this.styleReady.delete(n),l}}return import(t)}async mount(t,o,r){let n=++this.generation;this.register(t);let s=t.scenarios.find(l=>l.id===o);if(!s)throw new Error("The selected scenario is no longer registered.");let i=await this.importModule(s.module);if(n!==this.generation||(await this.unmount?.(),n!==this.generation||this.closed))return!1;r.replaceChildren();let c=new Set(this.closure(s.module));for(let[l,u]of this.styles)c.has(l)||(u.remove(),this.styles.delete(l),this.styleReady.delete(l));for(let l of this.closure(s.module))this.styles.has(l)&&document.head.append(this.styles.get(l));if(typeof i.mount!="function")throw new Error("The compiled scenario has no mount adapter.");if(this.unmount=i.mount(r),typeof this.unmount!="function")throw new Error("The scenario mount adapter must return an unmount callback.");return!0}async dispose(){if(!this.closed){this.closed=!0,++this.generation,await this.unmount?.();for(let t of this.styles.values())t.remove();for(let t of this.urls.values())URL.revokeObjectURL(t);this.styles.clear(),this.styleReady.clear(),this.urls.clear(),this.bytes.clear(),this.records.clear(),delete globalThis.__dsPreviewImport}}};function P_(e){return Array.isArray(e)&&e.length>0&&e.every(t=>t&&typeof t=="object"&&typeof t.token=="string"&&/^--[\w-]+$/.test(t.token)&&typeof t.property=="string"&&/^-?[a-z][a-z0-9-]*$/.test(t.property)&&typeof t.probe=="string"&&t.probe.trim().length>0)&&new Set(e.map(t=>`${t.token}:${t.property}`)).size===e.length}function T_(e,t){let o=t.getComputedStyle(e),r=e.getBoundingClientRect();if(o.display==="none"||o.visibility!=="visible"||!e.getClientRects().length||!(r.width||r.height))return!1;for(let n=e;n;n=n.parentElement)if(Number(t.getComputedStyle(n).opacity)===0)return!1;return!0}function k_(e,t){if(!P_(t))return{ok:!1,diagnostics:["Token preview needs explicit { token, property, probe } samples from the project fixture. Rebuild with the current compiler."]};let o=e.ownerDocument,r=o.defaultView,n=o.documentElement,s=[],i=[],c=[...e.querySelectorAll("[data-ads-token], [data-ads-property]")],l=new Set(t.map(u=>`${u.token}:${u.property}`));for(let u of c)l.has(`${u.dataset.adsToken}:${u.dataset.adsProperty}`)||s.push(`Undeclared rendered token sample ${u.dataset.adsToken}:${u.dataset.adsProperty}.`);for(let u of t){if(!r.CSS.supports(u.property,u.probe)){s.push(`${u.token}: the fixture probe is invalid for ${u.property}.`);continue}let m=r.getComputedStyle(n).getPropertyValue(u.token).trim();if(!m||!r.CSS.supports(u.property,m)){s.push(`${u.token}: its current value is invalid for the fixture's ${u.property} binding.`);continue}let S=c.filter(y=>y.dataset.adsToken===u.token&&y.dataset.adsProperty===u.property);if(!S.length){s.push(`${u.token} needs a rendered ${u.property} sample, not only a name/value label.`);continue}let _=!1;for(let y of S){let P=[];for(let q=y.parentElement;q&&q!==e;q=q.parentElement)q.tagName==="DETAILS"&&!q.open&&(q.open=!0,P.push(q));let E=n.getAttribute("style"),C=y.getAttribute("style");try{y.style.setProperty("transition","none","important");let q=r.getComputedStyle(y).getPropertyValue(u.property),J=T_(y,r);n.style.setProperty(u.token,u.probe,"important");let L=r.getComputedStyle(y).getPropertyValue(u.property);_=!!(L&&L!==q&&(J||T_(y,r)))}finally{E===null?n.removeAttribute("style"):n.setAttribute("style",E),C===null?y.removeAttribute("style"):y.setAttribute("style",C);for(let q of P)q.open=!1}if(_)break}_?i.push(u):s.push(`${u.token} has no visible ${u.property} sample that responds to its value. Hardcoded copies do not satisfy coverage.`)}return{ok:!s.length,verified:i,diagnostics:s}}var We=new Jl({name:"ADS resource preview",version:"1.0.0"},{},{autoResize:!0}),E_=document.getElementById("root"),bf=document.getElementById("ads-preview-status"),vf=async e=>{if(!We.getHostCapabilities()?.serverResources)throw new Error("This host does not forward MCP resource reads. Open the same preview in the ADS showcase.");try{return await We.readServerResource({uri:e},{timeout:15e3})}catch(t){throw t.code===-32601||t.message==="Method not found"?new Error("This host does not forward MCP resource reads. Open the same preview in the ADS showcase."):t}},Gl=new Bl(vf),Vr,ha,$_,Rf,zf=!1,vo=0,pa={},Be="";function Mt(e,t=!1){bf.textContent=e,bf.hidden=!e,bf.setAttribute("role",t?"alert":"status")}async function Wl(e,t={}){try{We.getHostCapabilities()?.logging&&await We.sendLog({level:e==="error"?"error":"info",logger:"ads-preview",data:{state:e,id:Vr,...t}})}catch{}}function C_(e){pa={...pa,...e,styles:{...pa.styles,...e.styles}},["light","dark"].includes(pa.theme)&&(document.documentElement.dataset.theme=pa.theme)}async function I_(){return JSON.parse((await vf("ads://preview/manifest")).contents.find(e=>typeof e.text=="string").text)}async function wf(e,t){if(e===Vr&&t&&t===ha){Mt(Be,!!Be),await Wl(Be?"error":"ready",Be?{message:Be}:{revision:ha});return}let o=++vo;++Gl.generation,Vr=e,Be="",Mt(""),await Wl("loading");try{let r=await I_();if(t&&r.revision!==t)throw new Error("Preview changed while opening. Select it again to use the current build.");if(o!==vo||!await Gl.mount(r,e,E_)||o!==vo||(ha=r.revision,$_=r.session,await new Promise(i=>requestAnimationFrame(()=>requestAnimationFrame(i))),await document.fonts.ready,o!==vo))return;let s=r.scenarios.find(i=>i.id===e);if(s?.kind==="tokens"){let i=k_(E_,s.samples);if(!i.ok)throw Be=`Token preview verification failed:
103
+ container holding the app. Specify either width or maxWidth, and either height or maxHeight.`),locale:d.string().optional().describe("User's language and region preference in BCP 47 format."),timeZone:d.string().optional().describe("User's timezone in IANA format."),userAgent:d.string().optional().describe("Host application identifier."),platform:d.union([d.literal("web"),d.literal("desktop"),d.literal("mobile")]).optional().describe("Platform type for responsive design decisions."),deviceCapabilities:d.object({touch:d.boolean().optional().describe("Whether the device supports touch input."),hover:d.boolean().optional().describe("Whether the device supports hover interactions.")}).optional().describe("Device input capabilities."),safeAreaInsets:d.object({top:d.number().describe("Top safe area inset in pixels."),right:d.number().describe("Right safe area inset in pixels."),bottom:d.number().describe("Bottom safe area inset in pixels."),left:d.number().describe("Left safe area inset in pixels.")}).optional().describe("Mobile safe area boundaries in pixels.")}).passthrough(),Fv=d.object({method:d.literal("ui/notifications/host-context-changed"),params:x_.describe("Partial context update containing only changed fields.")}),Xx=d.object({method:d.literal("ui/update-model-context"),params:d.object({content:d.array(Fe).optional().describe("Context content blocks (text, image, etc.)."),structuredContent:d.record(d.string(),d.unknown().describe("Structured content for machine-readable context data.")).optional().describe("Structured content for machine-readable context data.")})}),eP=d.object({method:d.literal("ui/initialize"),params:d.object({appInfo:Ve.describe("App identification (name and version)."),appCapabilities:Zv.describe("Features and capabilities this app provides."),protocolVersion:d.string().describe("Protocol version this app supports.")})}),Jv=d.object({protocolVersion:d.string().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:Ve.describe("Host application identification and version."),hostCapabilities:Uv.describe("Features and capabilities provided by the host."),hostContext:x_.describe("Rich context about the host environment.")}).passthrough(),Bv={target:"draft-2020-12"};async function R_(e,t){let o=e["~standard"];if(o.jsonSchema)return o.jsonSchema[t](Bv);throw Error(`Schema (vendor: ${o.vendor}) does not implement Standard JSON Schema (~standard.jsonSchema). Use a library that does (zod v4, ArkType, Valibot) or wrap your schema accordingly.`)}async function w_(e,t,o=""){let r=await e["~standard"].validate(t);if(r.issues){let n=r.issues.map(s=>{let i=s.path?.map(c=>typeof c=="object"?c.key:c).join(".");return i?`${i}: ${s.message}`:s.message}).join("; ");throw Error(o+n)}return r.value}function Wv(e,t){let o={...e,...t};return t.experimental!==void 0&&(o.experimental={...e.experimental,...t.experimental}),t.tools!==void 0&&(o.tools={...e.tools,...t.tools}),o}var Jl=class e extends mf{_appInfo;_appCapabilities;options;_hostCapabilities;_hostInfo;_hostContext;_registeredTools={};_initializedSent=!1;_registeredEvents=new Set;_events=new ff;_methods=new gf;setRequestHandler=(...t)=>{let[o,...r]=z_("request",t)??t;this._methods.claim(o,"setRequestHandler"),super.setRequestHandler.call(this,o,...r)};setNotificationHandler=(...t)=>{let[o,...r]=z_("notification",t)??t;this._methods.claim(o,"setNotificationHandler"),super.setNotificationHandler.call(this,o,...r)};removeRequestHandler=t=>{this._methods.release(t),super.removeRequestHandler(t)};removeNotificationHandler=t=>{this._methods.release(t),super.removeNotificationHandler(t)};replaceRequestHandler=(t,...o)=>{this._methods.replace(t),super.setRequestHandler.call(this,t,...o)};_assertInitialized(t){if(this._initializedSent)return;let o=`[ext-apps] App.${t}() called before connect() completed the ui/initialize handshake. Await app.connect() before calling this method, or move data loading to an ontoolresult handler.`;if(this.options?.strict)throw Error(o);console.warn(`${o}. This will throw in a future release.`)}eventSchemas={toolinput:Ov,toolinputpartial:Nv,toolresult:Hv,toolcancelled:Mv,hostcontextchanged:Fv};static ONE_SHOT_EVENTS=new Set(["toolinput","toolinputpartial","toolresult","toolcancelled"]);_everHadListener=new Set;_assertHandlerTiming(t){if(!e.ONE_SHOT_EVENTS.has(t)||this._everHadListener.has(t)||(this._everHadListener.add(t),!this._initializedSent))return;let o=`[ext-apps] "${String(t)}" handler registered after connect() completed the ui/initialize handshake. The host may have already sent this notification. Register handlers before calling app.connect().`;if(this.options?.strict)throw Error(o);console.warn(o)}setEventHandler(t,o){o&&this._assertHandlerTiming(t),this._ensureEventSlot(t),this._events.setHandler(t,o)}addEventListener(t,o){this._assertHandlerTiming(t),this._ensureEventSlot(t),this._events.addEventListener(t,o)}removeEventListener(t,o){this._events.removeEventListener(t,o)}_ensureEventSlot(t){if(this._registeredEvents.has(t))return;let o=this.eventSchemas[t];if(!o)throw Error(`Unknown event: ${String(t)}`);this.setNotificationHandler(o.shape.method.value,{params:o.shape.params},r=>{let n=r;t==="hostcontextchanged"&&(this._hostContext={...this._hostContext,...n}),this._events.dispatch(t,n)}),this._registeredEvents.add(t)}getEventHandler(t){return this._events.getHandler(t)}warnIfRequestHandlerReplaced(t,o,r){o&&r&&console.warn(`[MCP Apps] ${t} handler replaced. Previous handler will no longer be called.`)}constructor(t,o={},r={autoResize:!0}){super(r),this._appInfo=t,this._appCapabilities=o,this.options=r,this._methods.replace("notifications/cancelled"),this._methods.replace("notifications/progress"),!r.allowUnsafeEval&&d.config({jitless:!0}),this.replaceRequestHandler("ping",n=>(console.log("Received ping:",n.params),{})),this.setEventHandler("hostcontextchanged",void 0)}buildContext(t){return t}registerAppCapabilities(t){if(this.transport)throw Error("Cannot register capabilities after transport is established");this._appCapabilities=Wv(this._appCapabilities,t)}registerTool(t,o,r){if(this._registeredTools[t])throw Error(`Tool ${t} is already registered`);let n=this,s=()=>{n._initializedSent&&n._appCapabilities.tools?.listChanged&&n.sendToolListChanged()},i=o.inputSchema!==void 0,c={title:o.title,description:o.description,inputSchema:o.inputSchema,outputSchema:o.outputSchema,annotations:o.annotations,_meta:o._meta,enabled:!0,enable(){this.enabled=!0,s()},disable(){this.enabled=!1,s()},update(l){Object.assign(this,l),s()},remove(){n._registeredTools[t]===c&&(delete n._registeredTools[t],s())},handler:async(l,u)=>{if(!c.enabled)throw Error(`Tool ${t} is disabled`);let m;if(i){let S=c.inputSchema,_=S?await w_(S,l??{},`Invalid input for tool ${t}: `):l??{};m=await r(_,u)}else m=await r(u);return c.outputSchema&&!m.isError&&(m.structuredContent=await w_(c.outputSchema,m.structuredContent,`Invalid output for tool ${t}: `)),m}};return this._registeredTools[t]=c,!this._appCapabilities.tools&&!this.transport&&this.registerAppCapabilities({tools:{listChanged:!0}}),this.ensureToolHandlersInitialized(),s(),c}_toolHandlersInitialized=!1;ensureToolHandlersInitialized(){this._toolHandlersInitialized||(this._toolHandlersInitialized=!0,this.oncalltool=async(t,o)=>{let r=this._registeredTools[t.name];if(!r)throw Error(`Tool ${t.name} not found`);return r.handler(t.arguments,o)},this.onlisttools=async(t,o)=>({tools:await Promise.all(Object.entries(this._registeredTools).filter(([r,n])=>n.enabled).map(async([r,n])=>{let s={name:r,title:n.title,description:n.description,inputSchema:n.inputSchema?await R_(n.inputSchema,"input"):{type:"object",properties:{}}};return n.outputSchema&&(s.outputSchema=await R_(n.outputSchema,"output")),n.annotations&&(s.annotations=n.annotations),n._meta&&(s._meta=n._meta),s}))}))}async sendToolListChanged(t={}){this._assertInitialized("sendToolListChanged"),await this.notification({method:"notifications/tools/list_changed",params:t})}getHostCapabilities(){return this._hostCapabilities}getHostVersion(){return this._hostInfo}getHostContext(){return this._hostContext}get ontoolinput(){return this.getEventHandler("toolinput")}set ontoolinput(t){this.setEventHandler("toolinput",t)}get ontoolinputpartial(){return this.getEventHandler("toolinputpartial")}set ontoolinputpartial(t){this.setEventHandler("toolinputpartial",t)}get ontoolresult(){return this.getEventHandler("toolresult")}set ontoolresult(t){this.setEventHandler("toolresult",t)}get ontoolcancelled(){return this.getEventHandler("toolcancelled")}set ontoolcancelled(t){this.setEventHandler("toolcancelled",t)}get onhostcontextchanged(){return this.getEventHandler("hostcontextchanged")}set onhostcontextchanged(t){this.setEventHandler("hostcontextchanged",t)}_onteardown;get onteardown(){return this._onteardown}set onteardown(t){this.warnIfRequestHandlerReplaced("onteardown",this._onteardown,t),this._onteardown=t,this.replaceRequestHandler("ui/resource-teardown",{params:jv.shape.params,result:Lv},(o,r)=>{if(!this._onteardown)throw Error("No onteardown handler set");return this._onteardown(o,r)})}_oncalltool;get oncalltool(){return this._oncalltool}set oncalltool(t){this.warnIfRequestHandlerReplaced("oncalltool",this._oncalltool,t),this._oncalltool=t,this.replaceRequestHandler("tools/call",(o,r)=>{if(!this._oncalltool)throw Error("No oncalltool handler set");return this._oncalltool(o.params,r)})}_onlisttools;get onlisttools(){return this._onlisttools}set onlisttools(t){this.warnIfRequestHandlerReplaced("onlisttools",this._onlisttools,t),this._onlisttools=t,this.replaceRequestHandler("tools/list",(o,r)=>{if(!this._onlisttools)throw Error("No onlisttools handler set");return this._onlisttools(o.params,r)})}assertCapabilityForMethod(t){switch(t){case"sampling/createMessage":if(!this._hostCapabilities?.sampling)throw Error(`Host does not support sampling (required for ${t})`);break}}assertRequestHandlerCapability(t){switch(t){case"tools/call":case"tools/list":if(!this._appCapabilities.tools)throw Error(`Client does not support tool capability (required for ${t})`);return;default:return}}assertNotificationCapability(t){}async callServerTool(t,o){if(this._assertInitialized("callServerTool"),typeof t=="string")throw Error(`callServerTool() expects an object as its first argument, but received a string ("${t}"). Did you mean: callServerTool({ name: "${t}", arguments: { ... } })?`);return await this.request({method:"tools/call",params:t},{onprogress:()=>{},resetTimeoutOnProgress:!0,...o})}async readServerResource(t,o){return this._assertInitialized("readServerResource"),await this.request({method:"resources/read",params:t},o)}async listServerResources(t,o){return this._assertInitialized("listServerResources"),await this.request({method:"resources/list",params:t},o)}async createSamplingMessage(t,o){return this._assertInitialized("createSamplingMessage"),await this.request({method:"sampling/createMessage",params:t},o)}sendMessage(t,o){return this._assertInitialized("sendMessage"),this.request({method:"ui/message",params:t},Iv,o)}sendLog(t){return this.notification({method:"notifications/message",params:t})}updateModelContext(t,o){return this._assertInitialized("updateModelContext"),this.request({method:"ui/update-model-context",params:t},Ct,o)}openLink(t,o){return this._assertInitialized("openLink"),this.request({method:"ui/open-link",params:t},$v,o)}sendOpenLink=this.openLink;downloadFile(t,o){return this._assertInitialized("downloadFile"),this.request({method:"ui/download-file",params:t},Cv,o)}requestTeardown(t={}){return this.notification({method:"ui/notifications/request-teardown",params:t})}requestDisplayMode(t,o){return this._assertInitialized("requestDisplayMode"),this.request({method:"ui/request-display-mode",params:t},Dv,o)}sendSizeChanged(t){return this.notification({method:"ui/notifications/size-changed",params:t})}setupSizeChangedNotifications(){let t=!1,o=0,r=0,n=()=>{t||(t=!0,requestAnimationFrame(()=>{t=!1;let i=document.documentElement,c=i.style.height;i.style.height="max-content";let l=Math.ceil(i.getBoundingClientRect().height);i.style.height=c;let u=Math.ceil(window.innerWidth);(u!==o||l!==r)&&(o=u,r=l,this.sendSizeChanged({width:u,height:l}))}))};n();let s=new ResizeObserver(n);return s.observe(document.documentElement),s.observe(document.body),()=>s.disconnect()}async connect(t=new Sf(window.parent,window.parent),o){if(this.transport)throw Error("App is already connected. Call close() before connecting again.");this._initializedSent=!1;try{await super.connect(t);let r=await this.request({method:"ui/initialize",params:{appCapabilities:this._appCapabilities,appInfo:this._appInfo,protocolVersion:xv}},Jv,o);if(r===void 0)throw Error(`Server sent invalid initialize result: ${r}`);this._hostCapabilities=r.hostCapabilities,this._hostInfo=r.hostInfo,this._hostContext=r.hostContext,await this.notification({method:"ui/notifications/initialized"}),this._initializedSent=!0,this.options?.autoResize&&this.setupSizeChangedNotifications()}catch(r){throw this.close(),r}}};var Bl=class{constructor(t){this.read=t,this.records=new Map,this.bytes=new Map,this.urls=new Map,this.styles=new Map,this.styleReady=new Map,this.revisions=new Set,this.importing=Promise.resolve(),this.generation=0,this.closed=!1,globalThis.__dsPreviewImport=o=>this.importModule(o)}register(t){if(t.schemaVersion!==1||!Array.isArray(t.scenarios)||!t.assets||typeof t.revision!="string")throw new Error("Unsupported preview manifest. Rebuild previews with the installed ADS version.");if(this.revisions.add(t.revision),this.revisions.size>20)throw Object.assign(new Error("Reopen the preview to release modules retained across 20 build revisions."),{code:"ADS_PREVIEW_REOPEN"});for(let[o,r]of Object.entries(t.assets)){if(!/^ads:\/\/assets\/[a-f0-9]{64}\.[a-z0-9]+$/.test(o)||!/^sha256:[a-f0-9]{64}$/.test(r.digest)||!["imports","dynamicImports","styles","references"].every(n=>Array.isArray(r[n])&&r[n].every(s=>Object.hasOwn(t.assets,s))))throw new Error("Invalid preview dependency graph.");if(this.records.has(o)&&this.records.get(o).digest!==r.digest)throw new Error("An immutable preview resource changed its content.");this.records.set(o,r)}}async resource(t){if(!this.bytes.has(t)){let o=(async()=>{let r=this.records.get(t);if(!r)throw new Error(`Unknown preview dependency: ${t}`);let n=await this.read(t);if(this.closed)throw new Error("The preview App was closed.");let s=n.contents.find(u=>u.uri===t);if(!s)throw new Error(`Missing preview dependency: ${t}`);let i=typeof s.text=="string"?new TextEncoder().encode(s.text):Uint8Array.from(atob(s.blob),u=>u.charCodeAt(0)),c=await crypto.subtle.digest("SHA-256",i),l="sha256:"+[...new Uint8Array(c)].map(u=>u.toString(16).padStart(2,"0")).join("");if(i.length!==r.size||l!==r.digest)throw new Error("Preview resource integrity check failed. Rebuild and reopen the preview.");return i})();this.bytes.set(t,o),o.catch(()=>this.bytes.delete(t))}return this.bytes.get(t)}closure(t){let o=[],r=new Set,n=s=>{if(r.has(s))return;r.add(s);let i=this.records.get(s);if(!i)throw new Error(`Missing preview dependency metadata: ${s}`);for(let c of[...i.imports,...i.styles,...i.references])n(c);o.push(s)};return n(t),o}async materialize(t){let o=this.closure(t);await Promise.all(o.map(n=>this.resource(n)));let r={};for(let n of o){if(this.closed)throw new Error("The preview App was closed.");if(this.urls.has(n))continue;let s=this.records.get(n),i=await this.resource(n);if(s.kind==="module"||s.mimeType==="text/css"){let l=new TextDecoder().decode(i);for(let u of s.references){let m=this.urls.get(u);if(!m)throw new Error("Unsupported cyclic asset reference.");l=l.replaceAll(u,m)}i=l}let c=URL.createObjectURL(new Blob([i],{type:s.mimeType}));this.urls.set(n,c),s.kind==="module"&&(r[n]=c)}if(Object.keys(r).length){let n=document.createElement("script");n.type="importmap",n.textContent=JSON.stringify({imports:r}),document.head.append(n)}return o}async importModule(t){let o=this.importing.then(()=>this.materialize(t));this.importing=o.catch(()=>{});let r=await o;for(let n of r){if(this.records.get(n).mimeType!=="text/css")continue;if(this.styles.has(n)){await this.styleReady.get(n);continue}let i=document.createElement("link");i.rel="stylesheet",i.href=this.urls.get(n),this.styles.set(n,i);let c=new Promise((l,u)=>{i.onload=l,i.onerror=()=>u(new Error("The host CSP blocked a preview stylesheet."))});this.styleReady.set(n,c),document.head.append(i);try{await c}catch(l){throw i.remove(),this.styles.delete(n),this.styleReady.delete(n),l}}return import(t)}async mount(t,o,r){let n=++this.generation;this.register(t);let s=t.scenarios.find(l=>l.id===o);if(!s)throw new Error("The selected scenario is no longer registered.");let i=await this.importModule(s.module);if(n!==this.generation||(await this.unmount?.(),n!==this.generation||this.closed))return!1;r.replaceChildren();let c=new Set(this.closure(s.module));for(let[l,u]of this.styles)c.has(l)||(u.remove(),this.styles.delete(l),this.styleReady.delete(l));for(let l of this.closure(s.module))this.styles.has(l)&&document.head.append(this.styles.get(l));if(typeof i.mount!="function")throw new Error("The compiled scenario has no mount adapter.");if(this.unmount=i.mount(r),typeof this.unmount!="function")throw new Error("The scenario mount adapter must return an unmount callback.");return!0}async dispose(){if(!this.closed){this.closed=!0,++this.generation,await this.unmount?.();for(let t of this.styles.values())t.remove();for(let t of this.urls.values())URL.revokeObjectURL(t);this.styles.clear(),this.styleReady.clear(),this.urls.clear(),this.bytes.clear(),this.records.clear(),delete globalThis.__dsPreviewImport}}};function P_(e){return Array.isArray(e)&&e.length>0&&e.every(t=>t&&typeof t=="object"&&typeof t.token=="string"&&/^--[\w-]+$/.test(t.token)&&typeof t.property=="string"&&/^-?[a-z][a-z0-9-]*$/.test(t.property)&&typeof t.probe=="string"&&t.probe.trim().length>0)&&new Set(e.map(t=>`${t.token}:${t.property}`)).size===e.length}function T_(e,t){let o=t.getComputedStyle(e),r=e.getBoundingClientRect();if(o.display==="none"||o.visibility!=="visible"||!e.getClientRects().length||!(r.width||r.height))return!1;for(let n=e;n;n=n.parentElement)if(Number(t.getComputedStyle(n).opacity)===0)return!1;return!0}function k_(e,t){if(!P_(t))return{ok:!1,diagnostics:["Token preview needs explicit { token, property, probe } samples from the project fixture. Rebuild with the current compiler."]};let o=e.ownerDocument,r=o.defaultView,n=o.documentElement,s=[],i=[],c=[...e.querySelectorAll("[data-ads-token], [data-ads-property]")],l=new Set(t.map(u=>`${u.token}:${u.property}`));for(let u of c)l.has(`${u.dataset.adsToken}:${u.dataset.adsProperty}`)||s.push(`Undeclared rendered token sample ${u.dataset.adsToken}:${u.dataset.adsProperty}.`);for(let u of t){if(!r.CSS.supports(u.property,u.probe)){s.push(`${u.token}: the fixture probe is invalid for ${u.property}.`);continue}let m=r.getComputedStyle(n).getPropertyValue(u.token).trim();if(!m||!r.CSS.supports(u.property,m)){s.push(`${u.token}: its current value is invalid for the fixture's ${u.property} binding.`);continue}let S=c.filter(y=>y.dataset.adsToken===u.token&&y.dataset.adsProperty===u.property);if(!S.length){s.push(`${u.token} needs a rendered ${u.property} sample, not only a name/value label.`);continue}let _=!1;for(let y of S){let P=[];for(let q=y.parentElement;q&&q!==e;q=q.parentElement)q.tagName==="DETAILS"&&!q.open&&(q.open=!0,P.push(q));let E=n.getAttribute("style"),C=y.getAttribute("style");try{y.style.setProperty("transition","none","important");let q=r.getComputedStyle(y).getPropertyValue(u.property),J=T_(y,r);n.style.setProperty(u.token,u.probe,"important");let L=r.getComputedStyle(y).getPropertyValue(u.property);_=!!(L&&L!==q&&(J||T_(y,r)))}finally{E===null?n.removeAttribute("style"):n.setAttribute("style",E),C===null?y.removeAttribute("style"):y.setAttribute("style",C);for(let q of P)q.open=!1}if(_)break}_?i.push(u):s.push(`${u.token} has no visible ${u.property} sample that responds to its value. Hardcoded copies do not satisfy coverage.`)}return{ok:!s.length,verified:i,diagnostics:s}}var We=new Jl({name:"ADS resource preview",version:"1.0.0"},{},{autoResize:!0}),E_=document.getElementById("root"),bf=document.getElementById("ads-preview-status"),vf=async e=>{if(!We.getHostCapabilities()?.serverResources)throw new Error("This host does not forward MCP resource reads. Open the same preview in the ADS showcase.");try{return await We.readServerResource({uri:e},{timeout:15e3})}catch(t){throw t.code===-32601||t.message==="Method not found"?new Error("This host does not forward MCP resource reads. Open the same preview in the ADS showcase."):t}},Gl=new Bl(vf),Vr,ha,$_,Rf,zf=!1,vo=0,pa={},Be="";function Mt(e,t=!1){bf.textContent=e,bf.hidden=!e,bf.setAttribute("role",t?"alert":"status")}async function Wl(e,t={}){try{We.getHostCapabilities()?.logging&&await We.sendLog({level:e==="error"?"error":"info",logger:"ads-preview",data:{state:e,id:Vr,...t}})}catch{}}function C_(e){pa={...pa,...e,styles:{...pa.styles,...e.styles}},["light","dark"].includes(pa.theme)&&(document.documentElement.dataset.theme=pa.theme)}async function I_(){return JSON.parse((await vf("ads://preview/manifest")).contents.find(e=>typeof e.text=="string").text)}async function wf(e,t){if(e===Vr&&t&&t===ha){Mt(Be,!!Be),await Wl(Be?"error":"ready",Be?{message:Be}:{revision:ha});return}let o=++vo;++Gl.generation,Vr=e,document.documentElement.dataset.adsScenario=e,document.documentElement.dataset.adsPreviewState="loading",Be="",Mt(""),await Wl("loading");try{let r=await I_();if(t&&r.revision!==t)throw new Error("Preview changed while opening. Select it again to use the current build.");if(o!==vo||!await Gl.mount(r,e,E_)||o!==vo||(ha=r.revision,$_=r.session,await new Promise(i=>requestAnimationFrame(()=>requestAnimationFrame(i))),await document.fonts.ready,o!==vo))return;let s=r.scenarios.find(i=>i.id===e);if(s?.kind==="tokens"){let i=k_(E_,s.samples);if(!i.ok)throw Be=`Token preview verification failed:
104
104
  ${i.diagnostics.slice(0,20).join(`
105
- `)}`,new Error(Be)}o===vo&&(Mt(""),await Wl("ready",{revision:ha}))}catch(r){o===vo&&(Mt(r.message,!0),await Wl("error",{message:r.message,...r.code?{code:r.code}:{}}))}}async function Gv(){if(!(!Vr||zf||document.visibilityState==="hidden")){zf=!0;try{let t=JSON.parse((await vf("ads://project/build")).contents.find(o=>typeof o.text=="string").text).previews.find(o=>o.id===Vr);if(!t||!t.current){let o=t?.diagnostics?.[0]?.message;Mt(o?`Build failed. Showing the previous preview.
105
+ `)}`,new Error(Be)}o===vo&&(document.documentElement.dataset.adsPreviewState="ready",Mt(""),await Wl("ready",{revision:ha}))}catch(r){o===vo&&(document.documentElement.dataset.adsPreviewState="error",Mt(r.message,!0),await Wl("error",{message:r.message,...r.code?{code:r.code}:{}}))}}async function Gv(){if(!(!Vr||zf||document.visibilityState==="hidden")){zf=!0;try{let t=JSON.parse((await vf("ads://project/build")).contents.find(o=>typeof o.text=="string").text).previews.find(o=>o.id===Vr);if(!t||!t.current){let o=t?.diagnostics?.[0]?.message;Mt(o?`Build failed. Showing the previous preview.
106
106
  ${o}`:"Rebuilding. This is the previous preview.",!!o)}else{let o=await I_();o.revision!==ha||o.session!==$_?await wf(Vr):Mt(Be,!!Be)}}catch(e){Mt(`Preview freshness unavailable: ${e.message}`,!0)}finally{zf=!1}}}We.ontoolresult=e=>{let t=e.structuredContent;t?.scenario?.id&&wf(t.scenario.id,t.manifestRevision)};We.onhostcontextchanged=C_;We.onteardown=async()=>(++vo,clearInterval(Rf),await Gl.dispose(),{});try{if(await We.connect(),C_(We.getHostContext()??{}),!We.getHostCapabilities()?.serverResources)throw new Error("This host does not forward MCP resource reads. Open the same preview in the ADS showcase.");Rf=setInterval(()=>{Gv()},2e3);let e=document.getElementById("ads-preview-selection");e&&wf(JSON.parse(e.textContent).id)}catch(e){Mt(e.message,!0)}window.addEventListener("pagehide",()=>{clearInterval(Rf),Gl.dispose()},{once:!0});
107
107
  /*! Bundled license information:
108
108
 
@@ -1,12 +1,12 @@
1
1
  export const reasoning = `ADS is development tooling. The project owns its components, patterns, token values and branding.
2
- Start with ads_status and use its projectRoot, workflow and project commands; the MCP workspace may be nested inside the client repository. If ADS tools are not exposed, read ads://project/guide then ads://project/status. Find existing public UI with ads_catalog and load one relevant ADS workflow via ads_skills/ads_read_skill. UI implementation/refinement uses ads-designer; library adaptation ads-assimilate; ADS transfer ads-import; motion ads-motion; stress testing ads-break-ui. Domain-model workflows are separate. Inspect the relevant public API, local guide, token definitions and real showcase fixture before proposing changes. Keep source/instruction discovery scoped to the target project, ancestry and selected external sources; broad scans of /mnt or home are not needed to discover this ADS.
2
+ Start with ads_status and use its projectRoot, workflow and project commands; the MCP workspace may be nested inside the client repository. If ADS tools are not exposed, read ads://project/guide then ads://project/status. Find existing public UI with ads_catalog and load one relevant ADS workflow via ads_skills/ads_read_skill. UI implementation/refinement uses ads-designer; library adaptation ads-migrate; ADS transfer ads-import; motion ads-motion; stress testing ads-break-ui. Domain-model workflows are separate. Inspect the relevant public API, local guide, token definitions and real showcase fixture before proposing changes. Keep source/instruction discovery scoped to the target project, ancestry and selected external sources; broad scans of /mnt or home are not needed to discover this ADS.
3
3
  When ads_status.mutations.enabled, discover one owner with ads_units, read relevant ranges with ads_read_file and send only a Git unified diff through ads_apply_patch({unit,patch,dryRun}). A unit includes its own scenarios/fixtures; imported peers remain separate. tokens is the token foundation unit. Only that owner's public re-exports may change in the shared entry; preserve unrelated statements including export {};. Token manifests list companionFiles: include required foundation import changes only for canonical token files created/deleted in the same patch, preserving other imports/comments and canonical order. Use dryRun to review validation. Rejected patches leave source unchanged; fix diagnostics rather than bypassing checks with direct writes. Shared globals use foundation; guides use documentation; assets use discovered assets/* units. Intentional configuration, registered project guards/builders and rule exceptions use configuration, which validates the proposed settings and refreshes its compiler. Keep mandatory ADS gates. For large/binary diffs, pass patchFile pointing to .ds/patches/<name>.patch rather than loading bytes into context; Git binary patches require installed Git. When editing is disabled, use local editing tools.
4
4
  Reason through Data (design-token values, semantic roles and relationships), UX (structure, behavior, accessibility, states and layout), and Skin (token-backed identity and visual states). Business data is a separate application-owned input, supplied through typed props or injected callbacks/services. The project uses Linaria extraction for owned styles; MCP adds no production runtime.
5
5
  Choose an existing public component or pattern first. Keep application data/routing/effects in the application and fixture data/state beside its scenario. Change presentation in its owning ADS implementation, then demonstrate the same public API in the showcase. Put demo-only sizing and framing in a public review/layout pattern; add component props for deliberate runtime API needs and preserve required behavior. Do not create a widget-only replica or override component internals.
6
6
  For token source inspection, call ads_tokens with a focused query. For token samples, components, patterns and composition, use ads_scenarios to select a path-derived id and invoke ads_show_scenario. Showcase and MCP mount that exact entry and its colocated fixture; no page initialization is required. The showcase is a minimal tool-owned shell plus an MCP client and Apps host. Its menus come from tools; it invokes the same entry tools and refreshes the active scenario through notifications. The reusable App loads framework runtime, foundation CSS, fonts and selected modules via MCP resources inside its sandbox. Keep project execution and styles out of the outer host; preserve mount/unmount cleanup and lazy dependencies. Use the existing ads_status showcase URL, or ads showcase when no MCP runtime is connected. Projects supply scenarios/fixtures and the compiler, not browser page files or another shell. Tool metadata selects an MCP App; a resources/read of HTML alone is not proof of rendered UI. If the host has no Apps view, give the configured showcase URL and use text/resources.
7
7
  Check ads_status.previewWatch before launching a separate preview builder. A configured watcher is managed by ads mcp. Check ads_status.build or ads://project/build after changes: running does not mean ready, and unreported freshness is not evidence of current source. Reported preview failures include structured diagnostics; fix the owning source and wait for a successful rebuild, then invoke the preview tool again. Notifications may not reach the agent, so read status explicitly. Build revisions identify reported compilation attempts, not proof of runtime/visual correctness. Do not invent replacement UI to bypass a failed preview.
8
8
  Skills: use ads_skills to select a workflow and read its SKILL.md with ads_read_skill. Resolve relative supporting files from that skill URI. Reading instructions does not prove native skill activation or authorize unrelated actions.
9
- Ask for missing intent only when it changes the outcome. State the proposed behavior, states and difficult content. Preserve lazy loading, workers, caches, cancellation, reduced motion, focus and keyboard behavior. Run ads_check plus the project's registered lint, typecheck, build and relevant tests; inspect the actual browser/rendered app at narrow and wide sizes in both themes. Static success does not establish visual or interaction parity.`;
9
+ Ask for missing intent only when it changes the outcome. State the proposed behavior, states and difficult content. Preserve lazy loading, workers, caches, cancellation, reduced motion, focus and keyboard behavior. For controllers and hooks, read ads://guides/behaviors. Discover guarantees/checks with ads_behaviors and run ads_verify; read current execution evidence. Keep behavior.json and checks development-only. Use the configured framework and declared dependencies, not MCP host globals. Preserve covers for rendered UI; exercises links behavioral APIs to demonstrations, not passing guarantees. Run ads_check plus the project's registered lint, typecheck, build and relevant tests; inspect the actual browser/rendered app at narrow and wide sizes in both themes. Static success does not establish visual or interaction parity.`;
10
10
 
11
11
  export function promptDefinitions(skills) {
12
12
  const prompts = [
@@ -1,10 +1,11 @@
1
1
  import { existsSync, readFileSync, statSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { Server, ProtocolError, INVALID_PARAMS, ResourceNotFoundError } from "@modelcontextprotocol/server";
4
- import { serveStdio } from "@modelcontextprotocol/server/stdio";
4
+ import { serveStdio, StdioServerTransport } from "@modelcontextprotocol/server/stdio";
5
5
  import { z } from "zod";
6
6
  import { checkProject } from "../check.mjs";
7
7
  import { loadConfig, sourceFiles } from "../config.mjs";
8
+ import { behaviorStatus, createVerificationController, BEHAVIOR_URI } from "../verification.mjs";
8
9
  import { inspectTokens } from "../tokens.mjs";
9
10
  import { inspectScenarios } from "../scenarios.mjs";
10
11
  import { startShowcaseHost } from "./showcase-host.mjs";
@@ -33,6 +34,7 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
33
34
  const cache = inspection ?? {};
34
35
  const skills = cache.skills ??= createSkillStore(skillDirectory);
35
36
  const edits = cache.edits ??= createMutationController(root);
37
+ const verification = cache.verification ??= createVerificationController(root);
36
38
  let options = loadConfig(root);
37
39
  const signature = `${root}:${JSON.stringify(options.config)}`;
38
40
  if (cache.signature !== signature) {
@@ -77,11 +79,13 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
77
79
  const skillEntries = new Set(skills.skills.map((skill) => skill.uri));
78
80
  return [
79
81
  { uri: GUIDE_URI, name: "Start here: design-system workflow", description: "Read first for UI/ADS tasks, especially when only resource access is exposed. Explains the exact workspace, tool sequence, skill selection, previews, project commands and fallback.", mimeType: "text/markdown" },
82
+ { uri: "ads://guides/behaviors", name: "Controller and behavioral verification guide", mimeType: "text/markdown" },
80
83
  { uri: `${PROJECT_URI}/status`, name: "ADS project status", mimeType: "application/json" },
81
84
  { uri: BUILD_URI, name: "Preview build status and diagnostics", mimeType: "application/json" },
82
85
  { uri: `${PROJECT_URI}/tokens`, name: "Project token values and themes", mimeType: "application/json" },
83
86
  { uri: `${PROJECT_URI}/catalog`, name: "Public components and patterns", mimeType: "application/json" },
84
87
  { uri: `${PROJECT_URI}/scenarios`, name: "Independently mountable scenarios", mimeType: "application/json" },
88
+ { uri: BEHAVIOR_URI, name: "Behavior contracts, demonstrations and current verification evidence", mimeType: "application/json" },
85
89
  { uri: `${PROJECT_URI}/checks`, name: "Current ADS architecture diagnostics", mimeType: "application/json" },
86
90
  { uri: TOKENS_APP_URI, name: "Experimental token widget", mimeType: APP_MIME },
87
91
  { uri: SCENARIO_APP_URI, name: "Experimental scenario widget", mimeType: APP_MIME },
@@ -98,18 +102,20 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
98
102
  const context = project();
99
103
  return { projectRoot: root, workflow: workflow([...previews(), ...scenarioApps()]), project: context,
100
104
  package: pkg.name, version: pkg.version, dsDir: options.config.dsDir, publicAlias: options.config.publicAlias,
101
- components: getCatalog().filter((item) => item.kind === "components").length,
102
- patterns: getCatalog().filter((item) => item.kind === "patterns").length,
105
+ components: getCatalog().filter((item) => item.kind === "components" && item.apiKind === "ui").length,
106
+ patterns: getCatalog().filter((item) => item.kind === "patterns" && item.apiKind === "ui").length,
103
107
  tokenCount: inspectTokens(root).tokens.length, showcaseUrl: getShowcaseUrl(),
104
108
  previewWatch: previewWatch?.status() ?? { state: "disabled" },
105
109
  mutations: mutations ? edits.status() : { enabled: false },
106
110
  build: summarizeBuild(builds()),
107
111
  previews: previews(), skills: skills.skills.map(({ uri, frontmatter }) => ({ uri, name: frontmatter.name, description: frontmatter.description })),
108
112
  scenarios: { count: getRegistry().scenarios.length, diagnostics: getRegistry().diagnostics, resourceUri: `${PROJECT_URI}/scenarios`, tool: "ads_show_scenario" },
113
+ behaviors: { resourceUri: BEHAVIOR_URI, discoveryTool: "ads_behaviors", verificationTool: "ads_verify", ...verification.status() },
109
114
  experimental: { apps: true, nativeSkills: era === "modern", nativeSkillActivation: "host-controlled" } };
110
115
  }
111
116
  async function read(uri, signal) {
112
117
  if (uri === GUIDE_URI) return { contents: [{ uri, mimeType: "text/markdown", text: projectGuide(project(), [...previews(), ...scenarioApps()]) }] };
118
+ if (uri === "ads://guides/behaviors") return { contents: [{ uri, mimeType: "text/markdown", text: readFileSync(new URL("../../docs/BEHAVIORS.md", import.meta.url), "utf8") }] };
113
119
  if (skills.files.has(uri)) return { contents: [skills.read(uri)] };
114
120
  let value;
115
121
  if (uri === `${PROJECT_URI}/status`) value = status();
@@ -118,6 +124,7 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
118
124
  else if (uri === `${PROJECT_URI}/catalog`) value = { items: getCatalog() };
119
125
  else if (uri === `${PROJECT_URI}/scenarios`) value = getRegistry();
120
126
  else if (uri === `${PROJECT_URI}/checks`) value = checkProject(root);
127
+ else if (uri === BEHAVIOR_URI) value = behaviorStatus(options);
121
128
  else if (uri === PREVIEW_MANIFEST_URI && options.config.mcp.previewManifest) {
122
129
  value = readPreviewManifest(options);
123
130
  if (!value) throw new Error("Preview resources are not built yet. Invoke ads_show_scenario to start the managed compiler.");
@@ -153,13 +160,13 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
153
160
  function tools() {
154
161
  return [
155
162
  { name: "ads_status", description: "Start here for UI/ADS work: components, patterns, tokens/themes/branding, layout, behavior, motion and showcase review. Returns the exact project root/public API, workflow, project commands, editing mode and build diagnostics. When mutations.enabled, use guarded ads_apply_patch for ADS edits; otherwise use local editing tools.", schema: empty, call: () => json(status()) },
156
- { name: "ads_catalog", description: "Before creating/changing UI, find existing public components/patterns with a small query/limit. Returns import alias and exact source/guide URIs so you can reuse or edit the owner. Read ads_status first for workspace/workflow. Metadata only; does not render UI.", schema: query.extend({ kind: z.enum(["components", "patterns"]).optional() }), call: ({ query: search = "", limit, kind }) => {
163
+ { name: "ads_catalog", description: "Before creating/changing UI or behavior, find existing public components/patterns/controllers and hooks with a small query/limit. Returns import alias and exact source/guide URIs so you can reuse or edit the owner. Read ads_status first for workspace/workflow. Metadata only; does not render UI.", schema: query.extend({ kind: z.enum(["components", "patterns", "controllers"]).optional() }), call: ({ query: search = "", limit, kind }) => {
157
164
  const matches = getCatalog().filter((item) => (!kind || item.kind === kind) && item.name.toLowerCase().includes(search.toLowerCase()));
158
165
  const units = edits.units();
159
166
  return json({ total: matches.length, items: matches.slice(0, limit).map(item => ({ ...item, unit: units.find(unit => unit.files.includes(item.file))?.id ?? null })), resourceUri: `${PROJECT_URI}/catalog` });
160
167
  } },
161
- { name: "ads_scenarios", description: "Find an independently mountable token, component, pattern or composition preview. IDs derive from source filenames; covers lists public UI and tokens lists the real token inputs. Use ads_show_scenario with an exact returned id. Showcase and MCP use this same registry.", schema: query.extend({ kind: z.enum(["tokens", "components", "patterns", "composition"]).optional(), offset: z.number().int().min(0).default(0) }), call: ({ query: search = "", limit, kind, offset }) => {
162
- const matches = getRegistry().scenarios.filter(item => (!kind || item.kind === kind) && `${item.id} ${item.title} ${item.covers.join(" ")} ${item.tokens.join(" ")}`.toLowerCase().includes(search.toLowerCase()));
168
+ { name: "ads_scenarios", description: "Find an independently mountable token, component, pattern or composition preview. IDs derive from source filenames; covers lists rendered public UI, exercises lists behavioral APIs and tokens lists the real token inputs. Use ads_show_scenario with an exact returned id. Showcase and MCP use this same registry.", schema: query.extend({ kind: z.enum(["tokens", "components", "patterns", "composition"]).optional(), offset: z.number().int().min(0).default(0) }), call: ({ query: search = "", limit, kind, offset }) => {
169
+ const matches = getRegistry().scenarios.filter(item => (!kind || item.kind === kind) && `${item.id} ${item.title} ${item.covers.join(" ")} ${(item.exercises ?? []).join(" ")} ${item.tokens.join(" ")}`.toLowerCase().includes(search.toLowerCase()));
163
170
  const units = edits.units();
164
171
  return json({ total: matches.length, scenarios: matches.slice(offset, offset + limit).map(item => ({ ...item, unit: units.find(unit => unit.files.includes(item.file))?.id ?? null })), ...(offset + limit < matches.length ? { nextOffset: offset + limit } : {}), diagnostics: getRegistry().diagnostics, resourceUri: `${PROJECT_URI}/scenarios` });
165
172
  } },
@@ -185,7 +192,7 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
185
192
  const result = checkProject(root);
186
193
  return json({ ok: result.ok, total: result.diagnostics.length, diagnostics: result.diagnostics.slice(0, limit), advisoryCount: result.advisories?.length ?? 0, advisories: (result.advisories ?? []).slice(0, limit), resourceUri: `${PROJECT_URI}/checks` });
187
194
  } },
188
- { name: "ads_skills", description: "Select one ADS workflow after ads_status: ads-designer for building/refining/debugging UI; ads-assimilate for a library reference; ads-import for another project's ADS; ads-motion for animation; ads-break-ui for stress testing. Then read that workflow with ads_read_skill. Listing does not activate a native skill.", schema: empty, call: () => json({ skills: skills.skills.map(({ uri, frontmatter }) => ({ uri, ...frontmatter })) }) },
195
+ { name: "ads_skills", description: "Select one ADS workflow after ads_status: ads-designer for building/refining/debugging UI; ads-migrate for a library reference; ads-import for another project's ADS; ads-motion for animation; ads-break-ui for stress testing. Then read that workflow with ads_read_skill. Listing does not activate a native skill.", schema: empty, call: () => json({ skills: skills.skills.map(({ uri, frontmatter }) => ({ uri, ...frontmatter })) }) },
189
196
  { name: "ads_read_skill", description: "Read a selected skill or relative supporting text file in bounded chunks. This is explicit instruction access, not automatic native skill activation/install.", schema: z.object({ name: z.string().max(64), file: z.string().max(300).default("SKILL.md"), offset: z.number().int().min(0).default(0), limit: z.number().int().min(1).max(8000).default(6000) }).strict(), call: ({ name, file, offset, limit }) => {
190
197
  const skill = skills.skills.find((entry) => entry.frontmatter.name === name);
191
198
  if (!skill) throw new Error("Unknown ADS skill name.");
@@ -214,7 +221,24 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
214
221
  }
215
222
  return json({ widget: { resourceUri: TOKENS_APP_URI }, category: null, tokenCount: inspectTokens(root).tokens.length, dataUri: `${PROJECT_URI}/tokens`, showcaseUrl: getShowcaseUrl() });
216
223
  } },
217
- { name: "ads_units", description: "Discover editable ADS units and their files: components/patterns with colocated previews; composition; tokens; foundation shared files; documentation; assets/brand, assets/fonts and asset groups; configuration with ds.config.json, package/build/lint configuration and registered project guard/compiler sources. List summaries with query, or pass an exact unit ID for its manifest. Manifests include companionFiles with permitted shared-entry or owner-only style-boundary bookkeeping; preserve peer permissions and unrelated configuration. Imported peers stay separate. New owners follow the existing colocation convention.", schema: query.extend({ unit: z.string().min(1).max(256).optional() }), call: ({ unit, query: search = "", limit }) => {
224
+ { name: "ads_behaviors", description: "Discover owner-local behavioral guarantees for public hooks/APIs or private controllers, their scenario demonstrations, registered checks/profiles and fresh or stale execution evidence. Optional exercises links behavioral APIs to scenarios; covers remains rendered UI. Metadata and reachable calls do not prove a guarantee passed. Use ads_verify for selected registered checks.", schema: query.extend({ unit: z.string().max(256).optional(), api: z.string().max(200).optional() }), call: ({ query: search = "", limit, unit, api }) => {
225
+ const inventory = behaviorStatus(options);
226
+ const matches = inventory.contracts.filter(item => (!unit || item.unit === unit) && (!api || Object.values(item.subjects).some(subject => subject.api === api)) && JSON.stringify(item).toLowerCase().includes(search.toLowerCase()));
227
+ return json({ total: matches.length, contracts: matches.slice(0, limit).map(item => {
228
+ if (unit) return item;
229
+ const apis = Object.values(item.subjects).filter(subject => subject.api).map(subject => subject.api);
230
+ const demonstrations = [...new Set(Object.values(item.demonstrations).flat())];
231
+ return { unit: item.unit, file: item.file, apis: apis.slice(0, 20), apiCount: apis.length,
232
+ guarantees: item.guarantees.slice(0, 5).map(guarantee => ({ id: guarantee.id, result: guarantee.result })), guaranteeCount: item.guarantees.length,
233
+ demonstrations: demonstrations.slice(0, 10), demonstrationCount: demonstrations.length, checkCount: Object.keys(item.checks).length };
234
+ }), diagnostics: inventory.diagnostics.slice(0, 50), resourceUri: BEHAVIOR_URI });
235
+ } },
236
+ { name: "ads_verify", description: "Run one owner's registered development behavior checks using its configured runner/profile. Executes trusted project commands with bounded output, cancellation and process-tree cleanup; installs nothing. Browser profiles use an isolated managed showcase/MCP App. React StrictMode requires a project development harness, not a production widget. Missing/skipped cases, stale inputs and process exit alone cannot produce passing evidence. Discover exact unit/check IDs with ads_behaviors first.", schema: z.object({ unit: z.string().min(1).max(256), checks: z.array(z.string().min(1).max(100)).min(1).max(100).optional() }).strict(), annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, call: async (args, signal) => {
237
+ const result = await verification.run(args, signal);
238
+ await refresh();
239
+ return { ...json(result), ...(!result.ok ? { isError: true } : {}) };
240
+ } },
241
+ { name: "ads_units", description: "Discover editable ADS units and their files: components/patterns with colocated previews; shared controllers; composition; tokens; foundation shared files; documentation; assets/brand, assets/fonts and asset groups; configuration with ds.config.json, package/build/lint configuration and registered project guard/compiler sources. List summaries with query, or pass an exact unit ID for its manifest. Manifests include companionFiles with permitted shared-entry or owner-only style-boundary bookkeeping; preserve peer permissions and unrelated configuration. Imported peers stay separate. New owners follow the existing colocation convention.", schema: query.extend({ unit: z.string().min(1).max(256).optional() }), call: ({ unit, query: search = "", limit }) => {
218
242
  const matches = edits.units().filter(item => unit ? item.id === unit : item.id.toLowerCase().includes(search.toLowerCase()));
219
243
  return json({ total: matches.length, units: matches.slice(0, limit).map(item => ({ id: item.id, kind: item.kind, fileCount: item.files.length, ...(unit ? { files: item.files.slice(0, 200), filesTruncated: item.files.length > 200, ...(item.companionFiles ? { companionFiles: item.companionFiles } : {}) } : {}) })) });
220
244
  } },
@@ -295,7 +319,7 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
295
319
  try { const stat = statSync(file); return `${file}:${stat.mtimeMs}:${stat.size}`; } catch { return `${file}:missing`; }
296
320
  }).join("\n");
297
321
  }
298
- const fingerprint = () => fileFingerprint([...sourceFiles(options.dsRoot), ...getDocuments().map((doc) => doc.file), path.join(root, "ds.config.json"), path.join(root, "package.json")]);
322
+ const fingerprint = () => fileFingerprint([...sourceFiles(options.dsRoot), ...getDocuments().map((doc) => doc.file), path.join(root, "ds.config.json"), path.join(root, "package.json"), path.join(root, ".ds/behavior-results.json")]);
299
323
  const artifactFingerprint = () => fileFingerprint(allApps().map(app => path.resolve(root, app.file)));
300
324
  const assetFingerprint = () => resourceAssets().map(asset => asset.uri).sort().join("\n");
301
325
  let previous = fingerprint();
@@ -330,7 +354,7 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
330
354
  previousAssets = assetFingerprint();
331
355
  previousBuild = JSON.stringify(builds());
332
356
  const targets = filesChanged ? era === "modern" ? resources().map((entry) => entry.uri).filter((uri) => !skills.files.has(uri)) : [...subscribed]
333
- : [BUILD_URI, `${PROJECT_URI}/status`, ...(options.config.mcp.previewManifest ? [PREVIEW_MANIFEST_URI] : []), ...(artifactsChanged ? allApps().map(app => appUri(app.id)) : [])].filter((uri) => era === "modern" || subscribed.has(uri));
357
+ : [BUILD_URI, BEHAVIOR_URI, `${PROJECT_URI}/status`, ...(options.config.mcp.previewManifest ? [PREVIEW_MANIFEST_URI] : []), ...(artifactsChanged ? allApps().map(app => appUri(app.id)) : [])].filter((uri) => era === "modern" || subscribed.has(uri));
334
358
  for (const uri of targets) { if (notifications) notifications.resourceUpdated(uri); else await server.sendResourceUpdated({ uri }); }
335
359
  return true;
336
360
  }
@@ -338,7 +362,7 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
338
362
  timer?.unref();
339
363
  server.oninitialized = () => previewWatch?.start();
340
364
  server.onclose = () => { if (timer) clearInterval(timer); };
341
- return { server, refresh, async close() { if (timer) clearInterval(timer); await server.close(); } };
365
+ return { server, refresh, async close() { if (timer) clearInterval(timer); if (!inspection) await verification.close(); await server.close(); } };
342
366
  }
343
367
 
344
368
  export async function startAdsMcp(directory = process.cwd(), { stdio = true, showcase = true, port = 0, mutations = false } = {}) {
@@ -353,24 +377,37 @@ export async function startAdsMcp(directory = process.cwd(), { stdio = true, sho
353
377
  { port, observe: notifications => createAdsMcp(directory, { era: "modern", previewWatch, showcaseUrl, inspection, mutations, notifications }) });
354
378
  process.stderr.write(`ADS showcase: ${host.url}\n`);
355
379
  }
356
- const handle = stdio ? serveStdio(({ era }) => createAdsMcp(directory, { era, previewWatch, showcaseUrl, inspection, mutations }).server, { legacy: "serve", onerror: (error) => process.stderr.write(`ads mcp: ${error.message}\n`) }) : null;
380
+ const transport = stdio ? new StdioServerTransport() : null;
381
+ const handle = stdio ? serveStdio(({ era }) => createAdsMcp(directory, { era, previewWatch, showcaseUrl, inspection, mutations }).server, { transport, legacy: "serve", onerror: (error) => process.stderr.write(`ads mcp: ${error.message}\n`) }) : null;
357
382
  let closing;
358
383
  function close() {
359
384
  return closing ??= (async () => {
360
385
  inspection.edits?.cancel();
361
- await host?.close();
362
- await previewWatch.stop();
363
- await handle?.close();
364
- process.stdin.removeListener("end", disconnect);
386
+ // Stop every owned resource even if another close fails or has in-flight RPCs.
387
+ const results = await Promise.allSettled([
388
+ Promise.resolve().then(() => host?.close()),
389
+ previewWatch.stop(),
390
+ Promise.resolve().then(() => inspection.verification?.close()),
391
+ Promise.resolve().then(() => handle?.close()),
392
+ ]);
393
+ for (const event of ["end", "close", "error"]) process.stdin.removeListener(event, disconnect);
365
394
  for (const signal of ["SIGINT", "SIGTERM"]) process.removeListener(signal, disconnect);
366
395
  process.removeListener("exit", forceCleanup);
396
+ const errors = results.filter(result => result.status === "rejected").map(result => result.reason);
397
+ if (errors.length) throw new AggregateError(errors, "ADS shutdown failed");
367
398
  })();
368
399
  }
369
- function disconnect() { void close(); }
400
+ function disconnect() { void close().catch(error => { process.stderr.write(`ads mcp: ${error.message}\n`); process.exitCode = 1; }); }
370
401
  function forceCleanup() { previewWatch.kill(); }
371
402
  // Probe instances have no managed child; a real connection activates it.
372
- // stdin EOF also cleans up the compiler when the host drops the transport.
373
- if (stdio) process.stdin.once("end", disconnect);
403
+ // The SDK also closes its wire on stdout failure. Closing a probe instance
404
+ // must not shut the runtime down, so observe the wire, not server.onclose.
405
+ if (transport) {
406
+ const wireClosed = transport.onclose;
407
+ transport.onclose = () => { try { wireClosed?.(); } finally { disconnect(); } };
408
+ for (const event of ["end", "close", "error"]) process.stdin.once(event, disconnect);
409
+ if (process.stdin.readableEnded || process.stdin.destroyed) disconnect();
410
+ }
374
411
  for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, disconnect);
375
412
  process.once("exit", forceCleanup);
376
413
  return { close, url: host?.url };
@@ -56,13 +56,17 @@ export async function startShowcaseHost(factory, { port = 0, observe } = {}) {
56
56
  let closing;
57
57
  return { url: `http://127.0.0.1:${web.address().port}/`, close() {
58
58
  return closing ??= (async () => {
59
- for (const request of requests) request.abort();
60
- await observer?.close();
61
- await handler.close();
62
- await sandbox?.close();
59
+ // Close sockets first: live subscriptions and preview requests must not
60
+ // hold shutdown open while protocol instances wait for cancellation.
63
61
  const stopped = new Promise(resolve => web.close(resolve));
62
+ for (const request of requests) request.abort();
64
63
  web.closeAllConnections();
65
- await stopped;
64
+ const results = await Promise.allSettled([stopped,
65
+ Promise.resolve().then(() => observer?.close()),
66
+ handler.close(), sandbox?.close(),
67
+ ]);
68
+ const errors = results.filter(result => result.status === "rejected").map(result => result.reason);
69
+ if (errors.length) throw new AggregateError(errors, "Showcase shutdown failed");
66
70
  })();
67
71
  } };
68
72
  }
@@ -0,0 +1,39 @@
1
+ import path from "node:path";
2
+ import ts from "typescript";
3
+ import { isDevelopmentFile, isInside } from "./config.mjs";
4
+ import { uiOperation } from "./ui-framework.mjs";
5
+
6
+ export function publicApi(program, options) {
7
+ const checker = program.getTypeChecker();
8
+ const entry = program.getSourceFile(path.join(options.dsRoot, options.config.publicEntry));
9
+ const module = entry && checker.getSymbolAtLocation(entry);
10
+ const items = [];
11
+ for (const exported of module ? checker.getExportsOfModule(module) : []) {
12
+ let symbol = exported;
13
+ if (symbol.flags & ts.SymbolFlags.Alias) symbol = checker.getAliasedSymbol(symbol);
14
+ const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0];
15
+ if (!declaration || !(symbol.flags & ts.SymbolFlags.Value)) continue;
16
+ const file = declaration.getSourceFile().fileName;
17
+ if (isDevelopmentFile(file, options)) continue;
18
+ const kind = isInside(path.join(options.dsRoot, "components"), file) ? "components" : isInside(path.join(options.dsRoot, "patterns"), file) ? "patterns" : isInside(path.join(options.dsRoot, "controllers"), file) ? "controllers" : null;
19
+ if (!kind) continue;
20
+ let renderable = false;
21
+ function visit(node) {
22
+ if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node)) renderable = true;
23
+ if (ts.isCallExpression(node) && ["createElement", "forwardRef", "memo"].includes(uiOperation(node.expression, checker))) renderable = true;
24
+ if (ts.isTaggedTemplateExpression(node)) {
25
+ let tag = node.tag;
26
+ if (ts.isCallExpression(tag)) tag = tag.expression;
27
+ let base = tag;
28
+ while (ts.isPropertyAccessExpression(base)) base = base.expression;
29
+ const symbol = checker.getSymbolAtLocation(base);
30
+ if (symbol?.declarations?.some(item => ts.isImportSpecifier(item) && (item.propertyName ?? item.name).text === "styled" && item.parent.parent.parent.moduleSpecifier.text.startsWith("@linaria/") || ts.isNamespaceImport(item) && item.parent.parent.moduleSpecifier.text.startsWith("@linaria/") && /^\.styled(?:\.|$)/.test(tag.getText().slice(base.getText().length)))) renderable = true;
31
+ }
32
+ ts.forEachChild(node, visit);
33
+ }
34
+ visit(declaration);
35
+ const type = checker.getTypeOfSymbolAtLocation(symbol, declaration);
36
+ items.push({ name: exported.name, kind, file, declaration, renderable: kind !== "controllers" && renderable, behavioral: !!(type.getCallSignatures().length || type.getConstructSignatures().length) });
37
+ }
38
+ return items;
39
+ }
@@ -126,6 +126,16 @@ export async function runScenarioCompiler(directory, { watched = false, validate
126
126
  cache.validate(stamp);
127
127
  });
128
128
  }
129
+ async function checkedBuild(stamp, build) {
130
+ const started = generation;
131
+ let checking = true, compiling = true;
132
+ function progress() { if (started === generation) reporter.progress(checking && compiling ? "validation-and-compilation" : checking ? "validation" : compiling ? "compilation" : "publication"); }
133
+ progress();
134
+ return Promise.allSettled([
135
+ gate(stamp).finally(() => { checking = false; progress(); }),
136
+ Promise.resolve().then(build).finally(() => { compiling = false; progress(); }),
137
+ ]);
138
+ }
129
139
  async function compile(entry) {
130
140
  if (buildGraph) return compileGraph();
131
141
  const revision = reporter.begin(entry.id);
@@ -154,16 +164,18 @@ export async function runScenarioCompiler(directory, { watched = false, validate
154
164
  if (path.isAbsolute(file) && existsSync(file)) dependencies.add(file);
155
165
  }
156
166
  } };
157
- const outcomes = await Promise.allSettled([gate(stamp), reuse ? Promise.resolve() : build(entry, options, track)]);
167
+ const outcomes = await checkedBuild(stamp, () => reuse ? undefined : build(entry, options, track));
158
168
  const failure = outcomes.find(outcome => outcome.status === "rejected");
159
169
  if (failure) throw failure.reason;
160
170
  cache.assertCompiler();
161
171
  if (stopped || started !== generation || stamp !== cache.inputs()) return; // Watch queued a newer revision.
162
172
  if (!reuse) cache.record(entry, dependencies);
163
173
  reporter.ready(entry.id, revision);
174
+ reporter.progress("idle");
164
175
  } catch (error) {
165
176
  if (started !== generation) return;
166
177
  reporter.error(entry.id, revision, error);
178
+ reporter.progress("idle");
167
179
  console.error(error);
168
180
  if (!watched) process.exitCode = 1;
169
181
  }
@@ -192,7 +204,7 @@ export async function runScenarioCompiler(directory, { watched = false, validate
192
204
  cache.assertCompiler();
193
205
  let reusable = entries.every(entry => cache.reusable(entry));
194
206
  if (reusable) { try { reusable = Boolean(readPreviewManifest(options, { verify: true })); } catch { reusable = false; } }
195
- const outcomes = await Promise.allSettled([gate(stamp), reusable ? Promise.resolve(null) : buildGraph(entries, options, track)]);
207
+ const outcomes = await checkedBuild(stamp, () => reusable ? null : buildGraph(entries, options, track));
196
208
  const failure = outcomes.find(outcome => outcome.status === "rejected");
197
209
  if (failure) throw failure.reason;
198
210
  cache.assertCompiler();
@@ -209,9 +221,11 @@ export async function runScenarioCompiler(directory, { watched = false, validate
209
221
  }
210
222
  for (const entry of entries) { pending.delete(entry.id); reporter.ready(entry.id, revisions.get(entry.id)); }
211
223
  publishedStamp = stamp;
224
+ reporter.progress("idle");
212
225
  } catch (error) {
213
226
  if (started !== generation) return;
214
227
  for (const entry of entries) { pending.delete(entry.id); reporter.error(entry.id, revisions.get(entry.id), error); }
228
+ reporter.progress("idle");
215
229
  console.error(error);
216
230
  if (!watched) process.exitCode = 1;
217
231
  }
@@ -261,6 +275,7 @@ export async function runScenarioCompiler(directory, { watched = false, validate
261
275
  clearTimeout(timer);
262
276
  timer = setTimeout(() => {
263
277
  ++generation;
278
+ reporter.progress("queued");
264
279
  validation = undefined;
265
280
  const changes = [...changed]; changed.clear();
266
281
  try {