@reqvire-org/ads 0.0.0-stage → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +284 -2
  3. package/dist/assets.mjs +52 -0
  4. package/dist/bootstrap-ds/ADS.md +203 -0
  5. package/dist/bootstrap-ds/README.md +98 -0
  6. package/dist/bootstrap-ds/biome.css.json +21 -0
  7. package/dist/bootstrap-ds/design-system/assets/brand/README.md +28 -0
  8. package/dist/bootstrap-ds/design-system/assets/fonts/.gitkeep +0 -0
  9. package/dist/bootstrap-ds/design-system/components/.gitkeep +0 -0
  10. package/dist/bootstrap-ds/design-system/composition/.gitkeep +0 -0
  11. package/dist/bootstrap-ds/design-system/index.ts +5 -0
  12. package/dist/bootstrap-ds/design-system/patterns/.gitkeep +0 -0
  13. package/dist/bootstrap-ds/design-system/showcase/mcp/index.html +1 -0
  14. package/dist/bootstrap-ds/design-system/showcase/mcp/main.tsx +8 -0
  15. package/dist/bootstrap-ds/design-system/styles.css +5 -0
  16. package/dist/bootstrap-ds/design-system/tokens/Foundation.fixture.ts +47 -0
  17. package/dist/bootstrap-ds/design-system/tokens/Foundation.scenario.tsx +14 -0
  18. package/dist/bootstrap-ds/design-system/tokens/base.css +27 -0
  19. package/dist/bootstrap-ds/design-system/tokens/colors.css +29 -0
  20. package/dist/bootstrap-ds/design-system/tokens/elevation.css +15 -0
  21. package/dist/bootstrap-ds/design-system/tokens/spacing.css +15 -0
  22. package/dist/bootstrap-ds/design-system/tokens/typography.css +13 -0
  23. package/dist/bootstrap-ds/ds.config.json +13 -0
  24. package/dist/bootstrap-ds/gitignore.template +8 -0
  25. package/dist/bootstrap-ds/package-lock.json +4660 -0
  26. package/dist/bootstrap-ds/package.json +41 -0
  27. package/dist/bootstrap-ds/scripts/build-mcp-previews.mjs +33 -0
  28. package/dist/bootstrap-ds/src/vite-env.d.ts +7 -0
  29. package/dist/bootstrap-ds/tsconfig.build.json +12 -0
  30. package/dist/bootstrap-ds/tsconfig.json +20 -0
  31. package/dist/bootstrap-ds/vite.config.ts +21 -0
  32. package/dist/boundaries.mjs +130 -0
  33. package/dist/brand-assets.mjs +82 -0
  34. package/dist/check.mjs +222 -0
  35. package/dist/cli.mjs +106 -0
  36. package/dist/config.mjs +181 -0
  37. package/dist/contracts/tokens.json +52 -0
  38. package/dist/html.mjs +30 -0
  39. package/dist/index.mjs +9 -0
  40. package/dist/init.mjs +194 -0
  41. package/dist/lint-config.mjs +40 -0
  42. package/dist/mcp/THIRD_PARTY_NOTICES.md +786 -0
  43. package/dist/mcp/apps.mjs +53 -0
  44. package/dist/mcp/build-status.mjs +164 -0
  45. package/dist/mcp/catalog.mjs +65 -0
  46. package/dist/mcp/context.mjs +69 -0
  47. package/dist/mcp/mutations.mjs +301 -0
  48. package/dist/mcp/patch.mjs +114 -0
  49. package/dist/mcp/preview-watch.mjs +169 -0
  50. package/dist/mcp/preview-widget.bundle.js +113 -0
  51. package/dist/mcp/project-widget.bundle.js +113 -0
  52. package/dist/mcp/prompts.mjs +28 -0
  53. package/dist/mcp/resource-loader.js +138 -0
  54. package/dist/mcp/sandbox-host.mjs +22 -0
  55. package/dist/mcp/server.mjs +377 -0
  56. package/dist/mcp/showcase-host.mjs +68 -0
  57. package/dist/mcp/skills.mjs +77 -0
  58. package/dist/mcp/widget.bundle.js +150 -0
  59. package/dist/oxlint.mjs +81 -0
  60. package/dist/preview-resources.mjs +198 -0
  61. package/dist/project-documents.mjs +27 -0
  62. package/dist/public-props.mjs +74 -0
  63. package/dist/scenarios-compiler.mjs +287 -0
  64. package/dist/scenarios-vite.d.mts +3 -0
  65. package/dist/scenarios-vite.mjs +71 -0
  66. package/dist/scenarios.mjs +212 -0
  67. package/dist/showcase/client.bundle.js +127 -0
  68. package/dist/showcase/index.html +17 -0
  69. package/dist/showcase/sandbox.js +30 -0
  70. package/dist/showcase/style.css +7 -0
  71. package/dist/showcase-coverage.mjs +118 -0
  72. package/dist/showcase.mjs +77 -0
  73. package/dist/skills/README.md +68 -0
  74. package/dist/skills/ads-assimilate/SKILL.md +292 -0
  75. package/dist/skills/ads-assimilate/agents/openai.yaml +4 -0
  76. package/dist/skills/ads-assimilate/references/cli.md +111 -0
  77. package/dist/skills/ads-assimilate/references/lint-and-docs.md +87 -0
  78. package/dist/skills/ads-assimilate/references/native-implementation.md +98 -0
  79. package/dist/skills/ads-assimilate/references/testing.md +107 -0
  80. package/dist/skills/ads-break-ui/LICENSE.upstream +21 -0
  81. package/dist/skills/ads-break-ui/README.md +16 -0
  82. package/dist/skills/ads-break-ui/SKILL.md +235 -0
  83. package/dist/skills/ads-break-ui/agents/openai.yaml +4 -0
  84. package/dist/skills/ads-break-ui/references/catalog.md +135 -0
  85. package/dist/skills/ads-break-ui/references/verification.md +48 -0
  86. package/dist/skills/ads-designer/LICENSE.upstream +21 -0
  87. package/dist/skills/ads-designer/README.md +19 -0
  88. package/dist/skills/ads-designer/SKILL.md +400 -0
  89. package/dist/skills/ads-designer/agents/openai.yaml +4 -0
  90. package/dist/skills/ads-designer/assets/bootstrap-ds/ADS.md +203 -0
  91. package/dist/skills/ads-designer/assets/bootstrap-ds/README.md +98 -0
  92. package/dist/skills/ads-designer/assets/bootstrap-ds/biome.css.json +21 -0
  93. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/assets/brand/README.md +28 -0
  94. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/assets/fonts/.gitkeep +0 -0
  95. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/components/.gitkeep +0 -0
  96. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/composition/.gitkeep +0 -0
  97. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/index.ts +5 -0
  98. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/patterns/.gitkeep +0 -0
  99. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/showcase/mcp/index.html +1 -0
  100. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/showcase/mcp/main.tsx +8 -0
  101. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/styles.css +5 -0
  102. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/Foundation.fixture.ts +47 -0
  103. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/Foundation.scenario.tsx +14 -0
  104. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/base.css +27 -0
  105. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/colors.css +29 -0
  106. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/elevation.css +15 -0
  107. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/spacing.css +15 -0
  108. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/typography.css +13 -0
  109. package/dist/skills/ads-designer/assets/bootstrap-ds/ds.config.json +13 -0
  110. package/dist/skills/ads-designer/assets/bootstrap-ds/gitignore.template +8 -0
  111. package/dist/skills/ads-designer/assets/bootstrap-ds/package-lock.json +4660 -0
  112. package/dist/skills/ads-designer/assets/bootstrap-ds/package.json +41 -0
  113. package/dist/skills/ads-designer/assets/bootstrap-ds/scripts/build-mcp-previews.mjs +33 -0
  114. package/dist/skills/ads-designer/assets/bootstrap-ds/src/vite-env.d.ts +7 -0
  115. package/dist/skills/ads-designer/assets/bootstrap-ds/tsconfig.build.json +12 -0
  116. package/dist/skills/ads-designer/assets/bootstrap-ds/tsconfig.json +20 -0
  117. package/dist/skills/ads-designer/assets/bootstrap-ds/vite.config.ts +21 -0
  118. package/dist/skills/ads-designer/references/cli.md +111 -0
  119. package/dist/skills/ads-designer/references/design-craft.md +65 -0
  120. package/dist/skills/ads-designer/references/mobile.md +73 -0
  121. package/dist/skills/ads-import/SKILL.md +335 -0
  122. package/dist/skills/ads-import/agents/openai.yaml +4 -0
  123. package/dist/skills/ads-import/references/branding.md +44 -0
  124. package/dist/skills/ads-import/references/cli.md +111 -0
  125. package/dist/skills/ads-import/references/implementation.md +111 -0
  126. package/dist/skills/ads-import/references/tokens.md +96 -0
  127. package/dist/skills/ads-motion/LICENSE.upstream +21 -0
  128. package/dist/skills/ads-motion/README.md +22 -0
  129. package/dist/skills/ads-motion/SKILL.md +217 -0
  130. package/dist/skills/ads-motion/agents/openai.yaml +4 -0
  131. package/dist/skills/ads-motion/references/audit.md +44 -0
  132. package/dist/skills/ads-motion/references/build.md +81 -0
  133. package/dist/skills/ads-motion/references/performance.md +55 -0
  134. package/dist/skills/ads-motion/references/review.md +35 -0
  135. package/dist/skills/ads-motion/references/verification.md +39 -0
  136. package/dist/token-policy.mjs +10 -0
  137. package/dist/tokens.mjs +142 -0
  138. package/dist/ui-framework.mjs +59 -0
  139. package/dist/units.mjs +168 -0
  140. package/docs/ADS.md +310 -0
  141. package/docs/ARCHITECTURE.md +311 -0
  142. package/docs/CHANGELOG.md +106 -0
  143. package/docs/GUIDE.md +722 -0
  144. package/docs/MCP.md +690 -0
  145. package/package.json +107 -4
@@ -0,0 +1,28 @@
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.
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. 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
+ 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
+ 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. Do not create a widget-only replica or override component internals.
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
+ 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
+ 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.`;
10
+
11
+ export function promptDefinitions(skills) {
12
+ const prompts = [
13
+ { name: "ads-design", description: "Turn a UI request into an ADS-owned implementation and review workflow.", arguments: [{ name: "request", description: "What to design or change", required: true }] },
14
+ { name: "ads-inspect", description: "Find public ADS elements, inspect their contract, and select a real preview.", arguments: [{ name: "element", description: "Component, pattern, token or question", required: true }] },
15
+ { name: "ads-show", description: "Choose project-built token, component, pattern or composition Apps with a text fallback.", arguments: [{ name: "target", description: "What the human wants to inspect", required: true }] },
16
+ ];
17
+ for (const skill of skills) prompts.push({ name: skill.frontmatter.name, description: skill.frontmatter.description, arguments: [{ name: "request", description: "Task and any source-library URL or ADS path", required: true }] });
18
+ return prompts;
19
+ }
20
+
21
+ export function getWorkflowPrompt(name, args, skills) {
22
+ const definition = promptDefinitions(skills).find((prompt) => prompt.name === name);
23
+ if (!definition) throw new Error("Unknown ADS prompt.");
24
+ const argument = definition.arguments[0].name;
25
+ if (!args || Object.keys(args).some((key) => key !== argument) || typeof args[argument] !== "string" || !args[argument].trim() || args[argument].length > 8000) throw new Error(`Prompt requires ${argument}, a non-empty string of at most 8000 characters.`);
26
+ const skill = skills.find((entry) => entry.frontmatter.name === name);
27
+ return { description: definition.description, messages: [{ role: "user", content: { type: "text", text: `${reasoning}\n\n${skill ? `Use ${skill.frontmatter.name}; first read ${skill.uri} and its relevant supporting references.\n\n` : ""}${argument}: ${args[argument]}` } }] };
28
+ }
@@ -0,0 +1,138 @@
1
+ /** Native ES modules keep framework semantics, cycles, and live bindings intact. */
2
+ export class PreviewResourceLoader {
3
+ constructor(read) {
4
+ this.read = read;
5
+ this.records = new Map();
6
+ this.bytes = new Map();
7
+ this.urls = new Map();
8
+ this.styles = new Map();
9
+ this.styleReady = new Map();
10
+ this.revisions = new Set();
11
+ this.importing = Promise.resolve();
12
+ this.generation = 0;
13
+ this.closed = false;
14
+ globalThis.__dsPreviewImport = uri => this.importModule(uri);
15
+ }
16
+ register(manifest) {
17
+ if (manifest.schemaVersion !== 1 || !Array.isArray(manifest.scenarios) || !manifest.assets || typeof manifest.revision !== "string") throw new Error("Unsupported preview manifest. Rebuild previews with the installed ADS version.");
18
+ this.revisions.add(manifest.revision);
19
+ if (this.revisions.size > 20) throw Object.assign(new Error("Reopen the preview to release modules retained across 20 build revisions."), { code: "ADS_PREVIEW_REOPEN" });
20
+ for (const [uri, record] of Object.entries(manifest.assets)) {
21
+ if (!/^ads:\/\/assets\/[a-f0-9]{64}\.[a-z0-9]+$/.test(uri) || !/^sha256:[a-f0-9]{64}$/.test(record.digest)
22
+ || !["imports", "dynamicImports", "styles", "references"].every(key => Array.isArray(record[key]) && record[key].every(ref => Object.hasOwn(manifest.assets, ref)))) throw new Error("Invalid preview dependency graph.");
23
+ if (this.records.has(uri) && this.records.get(uri).digest !== record.digest) throw new Error("An immutable preview resource changed its content.");
24
+ this.records.set(uri, record);
25
+ }
26
+ }
27
+ async resource(uri) {
28
+ if (!this.bytes.has(uri)) {
29
+ const task = (async () => {
30
+ const record = this.records.get(uri);
31
+ if (!record) throw new Error(`Unknown preview dependency: ${uri}`);
32
+ const response = await this.read(uri);
33
+ if (this.closed) throw new Error("The preview App was closed.");
34
+ const content = response.contents.find(item => item.uri === uri);
35
+ if (!content) throw new Error(`Missing preview dependency: ${uri}`);
36
+ const bytes = typeof content.text === "string" ? new TextEncoder().encode(content.text) : Uint8Array.from(atob(content.blob), letter => letter.charCodeAt(0));
37
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
38
+ const actual = "sha256:" + [...new Uint8Array(digest)].map(byte => byte.toString(16).padStart(2, "0")).join("");
39
+ if (bytes.length !== record.size || actual !== record.digest) throw new Error("Preview resource integrity check failed. Rebuild and reopen the preview.");
40
+ return bytes;
41
+ })();
42
+ this.bytes.set(uri, task);
43
+ task.catch(() => this.bytes.delete(uri));
44
+ }
45
+ return this.bytes.get(uri);
46
+ }
47
+ closure(uri) {
48
+ const ordered = [];
49
+ const seen = new Set();
50
+ const visit = id => {
51
+ if (seen.has(id)) return;
52
+ seen.add(id);
53
+ const record = this.records.get(id);
54
+ if (!record) throw new Error(`Missing preview dependency metadata: ${id}`);
55
+ for (const dependency of [...record.imports, ...record.styles, ...record.references]) visit(dependency);
56
+ ordered.push(id);
57
+ };
58
+ visit(uri);
59
+ return ordered;
60
+ }
61
+ async materialize(uri) {
62
+ const ordered = this.closure(uri);
63
+ await Promise.all(ordered.map(id => this.resource(id)));
64
+ const imports = {};
65
+ for (const id of ordered) {
66
+ if (this.closed) throw new Error("The preview App was closed.");
67
+ if (this.urls.has(id)) continue;
68
+ const record = this.records.get(id);
69
+ let bytes = await this.resource(id);
70
+ if (record.kind === "module" || record.mimeType === "text/css") {
71
+ let text = new TextDecoder().decode(bytes);
72
+ for (const reference of record.references) {
73
+ const url = this.urls.get(reference);
74
+ if (!url) throw new Error("Unsupported cyclic asset reference.");
75
+ text = text.replaceAll(reference, url);
76
+ }
77
+ bytes = text;
78
+ }
79
+ const url = URL.createObjectURL(new Blob([bytes], { type: record.mimeType }));
80
+ this.urls.set(id, url);
81
+ if (record.kind === "module") imports[id] = url;
82
+ }
83
+ if (Object.keys(imports).length) {
84
+ const script = document.createElement("script");
85
+ script.type = "importmap";
86
+ script.textContent = JSON.stringify({ imports });
87
+ document.head.append(script);
88
+ }
89
+ return ordered;
90
+ }
91
+ async importModule(uri) {
92
+ const prepare = this.importing.then(() => this.materialize(uri));
93
+ this.importing = prepare.catch(() => {});
94
+ const dependencies = await prepare;
95
+ for (const id of dependencies) {
96
+ const record = this.records.get(id);
97
+ if (record.mimeType !== "text/css") continue;
98
+ if (this.styles.has(id)) { await this.styleReady.get(id); continue; }
99
+ const style = document.createElement("link");
100
+ style.rel = "stylesheet";
101
+ style.href = this.urls.get(id);
102
+ this.styles.set(id, style);
103
+ const ready = new Promise((resolve, reject) => { style.onload = resolve; style.onerror = () => reject(new Error("The host CSP blocked a preview stylesheet.")); });
104
+ this.styleReady.set(id, ready);
105
+ document.head.append(style);
106
+ try { await ready; } catch (error) { style.remove(); this.styles.delete(id); this.styleReady.delete(id); throw error; }
107
+ }
108
+ return import(/* @vite-ignore */ uri);
109
+ }
110
+ async mount(manifest, id, container) {
111
+ const generation = ++this.generation;
112
+ this.register(manifest);
113
+ const scenario = manifest.scenarios.find(entry => entry.id === id);
114
+ if (!scenario) throw new Error("The selected scenario is no longer registered.");
115
+ const module = await this.importModule(scenario.module);
116
+ if (generation !== this.generation) return false;
117
+ await this.unmount?.();
118
+ if (generation !== this.generation || this.closed) return false;
119
+ container.replaceChildren();
120
+ const keep = new Set(this.closure(scenario.module));
121
+ for (const [uri, style] of this.styles) if (!keep.has(uri)) { style.remove(); this.styles.delete(uri); this.styleReady.delete(uri); }
122
+ for (const uri of this.closure(scenario.module)) if (this.styles.has(uri)) document.head.append(this.styles.get(uri));
123
+ if (typeof module.mount !== "function") throw new Error("The compiled scenario has no mount adapter.");
124
+ this.unmount = module.mount(container);
125
+ if (typeof this.unmount !== "function") throw new Error("The scenario mount adapter must return an unmount callback.");
126
+ return true;
127
+ }
128
+ async dispose() {
129
+ if (this.closed) return;
130
+ this.closed = true;
131
+ ++this.generation;
132
+ await this.unmount?.();
133
+ for (const style of this.styles.values()) style.remove();
134
+ for (const url of this.urls.values()) URL.revokeObjectURL(url);
135
+ this.styles.clear(); this.styleReady.clear(); this.urls.clear(); this.bytes.clear(); this.records.clear();
136
+ delete globalThis.__dsPreviewImport;
137
+ }
138
+ }
@@ -0,0 +1,22 @@
1
+ import { createServer } from "node:http";
2
+ import { readFile } from "node:fs/promises";
3
+
4
+ /** A different loopback port provides the browser host's separate sandbox origin. */
5
+ export async function startSandboxHost(parentOrigin) {
6
+ const script = await readFile(new URL("../showcase/sandbox.js", import.meta.url));
7
+ const web = createServer((req, res) => {
8
+ const origin = `http://127.0.0.1:${web.address().port}`;
9
+ const url = new URL(req.url, origin);
10
+ if (req.headers.host !== new URL(origin).host || req.method !== "GET") { res.writeHead(403); res.end(); return; }
11
+ const javascript = url.pathname === "/sandbox.js";
12
+ if (!javascript && (url.pathname !== "/sandbox.html" || url.searchParams.get("parent") !== parentOrigin)) { res.writeHead(404); res.end(); return; }
13
+ res.writeHead(200, { "Content-Type": javascript ? "text/javascript" : "text/html", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff", "Content-Security-Policy": `frame-ancestors ${parentOrigin};object-src 'none'` });
14
+ res.end(javascript ? script : '<!doctype html><html><head><meta charset="utf-8"><style>html,body,iframe{height:100%;width:100%;margin:0;border:0;display:block;overflow:hidden}</style></head><body><script src="/sandbox.js"></script></body></html>');
15
+ });
16
+ await new Promise((resolve, reject) => { web.once("error", reject); web.listen(0, "127.0.0.1", resolve); });
17
+ return { url: `http://127.0.0.1:${web.address().port}/sandbox.html?parent=${encodeURIComponent(parentOrigin)}`, close() {
18
+ const stopped = new Promise(resolve => web.close(resolve));
19
+ web.closeAllConnections();
20
+ return stopped;
21
+ } };
22
+ }
@@ -0,0 +1,377 @@
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { Server, ProtocolError, INVALID_PARAMS, ResourceNotFoundError } from "@modelcontextprotocol/server";
4
+ import { serveStdio } from "@modelcontextprotocol/server/stdio";
5
+ import { z } from "zod";
6
+ import { checkProject } from "../check.mjs";
7
+ import { loadConfig, sourceFiles } from "../config.mjs";
8
+ import { inspectTokens } from "../tokens.mjs";
9
+ import { inspectScenarios } from "../scenarios.mjs";
10
+ import { startShowcaseHost } from "./showcase-host.mjs";
11
+ import { createSkillStore, page, SKILLS_EXTENSION } from "./skills.mjs";
12
+ import { inspectCatalog, projectDocuments, PROJECT_URI, readProjectDocument } from "./catalog.mjs";
13
+ import { APP_MIME, appMetadata, appUri, projectApp, TOKENS_APP_URI, tokensApp, tokenWidgetData, SCENARIO_APP_URI, scenarioApp, previewApp, PREVIEW_APP_URI } from "./apps.mjs";
14
+ import { PREVIEW_MANIFEST_URI, readPreviewManifest, readPreviewAsset } from "../preview-resources.mjs";
15
+ import { getWorkflowPrompt, promptDefinitions } from "./prompts.mjs";
16
+ import { createReconfigurablePreviewWatch } from "./preview-watch.mjs";
17
+ import { BUILD_URI, buildFailure, inspectBuildStatus, summarizeBuild } from "./build-status.mjs";
18
+ import { GUIDE_URI, projectContext, projectGuide, serverInstructions, workflow } from "./context.mjs";
19
+ import { createMutationController, mutationLimits } from "./mutations.mjs";
20
+
21
+ const pkg = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
22
+ class InvalidParamsError extends ProtocolError {
23
+ constructor(message) { super(INVALID_PARAMS, message); }
24
+ }
25
+ const json = (value) => ({ content: [{ type: "text", text: JSON.stringify(value) }], structuredContent: value });
26
+ const empty = z.object({}).strict();
27
+ const query = z.object({ query: z.string().max(200).optional(), limit: z.number().int().min(1).max(50).default(20) }).strict();
28
+ const annotations = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
29
+ const schemas = { params: z.object({ cursor: z.string().optional() }).passthrough(), result: z.object({ resultType: z.literal("complete") }).passthrough() };
30
+
31
+ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollInterval = 1000, previewWatch, showcaseUrl, notifications, inspection, mutations = false } = {}) {
32
+ const root = path.resolve(directory);
33
+ const cache = inspection ?? {};
34
+ const skills = cache.skills ??= createSkillStore(skillDirectory);
35
+ const edits = cache.edits ??= createMutationController(root);
36
+ let options = loadConfig(root);
37
+ const signature = `${root}:${JSON.stringify(options.config)}`;
38
+ if (cache.signature !== signature) {
39
+ cache.signature = signature;
40
+ cache.registry = cache.catalog = cache.documents = undefined;
41
+ }
42
+ const getShowcaseUrl = () => showcaseUrl?.() ?? options.config.mcp.showcaseUrl;
43
+ const project = () => { const context = projectContext(options); context.showcase.url = getShowcaseUrl(); return context; };
44
+ const subscribed = new Set();
45
+ const server = new Server({ name: "reqvire-ads", version: pkg.version }, {
46
+ instructions: `${serverInstructions} ${mutations ? "Guarded editing is enabled: discover one owner with ads_units, read relevant ranges with ads_read_file and send only a Git unified diff to ads_apply_patch with that unit ID. Include its own scenarios/fixtures and required re-exports together, never unrelated units. The server validates architecture, registered Oxlint and project checks before writing. Fix rejected patches in their owner. Intentional settings, project guards and builders use the configuration unit; mandatory ADS gates still run." : "Source mutation tools are disabled; source edits use the client's editor/filesystem tools."}`,
47
+ capabilities: { tools: { listChanged: true }, resources: { listChanged: true, subscribe: true }, prompts: {},
48
+ extensions: { "io.modelcontextprotocol/ui": { mimeTypes: [APP_MIME] }, ...(era === "modern" ? { [SKILLS_EXTENSION]: { directoryRead: true } } : {}) } },
49
+ });
50
+ const getRegistry = () => cache.registry ??= inspectScenarios(options);
51
+ const scenarioApps = () => getRegistry().scenarios.map(item => ({ id: item.id, title: item.title, kind: item.kind, file: item.artifact }));
52
+ const tokenScenarios = () => getRegistry().scenarios.filter(item => item.kind === "tokens").map(item => ({ id: item.id, title: item.title, count: item.tokens.length }));
53
+ const allApps = () => [...options.config.mcp.apps, ...scenarioApps()];
54
+ const scenarioWidget = () => options.config.mcp.previewManifest ? PREVIEW_APP_URI : SCENARIO_APP_URI;
55
+ const resourceAssets = () => {
56
+ try { return Object.entries(readPreviewManifest(options)?.assets ?? {}).map(([uri, record]) => ({ uri, name: `Preview asset ${uri.slice("ads://assets/".length)}`, mimeType: record.mimeType, size: record.size })); }
57
+ catch { return []; } // Discovery and diagnostics remain usable after a failed graph.
58
+ };
59
+ function graphSelection(id) {
60
+ if (!options.config.mcp.previewManifest) return {};
61
+ const manifest = readPreviewManifest(options);
62
+ if (!manifest?.scenarios.some(entry => entry.id === id)) throw new Error("The preview resource graph is not ready for this scenario. Rebuild previews.");
63
+ return { manifestUri: PREVIEW_MANIFEST_URI, manifestRevision: manifest.revision };
64
+ }
65
+ const getCatalog = () => cache.catalog ??= inspectCatalog(options, getRegistry().scenarios);
66
+ const getDocuments = () => cache.documents ??= projectDocuments(options);
67
+ const builds = () => {
68
+ previewWatch?.setApps(allApps());
69
+ return previewWatch?.buildStatus() ?? inspectBuildStatus(root, { ...options.config.mcp, apps: allApps() });
70
+ };
71
+ const previews = () => options.config.mcp.apps.map((app) => ({ id: app.id, title: app.title, kind: app.kind, tool: `ads_show_${app.id.replaceAll("-", "_")}`, resourceUri: appUri(app.id), available: existsSync(path.resolve(root, app.file)), file: app.file }));
72
+
73
+ function resources() {
74
+ const context = project();
75
+ const documents = getDocuments();
76
+ const promoted = new Set([context.publicEntryUri, ...context.guides.map((guide) => guide.uri)]);
77
+ const skillEntries = new Set(skills.skills.map((skill) => skill.uri));
78
+ return [
79
+ { 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" },
80
+ { uri: `${PROJECT_URI}/status`, name: "ADS project status", mimeType: "application/json" },
81
+ { uri: BUILD_URI, name: "Preview build status and diagnostics", mimeType: "application/json" },
82
+ { uri: `${PROJECT_URI}/tokens`, name: "Project token values and themes", mimeType: "application/json" },
83
+ { uri: `${PROJECT_URI}/catalog`, name: "Public components and patterns", mimeType: "application/json" },
84
+ { uri: `${PROJECT_URI}/scenarios`, name: "Independently mountable scenarios", mimeType: "application/json" },
85
+ { uri: `${PROJECT_URI}/checks`, name: "Current ADS architecture diagnostics", mimeType: "application/json" },
86
+ { uri: TOKENS_APP_URI, name: "Experimental token widget", mimeType: APP_MIME },
87
+ { uri: SCENARIO_APP_URI, name: "Experimental scenario widget", mimeType: APP_MIME },
88
+ ...(options.config.mcp.previewManifest ? [{ uri: PREVIEW_APP_URI, name: "Shared-resource preview App", mimeType: APP_MIME }, { uri: PREVIEW_MANIFEST_URI, name: "Preview dependency graph", mimeType: "application/json" }] : []),
89
+ ...(options.config.mcp.previewManifest ? resourceAssets() : []),
90
+ ...allApps().map((app) => ({ uri: appUri(app.id), name: app.title, description: app.description, mimeType: APP_MIME })),
91
+ ...skills.skills.map(({ uri, frontmatter }) => ({ uri, name: frontmatter.name, description: frontmatter.description, mimeType: "text/markdown" })),
92
+ ...documents.filter((document) => promoted.has(document.uri)).map(({ file: _file, ...document }) => document),
93
+ ...documents.filter((document) => !promoted.has(document.uri)).map(({ file: _file, ...document }) => document),
94
+ ...[...skills.files.values()].filter((file) => !skillEntries.has(file.uri)).map(({ uri, name, mimeType }) => ({ uri, name, mimeType })),
95
+ ];
96
+ }
97
+ function status() {
98
+ const context = project();
99
+ return { projectRoot: root, workflow: workflow([...previews(), ...scenarioApps()]), project: context,
100
+ 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,
103
+ tokenCount: inspectTokens(root).tokens.length, showcaseUrl: getShowcaseUrl(),
104
+ previewWatch: previewWatch?.status() ?? { state: "disabled" },
105
+ mutations: mutations ? edits.status() : { enabled: false },
106
+ build: summarizeBuild(builds()),
107
+ previews: previews(), skills: skills.skills.map(({ uri, frontmatter }) => ({ uri, name: frontmatter.name, description: frontmatter.description })),
108
+ scenarios: { count: getRegistry().scenarios.length, diagnostics: getRegistry().diagnostics, resourceUri: `${PROJECT_URI}/scenarios`, tool: "ads_show_scenario" },
109
+ experimental: { apps: true, nativeSkills: era === "modern", nativeSkillActivation: "host-controlled" } };
110
+ }
111
+ async function read(uri, signal) {
112
+ if (uri === GUIDE_URI) return { contents: [{ uri, mimeType: "text/markdown", text: projectGuide(project(), [...previews(), ...scenarioApps()]) }] };
113
+ if (skills.files.has(uri)) return { contents: [skills.read(uri)] };
114
+ let value;
115
+ if (uri === `${PROJECT_URI}/status`) value = status();
116
+ else if (uri === BUILD_URI) value = builds();
117
+ else if (uri === `${PROJECT_URI}/tokens`) value = tokenWidgetData(inspectTokens(root));
118
+ else if (uri === `${PROJECT_URI}/catalog`) value = { items: getCatalog() };
119
+ else if (uri === `${PROJECT_URI}/scenarios`) value = getRegistry();
120
+ else if (uri === `${PROJECT_URI}/checks`) value = checkProject(root);
121
+ else if (uri === PREVIEW_MANIFEST_URI && options.config.mcp.previewManifest) {
122
+ value = readPreviewManifest(options);
123
+ if (!value) throw new Error("Preview resources are not built yet. Invoke ads_show_scenario to start the managed compiler.");
124
+ }
125
+ if (value !== undefined) return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify(value) }] };
126
+ if (uri === TOKENS_APP_URI) return { contents: [{ uri, mimeType: APP_MIME, text: tokensApp(tokenWidgetData(inspectTokens(root))), _meta: appMetadata({ nested: true }) }] };
127
+ if (uri === SCENARIO_APP_URI) return { contents: [{ uri, mimeType: APP_MIME, text: scenarioApp(), _meta: appMetadata({ nested: true }) }] };
128
+ if (uri === PREVIEW_APP_URI && options.config.mcp.previewManifest) return { contents: [{ uri, mimeType: APP_MIME, text: previewApp(), _meta: appMetadata() }] };
129
+ if (uri.startsWith("ads://assets/") && options.config.mcp.previewManifest) {
130
+ try { return { contents: [readPreviewAsset(options, uri)] }; }
131
+ catch (error) { if (error.code === "ADS_PREVIEW_NOT_FOUND") throw new ResourceNotFoundError(uri, error.message); throw error; }
132
+ }
133
+ const app = allApps().find((entry) => appUri(entry.id) === uri);
134
+ if (app) {
135
+ if (scenarioApps().some(entry => entry.id === app.id) && getRegistry().diagnostics.length) throw new Error("Scenario registry has errors. Read ads://project/scenarios before opening a preview.");
136
+ const build = await previewBuild(app, signal);
137
+ const graph = options.config.mcp.previewManifest && scenarioApps().some(entry => entry.id === app.id);
138
+ return { contents: [{ uri, mimeType: APP_MIME, text: graph ? previewApp({ id: app.id }) : projectApp(options, app, build), _meta: appMetadata() }] };
139
+ }
140
+ const document = getDocuments().find((entry) => entry.uri === uri);
141
+ if (document) return { contents: [{ uri, mimeType: document.mimeType, text: readProjectDocument(document) }] };
142
+ throw new ResourceNotFoundError(uri, `Unknown ADS resource: ${uri}`);
143
+ }
144
+ async function previewBuild(app, signal) {
145
+ previewWatch?.setApps(allApps());
146
+ if (previewWatch) return previewWatch.ensure(path.resolve(root, app.file), signal);
147
+ const build = builds().previews.find((entry) => entry.id === app.id);
148
+ if (build?.state === "error") throw buildFailure(build);
149
+ if (["building", "idle"].includes(build?.state)) throw Object.assign(new Error("Preview build is still pending. Invoke the preview again after rebuilding."), { build: { ...build, resourceUri: BUILD_URI } });
150
+ return build;
151
+ }
152
+
153
+ function tools() {
154
+ return [
155
+ { 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 }) => {
157
+ const matches = getCatalog().filter((item) => (!kind || item.kind === kind) && item.name.toLowerCase().includes(search.toLowerCase()));
158
+ const units = edits.units();
159
+ 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
+ } },
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()));
163
+ const units = edits.units();
164
+ 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
+ } },
166
+ { name: "ads_show_scenario", description: "Render one real token, component, pattern or composition scenario with its colocated fixtures and state adapter. First get an exact id from ads_scenarios or ads_catalog.previews. The managed compiler builds it on demand; errors return diagnostics instead of falsely current output. Requires MCP Apps plus host resource forwarding; fallback links to the same scenario in the project showcase.", schema: z.object({ id: z.string().min(1).max(256) }).strict(), _meta: { ui: { resourceUri: scenarioWidget() } }, call: async ({ id }, signal) => {
167
+ const entry = getRegistry().scenarios.find(item => item.id === id);
168
+ if (!entry) throw new Error("Unknown scenario id. Read ads_scenarios; IDs must match discovered source entries.");
169
+ if (getRegistry().diagnostics.length) throw new Error("Scenario registry has errors. Read ads://project/scenarios and fix its diagnostics.");
170
+ const app = scenarioApps().find(item => item.id === id);
171
+ const build = await previewBuild(app, signal);
172
+ const graph = graphSelection(id);
173
+ if (!options.config.mcp.previewManifest) projectApp(options, app, build);
174
+ const url = getShowcaseUrl() ? new URL(getShowcaseUrl()) : null;
175
+ url?.searchParams.delete("category");
176
+ url?.searchParams.set("scenario", id);
177
+ return json({ widget: { resourceUri: scenarioWidget() }, scenario: { ...entry, resourceUri: appUri(id) }, ...graph, build, showcaseUrl: url?.href ?? null });
178
+ } },
179
+ { name: "ads_tokens", description: "Use before styling, theme, brand or motion-token changes to inspect existing semantic definitions/values. Query specific token names with a small limit. ads_show_tokens renders project token scenarios; definition inspection does not validate component layout.", schema: query, call: ({ query: search = "", limit }) => {
180
+ const inventory = inspectTokens(root);
181
+ const matches = inventory.tokens.filter((token) => token.name.toLowerCase().includes(search.toLowerCase()));
182
+ return json({ total: matches.length, tokens: matches.slice(0, limit), categories: tokenScenarios(), diagnosticCount: inventory.diagnostics.length, resourceUri: `${PROJECT_URI}/tokens` });
183
+ } },
184
+ { name: "ads_check", description: "Run read-only core ADS architecture checks. Project Oxlint, typecheck, build and browser tests remain separate.", schema: z.object({ limit: z.number().int().min(1).max(50).default(20) }).strict(), call: ({ limit }) => {
185
+ const result = checkProject(root);
186
+ return json({ ok: result.ok, total: result.diagnostics.length, diagnostics: result.diagnostics.slice(0, limit), resourceUri: `${PROJECT_URI}/checks` });
187
+ } },
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 })) }) },
189
+ { 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
+ const skill = skills.skills.find((entry) => entry.frontmatter.name === name);
191
+ if (!skill) throw new Error("Unknown ADS skill name.");
192
+ const uri = `${skill.uri.slice(0, -"SKILL.md".length)}${file.split("/").map(encodeURIComponent).join("/")}`;
193
+ const manifest = skill.resources.find((resource) => resource.uri === uri);
194
+ if (!manifest) throw new Error("Unknown relative skill file. Consult its manifest with native skills/get or the resources catalog.");
195
+ const content = skills.read(uri);
196
+ if (!("text" in content)) return json({ ...manifest, mimeType: content.mimeType, binary: true, note: "Read binary bytes via resources/read; this tool does not embed them." });
197
+ return json({ ...manifest, text: content.text.slice(offset, offset + limit), offset, ...(offset + limit < content.text.length ? { nextOffset: offset + limit } : {}) });
198
+ } },
199
+ { name: "ads_show_tokens", description: "Render an exact discovered token scenario with its project-owned templates and fixtures. Pass its id as category, discovered through ads_tokens.categories or ads_scenarios(kind: tokens). Omit category for the first project token scenario, or a generic definition inspector for older compilers.", schema: z.object({ category: z.string().max(256).optional() }).strict(), _meta: { ui: { resourceUri: options.config.mcp.previewManifest ? PREVIEW_APP_URI : TOKENS_APP_URI } }, call: async ({ category }, signal) => {
200
+ if (!category && options.config.mcp.previewManifest) category = tokenScenarios()[0]?.id;
201
+ if (!category && options.config.mcp.previewManifest) throw new Error("No token scenario is registered. Colocate a token scenario and fixture before opening the preview; ads_tokens still inspects definitions.");
202
+ if (category) {
203
+ const entry = getRegistry().scenarios.find(item => item.kind === "tokens" && item.id === category);
204
+ if (!entry) throw new Error("Unknown token scenario. Discover categories with ads_tokens or ads_scenarios(kind: tokens) first.");
205
+ if (getRegistry().diagnostics.length) throw new Error("Scenario registry has errors. Fix its diagnostics before reviewing.");
206
+ const app = scenarioApps().find(item => item.id === category);
207
+ const build = await previewBuild(app, signal);
208
+ const graph = graphSelection(category);
209
+ if (!options.config.mcp.previewManifest) projectApp(options, app, build);
210
+ const url = getShowcaseUrl() ? new URL(getShowcaseUrl()) : null;
211
+ url?.searchParams.delete("scenario");
212
+ url?.searchParams.set("category", category);
213
+ return json({ widget: { resourceUri: options.config.mcp.previewManifest ? PREVIEW_APP_URI : TOKENS_APP_URI }, category, scenario: { ...entry, resourceUri: appUri(category) }, ...graph, build, showcaseUrl: url?.href ?? null });
214
+ }
215
+ return json({ widget: { resourceUri: TOKENS_APP_URI }, category: null, tokenCount: inspectTokens(root).tokens.length, dataUri: `${PROJECT_URI}/tokens`, showcaseUrl: getShowcaseUrl() });
216
+ } },
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. 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
+ const matches = edits.units().filter(item => unit ? item.id === unit : item.id.toLowerCase().includes(search.toLowerCase()));
219
+ 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 } : {}) })) });
220
+ } },
221
+ { name: "ads_read_file", description: "Read a bounded range of ADS or registered configuration/extension source. Returns unit, full-file digest, startLine and nextOffset; default/max is 12000 UTF-16 characters. Binary assets return byte count/digest and no encoded content. Paths come from ads_units or discovery. Missing new paths return digest null. Read only relevant context before preparing a Git patch.", schema: z.object({ path: z.string().min(1).max(500), offset: z.number().int().min(0).default(0), limit: z.number().int().min(1).max(mutationLimits.readChars).default(mutationLimits.readChars) }).strict(), call: ({ path: file, ...range }) => json(edits.read(file, range)) },
222
+ ...(mutations ? [{ name: "ads_apply_patch", description: "Apply one Git diff to one ADS unit from ads_units. Pass exactly one of patch (inline) or patchFile (a regular .patch/.diff under .ds/patches/, for large/binary diffs without model-context bytes). Text uses --- a/path, +++ b/path and exact @@ hunks; /dev/null creates/deletes. Git binary patches require git diff --binary --full-index and installed Git. A unit owns its implementation, previews and fixtures; imported peers are separate. Foundation, documentation, assets and configuration are editable too. Configuration changes may register/edit project guards and builders; the proposed schema, mandatory core/Oxlint and candidate project checks must pass. Only selected-unit public re-exports may change. Set dryRun true to validate without writing. Fix rejected patches and inspect current previews after acceptance.",
223
+ schema: z.object({ unit: z.string().min(1).max(256), dryRun: z.boolean(), patch: z.string().min(1).max(mutationLimits.patchBytes).optional(), patchFile: z.string().min(1).max(500).optional() }).strict().refine(args => (args.patch !== undefined) !== (args.patchFile !== undefined), "Provide exactly one of patch or patchFile."),
224
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false }, _meta: { ui: { visibility: ["model"] } },
225
+ call: async (args, signal) => {
226
+ const result = await edits.apply(args, signal);
227
+ if (result.applied) {
228
+ try {
229
+ if (result.unit === "configuration") await previewWatch?.reconfigure?.(loadConfig(root).config.mcp, true);
230
+ await refresh(true);
231
+ }
232
+ catch (error) { result.warnings = [...(result.warnings ?? []), { rule: "mutation-notification", message: `Changes were applied, but preview refresh failed: ${error.message.slice(0, 2000)}. Recheck ads_status and reopen the scenario.` }]; }
233
+ }
234
+ return { ...json(result), ...(!result.ok ? { isError: true } : {}) };
235
+ },
236
+ }] : []),
237
+ ...options.config.mcp.apps.map((app) => ({ name: `ads_show_${app.id.replaceAll("-", "_")}`, description: `${app.description ?? app.title}. Renders the project's built ${app.kind} examples through MCP Apps; text fallback provides the showcase URL.`, schema: empty, _meta: { ui: { resourceUri: appUri(app.id) } }, call: async (_args, signal) => {
238
+ // Validate readiness without embedding executable HTML in model results.
239
+ const build = await previewBuild(app, signal);
240
+ projectApp(options, app);
241
+ return json({ widget: { resourceUri: appUri(app.id) }, title: app.title, kind: app.kind, build, showcaseUrl: getShowcaseUrl() });
242
+ } })),
243
+ ];
244
+ }
245
+ server.setRequestHandler("tools/list", ({ params = {} }) => {
246
+ previewWatch?.start();
247
+ const { cursor } = params;
248
+ const listing = page(tools().map(({ schema, call: _call, ...tool }) => ({ ...tool, inputSchema: z.toJSONSchema(schema), annotations: tool.annotations ?? annotations })), cursor, "tools");
249
+ return { tools: listing.items, ...(listing.nextCursor ? { nextCursor: listing.nextCursor } : {}) };
250
+ });
251
+ server.setRequestHandler("tools/call", async ({ params: { name, arguments: args } }, context) => {
252
+ const tool = tools().find((entry) => entry.name === name);
253
+ if (!tool) throw new InvalidParamsError("Unknown ADS tool.");
254
+ const input = tool.schema.safeParse(args ?? {});
255
+ if (!input.success) throw new InvalidParamsError(input.error.message);
256
+ previewWatch?.start();
257
+ try { return await tool.call(input.data, context.mcpReq.signal); }
258
+ catch (error) { return { isError: true, content: [{ type: "text", text: error.message }], ...(error.build ? { structuredContent: { message: error.message, build: error.build } } : {}) }; }
259
+ });
260
+ server.setRequestHandler("resources/list", ({ params = {} }) => {
261
+ previewWatch?.start();
262
+ const { cursor } = params;
263
+ const listing = page(resources(), cursor, "resources");
264
+ return { resources: listing.items, ...(listing.nextCursor ? { nextCursor: listing.nextCursor } : {}) };
265
+ });
266
+ server.setRequestHandler("resources/templates/list", () => ({ resourceTemplates: [] }));
267
+ server.setRequestHandler("resources/read", ({ params: { uri } }, context) => read(uri, context.mcpReq.signal));
268
+ server.setRequestHandler("resources/subscribe", ({ params: { uri } }) => {
269
+ if (!resources().some((entry) => entry.uri === uri)) throw new ResourceNotFoundError(uri, "Unknown ADS resource subscription.");
270
+ subscribed.add(uri); return {};
271
+ });
272
+ server.setRequestHandler("resources/unsubscribe", ({ params: { uri } }) => { subscribed.delete(uri); return {}; });
273
+ server.setRequestHandler("prompts/list", () => ({ prompts: promptDefinitions(skills.skills) }));
274
+ server.setRequestHandler("prompts/get", ({ params: { name, arguments: args } }) => {
275
+ try { return getWorkflowPrompt(name, args, skills.skills); } catch (error) { throw new InvalidParamsError(error.message); }
276
+ });
277
+ if (era === "modern") {
278
+ server.setRequestHandler("skills/list", schemas, ({ cursor }) => {
279
+ const listing = page(skills.skills, cursor, "skills", 2);
280
+ return { resultType: "complete", ttlMs: 300000, cacheScope: "public", skills: listing.items, ...(listing.nextCursor ? { nextCursor: listing.nextCursor } : {}) };
281
+ });
282
+ server.setRequestHandler("skills/get", { ...schemas, params: z.object({ uri: z.string() }).passthrough() }, ({ uri }) => {
283
+ try { return { resultType: "complete", ttlMs: 300000, cacheScope: "public", skill: skills.get(uri) }; } catch (error) { throw new InvalidParamsError(error.message); }
284
+ });
285
+ server.setRequestHandler("resources/directory/read", { ...schemas, params: z.object({ uri: z.string(), cursor: z.string().optional() }).passthrough() }, ({ uri, cursor }) => {
286
+ const children = skills.directories.get(uri);
287
+ if (!children) throw new ResourceNotFoundError(uri, "Unknown skill directory (no trailing slash).");
288
+ const listing = page(children, cursor, "directory");
289
+ return { resultType: "complete", resources: listing.items, ...(listing.nextCursor ? { nextCursor: listing.nextCursor } : {}) };
290
+ });
291
+ }
292
+
293
+ function fileFingerprint(files) {
294
+ return files.sort().map((file) => {
295
+ try { const stat = statSync(file); return `${file}:${stat.mtimeMs}:${stat.size}`; } catch { return `${file}:missing`; }
296
+ }).join("\n");
297
+ }
298
+ const fingerprint = () => fileFingerprint([...sourceFiles(options.dsRoot), ...getDocuments().map((doc) => doc.file), path.join(root, "ds.config.json"), path.join(root, "package.json")]);
299
+ const artifactFingerprint = () => fileFingerprint(allApps().map(app => path.resolve(root, app.file)));
300
+ const assetFingerprint = () => resourceAssets().map(asset => asset.uri).sort().join("\n");
301
+ let previous = fingerprint();
302
+ let previousArtifacts = artifactFingerprint();
303
+ let previousAssets = assetFingerprint();
304
+ let previousBuild = JSON.stringify(builds());
305
+ async function refresh(force = false) {
306
+ // Polling covers replaced files and new directories consistently across OSes.
307
+ cache.documents = undefined;
308
+ const next = fingerprint();
309
+ const artifacts = artifactFingerprint();
310
+ const assets = assetFingerprint();
311
+ const build = JSON.stringify(builds());
312
+ const filesChanged = force || next !== previous;
313
+ const buildChanged = build !== previousBuild;
314
+ const artifactsChanged = artifacts !== previousArtifacts;
315
+ const assetsChanged = assets !== previousAssets;
316
+ if (!filesChanged && !buildChanged && !artifactsChanged && !assetsChanged) return false;
317
+ if (filesChanged) {
318
+ const updated = loadConfig(root);
319
+ await previewWatch?.reconfigure?.(updated.config.mcp);
320
+ const appsChanged = JSON.stringify(updated.config.mcp.apps) !== JSON.stringify(options.config.mcp.apps) || updated.config.mcp.previewManifest !== options.config.mcp.previewManifest;
321
+ options = updated;
322
+ cache.signature = `${root}:${JSON.stringify(options.config)}`;
323
+ cache.catalog = undefined;
324
+ cache.registry = undefined;
325
+ previous = fingerprint();
326
+ if (appsChanged) { if (notifications) notifications.toolsChanged(); else await server.sendToolListChanged(); }
327
+ }
328
+ if (filesChanged || assetsChanged) { if (notifications) notifications.resourcesChanged(); else await server.sendResourceListChanged(); }
329
+ previousArtifacts = artifactFingerprint();
330
+ previousAssets = assetFingerprint();
331
+ previousBuild = JSON.stringify(builds());
332
+ 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));
334
+ for (const uri of targets) { if (notifications) notifications.resourceUpdated(uri); else await server.sendResourceUpdated({ uri }); }
335
+ return true;
336
+ }
337
+ const timer = pollInterval > 0 ? setInterval(() => { refresh().catch((error) => process.stderr.write(`ads mcp: ${error.message}\n`)); }, pollInterval) : null;
338
+ timer?.unref();
339
+ server.oninitialized = () => previewWatch?.start();
340
+ server.onclose = () => { if (timer) clearInterval(timer); };
341
+ return { server, refresh, async close() { if (timer) clearInterval(timer); await server.close(); } };
342
+ }
343
+
344
+ export async function startAdsMcp(directory = process.cwd(), { stdio = true, showcase = true, port = 0, mutations = false } = {}) {
345
+ // Fail before opening the transport for configuration/distribution errors.
346
+ const { root, config } = loadConfig(directory);
347
+ const inspection = { skills: createSkillStore() };
348
+ const previewWatch = createReconfigurablePreviewWatch(root, config.mcp);
349
+ let host;
350
+ const showcaseUrl = () => host?.url;
351
+ if (showcase) {
352
+ host = await startShowcaseHost(({ era }) => createAdsMcp(directory, { era, previewWatch, showcaseUrl, inspection, mutations, pollInterval: 0 }).server,
353
+ { port, observe: notifications => createAdsMcp(directory, { era: "modern", previewWatch, showcaseUrl, inspection, mutations, notifications }) });
354
+ process.stderr.write(`ADS showcase: ${host.url}\n`);
355
+ }
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;
357
+ let closing;
358
+ function close() {
359
+ return closing ??= (async () => {
360
+ inspection.edits?.cancel();
361
+ await host?.close();
362
+ await previewWatch.stop();
363
+ await handle?.close();
364
+ process.stdin.removeListener("end", disconnect);
365
+ for (const signal of ["SIGINT", "SIGTERM"]) process.removeListener(signal, disconnect);
366
+ process.removeListener("exit", forceCleanup);
367
+ })();
368
+ }
369
+ function disconnect() { void close(); }
370
+ function forceCleanup() { previewWatch.kill(); }
371
+ // 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);
374
+ for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, disconnect);
375
+ process.once("exit", forceCleanup);
376
+ return { close, url: host?.url };
377
+ }