@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.
- package/.docs/organized/code-examples/waterfall.md +8 -9
- package/.docs/organized/code-examples/with-a2a.md +16 -11
- package/.docs/organized/code-examples/with-ag-ui.md +16 -11
- package/.docs/organized/code-examples/{with-ai-sdk-v6.md → with-ai-sdk-v7.md} +32 -21
- package/.docs/organized/code-examples/with-artifacts.md +17 -10
- package/.docs/organized/code-examples/with-assistant-transport.md +15 -8
- package/.docs/organized/code-examples/with-browser-extension.md +15 -8
- package/.docs/organized/code-examples/with-chain-of-thought.md +17 -10
- package/.docs/organized/code-examples/with-cloud-standalone.md +10 -9
- package/.docs/organized/code-examples/with-cloud.md +18 -13
- package/.docs/organized/code-examples/with-custom-thread-list.md +17 -10
- package/.docs/organized/code-examples/with-elevenlabs-conversational.md +20 -13
- package/.docs/organized/code-examples/with-elevenlabs-scribe.md +20 -13
- package/.docs/organized/code-examples/with-eve.md +16 -9
- package/.docs/organized/code-examples/with-expo.md +26 -23
- package/.docs/organized/code-examples/with-external-store.md +16 -11
- package/.docs/organized/code-examples/with-ffmpeg.md +18 -13
- package/.docs/organized/code-examples/with-generative-ui.md +20 -15
- package/.docs/organized/code-examples/with-google-adk.md +16 -9
- package/.docs/organized/code-examples/with-heat-graph.md +8 -9
- package/.docs/organized/code-examples/with-image-generation.md +17 -10
- package/.docs/organized/code-examples/with-interactables.md +19 -15
- package/.docs/organized/code-examples/with-langchain.md +17 -10
- package/.docs/organized/code-examples/with-langgraph.md +17 -10
- package/.docs/organized/code-examples/with-livekit.md +21 -14
- package/.docs/organized/code-examples/with-mcp.md +40 -18
- package/.docs/organized/code-examples/with-opencode.md +22 -17
- package/.docs/organized/code-examples/with-pi.md +47 -12
- package/.docs/organized/code-examples/with-react-hook-form.md +19 -14
- package/.docs/organized/code-examples/with-react-ink-web.md +6 -6
- package/.docs/organized/code-examples/with-react-ink.md +2 -2
- package/.docs/organized/code-examples/with-react-router.md +20 -15
- package/.docs/organized/code-examples/with-resumable-stream.md +19 -12
- package/.docs/organized/code-examples/with-store.md +8 -9
- package/.docs/organized/code-examples/with-tanstack.md +17 -11
- package/.docs/organized/code-examples/with-tap-runtime.md +16 -11
- package/.docs/organized/code-examples/with-virtualized-thread.md +17 -12
- package/.docs/raw/docs/(docs)/base-ui.mdx +39 -0
- package/.docs/raw/docs/(docs)/cli.mdx +18 -2
- package/.docs/raw/docs/(docs)/installation.mdx +15 -1
- package/.docs/raw/docs/(docs)/rtl.mdx +2 -4
- package/.docs/raw/docs/(reference)/api-reference/generative-ui/actions.mdx +56 -0
- package/.docs/raw/docs/(reference)/api-reference/generative-ui/components.mdx +86 -0
- package/.docs/raw/docs/(reference)/api-reference/generative-ui/index.mdx +19 -1
- package/.docs/raw/docs/(reference)/api-reference/generative-ui/json-generative-ui.mdx +42 -0
- package/.docs/raw/docs/(reference)/api-reference/generative-ui/rendering.mdx +53 -2
- package/.docs/raw/docs/(reference)/api-reference/generative-ui/slack.mdx +81 -0
- package/.docs/raw/docs/(reference)/api-reference/generative-ui/teams.mdx +86 -0
- package/.docs/raw/docs/(reference)/api-reference/generative-ui/tokens.mdx +62 -0
- package/.docs/raw/docs/(reference)/api-reference/integrations/index.mdx +4 -1
- package/.docs/raw/docs/(reference)/api-reference/integrations/react-ai-sdk.mdx +4 -2
- package/.docs/raw/docs/(reference)/api-reference/integrations/react-data-stream.mdx +37 -0
- package/.docs/raw/docs/(reference)/api-reference/overview.mdx +1 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/assistant-if.mdx +29 -29
- package/.docs/raw/docs/(reference)/api-reference/primitives/composition.mdx +1 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/index.mdx +3 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/selection-toolbar.mdx +2 -0
- package/.docs/raw/docs/(reference)/api-reference/tools/status.mdx +0 -2
- package/.docs/raw/docs/(reference)/api-reference/tools/toolkits.mdx +12 -9
- package/.docs/raw/docs/(reference)/api-reference/utilities/miscellaneous.mdx +4 -0
- package/.docs/raw/docs/cloud/ai-sdk-assistant-ui.mdx +7 -1
- package/.docs/raw/docs/cloud/langgraph.mdx +4 -2
- package/.docs/raw/docs/guides/chatgpt-subscription.mdx +108 -0
- package/.docs/raw/docs/guides/dictation.mdx +185 -257
- package/.docs/raw/docs/guides/index.mdx +10 -0
- package/.docs/raw/docs/guides/mentions.mdx +31 -3
- package/.docs/raw/docs/guides/resumable-streams.mdx +12 -1
- package/.docs/raw/docs/guides/speech.mdx +47 -29
- package/.docs/raw/docs/guides/suggestions.mdx +70 -1
- package/.docs/raw/docs/guides/voice.mdx +197 -267
- package/.docs/raw/docs/ink/primitives.mdx +35 -1
- package/.docs/raw/docs/integrations/frameworks/ai-sdk.mdx +13 -7
- package/.docs/raw/docs/integrations/frameworks/cloudflare-agents/overview.mdx +3 -3
- package/.docs/raw/docs/integrations/frameworks/mastra/full-stack.mdx +1 -1
- package/.docs/raw/docs/integrations/frameworks/mastra/overview.mdx +3 -3
- package/.docs/raw/docs/integrations/frameworks/mastra/separate-server.mdx +1 -1
- package/.docs/raw/docs/integrations/gateways/index.mdx +2 -2
- package/.docs/raw/docs/integrations/index.mdx +1 -1
- package/.docs/raw/docs/integrations/observability/helicone.mdx +1 -1
- package/.docs/raw/docs/integrations/observability/langfuse.mdx +1 -1
- package/.docs/raw/docs/integrations/observability/langsmith.mdx +3 -3
- package/.docs/raw/docs/migrations/index.mdx +50 -0
- package/.docs/raw/docs/migrations/react-langgraph-v0-7.mdx +2 -2
- package/.docs/raw/docs/migrations/toolkit-tools.mdx +4 -2
- package/.docs/raw/docs/primitives/chain-of-thought.mdx +6 -1
- package/.docs/raw/docs/primitives/composer.mdx +16 -0
- package/.docs/raw/docs/primitives/selection-toolbar.mdx +25 -0
- package/.docs/raw/docs/primitives/thread-list.mdx +30 -6
- package/.docs/raw/docs/react-native/primitives.mdx +23 -0
- package/.docs/raw/docs/runtimes/a2a/quickstart.mdx +1 -1
- package/.docs/raw/docs/runtimes/ag-ui/agent-state.mdx +124 -0
- package/.docs/raw/docs/runtimes/ag-ui/quickstart.mdx +1 -1
- package/.docs/raw/docs/runtimes/ag-ui/runtime-options.mdx +13 -2
- package/.docs/raw/docs/runtimes/ai-sdk/overview.mdx +13 -4
- package/.docs/raw/docs/runtimes/ai-sdk/v4-legacy.mdx +11 -11
- package/.docs/raw/docs/runtimes/ai-sdk/v5-legacy.mdx +5 -5
- package/.docs/raw/docs/runtimes/ai-sdk/{v6.mdx → v6-legacy.mdx} +8 -6
- package/.docs/raw/docs/runtimes/ai-sdk/v7.mdx +717 -0
- package/.docs/raw/docs/runtimes/concepts/adapters.mdx +1 -1
- package/.docs/raw/docs/runtimes/concepts/architecture.mdx +1 -1
- package/.docs/raw/docs/runtimes/custom/data-stream.mdx +10 -19
- package/.docs/raw/docs/runtimes/custom/external-store.mdx +1 -1
- package/.docs/raw/docs/runtimes/custom/local-runtime.mdx +2 -0
- package/.docs/raw/docs/runtimes/eve/overview.mdx +1 -1
- package/.docs/raw/docs/runtimes/google-adk/quickstart.mdx +1 -1
- package/.docs/raw/docs/runtimes/langchain.mdx +2 -2
- package/.docs/raw/docs/runtimes/langgraph/agent-state.mdx +181 -0
- package/.docs/raw/docs/runtimes/opencode/hooks.mdx +1 -1
- package/.docs/raw/docs/tools/backend.mdx +6 -3
- package/.docs/raw/docs/tools/defining-tools.mdx +12 -1
- package/.docs/raw/docs/tools/generative-ui.mdx +95 -0
- package/.docs/raw/docs/tools/mcp-apps.mdx +37 -10
- package/.docs/raw/docs/tools/mcp.mdx +105 -3
- package/.docs/raw/docs/tools/tool-ui.mdx +6 -4
- package/.docs/raw/docs/tools/user-managed-mcp.mdx +41 -4
- package/.docs/raw/docs/ui/accordion.mdx +16 -10
- package/.docs/raw/docs/ui/assistant-modal.mdx +8 -4
- package/.docs/raw/docs/ui/attachment.mdx +5 -1
- package/.docs/raw/docs/ui/badge.mdx +23 -12
- package/.docs/raw/docs/ui/follow-up-suggestions.mdx +80 -0
- package/.docs/raw/docs/ui/model-selector.mdx +69 -6
- package/.docs/raw/docs/ui/select.mdx +22 -14
- package/.docs/raw/docs/ui/sources.mdx +1 -1
- package/.docs/raw/docs/ui/tabs.mdx +25 -14
- package/.docs/raw/docs/utilities/heat-graph.mdx +2 -2
- package/README.md +1 -1
- package/dist/constants.d.ts +2 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +2 -1
- package/dist/constants.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -1
- package/dist/prepare-docs/code-examples.d.ts.map +1 -1
- package/dist/prepare-docs/copy-raw.d.ts.map +1 -1
- package/dist/prepare-docs/copy-raw.js +0 -4
- package/dist/prepare-docs/copy-raw.js.map +1 -1
- package/dist/prompts/xulux-playground.d.ts +12 -0
- package/dist/prompts/xulux-playground.d.ts.map +1 -0
- package/dist/prompts/xulux-playground.js +33 -0
- package/dist/prompts/xulux-playground.js.map +1 -0
- package/dist/tools/docs.d.ts +2 -4
- package/dist/tools/docs.d.ts.map +1 -1
- package/dist/tools/docs.js +38 -10
- package/dist/tools/docs.js.map +1 -1
- package/dist/tools/examples.d.ts +5 -5
- package/dist/tools/examples.d.ts.map +1 -1
- package/dist/tools/examples.js +10 -7
- package/dist/tools/examples.js.map +1 -1
- package/dist/tools/resources.d.ts +6 -0
- package/dist/tools/resources.d.ts.map +1 -0
- package/dist/tools/resources.js +74 -0
- package/dist/tools/resources.js.map +1 -0
- package/dist/tools/search.d.ts +30 -0
- package/dist/tools/search.d.ts.map +1 -0
- package/dist/tools/search.js +39 -0
- package/dist/tools/search.js.map +1 -0
- package/dist/tools/tests/test-setup.d.ts.map +1 -1
- package/dist/tools/tests/test-setup.js +7 -1
- package/dist/tools/tests/test-setup.js.map +1 -1
- package/dist/tools/xulux-templates.d.ts +72 -0
- package/dist/tools/xulux-templates.d.ts.map +1 -0
- package/dist/tools/xulux-templates.js +82 -0
- package/dist/tools/xulux-templates.js.map +1 -0
- package/dist/utils/cache.d.ts +5 -0
- package/dist/utils/cache.d.ts.map +1 -0
- package/dist/utils/cache.js +18 -0
- package/dist/utils/cache.js.map +1 -0
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/mcp-format.d.ts +1 -0
- package/dist/utils/mcp-format.d.ts.map +1 -1
- package/dist/utils/mcp-format.js +7 -4
- package/dist/utils/mcp-format.js.map +1 -1
- package/dist/utils/mdx.d.ts +2 -1
- package/dist/utils/mdx.d.ts.map +1 -1
- package/dist/utils/mdx.js +19 -2
- package/dist/utils/mdx.js.map +1 -1
- package/dist/utils/paths.d.ts +2 -1
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +20 -1
- package/dist/utils/paths.js.map +1 -1
- package/dist/utils/search.d.ts +10 -0
- package/dist/utils/search.d.ts.map +1 -0
- package/dist/utils/search.js +97 -0
- package/dist/utils/search.js.map +1 -0
- package/dist/utils/security.d.ts.map +1 -1
- package/dist/xulux/catalog-client.d.ts +14 -0
- package/dist/xulux/catalog-client.d.ts.map +1 -0
- package/dist/xulux/catalog-client.js +67 -0
- package/dist/xulux/catalog-client.js.map +1 -0
- package/dist/xulux/fallback-catalog.d.ts +7 -0
- package/dist/xulux/fallback-catalog.d.ts.map +1 -0
- package/dist/xulux/fallback-catalog.js +47 -0
- package/dist/xulux/fallback-catalog.js.map +1 -0
- package/dist/xulux/fetch-sandbox.d.ts +5 -0
- package/dist/xulux/fetch-sandbox.d.ts.map +1 -0
- package/dist/xulux/fetch-sandbox.js +40 -0
- package/dist/xulux/fetch-sandbox.js.map +1 -0
- package/dist/xulux/template-service.d.ts +84 -0
- package/dist/xulux/template-service.d.ts.map +1 -0
- package/dist/xulux/template-service.js +223 -0
- package/dist/xulux/template-service.js.map +1 -0
- package/dist/xulux/types.d.ts +55 -0
- package/dist/xulux/types.d.ts.map +1 -0
- package/dist/xulux/types.js +6 -0
- package/dist/xulux/types.js.map +1 -0
- package/package.json +5 -5
- package/src/constants.ts +2 -0
- package/src/index.ts +67 -0
- package/src/prepare-docs/copy-raw.ts +0 -5
- package/src/prompts/xulux-playground.ts +36 -0
- package/src/tools/docs.ts +52 -4
- package/src/tools/examples.ts +18 -11
- package/src/tools/resources.ts +114 -0
- package/src/tools/search.ts +46 -0
- package/src/tools/tests/completions.test.ts +46 -0
- package/src/tools/tests/directory-size-cap.test.ts +50 -0
- package/src/tools/tests/docs.test.ts +20 -0
- package/src/tools/tests/examples.test.ts +5 -5
- package/src/tools/tests/listings-cache.test.ts +19 -0
- package/src/tools/tests/mcp-protocol.test.ts +92 -1
- package/src/tools/tests/resources.test.ts +102 -0
- package/src/tools/tests/search.test.ts +37 -0
- package/src/tools/tests/test-setup.ts +10 -0
- package/src/tools/tests/xulux-templates.test.ts +262 -0
- package/src/tools/xulux-templates.ts +141 -0
- package/src/utils/cache.ts +20 -0
- package/src/utils/mcp-format.ts +8 -6
- package/src/utils/mdx.ts +21 -1
- package/src/utils/paths.ts +25 -0
- package/src/utils/search.ts +131 -0
- package/src/utils/tests/cache.test.ts +51 -0
- package/src/utils/tests/mcp-format.test.ts +22 -0
- package/src/utils/tests/security.test.ts +1 -1
- package/src/xulux/catalog-client.ts +105 -0
- package/src/xulux/fallback-catalog.ts +63 -0
- package/src/xulux/fetch-sandbox.ts +56 -0
- package/src/xulux/template-service.ts +406 -0
- package/src/xulux/types.ts +60 -0
- package/.docs/raw/blog/2024-07-29-hello/index.mdx +0 -64
- package/.docs/raw/blog/2024-09-11/index.mdx +0 -10
- package/.docs/raw/blog/2024-12-15/index.mdx +0 -10
- package/.docs/raw/blog/2025-01-31-changelog/index.mdx +0 -127
- package/.docs/raw/blog/2026-03-launch-week/index.mdx +0 -258
- package/.docs/raw/docs/(reference)/api-reference/hooks/utilities.mdx +0 -464
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { server } from "../../index.js";
|
|
3
|
+
|
|
4
|
+
function handler(method: string) {
|
|
5
|
+
const handlers = (server as any).server._requestHandlers;
|
|
6
|
+
const h = handlers.get(method);
|
|
7
|
+
if (!h) throw new Error(`No handler for ${method}`);
|
|
8
|
+
return h;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe("MCP resources", () => {
|
|
12
|
+
it("lists docs and example resources", async () => {
|
|
13
|
+
const result = await handler("resources/list")(
|
|
14
|
+
{ method: "resources/list", params: {} },
|
|
15
|
+
{},
|
|
16
|
+
);
|
|
17
|
+
expect(result.resources.length).toBeGreaterThan(0);
|
|
18
|
+
expect(
|
|
19
|
+
result.resources.some((r: any) => r.uri.startsWith("aui-docs:///")),
|
|
20
|
+
).toBe(true);
|
|
21
|
+
expect(
|
|
22
|
+
result.resources.some((r: any) => r.uri.startsWith("aui-example:///")),
|
|
23
|
+
).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("reads a documentation resource as markdown", async () => {
|
|
27
|
+
const list = await handler("resources/list")(
|
|
28
|
+
{ method: "resources/list", params: {} },
|
|
29
|
+
{},
|
|
30
|
+
);
|
|
31
|
+
const doc = list.resources.find((r: any) =>
|
|
32
|
+
r.uri.startsWith("aui-docs:///"),
|
|
33
|
+
);
|
|
34
|
+
const read = await handler("resources/read")(
|
|
35
|
+
{ method: "resources/read", params: { uri: doc.uri } },
|
|
36
|
+
{},
|
|
37
|
+
);
|
|
38
|
+
expect(read.contents[0].mimeType).toBe("text/markdown");
|
|
39
|
+
expect(typeof read.contents[0].text).toBe("string");
|
|
40
|
+
expect(read.contents[0].text.length).toBeGreaterThan(0);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("reads a slash-containing documentation resource", async () => {
|
|
44
|
+
const list = await handler("resources/list")(
|
|
45
|
+
{ method: "resources/list", params: {} },
|
|
46
|
+
{},
|
|
47
|
+
);
|
|
48
|
+
const prefix = "aui-docs:///";
|
|
49
|
+
const nested = list.resources.find(
|
|
50
|
+
(r: any) =>
|
|
51
|
+
r.uri.startsWith(prefix) && r.uri.slice(prefix.length).includes("/"),
|
|
52
|
+
);
|
|
53
|
+
expect(nested).toBeDefined();
|
|
54
|
+
const read = await handler("resources/read")(
|
|
55
|
+
{ method: "resources/read", params: { uri: nested.uri } },
|
|
56
|
+
{},
|
|
57
|
+
);
|
|
58
|
+
expect(read.contents[0].mimeType).toBe("text/markdown");
|
|
59
|
+
expect(read.contents[0].text.length).toBeGreaterThan(0);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("errors on an unknown documentation resource", async () => {
|
|
63
|
+
await expect(
|
|
64
|
+
handler("resources/read")(
|
|
65
|
+
{
|
|
66
|
+
method: "resources/read",
|
|
67
|
+
params: { uri: "aui-docs:///definitely-not-a-real-doc" },
|
|
68
|
+
},
|
|
69
|
+
{},
|
|
70
|
+
),
|
|
71
|
+
).rejects.toThrow();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("reads an example resource as markdown", async () => {
|
|
75
|
+
const list = await handler("resources/list")(
|
|
76
|
+
{ method: "resources/list", params: {} },
|
|
77
|
+
{},
|
|
78
|
+
);
|
|
79
|
+
const example = list.resources.find((r: any) =>
|
|
80
|
+
r.uri.startsWith("aui-example:///"),
|
|
81
|
+
);
|
|
82
|
+
const read = await handler("resources/read")(
|
|
83
|
+
{ method: "resources/read", params: { uri: example.uri } },
|
|
84
|
+
{},
|
|
85
|
+
);
|
|
86
|
+
expect(read.contents[0].mimeType).toBe("text/markdown");
|
|
87
|
+
expect(typeof read.contents[0].text).toBe("string");
|
|
88
|
+
expect(read.contents[0].text.length).toBeGreaterThan(0);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("errors on an unknown example resource", async () => {
|
|
92
|
+
await expect(
|
|
93
|
+
handler("resources/read")(
|
|
94
|
+
{
|
|
95
|
+
method: "resources/read",
|
|
96
|
+
params: { uri: "aui-example:///definitely-not-a-real-example" },
|
|
97
|
+
},
|
|
98
|
+
{},
|
|
99
|
+
),
|
|
100
|
+
).rejects.toThrow();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { testContext } from "./test-setup.js";
|
|
3
|
+
|
|
4
|
+
describe("assistantUISearch", () => {
|
|
5
|
+
it("returns ranked matches for a known term", async () => {
|
|
6
|
+
const result = await testContext.callTool("assistantUISearch", {
|
|
7
|
+
query: "thread",
|
|
8
|
+
});
|
|
9
|
+
expect(result.total).toBeGreaterThan(0);
|
|
10
|
+
expect(result.results[0].path).toBeDefined();
|
|
11
|
+
expect(result.results[0].title).toBeDefined();
|
|
12
|
+
expect(result.results[0].snippet).toBeDefined();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("returns no matches for a nonsense query", async () => {
|
|
16
|
+
const result = await testContext.callTool("assistantUISearch", {
|
|
17
|
+
query: "zzzqqxnotarealdocterm",
|
|
18
|
+
});
|
|
19
|
+
expect(result.total).toBe(0);
|
|
20
|
+
expect(result.results).toHaveLength(0);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("respects the limit", async () => {
|
|
24
|
+
const result = await testContext.callTool("assistantUISearch", {
|
|
25
|
+
query: "ui",
|
|
26
|
+
limit: 3,
|
|
27
|
+
});
|
|
28
|
+
expect(result.results.length).toBeLessThanOrEqual(3);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("defaults to at most 10 results when no limit is given", async () => {
|
|
32
|
+
const result = await testContext.callTool("assistantUISearch", {
|
|
33
|
+
query: "ui",
|
|
34
|
+
});
|
|
35
|
+
expect(result.results.length).toBeLessThanOrEqual(10);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -4,10 +4,20 @@ import { join } from "node:path";
|
|
|
4
4
|
import { PACKAGE_DIR } from "../../constants.js";
|
|
5
5
|
import { docsTools } from "../docs.js";
|
|
6
6
|
import { examplesTools } from "../examples.js";
|
|
7
|
+
import { searchTools } from "../search.js";
|
|
8
|
+
import {
|
|
9
|
+
xuluxTemplatesListTool,
|
|
10
|
+
xuluxTemplateDetailsTool,
|
|
11
|
+
xuluxTemplatePreviewTool,
|
|
12
|
+
} from "../xulux-templates.js";
|
|
7
13
|
|
|
8
14
|
const tools = {
|
|
9
15
|
assistantUIDocs: docsTools,
|
|
10
16
|
assistantUIExamples: examplesTools,
|
|
17
|
+
assistantUISearch: searchTools,
|
|
18
|
+
assistantUITemplates: xuluxTemplatesListTool,
|
|
19
|
+
assistantUITemplateDetails: xuluxTemplateDetailsTool,
|
|
20
|
+
assistantUITemplatePreview: xuluxTemplatePreviewTool,
|
|
11
21
|
};
|
|
12
22
|
|
|
13
23
|
export const testContext = {
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { clearCatalogCache } from "../../xulux/catalog-client.js";
|
|
3
|
+
import {
|
|
4
|
+
xuluxTemplatesListTool,
|
|
5
|
+
xuluxTemplateDetailsTool,
|
|
6
|
+
xuluxTemplatePreviewTool,
|
|
7
|
+
} from "../xulux-templates.js";
|
|
8
|
+
|
|
9
|
+
const LIVE_CATALOG = {
|
|
10
|
+
version: 1,
|
|
11
|
+
generatedAt: "2026-07-10T00:00:00.000Z",
|
|
12
|
+
docsOrigin: "https://www.assistant-ui.com",
|
|
13
|
+
templates: [
|
|
14
|
+
{
|
|
15
|
+
id: "base-assistant-ui",
|
|
16
|
+
templateId: "base-assistant-ui",
|
|
17
|
+
versionId: "default",
|
|
18
|
+
kind: "template",
|
|
19
|
+
name: "Configurable Base Assistant UI",
|
|
20
|
+
summary: "Base chat template",
|
|
21
|
+
assistantPlacement: "full-page chat shell",
|
|
22
|
+
features: ["threads", "composer"],
|
|
23
|
+
customizable: ["assistant", "brandTheme"],
|
|
24
|
+
versions: [
|
|
25
|
+
{
|
|
26
|
+
id: "default",
|
|
27
|
+
entryId: "base-assistant-ui-default",
|
|
28
|
+
name: "Default",
|
|
29
|
+
description: "Default base chat",
|
|
30
|
+
previewUrl: "https://base.example.com",
|
|
31
|
+
downloadUrl: "https://base.example.com/api/download",
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
previewUrl: "https://base.example.com",
|
|
35
|
+
downloadUrl: "https://base.example.com/api/download",
|
|
36
|
+
sandboxBaseUrl: "https://base.example.com",
|
|
37
|
+
configRoots: {
|
|
38
|
+
assistant: { description: "Assistant config" },
|
|
39
|
+
},
|
|
40
|
+
rules: {
|
|
41
|
+
required: ["Only pass documented top-level config roots."],
|
|
42
|
+
},
|
|
43
|
+
tools: {
|
|
44
|
+
builtIn: [{ id: "getWeather" }],
|
|
45
|
+
customToolSupported: true,
|
|
46
|
+
renderers: [{ type: "generic" }],
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "base",
|
|
51
|
+
templateId: "base",
|
|
52
|
+
versionId: null,
|
|
53
|
+
kind: "example",
|
|
54
|
+
name: "Base Demo",
|
|
55
|
+
summary: "Fixed base demo",
|
|
56
|
+
assistantPlacement: "full-page demo route",
|
|
57
|
+
features: ["fixed demo"],
|
|
58
|
+
customizable: [],
|
|
59
|
+
versions: [],
|
|
60
|
+
previewUrl: "https://www.assistant-ui.com/demos/base",
|
|
61
|
+
downloadUrl:
|
|
62
|
+
"https://www.assistant-ui.com/api/xulux/demo-download?slug=base",
|
|
63
|
+
rules: {
|
|
64
|
+
required: [
|
|
65
|
+
"This entry is a fixed demo and is not schema-customizable.",
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
function jsonResponse(body: unknown, init?: ResponseInit) {
|
|
73
|
+
return new Response(JSON.stringify(body), {
|
|
74
|
+
status: 200,
|
|
75
|
+
headers: { "Content-Type": "application/json" },
|
|
76
|
+
...init,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function callTool(
|
|
81
|
+
tool:
|
|
82
|
+
| typeof xuluxTemplatesListTool
|
|
83
|
+
| typeof xuluxTemplateDetailsTool
|
|
84
|
+
| typeof xuluxTemplatePreviewTool,
|
|
85
|
+
args: unknown,
|
|
86
|
+
) {
|
|
87
|
+
const result = await tool.execute(args as never);
|
|
88
|
+
const text = result.content?.[0]?.text;
|
|
89
|
+
if (!text) throw new Error(`Tool ${tool.name} returned no text content`);
|
|
90
|
+
return JSON.parse(text);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
describe("assistant-ui template MCP tools", () => {
|
|
94
|
+
beforeEach(() => {
|
|
95
|
+
clearCatalogCache();
|
|
96
|
+
vi.stubEnv("XULUX_CATALOG_URL", "https://catalog.example.com/mcp-catalog");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
afterEach(() => {
|
|
100
|
+
vi.unstubAllEnvs();
|
|
101
|
+
vi.unstubAllGlobals();
|
|
102
|
+
clearCatalogCache();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("lists templates from the live catalog", async () => {
|
|
106
|
+
vi.stubGlobal(
|
|
107
|
+
"fetch",
|
|
108
|
+
vi.fn(async () => jsonResponse(LIVE_CATALOG)),
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const result = await callTool(xuluxTemplatesListTool, {});
|
|
112
|
+
expect(result.catalogDegraded).toBeUndefined();
|
|
113
|
+
expect(result.templates).toHaveLength(2);
|
|
114
|
+
expect(result.templates[0]).toMatchObject({
|
|
115
|
+
id: "base-assistant-ui",
|
|
116
|
+
kind: "template",
|
|
117
|
+
name: "Configurable Base Assistant UI",
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("uses the minimal fallback catalog when the live catalog is unavailable", async () => {
|
|
122
|
+
vi.stubGlobal(
|
|
123
|
+
"fetch",
|
|
124
|
+
vi.fn(async () => {
|
|
125
|
+
throw new Error("network unavailable");
|
|
126
|
+
}),
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const result = await callTool(xuluxTemplatesListTool, {});
|
|
130
|
+
expect(result.catalogDegraded).toBe(true);
|
|
131
|
+
expect(result.catalogDegradedReason).toContain("network unavailable");
|
|
132
|
+
expect(result.templates.map((t: any) => t.id)).toContain(
|
|
133
|
+
"base-assistant-ui",
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("returns details and fetches exampleConfig from the selected sandbox contract", async () => {
|
|
138
|
+
const fetchMock = vi.fn(async (url: string | URL) => {
|
|
139
|
+
const href = String(url);
|
|
140
|
+
if (href === "https://catalog.example.com/mcp-catalog") {
|
|
141
|
+
return jsonResponse(LIVE_CATALOG);
|
|
142
|
+
}
|
|
143
|
+
if (href === "https://base.example.com/api/template/contract?v=default") {
|
|
144
|
+
return jsonResponse({
|
|
145
|
+
exampleCompleteConfig: {
|
|
146
|
+
assistant: { appName: "Example" },
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
throw new Error(`Unexpected fetch: ${href}`);
|
|
151
|
+
});
|
|
152
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
153
|
+
|
|
154
|
+
const result = await callTool(xuluxTemplateDetailsTool, {
|
|
155
|
+
templateId: "base-assistant-ui",
|
|
156
|
+
versionId: "default",
|
|
157
|
+
});
|
|
158
|
+
expect(result.selectedVersionId).toBe("default");
|
|
159
|
+
expect(result.configRoots.assistant).toBeDefined();
|
|
160
|
+
expect(result.exampleConfig).toEqual({
|
|
161
|
+
assistant: { appName: "Example" },
|
|
162
|
+
});
|
|
163
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
164
|
+
"https://base.example.com/api/template/contract?v=default",
|
|
165
|
+
expect.objectContaining({
|
|
166
|
+
cache: "no-store",
|
|
167
|
+
headers: expect.any(Headers),
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
const sandboxCall = fetchMock.mock.calls.find((call) =>
|
|
171
|
+
String(call[0]).includes("/api/template/contract"),
|
|
172
|
+
) as [string | URL, RequestInit] | undefined;
|
|
173
|
+
expect(sandboxCall?.[1].headers).toBeInstanceOf(Headers);
|
|
174
|
+
expect((sandboxCall?.[1].headers as Headers).get("User-Agent")).toBe(
|
|
175
|
+
"curl/8.7.1",
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("returns fixed demo preview URLs and rejects config for fixed demos", async () => {
|
|
180
|
+
vi.stubGlobal(
|
|
181
|
+
"fetch",
|
|
182
|
+
vi.fn(async () => jsonResponse(LIVE_CATALOG)),
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const ok = await callTool(xuluxTemplatePreviewTool, {
|
|
186
|
+
templateId: "base",
|
|
187
|
+
});
|
|
188
|
+
expect(ok).toMatchObject({
|
|
189
|
+
success: true,
|
|
190
|
+
templateId: "base",
|
|
191
|
+
previewUrl: "https://www.assistant-ui.com/demos/base",
|
|
192
|
+
customized: false,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const rejected = await callTool(xuluxTemplatePreviewTool, {
|
|
196
|
+
templateId: "base",
|
|
197
|
+
config: { assistant: { appName: "Nope" } },
|
|
198
|
+
});
|
|
199
|
+
expect(rejected.success).toBe(false);
|
|
200
|
+
expect(rejected.error).toContain("fixed demo");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("creates configured hosted previews through the sandbox session endpoint", async () => {
|
|
204
|
+
const fetchMock = vi.fn(async (url: string | URL, init?: RequestInit) => {
|
|
205
|
+
const href = String(url);
|
|
206
|
+
if (href === "https://catalog.example.com/mcp-catalog") {
|
|
207
|
+
return jsonResponse(LIVE_CATALOG);
|
|
208
|
+
}
|
|
209
|
+
if (href === "https://base.example.com/api/preview/session?v=default") {
|
|
210
|
+
expect(init?.method).toBe("POST");
|
|
211
|
+
expect(init?.body).toBe(
|
|
212
|
+
JSON.stringify({ assistant: { appName: "Custom" } }),
|
|
213
|
+
);
|
|
214
|
+
return jsonResponse({
|
|
215
|
+
previewUrl: "/preview/session-1",
|
|
216
|
+
downloadUrl: "/api/download?session=1",
|
|
217
|
+
validationWarnings: [],
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
throw new Error(`Unexpected fetch: ${href}`);
|
|
221
|
+
});
|
|
222
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
223
|
+
|
|
224
|
+
const result = await callTool(xuluxTemplatePreviewTool, {
|
|
225
|
+
templateId: "base-assistant-ui-default",
|
|
226
|
+
config: { assistant: { appName: "Custom" } },
|
|
227
|
+
});
|
|
228
|
+
expect(result).toMatchObject({
|
|
229
|
+
success: true,
|
|
230
|
+
templateId: "base-assistant-ui",
|
|
231
|
+
versionId: "default",
|
|
232
|
+
previewUrl: "https://base.example.com/preview/session-1?v=default",
|
|
233
|
+
downloadUrl: "https://base.example.com/api/download?session=1&v=default",
|
|
234
|
+
customized: true,
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("matches the web tool shape for configured preview network failures", async () => {
|
|
239
|
+
const fetchMock = vi.fn(async (url: string | URL) => {
|
|
240
|
+
const href = String(url);
|
|
241
|
+
if (href === "https://catalog.example.com/mcp-catalog") {
|
|
242
|
+
return jsonResponse(LIVE_CATALOG);
|
|
243
|
+
}
|
|
244
|
+
if (href === "https://base.example.com/api/preview/session?v=default") {
|
|
245
|
+
throw new Error("fetch failed");
|
|
246
|
+
}
|
|
247
|
+
throw new Error(`Unexpected fetch: ${href}`);
|
|
248
|
+
});
|
|
249
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
250
|
+
|
|
251
|
+
const result = await callTool(xuluxTemplatePreviewTool, {
|
|
252
|
+
templateId: "base-assistant-ui",
|
|
253
|
+
config: { assistant: { appName: "Custom" } },
|
|
254
|
+
});
|
|
255
|
+
expect(result).toEqual({
|
|
256
|
+
success: false,
|
|
257
|
+
templateId: "base-assistant-ui",
|
|
258
|
+
versionId: "default",
|
|
259
|
+
error: "fetch failed",
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { z } from "zod/v3";
|
|
2
|
+
import { logger } from "../utils/logger.js";
|
|
3
|
+
import { formatMCPResponse } from "../utils/mcp-format.js";
|
|
4
|
+
import { getXuluxCatalog } from "../xulux/catalog-client.js";
|
|
5
|
+
import {
|
|
6
|
+
createTemplatePreview,
|
|
7
|
+
getTemplateDetails,
|
|
8
|
+
listTemplates,
|
|
9
|
+
} from "../xulux/template-service.js";
|
|
10
|
+
|
|
11
|
+
const emptyInputSchema = z.object({});
|
|
12
|
+
|
|
13
|
+
const detailsInputSchema = z.object({
|
|
14
|
+
templateId: z.string().describe("The template id from assistantUITemplates"),
|
|
15
|
+
versionId: z
|
|
16
|
+
.string()
|
|
17
|
+
.optional()
|
|
18
|
+
.describe(
|
|
19
|
+
"Optional version id. When provided, exampleConfig reflects that version's resolved defaults.",
|
|
20
|
+
),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const previewInputSchema = z.object({
|
|
24
|
+
templateId: z.string().describe("The template id from assistantUITemplates"),
|
|
25
|
+
versionId: z
|
|
26
|
+
.string()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe("Version id to use. Uses the template default if omitted."),
|
|
29
|
+
config: z
|
|
30
|
+
.record(z.string(), z.unknown())
|
|
31
|
+
.optional()
|
|
32
|
+
.describe(
|
|
33
|
+
"Customization config for the preview. Must contain only the top-level keys: hostUi, assistant, and brandTheme. " +
|
|
34
|
+
"Use the schemas from assistantUITemplateDetails.configRoots as the source of truth for each root. " +
|
|
35
|
+
"Do not pass any other root keys.",
|
|
36
|
+
),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
function withDegradedNote<T extends Record<string, unknown>>(
|
|
40
|
+
payload: T,
|
|
41
|
+
degraded: boolean,
|
|
42
|
+
degradedReason?: string,
|
|
43
|
+
): T & { catalogDegraded?: true; catalogDegradedReason?: string } {
|
|
44
|
+
if (!degraded) return payload;
|
|
45
|
+
return {
|
|
46
|
+
...payload,
|
|
47
|
+
catalogDegraded: true,
|
|
48
|
+
...(degradedReason ? { catalogDegradedReason: degradedReason } : {}),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const xuluxTemplatesListTool = {
|
|
53
|
+
name: "assistantUITemplates",
|
|
54
|
+
description:
|
|
55
|
+
"List the available assistant-ui hosted app templates and fixed demos with their features, customizable surfaces, and versions. " +
|
|
56
|
+
"Call this first for any assistant-ui app-building request. Use the features and customizable fields to decide which template fits. " +
|
|
57
|
+
"If customizable is empty, the entry is a fixed demo that should be used as-is rather than configured. " +
|
|
58
|
+
"Call assistantUITemplateDetails on the chosen template before requesting a preview.",
|
|
59
|
+
parameters: emptyInputSchema.shape,
|
|
60
|
+
execute: async (_args: z.infer<typeof emptyInputSchema>) => {
|
|
61
|
+
logger.info("Listing assistant-ui templates");
|
|
62
|
+
try {
|
|
63
|
+
const { catalog, degraded, degradedReason } = await getXuluxCatalog();
|
|
64
|
+
return formatMCPResponse(
|
|
65
|
+
withDegradedNote(listTemplates(catalog), degraded, degradedReason),
|
|
66
|
+
);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
logger.error("Failed to list assistant-ui templates", error);
|
|
69
|
+
return formatMCPResponse({
|
|
70
|
+
error: "Failed to list assistant-ui templates",
|
|
71
|
+
message: error instanceof Error ? error.message : String(error),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const xuluxTemplateDetailsTool = {
|
|
78
|
+
name: "assistantUITemplateDetails",
|
|
79
|
+
description:
|
|
80
|
+
"Get the full authoring surface for a specific assistant-ui hosted template. " +
|
|
81
|
+
"Returns configRoots schemas (with types, defaults, and enums), rules, built-in tool input/outputShape, renderer contracts, and an exampleConfig. " +
|
|
82
|
+
"Fixed demos return no configRoots; use those as-is without config. " +
|
|
83
|
+
"Use this before calling assistantUITemplatePreview to understand exactly what config to write. " +
|
|
84
|
+
"If assistantUITemplatePreview returns validationWarnings, call this again and cross-reference configRoots to correct the config.",
|
|
85
|
+
parameters: detailsInputSchema.shape,
|
|
86
|
+
execute: async (args: z.infer<typeof detailsInputSchema>) => {
|
|
87
|
+
logger.info(
|
|
88
|
+
`Getting assistant-ui template details for: ${args.templateId}`,
|
|
89
|
+
);
|
|
90
|
+
try {
|
|
91
|
+
const { catalog, degraded, degradedReason } = await getXuluxCatalog();
|
|
92
|
+
const details = await getTemplateDetails(catalog, args);
|
|
93
|
+
return formatMCPResponse(
|
|
94
|
+
withDegradedNote(
|
|
95
|
+
details as unknown as Record<string, unknown>,
|
|
96
|
+
degraded,
|
|
97
|
+
degradedReason,
|
|
98
|
+
),
|
|
99
|
+
);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
logger.error("Failed to get assistant-ui template details", error);
|
|
102
|
+
return formatMCPResponse({
|
|
103
|
+
error: "Failed to get assistant-ui template details",
|
|
104
|
+
message: error instanceof Error ? error.message : String(error),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export const xuluxTemplatePreviewTool = {
|
|
111
|
+
name: "assistantUITemplatePreview",
|
|
112
|
+
description:
|
|
113
|
+
"Return preview and download URLs for a selected assistant-ui hosted template. " +
|
|
114
|
+
"The previewUrl is a hosted app URL; show it to the user first, or open it with an available browser/in-app-browser tool if your client provides one. " +
|
|
115
|
+
"If you pass config, a preview session is created on the hosted template sandbox and the returned URLs reflect that configuration. " +
|
|
116
|
+
"Do not pass config for fixed demos that have no configRoots in assistantUITemplateDetails. " +
|
|
117
|
+
"This tool only returns URLs.",
|
|
118
|
+
parameters: previewInputSchema.shape,
|
|
119
|
+
execute: async (args: z.infer<typeof previewInputSchema>) => {
|
|
120
|
+
logger.info(
|
|
121
|
+
`Creating assistant-ui template preview for: ${args.templateId}`,
|
|
122
|
+
);
|
|
123
|
+
try {
|
|
124
|
+
const { catalog, degraded, degradedReason } = await getXuluxCatalog();
|
|
125
|
+
const result = await createTemplatePreview(catalog, args);
|
|
126
|
+
return formatMCPResponse(
|
|
127
|
+
withDegradedNote(
|
|
128
|
+
result as unknown as Record<string, unknown>,
|
|
129
|
+
degraded,
|
|
130
|
+
degradedReason,
|
|
131
|
+
),
|
|
132
|
+
);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
logger.error("Failed to create assistant-ui template preview", error);
|
|
135
|
+
return formatMCPResponse({
|
|
136
|
+
error: "Failed to create assistant-ui template preview",
|
|
137
|
+
message: error instanceof Error ? error.message : String(error),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function cacheListing(
|
|
2
|
+
scan: () => Promise<string[]>,
|
|
3
|
+
): () => Promise<string[]> {
|
|
4
|
+
let cache: Promise<string[]> | null = null;
|
|
5
|
+
return () => {
|
|
6
|
+
if (!cache) {
|
|
7
|
+
cache = scan().then(
|
|
8
|
+
(result) => {
|
|
9
|
+
if (result.length === 0) cache = null;
|
|
10
|
+
return result;
|
|
11
|
+
},
|
|
12
|
+
(error) => {
|
|
13
|
+
cache = null;
|
|
14
|
+
throw error;
|
|
15
|
+
},
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
return cache;
|
|
19
|
+
};
|
|
20
|
+
}
|
package/src/utils/mcp-format.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
export function formatMCPResponse(data: any): {
|
|
2
2
|
content: Array<{ type: "text"; text: string }>;
|
|
3
|
+
structuredContent?: Record<string, unknown>;
|
|
3
4
|
} {
|
|
5
|
+
const text = typeof data === "string" ? data : JSON.stringify(data, null, 2);
|
|
6
|
+
const isStructured =
|
|
7
|
+
typeof data === "object" && data !== null && !Array.isArray(data);
|
|
4
8
|
return {
|
|
5
|
-
content: [
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
},
|
|
10
|
-
],
|
|
9
|
+
content: [{ type: "text", text }],
|
|
10
|
+
...(isStructured && {
|
|
11
|
+
structuredContent: data as Record<string, unknown>,
|
|
12
|
+
}),
|
|
11
13
|
};
|
|
12
14
|
}
|
package/src/utils/mdx.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
1
|
+
import { readFile, lstat } from "node:fs/promises";
|
|
2
2
|
import matter from "gray-matter";
|
|
3
|
+
import { MAX_FILE_SIZE } from "../constants.js";
|
|
3
4
|
import { logger } from "./logger.js";
|
|
4
5
|
|
|
5
6
|
interface MDXContent {
|
|
@@ -32,6 +33,25 @@ export async function readMDXFile(
|
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
export async function readMDXFileSafe(
|
|
37
|
+
filePath: string,
|
|
38
|
+
): Promise<MDXContent | null> {
|
|
39
|
+
try {
|
|
40
|
+
const stats = await lstat(filePath);
|
|
41
|
+
if (stats.isSymbolicLink()) {
|
|
42
|
+
logger.warn(`Attempted to read symlink: ${filePath}`);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
if (stats.size > MAX_FILE_SIZE) {
|
|
46
|
+
logger.warn(`File size exceeds limit: ${filePath} (${stats.size} bytes)`);
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
return readMDXFile(filePath);
|
|
53
|
+
}
|
|
54
|
+
|
|
35
55
|
export function formatMDXContent(mdxContent: MDXContent): string {
|
|
36
56
|
const { content, frontmatter } = mdxContent;
|
|
37
57
|
|
package/src/utils/paths.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readdir, stat } from "node:fs/promises";
|
|
2
2
|
import { join, extname } from "node:path";
|
|
3
3
|
import { DOCS_PATH, MDX_EXTENSION, MD_EXTENSION } from "../constants.js";
|
|
4
|
+
import { cacheListing } from "./cache.js";
|
|
4
5
|
import { logger } from "./logger.js";
|
|
5
6
|
|
|
6
7
|
const SIMILARITY_THRESHOLDS = {
|
|
@@ -66,6 +67,30 @@ export async function getAvailablePaths(): Promise<string[]> {
|
|
|
66
67
|
return paths.sort();
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
export const getAvailableDocFiles = cacheListing(scanDocFiles);
|
|
71
|
+
|
|
72
|
+
async function scanDocFiles(): Promise<string[]> {
|
|
73
|
+
const files: string[] = [];
|
|
74
|
+
|
|
75
|
+
async function scanDirectory(dir: string, prefix = ""): Promise<void> {
|
|
76
|
+
const { directories, files: dirFiles } = await listDirContents(dir);
|
|
77
|
+
|
|
78
|
+
for (const file of dirFiles) {
|
|
79
|
+
if (extname(file) !== MDX_EXTENSION) continue;
|
|
80
|
+
const name = file.replace(MDX_EXTENSION, "");
|
|
81
|
+
files.push(prefix ? `${prefix}/${name}` : name);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const subdir of directories) {
|
|
85
|
+
const newPrefix = prefix ? `${prefix}/${subdir}` : subdir;
|
|
86
|
+
await scanDirectory(join(dir, subdir), newPrefix);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
await scanDirectory(DOCS_PATH);
|
|
91
|
+
return files.sort();
|
|
92
|
+
}
|
|
93
|
+
|
|
69
94
|
export function findNearestPaths(
|
|
70
95
|
requestedPath: string,
|
|
71
96
|
availablePaths: string[],
|