@assistant-ui/mcp-docs-server 0.1.38 → 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 +27 -24
- 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 +15 -8
- 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 +25 -12
- 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 +16 -10
- 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 +17 -1
- 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 +3 -1
- 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 +10 -4
- 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/observability/helicone.mdx +1 -1
- package/.docs/raw/docs/integrations/observability/langfuse.mdx +1 -1
- package/.docs/raw/docs/integrations/observability/langsmith.mdx +1 -1
- package/.docs/raw/docs/migrations/index.mdx +50 -0
- 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/react-native/primitives.mdx +23 -0
- package/.docs/raw/docs/runtimes/ag-ui/agent-state.mdx +124 -0
- package/.docs/raw/docs/runtimes/ag-ui/runtime-options.mdx +10 -1
- 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/langgraph/agent-state.mdx +181 -0
- package/.docs/raw/docs/tools/backend.mdx +6 -3
- package/.docs/raw/docs/tools/defining-tools.mdx +7 -1
- package/.docs/raw/docs/tools/generative-ui.mdx +60 -2
- package/.docs/raw/docs/tools/mcp-apps.mdx +29 -11
- package/.docs/raw/docs/tools/mcp.mdx +100 -3
- package/.docs/raw/docs/tools/tool-ui.mdx +6 -4
- package/.docs/raw/docs/tools/user-managed-mcp.mdx +26 -3
- 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 +2 -2
- package/.docs/raw/docs/ui/model-selector.mdx +32 -2
- 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/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +39 -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/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 +24 -8
- package/dist/tools/docs.js.map +1 -1
- package/dist/tools/examples.d.ts +2 -4
- package/dist/tools/examples.d.ts.map +1 -1
- package/dist/tools/examples.js +9 -6
- package/dist/tools/examples.js.map +1 -1
- package/dist/tools/resources.d.ts +0 -1
- package/dist/tools/resources.d.ts.map +1 -1
- package/dist/tools/search.d.ts +2 -5
- package/dist/tools/search.d.ts.map +1 -1
- package/dist/tools/tests/test-setup.d.ts.map +1 -1
- package/dist/tools/tests/test-setup.js +5 -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.map +1 -1
- package/dist/utils/paths.d.ts +1 -1
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +3 -1
- package/dist/utils/paths.js.map +1 -1
- package/dist/utils/search.d.ts.map +1 -1
- 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/index.ts +53 -0
- package/src/prompts/xulux-playground.ts +36 -0
- package/src/tools/docs.ts +25 -3
- package/src/tools/examples.ts +15 -10
- 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 +81 -1
- package/src/tools/tests/test-setup.ts +8 -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/paths.ts +4 -1
- 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/docs/(reference)/api-reference/hooks/utilities.mdx +0 -464
|
@@ -4,9 +4,13 @@ import {
|
|
|
4
4
|
InitializeRequestSchema,
|
|
5
5
|
ListToolsRequestSchema,
|
|
6
6
|
CallToolRequestSchema,
|
|
7
|
+
ListPromptsRequestSchema,
|
|
8
|
+
GetPromptRequestSchema,
|
|
7
9
|
type InitializeRequest,
|
|
8
10
|
type ListToolsRequest,
|
|
9
11
|
type CallToolRequest,
|
|
12
|
+
type ListPromptsRequest,
|
|
13
|
+
type GetPromptRequest,
|
|
10
14
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
11
15
|
|
|
12
16
|
describe("MCP Protocol Integration", () => {
|
|
@@ -55,7 +59,7 @@ describe("MCP Protocol Integration", () => {
|
|
|
55
59
|
|
|
56
60
|
expect(result).toBeDefined();
|
|
57
61
|
expect(result.tools).toBeInstanceOf(Array);
|
|
58
|
-
expect(result.tools).toHaveLength(
|
|
62
|
+
expect(result.tools).toHaveLength(6);
|
|
59
63
|
|
|
60
64
|
// Check the tools have proper JSON schemas
|
|
61
65
|
const docsTool = result.tools.find(
|
|
@@ -93,6 +97,30 @@ describe("MCP Protocol Integration", () => {
|
|
|
93
97
|
expect(searchTool.title).toBe("Search assistant-ui Documentation");
|
|
94
98
|
expect(searchTool.annotations?.readOnlyHint).toBe(true);
|
|
95
99
|
expect(searchTool.annotations?.openWorldHint).toBe(false);
|
|
100
|
+
|
|
101
|
+
const xuluxListTool = result.tools.find(
|
|
102
|
+
(t: any) => t.name === "assistantUITemplates",
|
|
103
|
+
);
|
|
104
|
+
expect(xuluxListTool).toBeDefined();
|
|
105
|
+
expect(xuluxListTool.title).toBe("assistant-ui Templates");
|
|
106
|
+
expect(xuluxListTool.annotations?.readOnlyHint).toBe(true);
|
|
107
|
+
expect(xuluxListTool.annotations?.openWorldHint).toBe(true);
|
|
108
|
+
|
|
109
|
+
const xuluxDetailsTool = result.tools.find(
|
|
110
|
+
(t: any) => t.name === "assistantUITemplateDetails",
|
|
111
|
+
);
|
|
112
|
+
expect(xuluxDetailsTool).toBeDefined();
|
|
113
|
+
expect(xuluxDetailsTool.title).toBe("assistant-ui Template Details");
|
|
114
|
+
expect(xuluxDetailsTool.annotations?.readOnlyHint).toBe(true);
|
|
115
|
+
expect(xuluxDetailsTool.annotations?.openWorldHint).toBe(true);
|
|
116
|
+
|
|
117
|
+
const xuluxPreviewTool = result.tools.find(
|
|
118
|
+
(t: any) => t.name === "assistantUITemplatePreview",
|
|
119
|
+
);
|
|
120
|
+
expect(xuluxPreviewTool).toBeDefined();
|
|
121
|
+
expect(xuluxPreviewTool.title).toBe("assistant-ui Template Preview URLs");
|
|
122
|
+
expect(xuluxPreviewTool.annotations?.readOnlyHint).toBe(false);
|
|
123
|
+
expect(xuluxPreviewTool.annotations?.openWorldHint).toBe(true);
|
|
96
124
|
});
|
|
97
125
|
|
|
98
126
|
it("should handle CallTool request for assistantUIDocs", async () => {
|
|
@@ -148,4 +176,56 @@ describe("MCP Protocol Integration", () => {
|
|
|
148
176
|
expect(result.content).toBeDefined();
|
|
149
177
|
expect(result.content[0].type).toBe("text");
|
|
150
178
|
});
|
|
179
|
+
|
|
180
|
+
it("should handle ListPrompts request", async () => {
|
|
181
|
+
const request: ListPromptsRequest = {
|
|
182
|
+
method: "prompts/list",
|
|
183
|
+
params: {},
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const parsed = ListPromptsRequestSchema.parse(request);
|
|
187
|
+
expect(parsed).toBeDefined();
|
|
188
|
+
|
|
189
|
+
const handlers = (server as any).server._requestHandlers;
|
|
190
|
+
expect(handlers.get("prompts/list")).toBeDefined();
|
|
191
|
+
|
|
192
|
+
const handler = handlers.get("prompts/list");
|
|
193
|
+
const result = await handler(parsed, {});
|
|
194
|
+
|
|
195
|
+
expect(result.prompts).toBeInstanceOf(Array);
|
|
196
|
+
const prompt = result.prompts.find(
|
|
197
|
+
(p: any) => p.name === "assistant-ui-template-workflow",
|
|
198
|
+
);
|
|
199
|
+
expect(prompt).toBeDefined();
|
|
200
|
+
expect(prompt.title).toBe("assistant-ui Template Workflow");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("should handle GetPrompt request for assistant-ui-template-workflow", async () => {
|
|
204
|
+
const request: GetPromptRequest = {
|
|
205
|
+
method: "prompts/get",
|
|
206
|
+
params: {
|
|
207
|
+
name: "assistant-ui-template-workflow",
|
|
208
|
+
arguments: {},
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const parsed = GetPromptRequestSchema.parse(request);
|
|
213
|
+
expect(parsed).toBeDefined();
|
|
214
|
+
|
|
215
|
+
const handlers = (server as any).server._requestHandlers;
|
|
216
|
+
expect(handlers.get("prompts/get")).toBeDefined();
|
|
217
|
+
|
|
218
|
+
const handler = handlers.get("prompts/get");
|
|
219
|
+
const result = await handler(parsed, {});
|
|
220
|
+
|
|
221
|
+
expect(result.messages).toHaveLength(1);
|
|
222
|
+
expect(result.messages[0].content.type).toBe("text");
|
|
223
|
+
expect(result.messages[0].content.text).toContain("assistantUITemplates");
|
|
224
|
+
expect(result.messages[0].content.text).toContain(
|
|
225
|
+
"assistantUITemplateDetails",
|
|
226
|
+
);
|
|
227
|
+
expect(result.messages[0].content.text).toContain(
|
|
228
|
+
"assistantUITemplatePreview",
|
|
229
|
+
);
|
|
230
|
+
});
|
|
151
231
|
});
|
|
@@ -5,11 +5,19 @@ import { PACKAGE_DIR } from "../../constants.js";
|
|
|
5
5
|
import { docsTools } from "../docs.js";
|
|
6
6
|
import { examplesTools } from "../examples.js";
|
|
7
7
|
import { searchTools } from "../search.js";
|
|
8
|
+
import {
|
|
9
|
+
xuluxTemplatesListTool,
|
|
10
|
+
xuluxTemplateDetailsTool,
|
|
11
|
+
xuluxTemplatePreviewTool,
|
|
12
|
+
} from "../xulux-templates.js";
|
|
8
13
|
|
|
9
14
|
const tools = {
|
|
10
15
|
assistantUIDocs: docsTools,
|
|
11
16
|
assistantUIExamples: examplesTools,
|
|
12
17
|
assistantUISearch: searchTools,
|
|
18
|
+
assistantUITemplates: xuluxTemplatesListTool,
|
|
19
|
+
assistantUITemplateDetails: xuluxTemplateDetailsTool,
|
|
20
|
+
assistantUITemplatePreview: xuluxTemplatePreviewTool,
|
|
13
21
|
};
|
|
14
22
|
|
|
15
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/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,7 +67,9 @@ export async function getAvailablePaths(): Promise<string[]> {
|
|
|
66
67
|
return paths.sort();
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
export
|
|
70
|
+
export const getAvailableDocFiles = cacheListing(scanDocFiles);
|
|
71
|
+
|
|
72
|
+
async function scanDocFiles(): Promise<string[]> {
|
|
70
73
|
const files: string[] = [];
|
|
71
74
|
|
|
72
75
|
async function scanDirectory(dir: string, prefix = ""): Promise<void> {
|
|
@@ -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-
|
|
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",
|