@iress-oss/ids-mcp-server 0.0.1-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +201 -0
- package/README.md +93 -0
- package/dist/componentHandlers.js +241 -0
- package/dist/componentHandlers.test.js +380 -0
- package/dist/config.js +16 -0
- package/dist/index.js +53 -0
- package/dist/iressHandlers.js +144 -0
- package/dist/iressHandlers.test.js +316 -0
- package/dist/resourceHandlers.js +67 -0
- package/dist/resourceHandlers.test.js +352 -0
- package/dist/searchHandlers.js +287 -0
- package/dist/searchHandlers.test.js +524 -0
- package/dist/toolHandler.js +31 -0
- package/dist/toolHandler.test.js +369 -0
- package/dist/tools.js +165 -0
- package/dist/types.js +4 -0
- package/dist/utils.js +59 -0
- package/dist/utils.test.js +286 -0
- package/generated/docs/components-alert-docs.md +130 -0
- package/generated/docs/components-autocomplete-docs.md +754 -0
- package/generated/docs/components-autocomplete-recipes-docs.md +104 -0
- package/generated/docs/components-badge-docs.md +148 -0
- package/generated/docs/components-button-docs.md +362 -0
- package/generated/docs/components-button-recipes-docs.md +76 -0
- package/generated/docs/components-buttongroup-docs.md +310 -0
- package/generated/docs/components-card-docs.md +494 -0
- package/generated/docs/components-card-recipes-docs.md +89 -0
- package/generated/docs/components-checkbox-docs.md +193 -0
- package/generated/docs/components-checkboxgroup-docs.md +692 -0
- package/generated/docs/components-checkboxgroup-recipes-docs.md +119 -0
- package/generated/docs/components-col-docs.md +466 -0
- package/generated/docs/components-combobox-docs.md +1016 -0
- package/generated/docs/components-container-docs.md +91 -0
- package/generated/docs/components-divider-docs.md +176 -0
- package/generated/docs/components-expander-docs.md +215 -0
- package/generated/docs/components-field-docs.md +675 -0
- package/generated/docs/components-filter-docs.md +1109 -0
- package/generated/docs/components-form-docs.md +2442 -0
- package/generated/docs/components-form-recipes-docs.md +892 -0
- package/generated/docs/components-hide-docs.md +265 -0
- package/generated/docs/components-icon-docs.md +553 -0
- package/generated/docs/components-inline-docs.md +868 -0
- package/generated/docs/components-input-docs.md +335 -0
- package/generated/docs/components-input-recipes-docs.md +140 -0
- package/generated/docs/components-inputcurrency-docs.md +157 -0
- package/generated/docs/components-inputcurrency-recipes-docs.md +116 -0
- package/generated/docs/components-label-docs.md +135 -0
- package/generated/docs/components-menu-docs.md +704 -0
- package/generated/docs/components-menu-menuitem-docs.md +193 -0
- package/generated/docs/components-modal-docs.md +587 -0
- package/generated/docs/components-navbar-docs.md +291 -0
- package/generated/docs/components-navbar-recipes-docs.md +413 -0
- package/generated/docs/components-panel-docs.md +380 -0
- package/generated/docs/components-placeholder-docs.md +27 -0
- package/generated/docs/components-popover-docs.md +464 -0
- package/generated/docs/components-popover-recipes-docs.md +245 -0
- package/generated/docs/components-progress-docs.md +104 -0
- package/generated/docs/components-radio-docs.md +107 -0
- package/generated/docs/components-radiogroup-docs.md +683 -0
- package/generated/docs/components-readonly-docs.md +89 -0
- package/generated/docs/components-richselect-docs.md +2433 -0
- package/generated/docs/components-row-docs.md +877 -0
- package/generated/docs/components-select-docs.md +456 -0
- package/generated/docs/components-skeleton-docs.md +214 -0
- package/generated/docs/components-skeleton-recipes-docs.md +76 -0
- package/generated/docs/components-skiplink-docs.md +66 -0
- package/generated/docs/components-slideout-docs.md +538 -0
- package/generated/docs/components-slider-docs.md +346 -0
- package/generated/docs/components-spinner-docs.md +59 -0
- package/generated/docs/components-stack-docs.md +265 -0
- package/generated/docs/components-table-ag-grid-docs.md +2666 -0
- package/generated/docs/components-table-docs.md +1305 -0
- package/generated/docs/components-tabset-docs.md +341 -0
- package/generated/docs/components-tabset-tab-docs.md +86 -0
- package/generated/docs/components-tag-docs.md +115 -0
- package/generated/docs/components-text-docs.md +394 -0
- package/generated/docs/components-toaster-docs.md +294 -0
- package/generated/docs/components-toaster-toast-docs.md +157 -0
- package/generated/docs/components-toggle-docs.md +158 -0
- package/generated/docs/components-tooltip-docs.md +311 -0
- package/generated/docs/components-validationmessage-docs.md +241 -0
- package/generated/docs/contact-us-docs.md +27 -0
- package/generated/docs/extensions-editor-docs.md +288 -0
- package/generated/docs/extensions-editor-recipes-docs.md +39 -0
- package/generated/docs/foundations-accessibility-docs.md +62 -0
- package/generated/docs/foundations-colours-docs.md +257 -0
- package/generated/docs/foundations-consistency-docs.md +52 -0
- package/generated/docs/foundations-content-docs.md +23 -0
- package/generated/docs/foundations-introduction-docs.md +17 -0
- package/generated/docs/foundations-principles-docs.md +70 -0
- package/generated/docs/foundations-typography-docs.md +191 -0
- package/generated/docs/foundations-user-experience-docs.md +63 -0
- package/generated/docs/foundations-visual-design-docs.md +46 -0
- package/generated/docs/frequently-asked-questions-docs.md +53 -0
- package/generated/docs/get-started-develop-docs.md +48 -0
- package/generated/docs/get-started-using-storybook-docs.md +68 -0
- package/generated/docs/guidelines.md +812 -0
- package/generated/docs/introduction-docs.md +43 -0
- package/generated/docs/patterns-loading-docs.md +1304 -0
- package/generated/docs/resources-changelog-docs.md +6 -0
- package/generated/docs/resources-code-katas-docs.md +29 -0
- package/generated/docs/resources-migration-guides-from-v4-to-v5-docs.md +437 -0
- package/generated/docs/themes-available-themes-docs.md +66 -0
- package/generated/docs/themes-introduction-docs.md +121 -0
- package/generated/docs/themes-tokens-docs.md +1200 -0
- package/generated/docs/versions-docs.md +17 -0
- package/package.json +81 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for resourceHandlers.ts
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import { handleListResources, handleReadResource } from './resourceHandlers.js';
|
|
7
|
+
import { getMarkdownFiles, readFileContent } from './utils.js';
|
|
8
|
+
import { DOCS_DIR } from './config.js';
|
|
9
|
+
const mockGetMarkdownFiles = vi.mocked(getMarkdownFiles);
|
|
10
|
+
const mockReadFileContent = vi.mocked(readFileContent);
|
|
11
|
+
// Mock dependencies
|
|
12
|
+
vi.mock('./utils.js', () => ({
|
|
13
|
+
getMarkdownFiles: vi.fn(),
|
|
14
|
+
readFileContent: vi.fn(),
|
|
15
|
+
}));
|
|
16
|
+
vi.mock('./config.js', () => ({
|
|
17
|
+
DOCS_DIR: '/mock/docs/ids',
|
|
18
|
+
}));
|
|
19
|
+
describe('resourceHandlers', () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
vi.clearAllMocks();
|
|
22
|
+
});
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
vi.restoreAllMocks();
|
|
25
|
+
});
|
|
26
|
+
describe('handleListResources', () => {
|
|
27
|
+
it('should return empty resources when no markdown files exist', () => {
|
|
28
|
+
mockGetMarkdownFiles.mockReturnValue([]);
|
|
29
|
+
const result = handleListResources();
|
|
30
|
+
expect(result).toEqual({
|
|
31
|
+
resources: [],
|
|
32
|
+
});
|
|
33
|
+
expect(mockGetMarkdownFiles).toHaveBeenCalledOnce();
|
|
34
|
+
});
|
|
35
|
+
it('should categorize component files correctly', () => {
|
|
36
|
+
const mockFiles = [
|
|
37
|
+
'components-button-docs.md',
|
|
38
|
+
'components-input-docs.md',
|
|
39
|
+
'components-modal-docs.md',
|
|
40
|
+
];
|
|
41
|
+
mockGetMarkdownFiles.mockReturnValue(mockFiles);
|
|
42
|
+
const result = handleListResources();
|
|
43
|
+
expect(result.resources).toHaveLength(3);
|
|
44
|
+
expect(result.resources[0]).toEqual({
|
|
45
|
+
uri: `file://${path.join(DOCS_DIR, 'components-button-docs.md')}`,
|
|
46
|
+
name: 'Components: button',
|
|
47
|
+
description: 'IDS components documentation for button',
|
|
48
|
+
mimeType: 'text/markdown',
|
|
49
|
+
});
|
|
50
|
+
expect(result.resources[1]).toEqual({
|
|
51
|
+
uri: `file://${path.join(DOCS_DIR, 'components-input-docs.md')}`,
|
|
52
|
+
name: 'Components: input',
|
|
53
|
+
description: 'IDS components documentation for input',
|
|
54
|
+
mimeType: 'text/markdown',
|
|
55
|
+
});
|
|
56
|
+
expect(result.resources[2]).toEqual({
|
|
57
|
+
uri: `file://${path.join(DOCS_DIR, 'components-modal-docs.md')}`,
|
|
58
|
+
name: 'Components: modal',
|
|
59
|
+
description: 'IDS components documentation for modal',
|
|
60
|
+
mimeType: 'text/markdown',
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
it('should categorize foundation files correctly', () => {
|
|
64
|
+
const mockFiles = [
|
|
65
|
+
'foundations-colors-docs.md',
|
|
66
|
+
'foundations-typography-docs.md',
|
|
67
|
+
'foundations-spacing-docs.md',
|
|
68
|
+
];
|
|
69
|
+
mockGetMarkdownFiles.mockReturnValue(mockFiles);
|
|
70
|
+
const result = handleListResources();
|
|
71
|
+
expect(result.resources).toHaveLength(3);
|
|
72
|
+
expect(result.resources[0]).toEqual({
|
|
73
|
+
uri: `file://${path.join(DOCS_DIR, 'foundations-colors-docs.md')}`,
|
|
74
|
+
name: 'Foundations: colors',
|
|
75
|
+
description: 'IDS foundations documentation for colors',
|
|
76
|
+
mimeType: 'text/markdown',
|
|
77
|
+
});
|
|
78
|
+
expect(result.resources[1]).toEqual({
|
|
79
|
+
uri: `file://${path.join(DOCS_DIR, 'foundations-typography-docs.md')}`,
|
|
80
|
+
name: 'Foundations: typography',
|
|
81
|
+
description: 'IDS foundations documentation for typography',
|
|
82
|
+
mimeType: 'text/markdown',
|
|
83
|
+
});
|
|
84
|
+
expect(result.resources[2]).toEqual({
|
|
85
|
+
uri: `file://${path.join(DOCS_DIR, 'foundations-spacing-docs.md')}`,
|
|
86
|
+
name: 'Foundations: spacing',
|
|
87
|
+
description: 'IDS foundations documentation for spacing',
|
|
88
|
+
mimeType: 'text/markdown',
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
it('should categorize resource files correctly', () => {
|
|
92
|
+
const mockFiles = [
|
|
93
|
+
'resources-guidelines-docs.md',
|
|
94
|
+
'resources-patterns-docs.md',
|
|
95
|
+
];
|
|
96
|
+
mockGetMarkdownFiles.mockReturnValue(mockFiles);
|
|
97
|
+
const result = handleListResources();
|
|
98
|
+
expect(result.resources).toHaveLength(2);
|
|
99
|
+
expect(result.resources[0]).toEqual({
|
|
100
|
+
uri: `file://${path.join(DOCS_DIR, 'resources-guidelines-docs.md')}`,
|
|
101
|
+
name: 'Resources: guidelines',
|
|
102
|
+
description: 'IDS resources documentation for guidelines',
|
|
103
|
+
mimeType: 'text/markdown',
|
|
104
|
+
});
|
|
105
|
+
expect(result.resources[1]).toEqual({
|
|
106
|
+
uri: `file://${path.join(DOCS_DIR, 'resources-patterns-docs.md')}`,
|
|
107
|
+
name: 'Resources: patterns',
|
|
108
|
+
description: 'IDS resources documentation for patterns',
|
|
109
|
+
mimeType: 'text/markdown',
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
it('should categorize introduction files correctly', () => {
|
|
113
|
+
const mockFiles = ['introduction.md', 'getting-started-introduction.md'];
|
|
114
|
+
mockGetMarkdownFiles.mockReturnValue(mockFiles);
|
|
115
|
+
const result = handleListResources();
|
|
116
|
+
expect(result.resources).toHaveLength(2);
|
|
117
|
+
expect(result.resources[0]).toEqual({
|
|
118
|
+
uri: `file://${path.join(DOCS_DIR, 'introduction.md')}`,
|
|
119
|
+
name: 'Getting Started: Introduction',
|
|
120
|
+
description: 'IDS getting started documentation for Introduction',
|
|
121
|
+
mimeType: 'text/markdown',
|
|
122
|
+
});
|
|
123
|
+
expect(result.resources[1]).toEqual({
|
|
124
|
+
uri: `file://${path.join(DOCS_DIR, 'getting-started-introduction.md')}`,
|
|
125
|
+
name: 'Getting Started: Introduction',
|
|
126
|
+
description: 'IDS getting started documentation for Introduction',
|
|
127
|
+
mimeType: 'text/markdown',
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
it('should categorize uncategorized files as Other', () => {
|
|
131
|
+
const mockFiles = ['readme.md', 'changelog.md', 'random-file.md'];
|
|
132
|
+
mockGetMarkdownFiles.mockReturnValue(mockFiles);
|
|
133
|
+
const result = handleListResources();
|
|
134
|
+
expect(result.resources).toHaveLength(3);
|
|
135
|
+
expect(result.resources[0]).toEqual({
|
|
136
|
+
uri: `file://${path.join(DOCS_DIR, 'readme.md')}`,
|
|
137
|
+
name: 'Other: readme.md',
|
|
138
|
+
description: 'IDS other documentation for readme.md',
|
|
139
|
+
mimeType: 'text/markdown',
|
|
140
|
+
});
|
|
141
|
+
expect(result.resources[1]).toEqual({
|
|
142
|
+
uri: `file://${path.join(DOCS_DIR, 'changelog.md')}`,
|
|
143
|
+
name: 'Other: changelog.md',
|
|
144
|
+
description: 'IDS other documentation for changelog.md',
|
|
145
|
+
mimeType: 'text/markdown',
|
|
146
|
+
});
|
|
147
|
+
expect(result.resources[2]).toEqual({
|
|
148
|
+
uri: `file://${path.join(DOCS_DIR, 'random-file.md')}`,
|
|
149
|
+
name: 'Other: random-file.md',
|
|
150
|
+
description: 'IDS other documentation for random-file.md',
|
|
151
|
+
mimeType: 'text/markdown',
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
it('should handle mixed file categories', () => {
|
|
155
|
+
const mockFiles = [
|
|
156
|
+
'components-button-docs.md',
|
|
157
|
+
'foundations-colors-docs.md',
|
|
158
|
+
'resources-guidelines-docs.md',
|
|
159
|
+
'introduction.md',
|
|
160
|
+
'readme.md',
|
|
161
|
+
];
|
|
162
|
+
mockGetMarkdownFiles.mockReturnValue(mockFiles);
|
|
163
|
+
const result = handleListResources();
|
|
164
|
+
expect(result.resources).toHaveLength(5);
|
|
165
|
+
// Check that each category is represented
|
|
166
|
+
const categories = result.resources.map((resource) => resource.name.split(':')[0]);
|
|
167
|
+
expect(categories).toContain('Components');
|
|
168
|
+
expect(categories).toContain('Foundations');
|
|
169
|
+
expect(categories).toContain('Resources');
|
|
170
|
+
expect(categories).toContain('Getting Started');
|
|
171
|
+
expect(categories).toContain('Other');
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
describe('handleReadResource', () => {
|
|
175
|
+
const mockFileContent = '# Test Documentation\n\nThis is test content.';
|
|
176
|
+
beforeEach(() => {
|
|
177
|
+
mockReadFileContent.mockReturnValue(mockFileContent);
|
|
178
|
+
});
|
|
179
|
+
it('should successfully read a valid file', () => {
|
|
180
|
+
const testFilePath = path.join(DOCS_DIR, 'components-button-docs.md');
|
|
181
|
+
const request = {
|
|
182
|
+
params: {
|
|
183
|
+
uri: `file://${testFilePath}`,
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
const result = handleReadResource(request);
|
|
187
|
+
expect(result).toEqual({
|
|
188
|
+
contents: [
|
|
189
|
+
{
|
|
190
|
+
uri: request.params.uri,
|
|
191
|
+
mimeType: 'text/markdown',
|
|
192
|
+
text: mockFileContent,
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
});
|
|
196
|
+
expect(mockReadFileContent).toHaveBeenCalledWith(testFilePath);
|
|
197
|
+
});
|
|
198
|
+
it('should throw error for unsupported protocol', () => {
|
|
199
|
+
const request = {
|
|
200
|
+
params: {
|
|
201
|
+
uri: 'http://example.com/file.md',
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
expect(() => handleReadResource(request)).toThrow('Unsupported protocol: http:');
|
|
205
|
+
});
|
|
206
|
+
it('should throw error for files outside docs directory', () => {
|
|
207
|
+
const request = {
|
|
208
|
+
params: {
|
|
209
|
+
uri: 'file:///etc/passwd',
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
expect(() => handleReadResource(request)).toThrow('Access denied: File is outside the docs directory');
|
|
213
|
+
});
|
|
214
|
+
it('should throw error for files using relative path traversal', () => {
|
|
215
|
+
const testFilePath = path.join(DOCS_DIR, '..', '..', 'secret.md');
|
|
216
|
+
const request = {
|
|
217
|
+
params: {
|
|
218
|
+
uri: `file://${testFilePath}`,
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
expect(() => handleReadResource(request)).toThrow('Access denied: File is outside the docs directory');
|
|
222
|
+
});
|
|
223
|
+
it('should handle file read errors gracefully', () => {
|
|
224
|
+
const testFilePath = path.join(DOCS_DIR, 'nonexistent-file.md');
|
|
225
|
+
const readError = new Error('File not found');
|
|
226
|
+
mockReadFileContent.mockImplementation(() => {
|
|
227
|
+
throw readError;
|
|
228
|
+
});
|
|
229
|
+
const request = {
|
|
230
|
+
params: {
|
|
231
|
+
uri: `file://${testFilePath}`,
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
expect(() => handleReadResource(request)).toThrow('Failed to read file: File not found');
|
|
235
|
+
expect(mockReadFileContent).toHaveBeenCalledWith(testFilePath);
|
|
236
|
+
});
|
|
237
|
+
it('should handle unknown errors gracefully', () => {
|
|
238
|
+
const testFilePath = path.join(DOCS_DIR, 'components-button-docs.md');
|
|
239
|
+
mockReadFileContent.mockImplementation(() => {
|
|
240
|
+
throw new Error('Unknown error string'); // Proper Error object
|
|
241
|
+
});
|
|
242
|
+
const request = {
|
|
243
|
+
params: {
|
|
244
|
+
uri: `file://${testFilePath}`,
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
expect(() => handleReadResource(request)).toThrow('Failed to read file: Unknown error string');
|
|
248
|
+
});
|
|
249
|
+
it('should handle files in subdirectories within docs directory', () => {
|
|
250
|
+
const testFilePath = path.join(DOCS_DIR, 'subdirectory', 'components-nested-docs.md');
|
|
251
|
+
const request = {
|
|
252
|
+
params: {
|
|
253
|
+
uri: `file://${testFilePath}`,
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
const result = handleReadResource(request);
|
|
257
|
+
expect(result).toEqual({
|
|
258
|
+
contents: [
|
|
259
|
+
{
|
|
260
|
+
uri: request.params.uri,
|
|
261
|
+
mimeType: 'text/markdown',
|
|
262
|
+
text: mockFileContent,
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
});
|
|
266
|
+
expect(mockReadFileContent).toHaveBeenCalledWith(testFilePath);
|
|
267
|
+
});
|
|
268
|
+
it('should handle Windows-style paths correctly', () => {
|
|
269
|
+
const normalizedPath = path.join(DOCS_DIR, 'components-button-docs.md');
|
|
270
|
+
const windowsStyleUri = `file://${normalizedPath.replace(/\\/g, '/')}`;
|
|
271
|
+
const request = {
|
|
272
|
+
params: {
|
|
273
|
+
uri: windowsStyleUri,
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
const result = handleReadResource(request);
|
|
277
|
+
expect(result).toEqual({
|
|
278
|
+
contents: [
|
|
279
|
+
{
|
|
280
|
+
uri: request.params.uri,
|
|
281
|
+
mimeType: 'text/markdown',
|
|
282
|
+
text: mockFileContent,
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
// Add type-specific tests at the end
|
|
289
|
+
describe('Type compliance tests', () => {
|
|
290
|
+
it('should return resources with correct interface structure', () => {
|
|
291
|
+
const mockFiles = ['components-button-docs.md'];
|
|
292
|
+
mockGetMarkdownFiles.mockReturnValue(mockFiles);
|
|
293
|
+
const result = handleListResources();
|
|
294
|
+
// Verify the returned structure matches expected interface
|
|
295
|
+
expect(result).toHaveProperty('resources');
|
|
296
|
+
expect(Array.isArray(result.resources)).toBe(true);
|
|
297
|
+
if (result.resources.length > 0) {
|
|
298
|
+
const resource = result.resources[0];
|
|
299
|
+
expect(resource).toHaveProperty('uri');
|
|
300
|
+
expect(resource).toHaveProperty('name');
|
|
301
|
+
expect(resource).toHaveProperty('description');
|
|
302
|
+
expect(resource).toHaveProperty('mimeType');
|
|
303
|
+
expect(typeof resource.uri).toBe('string');
|
|
304
|
+
expect(typeof resource.name).toBe('string');
|
|
305
|
+
expect(typeof resource.description).toBe('string');
|
|
306
|
+
expect(resource.mimeType).toBe('text/markdown');
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
it('should return read resource with correct content structure', () => {
|
|
310
|
+
const mockContent = '# Component Documentation\n\nDescription here.';
|
|
311
|
+
mockReadFileContent.mockReturnValue(mockContent);
|
|
312
|
+
const testFilePath = path.join(DOCS_DIR, 'components-button-docs.md');
|
|
313
|
+
const request = {
|
|
314
|
+
params: {
|
|
315
|
+
uri: `file://${testFilePath}`,
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
const result = handleReadResource(request);
|
|
319
|
+
// Verify the returned structure matches expected interface
|
|
320
|
+
expect(result).toHaveProperty('contents');
|
|
321
|
+
expect(Array.isArray(result.contents)).toBe(true);
|
|
322
|
+
expect(result.contents).toHaveLength(1);
|
|
323
|
+
const content = result.contents[0];
|
|
324
|
+
expect(content).toHaveProperty('uri');
|
|
325
|
+
expect(content).toHaveProperty('mimeType');
|
|
326
|
+
expect(content).toHaveProperty('text');
|
|
327
|
+
expect(typeof content.uri).toBe('string');
|
|
328
|
+
expect(content.mimeType).toBe('text/markdown');
|
|
329
|
+
expect(typeof content.text).toBe('string');
|
|
330
|
+
expect(content.text).toBe(mockContent);
|
|
331
|
+
});
|
|
332
|
+
it('should handle empty file names correctly', () => {
|
|
333
|
+
const mockFiles = ['', ' ', '.md'];
|
|
334
|
+
mockGetMarkdownFiles.mockReturnValue(mockFiles);
|
|
335
|
+
const result = handleListResources();
|
|
336
|
+
expect(result.resources).toHaveLength(3);
|
|
337
|
+
// All should be categorized as "Other" since they don't match patterns
|
|
338
|
+
result.resources.forEach((resource) => {
|
|
339
|
+
expect(resource.name).toMatch(/^Other:/);
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
it('should preserve original filename in Other category', () => {
|
|
343
|
+
const originalFilename = 'some-random-documentation.md';
|
|
344
|
+
const mockFiles = [originalFilename];
|
|
345
|
+
mockGetMarkdownFiles.mockReturnValue(mockFiles);
|
|
346
|
+
const result = handleListResources();
|
|
347
|
+
expect(result.resources).toHaveLength(1);
|
|
348
|
+
expect(result.resources[0].name).toBe(`Other: ${originalFilename}`);
|
|
349
|
+
expect(result.resources[0].description).toBe(`IDS other documentation for ${originalFilename}`);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
});
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool handlers for search and documentation operations
|
|
3
|
+
*/
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import { getMarkdownFiles, readFileContent } from './utils.js';
|
|
7
|
+
import { DOCS_DIR } from './config.js';
|
|
8
|
+
export function handleGetUsageExamples(args) {
|
|
9
|
+
const schema = z.object({
|
|
10
|
+
component: z.string(),
|
|
11
|
+
pattern: z.string().optional(),
|
|
12
|
+
});
|
|
13
|
+
const { component, pattern } = schema.parse(args);
|
|
14
|
+
const markdownFiles = getMarkdownFiles();
|
|
15
|
+
// Find component files (main + recipes)
|
|
16
|
+
const componentFiles = markdownFiles.filter((file) => file.includes(`${component.toLowerCase()}`) ||
|
|
17
|
+
(file.includes('recipes') &&
|
|
18
|
+
file.toLowerCase().includes(component.toLowerCase())));
|
|
19
|
+
if (componentFiles.length === 0) {
|
|
20
|
+
return {
|
|
21
|
+
content: [
|
|
22
|
+
{
|
|
23
|
+
type: 'text',
|
|
24
|
+
text: `No examples found for "${component}". Try: ${markdownFiles
|
|
25
|
+
.filter((f) => f.startsWith('components-'))
|
|
26
|
+
.slice(0, 5)
|
|
27
|
+
.map((f) => f.replace('components-', '').replace('-docs.md', ''))
|
|
28
|
+
.join(', ')}`,
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const examples = [];
|
|
34
|
+
for (const file of componentFiles) {
|
|
35
|
+
try {
|
|
36
|
+
const filePath = path.join(DOCS_DIR, file);
|
|
37
|
+
const content = readFileContent(filePath);
|
|
38
|
+
// Extract code examples
|
|
39
|
+
const codeBlocks = content.match(/```[\s\S]*?```/g) ?? [];
|
|
40
|
+
const jsxBlocks = content.match(/<[A-Z][^>]*>[\s\S]*?<\/[A-Z][^>]*>/g) ?? [];
|
|
41
|
+
if (pattern) {
|
|
42
|
+
// Filter examples by pattern
|
|
43
|
+
const patternMatches = [...codeBlocks, ...jsxBlocks].filter((block) => block.toLowerCase().includes(pattern.toLowerCase()));
|
|
44
|
+
examples.push(...patternMatches);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
examples.push(...codeBlocks, ...jsxBlocks);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
console.error(`Error reading file ${file}:`, error);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const patternSuffix = pattern ? ` (${pattern} pattern)` : '';
|
|
55
|
+
const patternNotFoundSuffix = pattern ? ` with pattern "${pattern}"` : '';
|
|
56
|
+
const examplesText = examples.length > 0
|
|
57
|
+
? `**${component} Usage Examples**${patternSuffix}:\n\n${examples
|
|
58
|
+
.slice(0, 5)
|
|
59
|
+
.join('\n\n---\n\n')}`
|
|
60
|
+
: `No usage examples found for "${component}"${patternNotFoundSuffix}.`;
|
|
61
|
+
return {
|
|
62
|
+
content: [
|
|
63
|
+
{
|
|
64
|
+
type: 'text',
|
|
65
|
+
text: examplesText,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export function handleSearchIdsDocs(args) {
|
|
71
|
+
const schema = z.object({
|
|
72
|
+
query: z.string(),
|
|
73
|
+
case_sensitive: z.boolean().default(false),
|
|
74
|
+
});
|
|
75
|
+
const { query, case_sensitive } = schema.parse(args);
|
|
76
|
+
const markdownFiles = getMarkdownFiles();
|
|
77
|
+
const results = [];
|
|
78
|
+
for (const file of markdownFiles) {
|
|
79
|
+
try {
|
|
80
|
+
const filePath = path.join(DOCS_DIR, file);
|
|
81
|
+
const content = readFileContent(filePath);
|
|
82
|
+
const lines = content.split('\n');
|
|
83
|
+
lines.forEach((line, index) => {
|
|
84
|
+
const searchLine = case_sensitive ? line : line.toLowerCase();
|
|
85
|
+
const searchQuery = case_sensitive ? query : query.toLowerCase();
|
|
86
|
+
if (searchLine.includes(searchQuery)) {
|
|
87
|
+
// Get context (surrounding lines)
|
|
88
|
+
const contextStart = Math.max(0, index - 1);
|
|
89
|
+
const contextEnd = Math.min(lines.length - 1, index + 1);
|
|
90
|
+
const context = lines.slice(contextStart, contextEnd + 1).join('\n');
|
|
91
|
+
results.push({
|
|
92
|
+
file: file.replace('-docs.md', '').replace(/^[a-z]+-/, ''),
|
|
93
|
+
line: index + 1,
|
|
94
|
+
content: line.trim(),
|
|
95
|
+
context,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
console.error(`Error reading file ${file}:`, error);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
content: [
|
|
106
|
+
{
|
|
107
|
+
type: 'text',
|
|
108
|
+
text: results.length > 0
|
|
109
|
+
? `Found ${results.length} matches in IDS documentation:\n\n${results
|
|
110
|
+
.slice(0, 15) // Limit results
|
|
111
|
+
.map((r) => `**${r.file}:${r.line}**\n\`\`\`\n${r.context}\n\`\`\``)
|
|
112
|
+
.join('\n\n')}`
|
|
113
|
+
: `No matches found for "${query}" in IDS documentation.`,
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
export function handleGetDesignTokens(args) {
|
|
119
|
+
const schema = z.object({
|
|
120
|
+
type: z
|
|
121
|
+
.enum(['colors', 'spacing', 'typography', 'breakpoints', 'all'])
|
|
122
|
+
.default('all'),
|
|
123
|
+
});
|
|
124
|
+
const { type } = schema.parse(args);
|
|
125
|
+
const markdownFiles = getMarkdownFiles();
|
|
126
|
+
// Find foundation files related to design tokens
|
|
127
|
+
const foundationFiles = markdownFiles.filter((file) => file.startsWith('foundations-') &&
|
|
128
|
+
(type === 'all' || file.includes(type)));
|
|
129
|
+
const tokenInfo = [];
|
|
130
|
+
for (const file of foundationFiles) {
|
|
131
|
+
try {
|
|
132
|
+
const filePath = path.join(DOCS_DIR, file);
|
|
133
|
+
const content = readFileContent(filePath);
|
|
134
|
+
// Extract CSS custom properties and token information
|
|
135
|
+
const cssVariables = content.match(/--iress-[a-z-]+/g) ?? [];
|
|
136
|
+
const tokenSections = content.match(/#{2,3}\s+[^#\n]+/g) ?? [];
|
|
137
|
+
if (cssVariables.length > 0 || tokenSections.length > 0) {
|
|
138
|
+
let fileInfo = `**${file
|
|
139
|
+
.replace('foundations-', '')
|
|
140
|
+
.replace('-docs.md', '')}**\n`;
|
|
141
|
+
if (cssVariables.length > 0) {
|
|
142
|
+
const uniqueVars = [...new Set(cssVariables)].slice(0, 10);
|
|
143
|
+
fileInfo += `CSS Variables: ${uniqueVars.join(', ')}\n`;
|
|
144
|
+
}
|
|
145
|
+
if (tokenSections.length > 0) {
|
|
146
|
+
fileInfo += `\n${tokenSections.slice(0, 3).join('\n\n')}`;
|
|
147
|
+
}
|
|
148
|
+
tokenInfo.push(fileInfo);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
console.error(`Error reading file ${file}:`, error);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const typeLabel = type !== 'all' ? ` (${type})` : '';
|
|
156
|
+
const typeNotFoundLabel = type !== 'all' ? ` for ${type}` : '';
|
|
157
|
+
return {
|
|
158
|
+
content: [
|
|
159
|
+
{
|
|
160
|
+
type: 'text',
|
|
161
|
+
text: tokenInfo.length > 0
|
|
162
|
+
? `**IDS Design Tokens${typeLabel}**\n\n${tokenInfo.join('\n\n---\n\n')}`
|
|
163
|
+
: `No design token information found${typeNotFoundLabel}. Available foundations: ${markdownFiles
|
|
164
|
+
.filter((f) => f.startsWith('foundations-'))
|
|
165
|
+
.map((f) => f.replace('foundations-', '').replace('-docs.md', ''))
|
|
166
|
+
.join(', ')}`,
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function findSectionContent(content, section) {
|
|
172
|
+
const sectionRegex = new RegExp(`(#{1,3}\\s+.*${section}.*?(?=#{1,3}|$))`, 'gis');
|
|
173
|
+
const sectionMatch = sectionRegex.exec(content);
|
|
174
|
+
if (sectionMatch) {
|
|
175
|
+
return sectionMatch[0];
|
|
176
|
+
}
|
|
177
|
+
// Try to find sections that contain the search term
|
|
178
|
+
const sections = content.split(/(?=#{1,3}\s+)/);
|
|
179
|
+
const matchingSections = sections.filter((sectionContent) => sectionContent.toLowerCase().includes(section.toLowerCase()));
|
|
180
|
+
return matchingSections.length > 0 ? matchingSections.join('\n\n') : null;
|
|
181
|
+
}
|
|
182
|
+
function addContextLines(lines, startIndex, endIndex, contextLines) {
|
|
183
|
+
for (let j = startIndex; j < endIndex; j++) {
|
|
184
|
+
if (!contextLines.includes(lines[j])) {
|
|
185
|
+
contextLines.push(lines[j]);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function filterContentByQuery(content, query) {
|
|
190
|
+
const lines = content.split('\n');
|
|
191
|
+
const contextLines = [];
|
|
192
|
+
const queryLower = query.toLowerCase();
|
|
193
|
+
for (let i = 0; i < lines.length; i++) {
|
|
194
|
+
const line = lines[i];
|
|
195
|
+
if (line.toLowerCase().includes(queryLower)) {
|
|
196
|
+
// Add context lines before
|
|
197
|
+
const startContext = Math.max(0, i - 3);
|
|
198
|
+
addContextLines(lines, startContext, i, contextLines);
|
|
199
|
+
// Add the matching line
|
|
200
|
+
if (!contextLines.includes(line)) {
|
|
201
|
+
contextLines.push(line);
|
|
202
|
+
}
|
|
203
|
+
// Add context lines after
|
|
204
|
+
const endContext = Math.min(lines.length, i + 4);
|
|
205
|
+
addContextLines(lines, i + 1, endContext, contextLines);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return contextLines.length > 0 ? contextLines.join('\n') : null;
|
|
209
|
+
}
|
|
210
|
+
export function handleGetDesignGuidelines(args) {
|
|
211
|
+
const schema = z.object({
|
|
212
|
+
section: z.string().optional(),
|
|
213
|
+
query: z.string().optional(),
|
|
214
|
+
});
|
|
215
|
+
const { section, query } = schema.parse(args);
|
|
216
|
+
try {
|
|
217
|
+
// Read the guidelines.md file from the docs directory
|
|
218
|
+
const guidelinesPath = path.join(DOCS_DIR, 'guidelines.md');
|
|
219
|
+
const content = readFileContent(guidelinesPath);
|
|
220
|
+
if (!content) {
|
|
221
|
+
return {
|
|
222
|
+
content: [
|
|
223
|
+
{
|
|
224
|
+
type: 'text',
|
|
225
|
+
text: 'Design guidelines file not found. Please ensure guidelines.md exists in the docs directory.',
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
let filteredContent = content;
|
|
231
|
+
// Filter by section if specified
|
|
232
|
+
if (section) {
|
|
233
|
+
const sectionContent = findSectionContent(content, section);
|
|
234
|
+
if (sectionContent) {
|
|
235
|
+
filteredContent = sectionContent;
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
return {
|
|
239
|
+
content: [
|
|
240
|
+
{
|
|
241
|
+
type: 'text',
|
|
242
|
+
text: `Section "${section}" not found in design guidelines. Available sections include: Core Design Principles, Visual Design Standards, Component Guidelines, Accessibility, Layout Systems, and Best Practices.`,
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// Filter by query if specified
|
|
249
|
+
if (query) {
|
|
250
|
+
const queryContent = filterContentByQuery(filteredContent, query);
|
|
251
|
+
if (queryContent) {
|
|
252
|
+
filteredContent = queryContent;
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
return {
|
|
256
|
+
content: [
|
|
257
|
+
{
|
|
258
|
+
type: 'text',
|
|
259
|
+
text: `No guidelines found matching "${query}". Try searching for terms like: accessibility, typography, colors, spacing, components, principles, or usability.`,
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const sectionSuffix = section ? ` - ${section}` : '';
|
|
266
|
+
const querySuffix = query ? ` (filtered by: ${query})` : '';
|
|
267
|
+
return {
|
|
268
|
+
content: [
|
|
269
|
+
{
|
|
270
|
+
type: 'text',
|
|
271
|
+
text: `**IDS Design Guidelines${sectionSuffix}${querySuffix}**\n\n${filteredContent}`,
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
console.error('Error reading guidelines:', error);
|
|
278
|
+
return {
|
|
279
|
+
content: [
|
|
280
|
+
{
|
|
281
|
+
type: 'text',
|
|
282
|
+
text: 'Error reading design guidelines. Please ensure the guidelines.md file exists and is accessible.',
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
}
|