@assistant-ui/mcp-docs-server 0.1.17 → 0.1.18

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 (95) hide show
  1. package/.docs/organized/code-examples/with-ag-ui.md +146 -152
  2. package/.docs/organized/code-examples/with-ai-sdk-v5.md +96 -101
  3. package/.docs/organized/code-examples/with-assistant-transport.md +132 -220
  4. package/.docs/organized/code-examples/with-cloud.md +124 -131
  5. package/.docs/organized/code-examples/with-custom-thread-list.md +26 -46
  6. package/.docs/organized/code-examples/with-external-store.md +146 -151
  7. package/.docs/organized/code-examples/with-ffmpeg.md +129 -139
  8. package/.docs/organized/code-examples/with-langgraph.md +231 -225
  9. package/.docs/organized/code-examples/with-parent-id-grouping.md +146 -151
  10. package/.docs/organized/code-examples/with-react-hook-form.md +146 -152
  11. package/.docs/organized/code-examples/{store-example.md → with-store.md} +16 -20
  12. package/.docs/organized/code-examples/with-tanstack.md +23 -41
  13. package/.docs/raw/docs/runtimes/custom/custom-thread-list.mdx +36 -0
  14. package/.docs/raw/docs/runtimes/custom/local.mdx +31 -8
  15. package/.docs/raw/docs/ui/Scrollbar.mdx +0 -6
  16. package/dist/constants.d.ts +10 -0
  17. package/dist/constants.d.ts.map +1 -0
  18. package/dist/constants.js +14 -0
  19. package/dist/constants.js.map +1 -0
  20. package/dist/index.d.ts +4 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +33 -1
  23. package/dist/index.js.map +1 -0
  24. package/dist/prepare-docs/code-examples.d.ts +2 -0
  25. package/dist/prepare-docs/code-examples.d.ts.map +1 -0
  26. package/dist/prepare-docs/code-examples.js +129 -0
  27. package/dist/prepare-docs/code-examples.js.map +1 -0
  28. package/dist/prepare-docs/copy-raw.d.ts +2 -0
  29. package/dist/prepare-docs/copy-raw.d.ts.map +1 -0
  30. package/dist/prepare-docs/copy-raw.js +50 -0
  31. package/dist/prepare-docs/copy-raw.js.map +1 -0
  32. package/dist/prepare-docs/prepare.d.ts +2 -0
  33. package/dist/prepare-docs/prepare.d.ts.map +1 -0
  34. package/dist/prepare-docs/prepare.js +18 -195
  35. package/dist/prepare-docs/prepare.js.map +1 -0
  36. package/dist/stdio.d.ts +3 -0
  37. package/dist/stdio.d.ts.map +1 -0
  38. package/dist/stdio.js +4 -5
  39. package/dist/stdio.js.map +1 -0
  40. package/dist/tools/docs.d.ts +23 -0
  41. package/dist/tools/docs.d.ts.map +1 -0
  42. package/dist/tools/docs.js +168 -0
  43. package/dist/tools/docs.js.map +1 -0
  44. package/dist/tools/examples.d.ts +23 -0
  45. package/dist/tools/examples.d.ts.map +1 -0
  46. package/dist/tools/examples.js +95 -0
  47. package/dist/tools/examples.js.map +1 -0
  48. package/dist/tools/tests/test-setup.d.ts +4 -0
  49. package/dist/tools/tests/test-setup.d.ts.map +1 -0
  50. package/dist/tools/tests/test-setup.js +36 -0
  51. package/dist/tools/tests/test-setup.js.map +1 -0
  52. package/dist/utils/logger.d.ts +7 -0
  53. package/dist/utils/logger.d.ts.map +1 -0
  54. package/dist/utils/logger.js +20 -0
  55. package/dist/utils/logger.js.map +1 -0
  56. package/dist/utils/mcp-format.d.ts +7 -0
  57. package/dist/utils/mcp-format.d.ts.map +1 -0
  58. package/dist/utils/mcp-format.js +11 -0
  59. package/dist/utils/mcp-format.js.map +1 -0
  60. package/dist/utils/mdx.d.ts +9 -0
  61. package/dist/utils/mdx.d.ts.map +1 -0
  62. package/dist/utils/mdx.js +27 -0
  63. package/dist/utils/mdx.js.map +1 -0
  64. package/dist/utils/paths.d.ts +8 -0
  65. package/dist/utils/paths.d.ts.map +1 -0
  66. package/dist/utils/paths.js +84 -0
  67. package/dist/utils/paths.js.map +1 -0
  68. package/dist/utils/security.d.ts +2 -0
  69. package/dist/utils/security.d.ts.map +1 -0
  70. package/dist/utils/security.js +43 -0
  71. package/dist/utils/security.js.map +1 -0
  72. package/package.json +37 -19
  73. package/src/constants.ts +22 -0
  74. package/src/index.ts +51 -0
  75. package/src/prepare-docs/code-examples.ts +158 -0
  76. package/src/prepare-docs/copy-raw.ts +55 -0
  77. package/src/prepare-docs/prepare.ts +24 -0
  78. package/src/stdio.ts +7 -0
  79. package/src/tools/docs.ts +207 -0
  80. package/src/tools/examples.ts +107 -0
  81. package/src/tools/tests/docs.test.ts +122 -0
  82. package/src/tools/tests/examples.test.ts +94 -0
  83. package/src/tools/tests/integration.test.ts +46 -0
  84. package/src/tools/tests/json-parsing.test.ts +23 -0
  85. package/src/tools/tests/mcp-protocol.test.ts +133 -0
  86. package/src/tools/tests/path-traversal.test.ts +81 -0
  87. package/src/tools/tests/test-setup.ts +40 -0
  88. package/src/utils/logger.ts +20 -0
  89. package/src/utils/mcp-format.ts +12 -0
  90. package/src/utils/mdx.ts +39 -0
  91. package/src/utils/paths.ts +114 -0
  92. package/src/utils/security.ts +52 -0
  93. package/src/utils/tests/security.test.ts +119 -0
  94. package/dist/chunk-M2RKUM66.js +0 -38
  95. package/dist/chunk-NVNFQ5ZO.js +0 -423
package/src/stdio.ts ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { runServer } from "./index.js";
3
+
4
+ void runServer().catch((error) => {
5
+ console.error("Failed to start server:", error);
6
+ process.exit(1);
7
+ });
@@ -0,0 +1,207 @@
1
+ import { z } from "zod/v3";
2
+ import { stat, lstat } from "node:fs/promises";
3
+ import { join, extname } from "node:path";
4
+ import { DOCS_PATH, MDX_EXTENSION, MAX_FILE_SIZE } from "../constants.js";
5
+ import { logger } from "../utils/logger.js";
6
+ import {
7
+ listDirContents,
8
+ getAvailablePaths,
9
+ findNearestPaths,
10
+ pathExists,
11
+ } from "../utils/paths.js";
12
+ import { readMDXFile, formatMDXContent } from "../utils/mdx.js";
13
+ import { formatMCPResponse } from "../utils/mcp-format.js";
14
+ import { sanitizePath } from "../utils/security.js";
15
+
16
+ const docsInputSchema = z.object({
17
+ paths: z
18
+ .array(z.string())
19
+ .min(1)
20
+ .describe(
21
+ 'Documentation paths to retrieve (e.g., ["getting-started", "api-reference/primitives/Thread"])',
22
+ ),
23
+ });
24
+
25
+ interface DocResult {
26
+ path: string;
27
+ found: boolean;
28
+ type?: "file" | "directory";
29
+ content?: string;
30
+ files?: string[];
31
+ directories?: string[];
32
+ suggestions?: string[];
33
+ error?: string;
34
+ }
35
+
36
+ async function readDocumentation(docPath: string): Promise<DocResult> {
37
+ logger.debug(`Reading documentation for path: ${docPath}`);
38
+
39
+ if (docPath === "/" || docPath === "") {
40
+ const { directories, files } = await listDirContents(DOCS_PATH);
41
+ return {
42
+ path: "/",
43
+ found: true,
44
+ type: "directory",
45
+ directories,
46
+ files: files.map((f) => f.replace(MDX_EXTENSION, "")),
47
+ };
48
+ }
49
+
50
+ try {
51
+ const sanitized = sanitizePath(docPath);
52
+ const fullPath = join(DOCS_PATH, sanitized);
53
+
54
+ try {
55
+ const lstats = await lstat(fullPath);
56
+ if (lstats.isSymbolicLink()) {
57
+ logger.warn(`Symlink detected at path: ${fullPath}`);
58
+ return {
59
+ path: docPath,
60
+ found: false,
61
+ error: "Symlinks are not allowed for security reasons",
62
+ };
63
+ }
64
+ } catch {}
65
+
66
+ if (await pathExists(fullPath)) {
67
+ const stats = await stat(fullPath);
68
+
69
+ if (stats.isFile() && stats.size > MAX_FILE_SIZE) {
70
+ logger.warn(`File too large: ${fullPath} (${stats.size} bytes)`);
71
+ return {
72
+ path: docPath,
73
+ found: false,
74
+ error: `File size exceeds maximum allowed size of ${MAX_FILE_SIZE} bytes`,
75
+ };
76
+ }
77
+
78
+ if (stats.isDirectory()) {
79
+ const { directories, files } = await listDirContents(fullPath);
80
+
81
+ const contents: Record<string, string> = {};
82
+ for (const file of files) {
83
+ const mdxContent = await readMDXFile(join(fullPath, file));
84
+ if (mdxContent) {
85
+ const fileName = file.replace(MDX_EXTENSION, "");
86
+ contents[fileName] = formatMDXContent(mdxContent);
87
+ }
88
+ }
89
+
90
+ const content =
91
+ Object.keys(contents).length > 0
92
+ ? JSON.stringify(contents, null, 2)
93
+ : undefined;
94
+ return {
95
+ path: docPath,
96
+ found: true,
97
+ type: "directory",
98
+ directories,
99
+ files: files.map((f) => f.replace(MDX_EXTENSION, "")),
100
+ ...(content !== undefined && { content }),
101
+ };
102
+ }
103
+ }
104
+
105
+ const mdxPath =
106
+ extname(fullPath) === MDX_EXTENSION
107
+ ? fullPath
108
+ : `${fullPath}${MDX_EXTENSION}`;
109
+
110
+ try {
111
+ const mdxLstats = await lstat(mdxPath);
112
+ if (mdxLstats.isSymbolicLink()) {
113
+ logger.warn(`Symlink detected at MDX path: ${mdxPath}`);
114
+ return {
115
+ path: docPath,
116
+ found: false,
117
+ error: "Symlinks are not allowed for security reasons",
118
+ };
119
+ }
120
+
121
+ if (mdxLstats.size > MAX_FILE_SIZE) {
122
+ logger.warn(`MDX file too large: ${mdxPath} (${mdxLstats.size} bytes)`);
123
+ return {
124
+ path: docPath,
125
+ found: false,
126
+ error: `File size exceeds maximum allowed size of ${MAX_FILE_SIZE} bytes`,
127
+ };
128
+ }
129
+ } catch {}
130
+
131
+ if (await pathExists(mdxPath)) {
132
+ const mdxContent = await readMDXFile(mdxPath);
133
+
134
+ if (mdxContent) {
135
+ return {
136
+ path: docPath,
137
+ found: true,
138
+ type: "file",
139
+ content: formatMDXContent(mdxContent),
140
+ };
141
+ }
142
+ }
143
+
144
+ const availablePaths = await getAvailablePaths();
145
+ const suggestions = findNearestPaths(docPath, availablePaths);
146
+
147
+ return {
148
+ path: docPath,
149
+ found: false,
150
+ suggestions,
151
+ };
152
+ } catch (error) {
153
+ if (error instanceof Error && error.message.includes("Invalid path")) {
154
+ return {
155
+ path: docPath,
156
+ found: false,
157
+ error: error.message,
158
+ };
159
+ }
160
+ throw error;
161
+ }
162
+ }
163
+
164
+ export const docsTools = {
165
+ name: "assistantUIDocs",
166
+ description:
167
+ 'Retrieve assistant-ui documentation by path. Use "/" to list all sections. Supports multiple paths in a single request.',
168
+ parameters: docsInputSchema.shape,
169
+ execute: async ({ paths }: z.infer<typeof docsInputSchema>) => {
170
+ logger.info(`Retrieving documentation for paths: ${paths.join(", ")}`);
171
+
172
+ try {
173
+ const results = await Promise.all(
174
+ paths.map((path) => readDocumentation(path)),
175
+ );
176
+
177
+ if (results.length === 1) {
178
+ const result = results[0]!;
179
+ if (result.error) {
180
+ return formatMCPResponse({
181
+ error: result.error,
182
+ path: result.path,
183
+ });
184
+ }
185
+ if (!result.found) {
186
+ return formatMCPResponse({
187
+ error: `Documentation not found for path: ${result.path}`,
188
+ suggestions: result.suggestions,
189
+ hint: 'Use "/" to list all available documentation sections',
190
+ });
191
+ }
192
+ return formatMCPResponse(result);
193
+ }
194
+
195
+ return formatMCPResponse({
196
+ results,
197
+ summary: `Retrieved ${results.filter((r) => r.found).length} of ${results.length} requested paths`,
198
+ });
199
+ } catch (error) {
200
+ logger.error("Failed to retrieve documentation", error);
201
+ return formatMCPResponse({
202
+ error: "Failed to retrieve documentation",
203
+ message: error instanceof Error ? error.message : String(error),
204
+ });
205
+ }
206
+ },
207
+ };
@@ -0,0 +1,107 @@
1
+ import { z } from "zod/v3";
2
+ import { readFile, readdir, lstat } from "node:fs/promises";
3
+ import { join, extname } from "node:path";
4
+ import { CODE_EXAMPLES_PATH, MAX_FILE_SIZE } from "../constants.js";
5
+ import { logger } from "../utils/logger.js";
6
+ import { formatMCPResponse } from "../utils/mcp-format.js";
7
+ import { sanitizePath } from "../utils/security.js";
8
+
9
+ const examplesInputSchema = z.object({
10
+ example: z
11
+ .string()
12
+ .optional()
13
+ .describe(
14
+ 'Example name (e.g., "with-ai-sdk"). Leave empty to list all examples.',
15
+ ),
16
+ });
17
+
18
+ async function listCodeExamples(): Promise<string[]> {
19
+ try {
20
+ const files = await readdir(CODE_EXAMPLES_PATH);
21
+ return files
22
+ .filter((file) => extname(file) === ".md")
23
+ .map((file) => file.replace(".md", ""))
24
+ .sort();
25
+ } catch (error) {
26
+ logger.error("Failed to list code examples", error);
27
+ return [];
28
+ }
29
+ }
30
+
31
+ async function readCodeExample(exampleName: string): Promise<string | null> {
32
+ try {
33
+ const sanitized = sanitizePath(exampleName);
34
+ const filePath = join(CODE_EXAMPLES_PATH, `${sanitized}.md`);
35
+
36
+ const stats = await lstat(filePath);
37
+ if (stats.isSymbolicLink()) {
38
+ logger.warn(`Attempted to read symlink: ${filePath}`);
39
+ return null;
40
+ }
41
+
42
+ if (stats.size > MAX_FILE_SIZE) {
43
+ logger.warn(`File size exceeds limit: ${filePath} (${stats.size} bytes)`);
44
+ return null;
45
+ }
46
+
47
+ const content = await readFile(filePath, "utf-8");
48
+ return content;
49
+ } catch (error) {
50
+ logger.error(`Failed to read example: ${exampleName}`, error);
51
+ return null;
52
+ }
53
+ }
54
+
55
+ export const examplesTools = {
56
+ name: "assistantUIExamples",
57
+ description:
58
+ "List available examples or retrieve complete code for a specific example",
59
+ parameters: examplesInputSchema.shape,
60
+ execute: async ({ example }: z.infer<typeof examplesInputSchema>) => {
61
+ try {
62
+ if (!example) {
63
+ logger.info("Listing all available examples");
64
+ const examples = await listCodeExamples();
65
+
66
+ if (examples.length === 0) {
67
+ return formatMCPResponse({
68
+ error:
69
+ "No examples found. Please run documentation preparation first.",
70
+ hint: "Run: pnpm prepare-docs",
71
+ });
72
+ }
73
+
74
+ return formatMCPResponse({
75
+ type: "list",
76
+ examples,
77
+ total: examples.length,
78
+ hint: "Use example parameter to get complete code for any example",
79
+ });
80
+ }
81
+
82
+ logger.info(`Retrieving example: ${example}`);
83
+ const content = await readCodeExample(example);
84
+
85
+ if (!content) {
86
+ const availableExamples = await listCodeExamples();
87
+ return formatMCPResponse({
88
+ error: `Example not found: ${example}`,
89
+ availableExamples,
90
+ hint: "Use without example parameter to list all available examples",
91
+ });
92
+ }
93
+
94
+ return formatMCPResponse({
95
+ type: "example",
96
+ name: example,
97
+ content,
98
+ });
99
+ } catch (error) {
100
+ logger.error("Failed to retrieve examples", error);
101
+ return formatMCPResponse({
102
+ error: "Failed to retrieve examples",
103
+ message: error instanceof Error ? error.message : String(error),
104
+ });
105
+ }
106
+ },
107
+ };
@@ -0,0 +1,122 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { testContext } from "./test-setup.js";
3
+ import * as fs from "node:fs/promises";
4
+
5
+ vi.mock("fs/promises", async () => {
6
+ const { readdir, readFile, stat, lstat } = await import("node:fs/promises");
7
+ return {
8
+ readdir,
9
+ readFile,
10
+ stat,
11
+ lstat: vi.fn(lstat),
12
+ };
13
+ });
14
+
15
+ describe("assistantUIDocs", () => {
16
+ beforeEach(() => {
17
+ vi.clearAllMocks();
18
+ });
19
+
20
+ afterEach(() => {
21
+ vi.restoreAllMocks();
22
+ });
23
+ it("should list root directory contents", async () => {
24
+ const result = await testContext.callTool("assistantUIDocs", {
25
+ paths: ["/"],
26
+ });
27
+
28
+ expect(result.path).toBe("/");
29
+ expect(result.found).toBe(true);
30
+ expect(result.type).toBe("directory");
31
+ expect(result.directories).toContain("api-reference");
32
+ expect(result.directories).toContain("guides");
33
+ expect(result.files).toContain("getting-started");
34
+ });
35
+
36
+ it("should retrieve specific documentation file", async () => {
37
+ const result = await testContext.callTool("assistantUIDocs", {
38
+ paths: ["getting-started"],
39
+ });
40
+
41
+ expect(result.path).toBe("getting-started");
42
+ expect(result.found).toBe(true);
43
+ expect(result.type).toBe("file");
44
+ expect(result.content).toBeDefined();
45
+ expect(result.content).toContain("Getting Started");
46
+ });
47
+
48
+ it("should handle non-existent paths", async () => {
49
+ const result = await testContext.callTool("assistantUIDocs", {
50
+ paths: ["non-existent-path"],
51
+ });
52
+
53
+ expect(result.error).toBeDefined();
54
+ expect(result.error).toContain("Documentation not found");
55
+ expect(result.suggestions).toBeDefined();
56
+ });
57
+
58
+ it("should support multiple path requests", async () => {
59
+ const result = await testContext.callTool("assistantUIDocs", {
60
+ paths: ["getting-started", "api-reference/primitives/Thread"],
61
+ });
62
+
63
+ expect(result.results).toBeDefined();
64
+ expect(result.results).toHaveLength(2);
65
+ expect(result.results[0].path).toBe("getting-started");
66
+ expect(result.results[1].path).toBe("api-reference/primitives/Thread");
67
+ });
68
+
69
+ it("should list directory contents with files", async () => {
70
+ const result = await testContext.callTool("assistantUIDocs", {
71
+ paths: ["api-reference/primitives"],
72
+ });
73
+
74
+ expect(result.path).toBe("api-reference/primitives");
75
+ expect(result.found).toBe(true);
76
+ expect(result.type).toBe("directory");
77
+ expect(result.files).toContain("Thread");
78
+ expect(result.files).toContain("Message");
79
+ expect(result.files).toContain("Composer");
80
+ });
81
+
82
+ it("should parse MDX files with frontmatter", async () => {
83
+ const result = await testContext.callTool("assistantUIDocs", {
84
+ paths: ["getting-started"],
85
+ });
86
+
87
+ expect(result.content).toBeDefined();
88
+ expect(result.content).toContain("title:");
89
+ expect(result.content).toContain("Getting Started");
90
+ });
91
+
92
+ it("should skip symlinks and large files", async () => {
93
+ const mockedLstat = vi.mocked(fs.lstat);
94
+
95
+ mockedLstat.mockResolvedValueOnce({
96
+ isSymbolicLink: () => true,
97
+ isFile: () => false,
98
+ isDirectory: () => false,
99
+ } as any);
100
+
101
+ const symlinkResult = await testContext.callTool("assistantUIDocs", {
102
+ paths: ["symlink-test"],
103
+ });
104
+ expect(symlinkResult.error).toBe(
105
+ "Symlinks are not allowed for security reasons",
106
+ );
107
+
108
+ mockedLstat.mockRejectedValueOnce(new Error("ENOENT"));
109
+ mockedLstat.mockResolvedValueOnce({
110
+ isSymbolicLink: () => false,
111
+ isFile: () => true,
112
+ size: 11 * 1024 * 1024,
113
+ } as any);
114
+
115
+ const largeFileResult = await testContext.callTool("assistantUIDocs", {
116
+ paths: ["large-file"],
117
+ });
118
+ expect(largeFileResult.error).toContain(
119
+ "File size exceeds maximum allowed size",
120
+ );
121
+ });
122
+ });
@@ -0,0 +1,94 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { testContext } from "./test-setup.js";
3
+ import * as fs from "node:fs/promises";
4
+
5
+ vi.mock("fs/promises", async () => {
6
+ const { readdir, readFile, lstat } = await import("node:fs/promises");
7
+ return {
8
+ readdir,
9
+ readFile,
10
+ lstat: vi.fn(lstat),
11
+ };
12
+ });
13
+
14
+ describe("assistantUIExamples", () => {
15
+ it("should list all available examples", async () => {
16
+ const result = await testContext.callTool("assistantUIExamples", {});
17
+
18
+ expect(result.type).toBe("list");
19
+ expect(result.examples).toBeDefined();
20
+ expect(Array.isArray(result.examples)).toBe(true);
21
+ expect(result.examples.length).toBeGreaterThan(0);
22
+ expect(result.examples).toContain("with-ai-sdk-v5");
23
+ expect(result.examples).toContain("with-langgraph");
24
+ expect(result.total).toBe(result.examples.length);
25
+ });
26
+
27
+ it("should retrieve specific example code", async () => {
28
+ const result = await testContext.callTool("assistantUIExamples", {
29
+ example: "with-ai-sdk-v5",
30
+ });
31
+
32
+ expect(result.type).toBe("example");
33
+ expect(result.name).toBe("with-ai-sdk-v5");
34
+ expect(result.content).toBeDefined();
35
+ expect(result.content).toContain("# Example: with-ai-sdk-v5");
36
+ expect(result.content).toContain("app/api/chat/route.ts");
37
+ expect(result.content).toContain("streamText");
38
+ });
39
+
40
+ it("should handle non-existent examples", async () => {
41
+ const result = await testContext.callTool("assistantUIExamples", {
42
+ example: "non-existent-example",
43
+ });
44
+
45
+ expect(result.error).toBeDefined();
46
+ expect(result.error).toContain("Example not found");
47
+ expect(result.availableExamples).toBeDefined();
48
+ expect(Array.isArray(result.availableExamples)).toBe(true);
49
+ });
50
+
51
+ it("should include all files in example", async () => {
52
+ const result = await testContext.callTool("assistantUIExamples", {
53
+ example: "with-ai-sdk-v5",
54
+ });
55
+
56
+ expect(result.content).toContain("package.json");
57
+ expect(result.content).toContain("components/assistant-ui/thread.tsx");
58
+ });
59
+
60
+ it("should handle empty example parameter", async () => {
61
+ const result = await testContext.callTool("assistantUIExamples", {
62
+ example: undefined,
63
+ });
64
+
65
+ expect(result.type).toBe("list");
66
+ expect(result.hint).toBeDefined();
67
+ });
68
+
69
+ it("should skip symlinks and large files", async () => {
70
+ const mockedLstat = vi.mocked(fs.lstat);
71
+
72
+ mockedLstat.mockResolvedValueOnce({
73
+ isSymbolicLink: () => true,
74
+ isFile: () => false,
75
+ size: 0,
76
+ } as any);
77
+
78
+ const symlinkResult = await testContext.callTool("assistantUIExamples", {
79
+ example: "symlink-example",
80
+ });
81
+ expect(symlinkResult.error).toContain("Example not found");
82
+
83
+ mockedLstat.mockResolvedValueOnce({
84
+ isSymbolicLink: () => false,
85
+ isFile: () => true,
86
+ size: 11 * 1024 * 1024, // 11MB - exceeds MAX_FILE_SIZE
87
+ } as any);
88
+
89
+ const largeFileResult = await testContext.callTool("assistantUIExamples", {
90
+ example: "large-example",
91
+ });
92
+ expect(largeFileResult.error).toContain("Example not found");
93
+ });
94
+ });
@@ -0,0 +1,46 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { server } from "../../index.js";
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { docsTools } from "../docs.js";
5
+ import { examplesTools } from "../examples.js";
6
+
7
+ describe("MCP Server Integration", () => {
8
+ it("should be an instance of McpServer", () => {
9
+ expect(server).toBeInstanceOf(McpServer);
10
+ });
11
+
12
+ it("should have tools with correct properties", () => {
13
+ expect(docsTools.name).toBe("assistantUIDocs");
14
+ expect(docsTools.description).toContain(
15
+ "Retrieve assistant-ui documentation",
16
+ );
17
+ expect(docsTools.parameters).toBeDefined();
18
+ expect(docsTools.execute).toBeInstanceOf(Function);
19
+
20
+ expect(examplesTools.name).toBe("assistantUIExamples");
21
+ expect(examplesTools.description).toContain("List available examples");
22
+ expect(examplesTools.parameters).toBeDefined();
23
+ expect(examplesTools.execute).toBeInstanceOf(Function);
24
+ });
25
+
26
+ it("should have valid input schemas", () => {
27
+ // docsTools.parameters is the Zod schema shape
28
+ expect(docsTools.parameters).toBeDefined();
29
+ expect(Object.keys(docsTools.parameters)).toContain("paths");
30
+
31
+ expect(examplesTools.parameters).toBeDefined();
32
+ expect(Object.keys(examplesTools.parameters)).toContain("example");
33
+ });
34
+
35
+ it("should execute tools successfully", async () => {
36
+ const docsResult = await docsTools.execute({ paths: ["/"] });
37
+ expect(docsResult).toBeDefined();
38
+ expect(docsResult.content).toBeDefined();
39
+ expect(docsResult.content[0].type).toBe("text");
40
+
41
+ const examplesResult = await examplesTools.execute({});
42
+ expect(examplesResult).toBeDefined();
43
+ expect(examplesResult.content).toBeDefined();
44
+ expect(examplesResult.content[0].type).toBe("text");
45
+ });
46
+ });
@@ -0,0 +1,23 @@
1
+ import { describe, it, expect, vi, afterEach } from "vitest";
2
+ import { testContext } from "./test-setup.js";
3
+ import { docsTools } from "../docs.js";
4
+
5
+ describe("JSON parsing error handling", () => {
6
+ afterEach(() => {
7
+ vi.restoreAllMocks();
8
+ });
9
+
10
+ it("should provide helpful error message for invalid JSON", async () => {
11
+ vi.spyOn(docsTools, "execute").mockResolvedValue({
12
+ content: [{ text: "invalid json {not valid}" }],
13
+ });
14
+
15
+ await expect(
16
+ testContext.callTool("assistantUIDocs", { paths: ["/"] }),
17
+ ).rejects.toThrow(/Tool assistantUIDocs returned invalid JSON/);
18
+
19
+ await expect(
20
+ testContext.callTool("assistantUIDocs", { paths: ["/"] }),
21
+ ).rejects.toThrow(/invalid json \{not valid\}/);
22
+ });
23
+ });