@assistant-ui/mcp-docs-server 0.1.36 → 0.1.39

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 (246) hide show
  1. package/.docs/organized/code-examples/waterfall.md +8 -9
  2. package/.docs/organized/code-examples/with-a2a.md +16 -11
  3. package/.docs/organized/code-examples/with-ag-ui.md +16 -11
  4. package/.docs/organized/code-examples/{with-ai-sdk-v6.md → with-ai-sdk-v7.md} +32 -21
  5. package/.docs/organized/code-examples/with-artifacts.md +17 -10
  6. package/.docs/organized/code-examples/with-assistant-transport.md +15 -8
  7. package/.docs/organized/code-examples/with-browser-extension.md +15 -8
  8. package/.docs/organized/code-examples/with-chain-of-thought.md +17 -10
  9. package/.docs/organized/code-examples/with-cloud-standalone.md +10 -9
  10. package/.docs/organized/code-examples/with-cloud.md +18 -13
  11. package/.docs/organized/code-examples/with-custom-thread-list.md +17 -10
  12. package/.docs/organized/code-examples/with-elevenlabs-conversational.md +20 -13
  13. package/.docs/organized/code-examples/with-elevenlabs-scribe.md +20 -13
  14. package/.docs/organized/code-examples/with-eve.md +16 -9
  15. package/.docs/organized/code-examples/with-expo.md +26 -23
  16. package/.docs/organized/code-examples/with-external-store.md +16 -11
  17. package/.docs/organized/code-examples/with-ffmpeg.md +18 -13
  18. package/.docs/organized/code-examples/with-generative-ui.md +20 -15
  19. package/.docs/organized/code-examples/with-google-adk.md +16 -9
  20. package/.docs/organized/code-examples/with-heat-graph.md +8 -9
  21. package/.docs/organized/code-examples/with-image-generation.md +17 -10
  22. package/.docs/organized/code-examples/with-interactables.md +19 -15
  23. package/.docs/organized/code-examples/with-langchain.md +17 -10
  24. package/.docs/organized/code-examples/with-langgraph.md +17 -10
  25. package/.docs/organized/code-examples/with-livekit.md +21 -14
  26. package/.docs/organized/code-examples/with-mcp.md +40 -18
  27. package/.docs/organized/code-examples/with-opencode.md +22 -17
  28. package/.docs/organized/code-examples/with-pi.md +47 -12
  29. package/.docs/organized/code-examples/with-react-hook-form.md +19 -14
  30. package/.docs/organized/code-examples/with-react-ink-web.md +6 -6
  31. package/.docs/organized/code-examples/with-react-ink.md +2 -2
  32. package/.docs/organized/code-examples/with-react-router.md +20 -15
  33. package/.docs/organized/code-examples/with-resumable-stream.md +19 -12
  34. package/.docs/organized/code-examples/with-store.md +8 -9
  35. package/.docs/organized/code-examples/with-tanstack.md +17 -11
  36. package/.docs/organized/code-examples/with-tap-runtime.md +16 -11
  37. package/.docs/organized/code-examples/with-virtualized-thread.md +17 -12
  38. package/.docs/raw/docs/(docs)/base-ui.mdx +39 -0
  39. package/.docs/raw/docs/(docs)/cli.mdx +18 -2
  40. package/.docs/raw/docs/(docs)/installation.mdx +15 -1
  41. package/.docs/raw/docs/(docs)/rtl.mdx +2 -4
  42. package/.docs/raw/docs/(reference)/api-reference/generative-ui/actions.mdx +56 -0
  43. package/.docs/raw/docs/(reference)/api-reference/generative-ui/components.mdx +86 -0
  44. package/.docs/raw/docs/(reference)/api-reference/generative-ui/index.mdx +19 -1
  45. package/.docs/raw/docs/(reference)/api-reference/generative-ui/json-generative-ui.mdx +42 -0
  46. package/.docs/raw/docs/(reference)/api-reference/generative-ui/rendering.mdx +53 -2
  47. package/.docs/raw/docs/(reference)/api-reference/generative-ui/slack.mdx +81 -0
  48. package/.docs/raw/docs/(reference)/api-reference/generative-ui/teams.mdx +86 -0
  49. package/.docs/raw/docs/(reference)/api-reference/generative-ui/tokens.mdx +62 -0
  50. package/.docs/raw/docs/(reference)/api-reference/integrations/index.mdx +4 -1
  51. package/.docs/raw/docs/(reference)/api-reference/integrations/react-ai-sdk.mdx +4 -2
  52. package/.docs/raw/docs/(reference)/api-reference/integrations/react-data-stream.mdx +37 -0
  53. package/.docs/raw/docs/(reference)/api-reference/overview.mdx +1 -0
  54. package/.docs/raw/docs/(reference)/api-reference/primitives/assistant-if.mdx +29 -29
  55. package/.docs/raw/docs/(reference)/api-reference/primitives/composition.mdx +1 -0
  56. package/.docs/raw/docs/(reference)/api-reference/primitives/index.mdx +3 -0
  57. package/.docs/raw/docs/(reference)/api-reference/primitives/selection-toolbar.mdx +2 -0
  58. package/.docs/raw/docs/(reference)/api-reference/tools/status.mdx +0 -2
  59. package/.docs/raw/docs/(reference)/api-reference/tools/toolkits.mdx +12 -9
  60. package/.docs/raw/docs/(reference)/api-reference/utilities/miscellaneous.mdx +4 -0
  61. package/.docs/raw/docs/cloud/ai-sdk-assistant-ui.mdx +7 -1
  62. package/.docs/raw/docs/cloud/langgraph.mdx +4 -2
  63. package/.docs/raw/docs/guides/chatgpt-subscription.mdx +108 -0
  64. package/.docs/raw/docs/guides/dictation.mdx +185 -257
  65. package/.docs/raw/docs/guides/index.mdx +10 -0
  66. package/.docs/raw/docs/guides/mentions.mdx +31 -3
  67. package/.docs/raw/docs/guides/resumable-streams.mdx +12 -1
  68. package/.docs/raw/docs/guides/speech.mdx +47 -29
  69. package/.docs/raw/docs/guides/suggestions.mdx +70 -1
  70. package/.docs/raw/docs/guides/voice.mdx +197 -267
  71. package/.docs/raw/docs/ink/primitives.mdx +35 -1
  72. package/.docs/raw/docs/integrations/frameworks/ai-sdk.mdx +13 -7
  73. package/.docs/raw/docs/integrations/frameworks/cloudflare-agents/overview.mdx +3 -3
  74. package/.docs/raw/docs/integrations/frameworks/mastra/full-stack.mdx +1 -1
  75. package/.docs/raw/docs/integrations/frameworks/mastra/overview.mdx +3 -3
  76. package/.docs/raw/docs/integrations/frameworks/mastra/separate-server.mdx +1 -1
  77. package/.docs/raw/docs/integrations/gateways/index.mdx +2 -2
  78. package/.docs/raw/docs/integrations/index.mdx +1 -1
  79. package/.docs/raw/docs/integrations/observability/helicone.mdx +1 -1
  80. package/.docs/raw/docs/integrations/observability/langfuse.mdx +1 -1
  81. package/.docs/raw/docs/integrations/observability/langsmith.mdx +3 -3
  82. package/.docs/raw/docs/migrations/index.mdx +50 -0
  83. package/.docs/raw/docs/migrations/react-langgraph-v0-7.mdx +2 -2
  84. package/.docs/raw/docs/migrations/toolkit-tools.mdx +4 -2
  85. package/.docs/raw/docs/primitives/chain-of-thought.mdx +6 -1
  86. package/.docs/raw/docs/primitives/composer.mdx +16 -0
  87. package/.docs/raw/docs/primitives/selection-toolbar.mdx +25 -0
  88. package/.docs/raw/docs/primitives/thread-list.mdx +30 -6
  89. package/.docs/raw/docs/react-native/primitives.mdx +23 -0
  90. package/.docs/raw/docs/runtimes/a2a/quickstart.mdx +1 -1
  91. package/.docs/raw/docs/runtimes/ag-ui/agent-state.mdx +124 -0
  92. package/.docs/raw/docs/runtimes/ag-ui/quickstart.mdx +1 -1
  93. package/.docs/raw/docs/runtimes/ag-ui/runtime-options.mdx +13 -2
  94. package/.docs/raw/docs/runtimes/ai-sdk/overview.mdx +13 -4
  95. package/.docs/raw/docs/runtimes/ai-sdk/v4-legacy.mdx +11 -11
  96. package/.docs/raw/docs/runtimes/ai-sdk/v5-legacy.mdx +5 -5
  97. package/.docs/raw/docs/runtimes/ai-sdk/{v6.mdx → v6-legacy.mdx} +8 -6
  98. package/.docs/raw/docs/runtimes/ai-sdk/v7.mdx +717 -0
  99. package/.docs/raw/docs/runtimes/concepts/adapters.mdx +1 -1
  100. package/.docs/raw/docs/runtimes/concepts/architecture.mdx +1 -1
  101. package/.docs/raw/docs/runtimes/custom/data-stream.mdx +10 -19
  102. package/.docs/raw/docs/runtimes/custom/external-store.mdx +1 -1
  103. package/.docs/raw/docs/runtimes/custom/local-runtime.mdx +2 -0
  104. package/.docs/raw/docs/runtimes/eve/overview.mdx +1 -1
  105. package/.docs/raw/docs/runtimes/google-adk/quickstart.mdx +1 -1
  106. package/.docs/raw/docs/runtimes/langchain.mdx +2 -2
  107. package/.docs/raw/docs/runtimes/langgraph/agent-state.mdx +181 -0
  108. package/.docs/raw/docs/runtimes/opencode/hooks.mdx +1 -1
  109. package/.docs/raw/docs/tools/backend.mdx +6 -3
  110. package/.docs/raw/docs/tools/defining-tools.mdx +12 -1
  111. package/.docs/raw/docs/tools/generative-ui.mdx +95 -0
  112. package/.docs/raw/docs/tools/mcp-apps.mdx +37 -10
  113. package/.docs/raw/docs/tools/mcp.mdx +105 -3
  114. package/.docs/raw/docs/tools/tool-ui.mdx +6 -4
  115. package/.docs/raw/docs/tools/user-managed-mcp.mdx +41 -4
  116. package/.docs/raw/docs/ui/accordion.mdx +16 -10
  117. package/.docs/raw/docs/ui/assistant-modal.mdx +8 -4
  118. package/.docs/raw/docs/ui/attachment.mdx +5 -1
  119. package/.docs/raw/docs/ui/badge.mdx +23 -12
  120. package/.docs/raw/docs/ui/follow-up-suggestions.mdx +80 -0
  121. package/.docs/raw/docs/ui/model-selector.mdx +69 -6
  122. package/.docs/raw/docs/ui/select.mdx +22 -14
  123. package/.docs/raw/docs/ui/sources.mdx +1 -1
  124. package/.docs/raw/docs/ui/tabs.mdx +25 -14
  125. package/.docs/raw/docs/utilities/heat-graph.mdx +2 -2
  126. package/README.md +1 -1
  127. package/dist/constants.d.ts +2 -1
  128. package/dist/constants.d.ts.map +1 -1
  129. package/dist/constants.js +2 -1
  130. package/dist/constants.js.map +1 -1
  131. package/dist/index.d.ts +0 -1
  132. package/dist/index.d.ts.map +1 -1
  133. package/dist/index.js +51 -0
  134. package/dist/index.js.map +1 -1
  135. package/dist/prepare-docs/code-examples.d.ts.map +1 -1
  136. package/dist/prepare-docs/copy-raw.d.ts.map +1 -1
  137. package/dist/prepare-docs/copy-raw.js +0 -4
  138. package/dist/prepare-docs/copy-raw.js.map +1 -1
  139. package/dist/prompts/xulux-playground.d.ts +12 -0
  140. package/dist/prompts/xulux-playground.d.ts.map +1 -0
  141. package/dist/prompts/xulux-playground.js +33 -0
  142. package/dist/prompts/xulux-playground.js.map +1 -0
  143. package/dist/tools/docs.d.ts +2 -4
  144. package/dist/tools/docs.d.ts.map +1 -1
  145. package/dist/tools/docs.js +38 -10
  146. package/dist/tools/docs.js.map +1 -1
  147. package/dist/tools/examples.d.ts +5 -5
  148. package/dist/tools/examples.d.ts.map +1 -1
  149. package/dist/tools/examples.js +10 -7
  150. package/dist/tools/examples.js.map +1 -1
  151. package/dist/tools/resources.d.ts +6 -0
  152. package/dist/tools/resources.d.ts.map +1 -0
  153. package/dist/tools/resources.js +74 -0
  154. package/dist/tools/resources.js.map +1 -0
  155. package/dist/tools/search.d.ts +30 -0
  156. package/dist/tools/search.d.ts.map +1 -0
  157. package/dist/tools/search.js +39 -0
  158. package/dist/tools/search.js.map +1 -0
  159. package/dist/tools/tests/test-setup.d.ts.map +1 -1
  160. package/dist/tools/tests/test-setup.js +7 -1
  161. package/dist/tools/tests/test-setup.js.map +1 -1
  162. package/dist/tools/xulux-templates.d.ts +72 -0
  163. package/dist/tools/xulux-templates.d.ts.map +1 -0
  164. package/dist/tools/xulux-templates.js +82 -0
  165. package/dist/tools/xulux-templates.js.map +1 -0
  166. package/dist/utils/cache.d.ts +5 -0
  167. package/dist/utils/cache.d.ts.map +1 -0
  168. package/dist/utils/cache.js +18 -0
  169. package/dist/utils/cache.js.map +1 -0
  170. package/dist/utils/logger.d.ts.map +1 -1
  171. package/dist/utils/mcp-format.d.ts +1 -0
  172. package/dist/utils/mcp-format.d.ts.map +1 -1
  173. package/dist/utils/mcp-format.js +7 -4
  174. package/dist/utils/mcp-format.js.map +1 -1
  175. package/dist/utils/mdx.d.ts +2 -1
  176. package/dist/utils/mdx.d.ts.map +1 -1
  177. package/dist/utils/mdx.js +19 -2
  178. package/dist/utils/mdx.js.map +1 -1
  179. package/dist/utils/paths.d.ts +2 -1
  180. package/dist/utils/paths.d.ts.map +1 -1
  181. package/dist/utils/paths.js +20 -1
  182. package/dist/utils/paths.js.map +1 -1
  183. package/dist/utils/search.d.ts +10 -0
  184. package/dist/utils/search.d.ts.map +1 -0
  185. package/dist/utils/search.js +97 -0
  186. package/dist/utils/search.js.map +1 -0
  187. package/dist/utils/security.d.ts.map +1 -1
  188. package/dist/xulux/catalog-client.d.ts +14 -0
  189. package/dist/xulux/catalog-client.d.ts.map +1 -0
  190. package/dist/xulux/catalog-client.js +67 -0
  191. package/dist/xulux/catalog-client.js.map +1 -0
  192. package/dist/xulux/fallback-catalog.d.ts +7 -0
  193. package/dist/xulux/fallback-catalog.d.ts.map +1 -0
  194. package/dist/xulux/fallback-catalog.js +47 -0
  195. package/dist/xulux/fallback-catalog.js.map +1 -0
  196. package/dist/xulux/fetch-sandbox.d.ts +5 -0
  197. package/dist/xulux/fetch-sandbox.d.ts.map +1 -0
  198. package/dist/xulux/fetch-sandbox.js +40 -0
  199. package/dist/xulux/fetch-sandbox.js.map +1 -0
  200. package/dist/xulux/template-service.d.ts +84 -0
  201. package/dist/xulux/template-service.d.ts.map +1 -0
  202. package/dist/xulux/template-service.js +223 -0
  203. package/dist/xulux/template-service.js.map +1 -0
  204. package/dist/xulux/types.d.ts +55 -0
  205. package/dist/xulux/types.d.ts.map +1 -0
  206. package/dist/xulux/types.js +6 -0
  207. package/dist/xulux/types.js.map +1 -0
  208. package/package.json +5 -5
  209. package/src/constants.ts +2 -0
  210. package/src/index.ts +67 -0
  211. package/src/prepare-docs/copy-raw.ts +0 -5
  212. package/src/prompts/xulux-playground.ts +36 -0
  213. package/src/tools/docs.ts +52 -4
  214. package/src/tools/examples.ts +18 -11
  215. package/src/tools/resources.ts +114 -0
  216. package/src/tools/search.ts +46 -0
  217. package/src/tools/tests/completions.test.ts +46 -0
  218. package/src/tools/tests/directory-size-cap.test.ts +50 -0
  219. package/src/tools/tests/docs.test.ts +20 -0
  220. package/src/tools/tests/examples.test.ts +5 -5
  221. package/src/tools/tests/listings-cache.test.ts +19 -0
  222. package/src/tools/tests/mcp-protocol.test.ts +92 -1
  223. package/src/tools/tests/resources.test.ts +102 -0
  224. package/src/tools/tests/search.test.ts +37 -0
  225. package/src/tools/tests/test-setup.ts +10 -0
  226. package/src/tools/tests/xulux-templates.test.ts +262 -0
  227. package/src/tools/xulux-templates.ts +141 -0
  228. package/src/utils/cache.ts +20 -0
  229. package/src/utils/mcp-format.ts +8 -6
  230. package/src/utils/mdx.ts +21 -1
  231. package/src/utils/paths.ts +25 -0
  232. package/src/utils/search.ts +131 -0
  233. package/src/utils/tests/cache.test.ts +51 -0
  234. package/src/utils/tests/mcp-format.test.ts +22 -0
  235. package/src/utils/tests/security.test.ts +1 -1
  236. package/src/xulux/catalog-client.ts +105 -0
  237. package/src/xulux/fallback-catalog.ts +63 -0
  238. package/src/xulux/fetch-sandbox.ts +56 -0
  239. package/src/xulux/template-service.ts +406 -0
  240. package/src/xulux/types.ts +60 -0
  241. package/.docs/raw/blog/2024-07-29-hello/index.mdx +0 -64
  242. package/.docs/raw/blog/2024-09-11/index.mdx +0 -10
  243. package/.docs/raw/blog/2024-12-15/index.mdx +0 -10
  244. package/.docs/raw/blog/2025-01-31-changelog/index.mdx +0 -127
  245. package/.docs/raw/blog/2026-03-launch-week/index.mdx +0 -258
  246. package/.docs/raw/docs/(reference)/api-reference/hooks/utilities.mdx +0 -464
@@ -0,0 +1,131 @@
1
+ import { join } from "node:path";
2
+ import { DOCS_PATH, MDX_EXTENSION } from "../constants.js";
3
+ import { getAvailablePaths, pathExists } from "./paths.js";
4
+ import { readMDXFile } from "./mdx.js";
5
+
6
+ const SCORE_WEIGHTS = {
7
+ FULL_QUERY: { title: 10, path: 8, description: 5 },
8
+ TOKEN: { title: 3, path: 2, description: 1, content: 0.5 },
9
+ } as const;
10
+
11
+ interface IndexEntry {
12
+ path: string;
13
+ title: string;
14
+ description: string;
15
+ content: string;
16
+ excerpt: string;
17
+ }
18
+
19
+ export interface SearchResult {
20
+ path: string;
21
+ title: string;
22
+ snippet: string;
23
+ }
24
+
25
+ let indexPromise: Promise<IndexEntry[]> | null = null;
26
+
27
+ function deriveTitle(
28
+ path: string,
29
+ frontmatter: Record<string, unknown>,
30
+ ): string {
31
+ const fmTitle = frontmatter["title"];
32
+ if (typeof fmTitle === "string" && fmTitle.trim()) return fmTitle;
33
+ return path.split("/").pop() ?? path;
34
+ }
35
+
36
+ async function buildIndex(): Promise<IndexEntry[]> {
37
+ const paths = await getAvailablePaths();
38
+ const entries = await Promise.all(
39
+ paths.map(async (path): Promise<IndexEntry | null> => {
40
+ const filePath = join(DOCS_PATH, `${path}${MDX_EXTENSION}`);
41
+ if (!(await pathExists(filePath))) return null;
42
+ const mdx = await readMDXFile(filePath);
43
+ if (!mdx) return null;
44
+ const description = mdx.frontmatter["description"];
45
+ return {
46
+ path,
47
+ title: deriveTitle(path, mdx.frontmatter),
48
+ description: typeof description === "string" ? description : "",
49
+ content: mdx.content,
50
+ excerpt: mdx.excerpt ?? "",
51
+ };
52
+ }),
53
+ );
54
+ return entries.filter((entry): entry is IndexEntry => entry !== null);
55
+ }
56
+
57
+ function getIndex(): Promise<IndexEntry[]> {
58
+ if (!indexPromise) {
59
+ indexPromise = buildIndex().catch((error) => {
60
+ indexPromise = null;
61
+ throw error;
62
+ });
63
+ }
64
+ return indexPromise;
65
+ }
66
+
67
+ function makeSnippet(
68
+ content: string,
69
+ tokens: string[],
70
+ excerpt: string,
71
+ ): string {
72
+ const lower = content.toLowerCase();
73
+ for (const token of tokens) {
74
+ const idx = lower.indexOf(token);
75
+ if (idx >= 0) {
76
+ const start = Math.max(0, idx - 60);
77
+ const end = Math.min(content.length, idx + token.length + 100);
78
+ const slice = content.slice(start, end).replace(/\s+/g, " ").trim();
79
+ return `${start > 0 ? "…" : ""}${slice}${end < content.length ? "…" : ""}`;
80
+ }
81
+ }
82
+ if (excerpt) return excerpt.slice(0, 160);
83
+ const firstLine = content.split("\n").find((line) => line.trim());
84
+ return firstLine ? firstLine.replace(/^#+\s*/, "").slice(0, 160) : "";
85
+ }
86
+
87
+ function scoreEntry(
88
+ entry: IndexEntry,
89
+ query: string,
90
+ tokens: string[],
91
+ ): number {
92
+ const title = entry.title.toLowerCase();
93
+ const path = entry.path.toLowerCase();
94
+ const description = entry.description.toLowerCase();
95
+ const content = entry.content.toLowerCase();
96
+
97
+ let score = 0;
98
+ if (title.includes(query)) score += SCORE_WEIGHTS.FULL_QUERY.title;
99
+ if (path.includes(query)) score += SCORE_WEIGHTS.FULL_QUERY.path;
100
+ if (description.includes(query))
101
+ score += SCORE_WEIGHTS.FULL_QUERY.description;
102
+
103
+ for (const token of tokens) {
104
+ if (title.includes(token)) score += SCORE_WEIGHTS.TOKEN.title;
105
+ if (path.includes(token)) score += SCORE_WEIGHTS.TOKEN.path;
106
+ if (description.includes(token)) score += SCORE_WEIGHTS.TOKEN.description;
107
+ if (content.includes(token)) score += SCORE_WEIGHTS.TOKEN.content;
108
+ }
109
+ return score;
110
+ }
111
+
112
+ export async function searchDocs(
113
+ query: string,
114
+ limit = 10,
115
+ ): Promise<SearchResult[]> {
116
+ const normalized = query.trim().toLowerCase();
117
+ if (!normalized) return [];
118
+ const tokens = normalized.split(/\s+/).filter(Boolean);
119
+
120
+ const index = await getIndex();
121
+ return index
122
+ .map((entry) => ({ entry, score: scoreEntry(entry, normalized, tokens) }))
123
+ .filter((scored) => scored.score > 0)
124
+ .sort((a, b) => b.score - a.score)
125
+ .slice(0, limit)
126
+ .map(({ entry }) => ({
127
+ path: entry.path,
128
+ title: entry.title,
129
+ snippet: makeSnippet(entry.content, tokens, entry.excerpt),
130
+ }));
131
+ }
@@ -0,0 +1,51 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { cacheListing } from "../cache.js";
3
+
4
+ describe("cacheListing", () => {
5
+ it("scans once and reuses the result", async () => {
6
+ let calls = 0;
7
+ const listing = cacheListing(async () => {
8
+ calls++;
9
+ return ["a", "b"];
10
+ });
11
+ const first = await listing();
12
+ const second = await listing();
13
+ expect(second).toBe(first);
14
+ expect(calls).toBe(1);
15
+ });
16
+
17
+ it("shares one in-flight scan across concurrent callers", async () => {
18
+ let calls = 0;
19
+ const listing = cacheListing(async () => {
20
+ calls++;
21
+ return ["a"];
22
+ });
23
+ const [first, second] = await Promise.all([listing(), listing()]);
24
+ expect(second).toBe(first);
25
+ expect(calls).toBe(1);
26
+ });
27
+
28
+ it("re-scans after a failed scan", async () => {
29
+ let calls = 0;
30
+ const listing = cacheListing(async () => {
31
+ calls++;
32
+ if (calls === 1) throw new Error("boom");
33
+ return ["a"];
34
+ });
35
+ await expect(listing()).rejects.toThrow("boom");
36
+ expect(await listing()).toEqual(["a"]);
37
+ expect(calls).toBe(2);
38
+ });
39
+
40
+ it("does not cache an empty listing", async () => {
41
+ let calls = 0;
42
+ const listing = cacheListing(async () => {
43
+ calls++;
44
+ return calls === 1 ? [] : ["a"];
45
+ });
46
+ expect(await listing()).toEqual([]);
47
+ expect(await listing()).toEqual(["a"]);
48
+ expect(await listing()).toEqual(["a"]);
49
+ expect(calls).toBe(2);
50
+ });
51
+ });
@@ -0,0 +1,22 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { formatMCPResponse } from "../mcp-format.js";
3
+
4
+ describe("formatMCPResponse", () => {
5
+ it("attaches structuredContent for object data, mirroring the text block", () => {
6
+ const data = { found: true, path: "getting-started", type: "file" };
7
+ const res = formatMCPResponse(data);
8
+ expect(res.content[0]!.text).toBe(JSON.stringify(data, null, 2));
9
+ expect(res.structuredContent).toEqual(data);
10
+ });
11
+
12
+ it("omits structuredContent for string data", () => {
13
+ const res = formatMCPResponse("plain text");
14
+ expect(res.content[0]!.text).toBe("plain text");
15
+ expect(res.structuredContent).toBeUndefined();
16
+ });
17
+
18
+ it("omits structuredContent for array data", () => {
19
+ const res = formatMCPResponse([1, 2, 3] as any);
20
+ expect(res.structuredContent).toBeUndefined();
21
+ });
22
+ });
@@ -85,7 +85,7 @@ describe("sanitizePath", () => {
85
85
  },
86
86
  { input: "guides/tools", expected: "guides/tools" },
87
87
  { input: "docs/index", expected: "docs/index" },
88
- { input: "examples/with-ai-sdk-v6", expected: "examples/with-ai-sdk-v6" },
88
+ { input: "examples/with-ai-sdk-v7", expected: "examples/with-ai-sdk-v7" },
89
89
  { input: "./current-dir-file", expected: "current-dir-file" },
90
90
  {
91
91
  input: "deeply/nested/path/to/file",
@@ -0,0 +1,105 @@
1
+ import { logger } from "../utils/logger.js";
2
+ import { FALLBACK_CATALOG, FALLBACK_NOTE } from "./fallback-catalog.js";
3
+ import type { XuluxCatalog, XuluxCatalogResult } from "./types.js";
4
+
5
+ export const DEFAULT_CATALOG_URL =
6
+ "https://www.assistant-ui.com/api/xulux/mcp-catalog";
7
+
8
+ const CATALOG_TTL_MS = 5 * 60 * 1000;
9
+
10
+ export function getCatalogUrl(): string {
11
+ const override = process.env.XULUX_CATALOG_URL?.trim();
12
+ return override || DEFAULT_CATALOG_URL;
13
+ }
14
+
15
+ function validateCatalog(data: unknown): XuluxCatalog {
16
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
17
+ throw new Error("Catalog response is not a JSON object.");
18
+ }
19
+ const catalog = data as XuluxCatalog;
20
+ if (catalog.version !== 1) {
21
+ throw new Error(
22
+ `Unsupported catalog version: ${String(catalog.version)}. Expected 1.`,
23
+ );
24
+ }
25
+ if (!Array.isArray(catalog.templates)) {
26
+ throw new Error("Catalog response has no templates array.");
27
+ }
28
+ for (const template of catalog.templates) {
29
+ if (
30
+ !template ||
31
+ typeof template !== "object" ||
32
+ typeof template.id !== "string" ||
33
+ typeof template.templateId !== "string" ||
34
+ (template.kind !== "template" && template.kind !== "example")
35
+ ) {
36
+ throw new Error(
37
+ "Catalog response contains a malformed template entry (missing id/templateId/kind).",
38
+ );
39
+ }
40
+ }
41
+ return catalog;
42
+ }
43
+
44
+ interface CacheEntry {
45
+ catalog: XuluxCatalog;
46
+ fetchedAt: number;
47
+ url: string;
48
+ }
49
+
50
+ let cache: CacheEntry | null = null;
51
+
52
+ export function clearCatalogCache(): void {
53
+ cache = null;
54
+ }
55
+
56
+ async function fetchCatalog(url: string): Promise<XuluxCatalog> {
57
+ const res = await fetch(url, {
58
+ headers: { Accept: "application/json" },
59
+ });
60
+ if (!res.ok) {
61
+ throw new Error(`Catalog fetch failed: HTTP ${res.status}`);
62
+ }
63
+ const data: unknown = await res.json();
64
+ return validateCatalog(data);
65
+ }
66
+
67
+ /**
68
+ * Returns the assistant-ui template catalog. Uses an in-memory five-minute cache, and falls
69
+ * back to a minimal bundled catalog (marked degraded) when the live fetch
70
+ * fails and no cached copy exists.
71
+ */
72
+ export async function getXuluxCatalog(): Promise<XuluxCatalogResult> {
73
+ const url = getCatalogUrl();
74
+ const now = Date.now();
75
+
76
+ if (cache && cache.url === url && now - cache.fetchedAt < CATALOG_TTL_MS) {
77
+ return { catalog: cache.catalog, degraded: false };
78
+ }
79
+
80
+ try {
81
+ const catalog = await fetchCatalog(url);
82
+ cache = { catalog, fetchedAt: now, url };
83
+ return { catalog, degraded: false };
84
+ } catch (error) {
85
+ const message = error instanceof Error ? error.message : String(error);
86
+ logger.warn(
87
+ `assistant-ui template catalog fetch failed (${url}): ${message}`,
88
+ );
89
+
90
+ // Prefer a stale cached copy over the minimal fallback.
91
+ if (cache && cache.url === url) {
92
+ return {
93
+ catalog: cache.catalog,
94
+ degraded: true,
95
+ degradedReason: `Live catalog refresh failed (${message}); using previously fetched catalog.`,
96
+ };
97
+ }
98
+
99
+ return {
100
+ catalog: FALLBACK_CATALOG,
101
+ degraded: true,
102
+ degradedReason: `${message} ${FALLBACK_NOTE}`,
103
+ };
104
+ }
105
+ }
@@ -0,0 +1,63 @@
1
+ import {
2
+ XULUX_MCP_CATALOG_VERSION,
3
+ type XuluxCatalog,
4
+ type XuluxCatalogTemplate,
5
+ } from "./types.js";
6
+
7
+ // Minimal bundled fallback catalog for degraded mode. It supports discovery
8
+ // (ids + names) only. Full authoring details and preview/download URLs
9
+ // require the live catalog endpoint. Do NOT grow this into a copy of the
10
+ // full template knowledge base — the docs endpoint owns freshness.
11
+
12
+ export const FALLBACK_NOTE =
13
+ "The live assistant-ui template catalog could not be fetched, so this is minimal bundled fallback data. " +
14
+ "Template details, previews, and downloads require live catalog access. " +
15
+ "Check network access to the docs site or set XULUX_CATALOG_URL to a reachable catalog endpoint.";
16
+
17
+ const fallbackTemplate = (
18
+ overrides: Pick<XuluxCatalogTemplate, "id" | "kind" | "name" | "summary">,
19
+ ): XuluxCatalogTemplate => ({
20
+ templateId: overrides.id,
21
+ versionId: null,
22
+ assistantPlacement: "unavailable in fallback mode",
23
+ features: [],
24
+ customizable: [],
25
+ versions: [],
26
+ ...overrides,
27
+ });
28
+
29
+ export const FALLBACK_CATALOG: XuluxCatalog = {
30
+ version: XULUX_MCP_CATALOG_VERSION,
31
+ generatedAt: "fallback",
32
+ docsOrigin: "https://www.assistant-ui.com",
33
+ templates: [
34
+ fallbackTemplate({
35
+ id: "base-assistant-ui",
36
+ kind: "template",
37
+ name: "Configurable Base Assistant UI",
38
+ summary:
39
+ "Hosted configurable assistant-ui Base chat template with threads, composer, suggestions, and no-key demo flows.",
40
+ }),
41
+ fallbackTemplate({
42
+ id: "webpage-assistant",
43
+ kind: "template",
44
+ name: "Webpage with Assistant",
45
+ summary:
46
+ "Docs/website layout with a sidebar or modal assistant grounded in configurable pages.",
47
+ }),
48
+ fallbackTemplate({
49
+ id: "product-page-assistant",
50
+ kind: "template",
51
+ name: "Product Page with Floating Assistant",
52
+ summary:
53
+ "Mock product dashboard with a floating modal support assistant and analyze-then-handoff flow.",
54
+ }),
55
+ fallbackTemplate({
56
+ id: "expo-react-native",
57
+ kind: "example",
58
+ name: "Expo React Native Assistant",
59
+ summary:
60
+ "Mobile AI chat app built with Expo and assistant-ui React Native primitives.",
61
+ }),
62
+ ],
63
+ };
@@ -0,0 +1,56 @@
1
+ const SANDBOX_FETCH_HEADERS = {
2
+ Accept: "application/json, application/zip, application/octet-stream, */*",
3
+ // Blaxel preview hosts intermittently reset Node's default fetch path.
4
+ "User-Agent": "curl/8.7.1",
5
+ };
6
+
7
+ const MAX_ATTEMPTS = 3;
8
+ const RETRY_DELAY_MS = 300;
9
+
10
+ function isRetryableFetchError(error: unknown) {
11
+ if (!(error instanceof Error)) return false;
12
+ const cause = error.cause as { code?: string } | undefined;
13
+ const code = cause?.code ?? error.message;
14
+ return (
15
+ code === "ECONNRESET" ||
16
+ code === "ETIMEDOUT" ||
17
+ code === "ECONNREFUSED" ||
18
+ code === "fetch failed"
19
+ );
20
+ }
21
+
22
+ function sleep(ms: number) {
23
+ return new Promise((resolve) => setTimeout(resolve, ms));
24
+ }
25
+
26
+ function mergeHeaders(headers?: HeadersInit): Headers {
27
+ const merged = new Headers(SANDBOX_FETCH_HEADERS);
28
+ if (!headers) return merged;
29
+ new Headers(headers).forEach((value, key) => merged.set(key, value));
30
+ return merged;
31
+ }
32
+
33
+ export async function fetchSandboxResource(
34
+ url: string | URL,
35
+ init?: RequestInit,
36
+ ): Promise<Response> {
37
+ let lastError: unknown;
38
+
39
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
40
+ try {
41
+ return await fetch(url, {
42
+ ...init,
43
+ cache: "no-store",
44
+ headers: mergeHeaders(init?.headers),
45
+ });
46
+ } catch (error) {
47
+ lastError = error;
48
+ if (!isRetryableFetchError(error) || attempt === MAX_ATTEMPTS) {
49
+ throw error;
50
+ }
51
+ await sleep(RETRY_DELAY_MS * attempt);
52
+ }
53
+ }
54
+
55
+ throw lastError;
56
+ }