@iress-oss/ids-mcp-server 0.0.1-dev.5 → 0.0.1-dev.6
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/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-autocomplete-docs.md +48 -4
- package/generated/docs/components-checkboxgroup-docs.md +17 -17
- package/generated/docs/components-col-docs.md +1 -1
- package/generated/docs/components-combobox-docs.md +4 -4
- package/generated/docs/components-filter-docs.md +3 -3
- package/generated/docs/components-form-docs.md +8 -73
- package/generated/docs/components-icon-docs.md +4 -4
- package/generated/docs/components-inputcurrency-docs.md +4 -47
- package/generated/docs/components-radiogroup-docs.md +21 -21
- package/generated/docs/components-richselect-docs.md +322 -1
- package/generated/docs/components-row-docs.md +4 -4
- package/generated/docs/components-skiplink-docs.md +1 -1
- package/generated/docs/components-table-ag-grid-docs.md +104 -1696
- package/generated/docs/components-table-docs.md +6 -6
- package/generated/docs/components-tabset-docs.md +28 -0
- package/generated/docs/extensions-editor-docs.md +8 -2
- package/generated/docs/introduction-docs.md +1 -1
- package/generated/docs/patterns-loading-docs.md +2 -2
- package/generated/docs/themes-available-themes-docs.md +29 -29
- package/package.json +12 -3
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool handlers for component-related operations
|
|
3
|
+
*/
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import { getMarkdownFiles, mapIressComponentToFile, extractIressComponents, readFileContent, } from './utils.js';
|
|
7
|
+
import { DOCS_DIR } from './config.js';
|
|
8
|
+
function checkIressComponentMatch(query) {
|
|
9
|
+
if (query.startsWith('Iress')) {
|
|
10
|
+
const componentFile = mapIressComponentToFile(query);
|
|
11
|
+
if (componentFile) {
|
|
12
|
+
return {
|
|
13
|
+
content: [
|
|
14
|
+
{
|
|
15
|
+
type: 'text',
|
|
16
|
+
text: `Found exact match for **${query}**:\n\n**${componentFile}**\n ${query} component documentation\n\n*Use \`get_iress_component_info\` with "${query}" for detailed information.*`,
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
function calculateRelevanceScore(file, content, query, lines) {
|
|
25
|
+
let relevanceScore = 0;
|
|
26
|
+
const queryLower = query.toLowerCase();
|
|
27
|
+
const filenameLower = file.toLowerCase();
|
|
28
|
+
// Check for Iress component mentions in content
|
|
29
|
+
const iressComponents = extractIressComponents(content);
|
|
30
|
+
const hasMatchingIressComponent = iressComponents.some((comp) => comp.toLowerCase().includes(queryLower) ||
|
|
31
|
+
queryLower.includes(comp.toLowerCase().replace('iress', '')));
|
|
32
|
+
if (hasMatchingIressComponent) {
|
|
33
|
+
relevanceScore += 75;
|
|
34
|
+
}
|
|
35
|
+
// High relevance for filename matches
|
|
36
|
+
if (filenameLower.includes(queryLower)) {
|
|
37
|
+
relevanceScore += 100;
|
|
38
|
+
}
|
|
39
|
+
// Medium relevance for title matches
|
|
40
|
+
const title = lines.find((line) => line.startsWith('#'))?.replace(/^#+\s*/, '') ?? '';
|
|
41
|
+
if (title.toLowerCase().includes(queryLower)) {
|
|
42
|
+
relevanceScore += 50;
|
|
43
|
+
}
|
|
44
|
+
// Lower relevance for content matches
|
|
45
|
+
const contentMatches = content.toLowerCase().split(queryLower).length - 1;
|
|
46
|
+
relevanceScore += contentMatches * 2;
|
|
47
|
+
return relevanceScore;
|
|
48
|
+
}
|
|
49
|
+
function createSearchResult(file, relevanceScore, lines) {
|
|
50
|
+
const componentName = file
|
|
51
|
+
.replace(/^components-/, '')
|
|
52
|
+
.replace('-docs.md', '');
|
|
53
|
+
const description = lines
|
|
54
|
+
.slice(0, 10)
|
|
55
|
+
.find((line) => line.trim() && !line.startsWith('#') && !line.startsWith('<!--'))
|
|
56
|
+
?.trim() ?? 'IDS component documentation';
|
|
57
|
+
return {
|
|
58
|
+
file,
|
|
59
|
+
relevance: relevanceScore,
|
|
60
|
+
description: `${componentName}: ${description}`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export function handleFindComponent(args) {
|
|
64
|
+
const schema = z.object({
|
|
65
|
+
query: z.string(),
|
|
66
|
+
category: z.enum(['components', 'foundations', 'resources']).optional(),
|
|
67
|
+
});
|
|
68
|
+
const { query, category } = schema.parse(args);
|
|
69
|
+
// Check if query is an Iress component name
|
|
70
|
+
const iressMatch = checkIressComponentMatch(query);
|
|
71
|
+
if (iressMatch) {
|
|
72
|
+
return iressMatch;
|
|
73
|
+
}
|
|
74
|
+
const markdownFiles = getMarkdownFiles();
|
|
75
|
+
// Filter files by category if specified
|
|
76
|
+
const filteredFiles = category
|
|
77
|
+
? markdownFiles.filter((file) => file.startsWith(`${category}-`))
|
|
78
|
+
: markdownFiles;
|
|
79
|
+
const results = [];
|
|
80
|
+
for (const file of filteredFiles) {
|
|
81
|
+
try {
|
|
82
|
+
const filePath = path.join(DOCS_DIR, file);
|
|
83
|
+
const content = readFileContent(filePath);
|
|
84
|
+
const lines = content.split('\n');
|
|
85
|
+
const relevanceScore = calculateRelevanceScore(file, content, query, lines);
|
|
86
|
+
if (relevanceScore > 0) {
|
|
87
|
+
results.push(createSearchResult(file, relevanceScore, lines));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
console.error(`Error reading file ${file}:`, error);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Sort by relevance
|
|
95
|
+
results.sort((a, b) => b.relevance - a.relevance);
|
|
96
|
+
return {
|
|
97
|
+
content: [
|
|
98
|
+
{
|
|
99
|
+
type: 'text',
|
|
100
|
+
text: results.length > 0
|
|
101
|
+
? `Found ${results.length} relevant IDS components:\n\n${results
|
|
102
|
+
.slice(0, 10) // Limit to top 10 results
|
|
103
|
+
.map((r, index) => `${index + 1}. **${r.file}**\n ${r.description}`)
|
|
104
|
+
.join('\n\n')}`
|
|
105
|
+
: `No IDS components found matching "${query}". Try searching for common component names like "button", "input", "table", or "modal".`,
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function findComponentFile(component, markdownFiles) {
|
|
111
|
+
return markdownFiles.find((file) => file.includes(`components-${component.toLowerCase()}`) ||
|
|
112
|
+
file.toLowerCase().includes(component.toLowerCase()));
|
|
113
|
+
}
|
|
114
|
+
function formatAvailableComponents(markdownFiles) {
|
|
115
|
+
return markdownFiles
|
|
116
|
+
.filter((f) => f.startsWith('components-'))
|
|
117
|
+
.map((f) => `- ${f.replace('components-', '').replace('-docs.md', '')}`)
|
|
118
|
+
.join('\n');
|
|
119
|
+
}
|
|
120
|
+
function isPropsRelatedLine(line) {
|
|
121
|
+
return (line.includes('mode') ||
|
|
122
|
+
line.includes('prop') ||
|
|
123
|
+
line.includes('Properties') ||
|
|
124
|
+
line.includes('API') ||
|
|
125
|
+
line.includes('Examples'));
|
|
126
|
+
}
|
|
127
|
+
function extractPropSections(content) {
|
|
128
|
+
const lines = content.split('\n');
|
|
129
|
+
const propSections = [];
|
|
130
|
+
let inPropsSection = false;
|
|
131
|
+
let currentSection = '';
|
|
132
|
+
for (const line of lines) {
|
|
133
|
+
if (isPropsRelatedLine(line)) {
|
|
134
|
+
if (currentSection) {
|
|
135
|
+
propSections.push(currentSection);
|
|
136
|
+
}
|
|
137
|
+
currentSection = line + '\n';
|
|
138
|
+
inPropsSection = true;
|
|
139
|
+
}
|
|
140
|
+
else if (inPropsSection && line.trim()) {
|
|
141
|
+
currentSection += line + '\n';
|
|
142
|
+
}
|
|
143
|
+
else if (inPropsSection && !line.trim()) {
|
|
144
|
+
if (currentSection) {
|
|
145
|
+
propSections.push(currentSection);
|
|
146
|
+
currentSection = '';
|
|
147
|
+
}
|
|
148
|
+
inPropsSection = false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (currentSection) {
|
|
152
|
+
propSections.push(currentSection);
|
|
153
|
+
}
|
|
154
|
+
return propSections;
|
|
155
|
+
}
|
|
156
|
+
function formatPropsResponse(propSections, content, componentFile) {
|
|
157
|
+
return propSections.length > 0
|
|
158
|
+
? propSections.join('\n---\n\n')
|
|
159
|
+
: `Props information extracted from ${componentFile}:\n\n${content.slice(0, 2000)}...`;
|
|
160
|
+
}
|
|
161
|
+
export function handleGetComponentProps(args) {
|
|
162
|
+
const schema = z.object({
|
|
163
|
+
component: z.string(),
|
|
164
|
+
});
|
|
165
|
+
const { component } = schema.parse(args);
|
|
166
|
+
const markdownFiles = getMarkdownFiles();
|
|
167
|
+
const componentFile = findComponentFile(component, markdownFiles);
|
|
168
|
+
if (!componentFile) {
|
|
169
|
+
return {
|
|
170
|
+
content: [
|
|
171
|
+
{
|
|
172
|
+
type: 'text',
|
|
173
|
+
text: `Component "${component}" not found. Available components:\n${formatAvailableComponents(markdownFiles)}`,
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
const filePath = path.join(DOCS_DIR, componentFile);
|
|
180
|
+
const content = readFileContent(filePath);
|
|
181
|
+
const propSections = extractPropSections(content);
|
|
182
|
+
return {
|
|
183
|
+
content: [
|
|
184
|
+
{
|
|
185
|
+
type: 'text',
|
|
186
|
+
text: `**${component} Component Props & API**\n\n${formatPropsResponse(propSections, content, componentFile)}`,
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
throw new Error(`Failed to read component file ${componentFile}: ${error instanceof Error ? error.message : String(error)}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
export function handleListComponents(args) {
|
|
196
|
+
const schema = z.object({
|
|
197
|
+
category: z
|
|
198
|
+
.enum(['components', 'foundations', 'resources', 'all'])
|
|
199
|
+
.default('all'),
|
|
200
|
+
});
|
|
201
|
+
const { category } = schema.parse(args);
|
|
202
|
+
const markdownFiles = getMarkdownFiles();
|
|
203
|
+
const categorized = {
|
|
204
|
+
components: markdownFiles
|
|
205
|
+
.filter((f) => f.startsWith('components-'))
|
|
206
|
+
.map((f) => f.replace('components-', '').replace('-docs.md', '')),
|
|
207
|
+
foundations: markdownFiles
|
|
208
|
+
.filter((f) => f.startsWith('foundations-'))
|
|
209
|
+
.map((f) => f.replace('foundations-', '').replace('-docs.md', '')),
|
|
210
|
+
resources: markdownFiles
|
|
211
|
+
.filter((f) => f.startsWith('resources-'))
|
|
212
|
+
.map((f) => f.replace('resources-', '').replace('-docs.md', '')),
|
|
213
|
+
};
|
|
214
|
+
let output = '**IDS Component Library**\n\n';
|
|
215
|
+
if (category === 'all') {
|
|
216
|
+
const componentsList = categorized.components
|
|
217
|
+
.map((c) => `- ${c}`)
|
|
218
|
+
.join('\n');
|
|
219
|
+
const foundationsList = categorized.foundations
|
|
220
|
+
.map((c) => `- ${c}`)
|
|
221
|
+
.join('\n');
|
|
222
|
+
const resourcesList = categorized.resources.map((c) => `- ${c}`).join('\n');
|
|
223
|
+
output += `**Components (${categorized.components.length})**\n${componentsList}\n\n`;
|
|
224
|
+
output += `**Foundations (${categorized.foundations.length})**\n${foundationsList}\n\n`;
|
|
225
|
+
output += `**Resources (${categorized.resources.length})**\n${resourcesList}`;
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
const items = categorized[category];
|
|
229
|
+
const itemsList = items.map((c) => `- ${c}`).join('\n');
|
|
230
|
+
const categoryTitle = category.charAt(0).toUpperCase() + category.slice(1);
|
|
231
|
+
output += `**${categoryTitle} (${items.length})**\n${itemsList}`;
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
content: [
|
|
235
|
+
{
|
|
236
|
+
type: 'text',
|
|
237
|
+
text: output,
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
};
|
|
241
|
+
}
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for component handlers
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
5
|
+
import { handleFindComponent, handleGetComponentProps, handleListComponents, } from './componentHandlers.js';
|
|
6
|
+
import * as utils from './utils.js';
|
|
7
|
+
// Mock the utils module
|
|
8
|
+
vi.mock('./utils.js', () => ({
|
|
9
|
+
getMarkdownFiles: vi.fn(),
|
|
10
|
+
mapIressComponentToFile: vi.fn(),
|
|
11
|
+
extractIressComponents: vi.fn(),
|
|
12
|
+
readFileContent: vi.fn(),
|
|
13
|
+
}));
|
|
14
|
+
// Mock the config module
|
|
15
|
+
vi.mock('./config.js', () => ({
|
|
16
|
+
DOCS_DIR: '/mocked/docs/path',
|
|
17
|
+
}));
|
|
18
|
+
const mockUtils = vi.mocked(utils);
|
|
19
|
+
describe('componentHandlers', () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
vi.clearAllMocks();
|
|
22
|
+
});
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
vi.restoreAllMocks();
|
|
25
|
+
});
|
|
26
|
+
describe('handleFindComponent', () => {
|
|
27
|
+
const mockMarkdownFiles = [
|
|
28
|
+
'components-button-docs.md',
|
|
29
|
+
'components-input-docs.md',
|
|
30
|
+
'components-table-docs.md',
|
|
31
|
+
'foundations-colors-docs.md',
|
|
32
|
+
'resources-icons-docs.md',
|
|
33
|
+
];
|
|
34
|
+
const mockButtonContent = `# Button Component
|
|
35
|
+
|
|
36
|
+
A versatile button component for user interactions.
|
|
37
|
+
|
|
38
|
+
## Props
|
|
39
|
+
- variant: string - Button style variant
|
|
40
|
+
- size: string - Button size
|
|
41
|
+
|
|
42
|
+
## Examples
|
|
43
|
+
\`\`\`jsx
|
|
44
|
+
<Button variant="primary">Click me</Button>
|
|
45
|
+
\`\`\`
|
|
46
|
+
`;
|
|
47
|
+
it('should find exact match for Iress component', () => {
|
|
48
|
+
const args = {
|
|
49
|
+
query: 'IressButton',
|
|
50
|
+
};
|
|
51
|
+
mockUtils.mapIressComponentToFile.mockReturnValue('components-button-docs.md');
|
|
52
|
+
const result = handleFindComponent(args);
|
|
53
|
+
expect(mockUtils.mapIressComponentToFile).toHaveBeenCalledWith('IressButton');
|
|
54
|
+
expect(result.content).toHaveLength(1);
|
|
55
|
+
expect(result.content[0].type).toBe('text');
|
|
56
|
+
expect(result.content[0].text).toContain('Found exact match for **IressButton**');
|
|
57
|
+
expect(result.content[0].text).toContain('components-button-docs.md');
|
|
58
|
+
expect(result.content[0].text).toContain('Use `get_iress_component_info` with "IressButton"');
|
|
59
|
+
});
|
|
60
|
+
it('should return null for non-matching Iress component', () => {
|
|
61
|
+
const args = {
|
|
62
|
+
query: 'IressNonExistent',
|
|
63
|
+
};
|
|
64
|
+
mockUtils.mapIressComponentToFile.mockReturnValue(null);
|
|
65
|
+
mockUtils.getMarkdownFiles.mockReturnValue(mockMarkdownFiles);
|
|
66
|
+
mockUtils.readFileContent.mockReturnValue('No relevant content');
|
|
67
|
+
mockUtils.extractIressComponents.mockReturnValue([]);
|
|
68
|
+
const result = handleFindComponent(args);
|
|
69
|
+
expect(result.content[0].text).toContain('No IDS components found matching "IressNonExistent"');
|
|
70
|
+
});
|
|
71
|
+
it('should search and rank components by relevance', () => {
|
|
72
|
+
const args = {
|
|
73
|
+
query: 'button',
|
|
74
|
+
};
|
|
75
|
+
mockUtils.mapIressComponentToFile.mockReturnValue(null);
|
|
76
|
+
mockUtils.getMarkdownFiles.mockReturnValue(mockMarkdownFiles);
|
|
77
|
+
mockUtils.extractIressComponents.mockReturnValue([]);
|
|
78
|
+
// Mock file content with different relevance scores
|
|
79
|
+
mockUtils.readFileContent.mockImplementation((filePath) => {
|
|
80
|
+
if (filePath.includes('button')) {
|
|
81
|
+
return mockButtonContent;
|
|
82
|
+
}
|
|
83
|
+
return 'Some other component content';
|
|
84
|
+
});
|
|
85
|
+
const result = handleFindComponent(args);
|
|
86
|
+
expect(result.content[0].text).toContain('Found');
|
|
87
|
+
expect(result.content[0].text).toContain('relevant IDS components');
|
|
88
|
+
expect(result.content[0].text).toContain('button-docs.md');
|
|
89
|
+
});
|
|
90
|
+
it('should filter by category when specified', () => {
|
|
91
|
+
const args = {
|
|
92
|
+
query: 'component',
|
|
93
|
+
category: 'components',
|
|
94
|
+
};
|
|
95
|
+
mockUtils.mapIressComponentToFile.mockReturnValue(null);
|
|
96
|
+
mockUtils.getMarkdownFiles.mockReturnValue(mockMarkdownFiles);
|
|
97
|
+
mockUtils.extractIressComponents.mockReturnValue([]);
|
|
98
|
+
mockUtils.readFileContent.mockReturnValue('Component content');
|
|
99
|
+
handleFindComponent(args);
|
|
100
|
+
// Should only search components category files
|
|
101
|
+
expect(mockUtils.readFileContent).toHaveBeenCalledWith('/mocked/docs/path/components-button-docs.md');
|
|
102
|
+
expect(mockUtils.readFileContent).toHaveBeenCalledWith('/mocked/docs/path/components-input-docs.md');
|
|
103
|
+
expect(mockUtils.readFileContent).not.toHaveBeenCalledWith('/mocked/docs/path/foundations-colors-docs.md');
|
|
104
|
+
});
|
|
105
|
+
it('should handle Iress component mentions in content', () => {
|
|
106
|
+
const args = {
|
|
107
|
+
query: 'button',
|
|
108
|
+
};
|
|
109
|
+
mockUtils.mapIressComponentToFile.mockReturnValue(null);
|
|
110
|
+
mockUtils.getMarkdownFiles.mockReturnValue(['components-form-docs.md']);
|
|
111
|
+
mockUtils.extractIressComponents.mockReturnValue(['IressButton']);
|
|
112
|
+
mockUtils.readFileContent.mockReturnValue('This form uses IressButton component');
|
|
113
|
+
const result = handleFindComponent(args);
|
|
114
|
+
expect(result.content[0].text).toContain('relevant IDS components');
|
|
115
|
+
});
|
|
116
|
+
it('should validate input parameters using zod schema', () => {
|
|
117
|
+
const invalidArgs = {
|
|
118
|
+
query: 123, // Should be string
|
|
119
|
+
};
|
|
120
|
+
expect(() => handleFindComponent(invalidArgs)).toThrow();
|
|
121
|
+
});
|
|
122
|
+
it('should validate category parameter using zod schema', () => {
|
|
123
|
+
const invalidArgs = {
|
|
124
|
+
query: 'button',
|
|
125
|
+
category: 'invalid-category', // Should be enum value
|
|
126
|
+
};
|
|
127
|
+
expect(() => handleFindComponent(invalidArgs)).toThrow();
|
|
128
|
+
});
|
|
129
|
+
it('should handle file reading errors gracefully', () => {
|
|
130
|
+
const args = {
|
|
131
|
+
query: 'button',
|
|
132
|
+
};
|
|
133
|
+
mockUtils.mapIressComponentToFile.mockReturnValue(null);
|
|
134
|
+
mockUtils.getMarkdownFiles.mockReturnValue(['components-button-docs.md']);
|
|
135
|
+
mockUtils.readFileContent.mockImplementation(() => {
|
|
136
|
+
throw new Error('File read error');
|
|
137
|
+
});
|
|
138
|
+
// Should not throw, but handle error gracefully
|
|
139
|
+
const result = handleFindComponent(args);
|
|
140
|
+
expect(result.content[0].text).toContain('No IDS components found matching "button"');
|
|
141
|
+
});
|
|
142
|
+
it('should limit results to top 10', () => {
|
|
143
|
+
const args = {
|
|
144
|
+
query: 'component',
|
|
145
|
+
};
|
|
146
|
+
const manyFiles = Array.from({ length: 15 }, (_, i) => `components-comp${i}-docs.md`);
|
|
147
|
+
mockUtils.mapIressComponentToFile.mockReturnValue(null);
|
|
148
|
+
mockUtils.getMarkdownFiles.mockReturnValue(manyFiles);
|
|
149
|
+
mockUtils.extractIressComponents.mockReturnValue([]);
|
|
150
|
+
mockUtils.readFileContent.mockReturnValue('component content');
|
|
151
|
+
const result = handleFindComponent(args);
|
|
152
|
+
// Count the number of component entries by splitting on numbered items
|
|
153
|
+
const entries = result.content[0].text.split(/\n\d+\. /).length - 1;
|
|
154
|
+
expect(entries).toBeLessThanOrEqual(10);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
describe('handleGetComponentProps', () => {
|
|
158
|
+
const mockMarkdownFiles = [
|
|
159
|
+
'components-button-docs.md',
|
|
160
|
+
'components-input-docs.md',
|
|
161
|
+
];
|
|
162
|
+
const mockComponentContent = `# Button Component
|
|
163
|
+
|
|
164
|
+
## Overview
|
|
165
|
+
A button component
|
|
166
|
+
|
|
167
|
+
## Props
|
|
168
|
+
- variant: string - The button variant
|
|
169
|
+
- size: string - The button size
|
|
170
|
+
|
|
171
|
+
Some text after props
|
|
172
|
+
|
|
173
|
+
## API
|
|
174
|
+
Additional API methods
|
|
175
|
+
|
|
176
|
+
Some text after API
|
|
177
|
+
|
|
178
|
+
## Examples
|
|
179
|
+
Usage examples here
|
|
180
|
+
|
|
181
|
+
Some text after examples
|
|
182
|
+
`;
|
|
183
|
+
it('should return component props when component exists', () => {
|
|
184
|
+
const args = {
|
|
185
|
+
component: 'button',
|
|
186
|
+
};
|
|
187
|
+
mockUtils.getMarkdownFiles.mockReturnValue(mockMarkdownFiles);
|
|
188
|
+
mockUtils.readFileContent.mockReturnValue(mockComponentContent);
|
|
189
|
+
const result = handleGetComponentProps(args);
|
|
190
|
+
expect(result.content).toHaveLength(1);
|
|
191
|
+
expect(result.content[0].type).toBe('text');
|
|
192
|
+
expect(result.content[0].text).toContain('**button Component Props & API**');
|
|
193
|
+
expect(result.content[0].text).toContain('## API');
|
|
194
|
+
expect(result.content[0].text).toContain('Additional API methods');
|
|
195
|
+
});
|
|
196
|
+
it('should return error when component not found', () => {
|
|
197
|
+
const args = {
|
|
198
|
+
component: 'nonexistent',
|
|
199
|
+
};
|
|
200
|
+
mockUtils.getMarkdownFiles.mockReturnValue(mockMarkdownFiles);
|
|
201
|
+
const result = handleGetComponentProps(args);
|
|
202
|
+
expect(result.content[0].text).toContain('Component "nonexistent" not found');
|
|
203
|
+
expect(result.content[0].text).toContain('Available components:');
|
|
204
|
+
expect(result.content[0].text).toContain('- button');
|
|
205
|
+
expect(result.content[0].text).toContain('- input');
|
|
206
|
+
});
|
|
207
|
+
it('should handle case-insensitive component matching', () => {
|
|
208
|
+
const args = {
|
|
209
|
+
component: 'BUTTON',
|
|
210
|
+
};
|
|
211
|
+
mockUtils.getMarkdownFiles.mockReturnValue(mockMarkdownFiles);
|
|
212
|
+
mockUtils.readFileContent.mockReturnValue(mockComponentContent);
|
|
213
|
+
const result = handleGetComponentProps(args);
|
|
214
|
+
expect(result.content[0].text).toContain('**BUTTON Component Props & API**');
|
|
215
|
+
});
|
|
216
|
+
it('should extract props from content with no specific props sections', () => {
|
|
217
|
+
const args = {
|
|
218
|
+
component: 'button',
|
|
219
|
+
};
|
|
220
|
+
const contentWithoutProps = `# Button Component
|
|
221
|
+
This is a simple button component without specific props sections.
|
|
222
|
+
Here is some general documentation.`;
|
|
223
|
+
mockUtils.getMarkdownFiles.mockReturnValue(mockMarkdownFiles);
|
|
224
|
+
mockUtils.readFileContent.mockReturnValue(contentWithoutProps);
|
|
225
|
+
const result = handleGetComponentProps(args);
|
|
226
|
+
expect(result.content[0].text).toContain('This is a simple button component');
|
|
227
|
+
});
|
|
228
|
+
it('should throw error when file reading fails', () => {
|
|
229
|
+
const args = {
|
|
230
|
+
component: 'button',
|
|
231
|
+
};
|
|
232
|
+
mockUtils.getMarkdownFiles.mockReturnValue(mockMarkdownFiles);
|
|
233
|
+
mockUtils.readFileContent.mockImplementation(() => {
|
|
234
|
+
throw new Error('File access denied');
|
|
235
|
+
});
|
|
236
|
+
expect(() => handleGetComponentProps(args)).toThrow('Failed to read component file components-button-docs.md: File access denied');
|
|
237
|
+
});
|
|
238
|
+
it('should validate input parameters using zod schema', () => {
|
|
239
|
+
const invalidArgs = {
|
|
240
|
+
component: 123, // Should be string
|
|
241
|
+
};
|
|
242
|
+
expect(() => handleGetComponentProps(invalidArgs)).toThrow();
|
|
243
|
+
});
|
|
244
|
+
it('should handle multiple prop-related sections', () => {
|
|
245
|
+
const contentWithMultipleSections = `# Button Component
|
|
246
|
+
|
|
247
|
+
## Props
|
|
248
|
+
- variant: string
|
|
249
|
+
|
|
250
|
+
## API
|
|
251
|
+
Methods available
|
|
252
|
+
|
|
253
|
+
## Properties
|
|
254
|
+
Additional properties
|
|
255
|
+
|
|
256
|
+
## mode
|
|
257
|
+
Different modes
|
|
258
|
+
`;
|
|
259
|
+
const args = {
|
|
260
|
+
component: 'button',
|
|
261
|
+
};
|
|
262
|
+
mockUtils.getMarkdownFiles.mockReturnValue(mockMarkdownFiles);
|
|
263
|
+
mockUtils.readFileContent.mockReturnValue(contentWithMultipleSections);
|
|
264
|
+
const result = handleGetComponentProps(args);
|
|
265
|
+
expect(result.content[0].text).toContain('Props');
|
|
266
|
+
expect(result.content[0].text).toContain('API');
|
|
267
|
+
expect(result.content[0].text).toContain('Properties');
|
|
268
|
+
expect(result.content[0].text).toContain('mode');
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
describe('handleListComponents', () => {
|
|
272
|
+
const mockMarkdownFiles = [
|
|
273
|
+
'components-button-docs.md',
|
|
274
|
+
'components-input-docs.md',
|
|
275
|
+
'components-table-docs.md',
|
|
276
|
+
'foundations-colors-docs.md',
|
|
277
|
+
'foundations-typography-docs.md',
|
|
278
|
+
'resources-icons-docs.md',
|
|
279
|
+
];
|
|
280
|
+
it('should list all components by default', () => {
|
|
281
|
+
const args = {};
|
|
282
|
+
mockUtils.getMarkdownFiles.mockReturnValue(mockMarkdownFiles);
|
|
283
|
+
const result = handleListComponents(args);
|
|
284
|
+
expect(result.content).toHaveLength(1);
|
|
285
|
+
expect(result.content[0].type).toBe('text');
|
|
286
|
+
expect(result.content[0].text).toContain('**IDS Component Library**');
|
|
287
|
+
expect(result.content[0].text).toContain('**Components (3)**');
|
|
288
|
+
expect(result.content[0].text).toContain('- button');
|
|
289
|
+
expect(result.content[0].text).toContain('- input');
|
|
290
|
+
expect(result.content[0].text).toContain('- table');
|
|
291
|
+
expect(result.content[0].text).toContain('**Foundations (2)**');
|
|
292
|
+
expect(result.content[0].text).toContain('- colors');
|
|
293
|
+
expect(result.content[0].text).toContain('- typography');
|
|
294
|
+
expect(result.content[0].text).toContain('**Resources (1)**');
|
|
295
|
+
expect(result.content[0].text).toContain('- icons');
|
|
296
|
+
});
|
|
297
|
+
it('should list only components when category is "components"', () => {
|
|
298
|
+
const args = {
|
|
299
|
+
category: 'components',
|
|
300
|
+
};
|
|
301
|
+
mockUtils.getMarkdownFiles.mockReturnValue(mockMarkdownFiles);
|
|
302
|
+
const result = handleListComponents(args);
|
|
303
|
+
expect(result.content[0].text).toContain('**Components (3)**');
|
|
304
|
+
expect(result.content[0].text).toContain('- button');
|
|
305
|
+
expect(result.content[0].text).toContain('- input');
|
|
306
|
+
expect(result.content[0].text).toContain('- table');
|
|
307
|
+
expect(result.content[0].text).not.toContain('**Foundations');
|
|
308
|
+
expect(result.content[0].text).not.toContain('**Resources');
|
|
309
|
+
});
|
|
310
|
+
it('should list only foundations when category is "foundations"', () => {
|
|
311
|
+
const args = {
|
|
312
|
+
category: 'foundations',
|
|
313
|
+
};
|
|
314
|
+
mockUtils.getMarkdownFiles.mockReturnValue(mockMarkdownFiles);
|
|
315
|
+
const result = handleListComponents(args);
|
|
316
|
+
expect(result.content[0].text).toContain('**Foundations (2)**');
|
|
317
|
+
expect(result.content[0].text).toContain('- colors');
|
|
318
|
+
expect(result.content[0].text).toContain('- typography');
|
|
319
|
+
expect(result.content[0].text).not.toContain('**Components');
|
|
320
|
+
expect(result.content[0].text).not.toContain('**Resources');
|
|
321
|
+
});
|
|
322
|
+
it('should list only resources when category is "resources"', () => {
|
|
323
|
+
const args = {
|
|
324
|
+
category: 'resources',
|
|
325
|
+
};
|
|
326
|
+
mockUtils.getMarkdownFiles.mockReturnValue(mockMarkdownFiles);
|
|
327
|
+
const result = handleListComponents(args);
|
|
328
|
+
expect(result.content[0].text).toContain('**Resources (1)**');
|
|
329
|
+
expect(result.content[0].text).toContain('- icons');
|
|
330
|
+
expect(result.content[0].text).not.toContain('**Components');
|
|
331
|
+
expect(result.content[0].text).not.toContain('**Foundations');
|
|
332
|
+
});
|
|
333
|
+
it('should list all categories when category is "all"', () => {
|
|
334
|
+
const args = {
|
|
335
|
+
category: 'all',
|
|
336
|
+
};
|
|
337
|
+
mockUtils.getMarkdownFiles.mockReturnValue(mockMarkdownFiles);
|
|
338
|
+
const result = handleListComponents(args);
|
|
339
|
+
expect(result.content[0].text).toContain('**Components (3)**');
|
|
340
|
+
expect(result.content[0].text).toContain('**Foundations (2)**');
|
|
341
|
+
expect(result.content[0].text).toContain('**Resources (1)**');
|
|
342
|
+
});
|
|
343
|
+
it('should handle empty file lists', () => {
|
|
344
|
+
const args = {
|
|
345
|
+
category: 'components',
|
|
346
|
+
};
|
|
347
|
+
mockUtils.getMarkdownFiles.mockReturnValue([]);
|
|
348
|
+
const result = handleListComponents(args);
|
|
349
|
+
expect(result.content[0].text).toContain('**Components (0)**');
|
|
350
|
+
});
|
|
351
|
+
it('should validate category parameter using zod schema', () => {
|
|
352
|
+
const invalidArgs = {
|
|
353
|
+
category: 'invalid-category', // Should be enum value
|
|
354
|
+
};
|
|
355
|
+
expect(() => handleListComponents(invalidArgs)).toThrow();
|
|
356
|
+
});
|
|
357
|
+
it('should use default category when not specified', () => {
|
|
358
|
+
const args = {};
|
|
359
|
+
mockUtils.getMarkdownFiles.mockReturnValue(mockMarkdownFiles);
|
|
360
|
+
const result = handleListComponents(args);
|
|
361
|
+
// Should default to 'all' and show all categories
|
|
362
|
+
expect(result.content[0].text).toContain('**Components');
|
|
363
|
+
expect(result.content[0].text).toContain('**Foundations');
|
|
364
|
+
expect(result.content[0].text).toContain('**Resources');
|
|
365
|
+
});
|
|
366
|
+
it('should properly format component names by removing prefixes and suffixes', () => {
|
|
367
|
+
const customFiles = [
|
|
368
|
+
'components-complex-button-component-docs.md',
|
|
369
|
+
'foundations-design-tokens-docs.md',
|
|
370
|
+
'resources-icon-library-docs.md',
|
|
371
|
+
];
|
|
372
|
+
const args = {};
|
|
373
|
+
mockUtils.getMarkdownFiles.mockReturnValue(customFiles);
|
|
374
|
+
const result = handleListComponents(args);
|
|
375
|
+
expect(result.content[0].text).toContain('- complex-button-component');
|
|
376
|
+
expect(result.content[0].text).toContain('- design-tokens');
|
|
377
|
+
expect(result.content[0].text).toContain('- icon-library');
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
});
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration constants for the IDS MCP Server
|
|
3
|
+
*/
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
export const DOCS_DIR = path.join(__dirname, '..', 'generated', 'docs');
|
|
9
|
+
export const SERVER_CONFIG = {
|
|
10
|
+
name: 'ids-mcp-server',
|
|
11
|
+
version: '1.0.0',
|
|
12
|
+
};
|
|
13
|
+
export const CAPABILITIES = {
|
|
14
|
+
resources: {},
|
|
15
|
+
tools: {},
|
|
16
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import { SERVER_CONFIG, CAPABILITIES } from './config.js';
|
|
6
|
+
import { toolDefinitions } from './tools.js';
|
|
7
|
+
import { handleToolCall } from './toolHandler.js';
|
|
8
|
+
import { handleListResources, handleReadResource } from './resourceHandlers.js';
|
|
9
|
+
/**
|
|
10
|
+
* Create MCP server for Iress Design System (IDS) component library context
|
|
11
|
+
*/
|
|
12
|
+
const server = new Server(SERVER_CONFIG, { capabilities: CAPABILITIES });
|
|
13
|
+
/**
|
|
14
|
+
* List available IDS component documentation resources
|
|
15
|
+
*/
|
|
16
|
+
server.setRequestHandler(ListResourcesRequestSchema, () => {
|
|
17
|
+
const result = handleListResources();
|
|
18
|
+
return result;
|
|
19
|
+
});
|
|
20
|
+
/**
|
|
21
|
+
* Read content from a specific markdown file
|
|
22
|
+
*/
|
|
23
|
+
server.setRequestHandler(ReadResourceRequestSchema, (request) => {
|
|
24
|
+
const result = handleReadResource(request);
|
|
25
|
+
return result;
|
|
26
|
+
});
|
|
27
|
+
/**
|
|
28
|
+
* List available IDS development tools
|
|
29
|
+
*/
|
|
30
|
+
server.setRequestHandler(ListToolsRequestSchema, () => {
|
|
31
|
+
return {
|
|
32
|
+
tools: toolDefinitions,
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
/**
|
|
36
|
+
* Handle IDS component library tool calls
|
|
37
|
+
*/
|
|
38
|
+
server.setRequestHandler(CallToolRequestSchema, (request) => {
|
|
39
|
+
const result = handleToolCall(request);
|
|
40
|
+
return result;
|
|
41
|
+
});
|
|
42
|
+
/**
|
|
43
|
+
* Start the server
|
|
44
|
+
*/
|
|
45
|
+
async function main() {
|
|
46
|
+
const transport = new StdioServerTransport();
|
|
47
|
+
await server.connect(transport);
|
|
48
|
+
console.error('IDS Component Library MCP Server running on stdio');
|
|
49
|
+
}
|
|
50
|
+
main().catch((error) => {
|
|
51
|
+
console.error('Server error:', error);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
});
|