ids-enterprise-mcp-server
Version:
Model Context Protocol (MCP) server providing comprehensive IDS Enterprise Web Components documentation access via GitLab API. Use with npx and GitLab token for instant access.
592 lines • 26.8 kB
JavaScript
/**
* Tool handlers for MCP server requests
*/
import { SearchUtils } from '../utils/index.js';
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
import { logger } from '../utils/logger.js';
export class ToolHandlers {
components = [];
documentation = [];
examples = [];
frameworks = [];
constructor(components, documentation, examples, frameworks) {
this.components = components;
this.documentation = documentation;
this.examples = examples;
this.frameworks = frameworks;
}
/**
* Search components by query and category
*/
async searchComponents(args) {
const { query, category, limit = 5 } = args;
if (!query || !query.trim()) {
throw new McpError(ErrorCode.InvalidParams, 'Search query cannot be empty');
}
logger.api(`Searching components for: "${query}" in category: "${category || 'all'}"`);
const results = [];
const searchTerms = query.toLowerCase().split(' ');
for (const component of this.components) {
if (category && !component.category.toLowerCase().includes(category.toLowerCase())) {
continue;
}
const relevanceScore = SearchUtils.calculateRelevanceScore(searchTerms, component);
if (relevanceScore > 0) {
results.push({
component: component.name,
content: component.description,
category: component.category,
relevanceScore,
filePath: component.filePath,
});
}
}
// Sort by relevance and limit results
results.sort((a, b) => b.relevanceScore - a.relevanceScore);
const limitedResults = results.slice(0, limit);
return {
content: [
{
type: 'text',
text: JSON.stringify({
query,
category: category || 'all',
totalResults: results.length,
results: limitedResults.map(r => ({
component: r.component,
description: r.content,
category: r.category,
relevanceScore: r.relevanceScore,
})),
}, null, 2),
},
],
};
}
/**
* Get detailed information about a specific component
*/
async getComponentDetails(args) {
const { component } = args;
if (!component || !component.trim()) {
throw new McpError(ErrorCode.InvalidParams, 'Component name cannot be empty');
}
logger.api(`Getting component details for: "${component}"`);
const foundComponent = this.components.find(c => c.name.toLowerCase() === component.toLowerCase() ||
c.name.toLowerCase().includes(component.toLowerCase()));
if (!foundComponent) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: `Component "${component}" not found`,
suggestion: 'Try searching with the search_components tool',
availableComponents: this.components.slice(0, 10).map(c => c.name),
}, null, 2),
},
],
};
}
// Count framework examples for this component
const normalizedComponent = component.startsWith('ids-') ? component : `ids-${component}`;
const frameworkExamples = this.examples.filter(example => {
const exampleName = example.name.toLowerCase();
const componentLower = normalizedComponent.toLowerCase();
const originalComponentLower = component.toLowerCase();
return exampleName.includes(componentLower) ||
exampleName.includes(originalComponentLower) ||
example.content.toLowerCase().includes(componentLower) ||
example.content.toLowerCase().includes(originalComponentLower);
});
return {
content: [
{
type: 'text',
text: JSON.stringify({
name: foundComponent.name,
category: foundComponent.category,
description: foundComponent.description,
features: foundComponent.features,
attributes: foundComponent.attributes,
methods: foundComponent.methods,
events: foundComponent.events,
readmeCodeSnippets: foundComponent.examples.length,
frameworkExamples: frameworkExamples.length,
frameworkExamplesByType: {
Angular: frameworkExamples.filter(ex => ex.framework === 'Angular').length,
React: frameworkExamples.filter(ex => ex.framework === 'React').length
},
documentation: foundComponent.content.substring(0, 1000) + (foundComponent.content.length > 1000 ? '...' : ''),
gitlabUrl: foundComponent.filePath,
}, null, 2),
},
],
};
}
/**
* List components by category
*/
async listComponentsByCategory(args) {
const { category } = args;
logger.api(`Listing components in category: "${category}"`);
const componentsInCategory = this.components.filter(c => c.category.toLowerCase().includes(category.toLowerCase()));
if (componentsInCategory.length === 0) {
const availableCategories = [...new Set(this.components.map(c => c.category))];
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: `No components found in category "${category}"`,
availableCategories,
}, null, 2),
},
],
};
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
category,
componentCount: componentsInCategory.length,
components: componentsInCategory.map(c => ({
name: c.name,
description: c.description,
gitlabUrl: c.filePath,
})),
}, null, 2),
},
],
};
}
/**
* Get component README examples (from main repository documentation)
*/
async getComponentReadmeExamples(args) {
const { component } = args;
if (!component || !component.trim()) {
throw new McpError(ErrorCode.InvalidParams, 'Component name cannot be empty');
}
logger.api(`Getting README examples for component: "${component}"`);
// Normalize component name (ensure it starts with ids-)
const normalizedComponent = component.startsWith('ids-') ? component : `ids-${component}`;
// Find the component
const foundComponent = this.components.find(c => c.name.toLowerCase() === normalizedComponent.toLowerCase() ||
c.name.toLowerCase() === component.toLowerCase());
if (!foundComponent) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: `Component "${component}" not found`,
suggestion: 'Use search_components to find available components',
availableComponents: this.components.slice(0, 10).map(c => c.name),
}, null, 2),
},
],
};
}
if (foundComponent.examples.length === 0) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
component: foundComponent.name,
message: 'No README examples found for this component',
suggestion: 'Try get_component_framework_examples for implementation examples',
componentDetails: {
description: foundComponent.description,
category: foundComponent.category,
features: foundComponent.features.length,
attributes: foundComponent.attributes.length,
gitlabUrl: foundComponent.filePath,
},
}, null, 2),
},
],
};
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
component: foundComponent.name,
source: 'README documentation',
repository: 'infor-design/enterprise-wc',
exampleCount: foundComponent.examples.length,
examples: foundComponent.examples.map((example, index) => ({
exampleNumber: index + 1,
codeSnippet: example,
type: 'README code example',
})),
componentInfo: {
description: foundComponent.description,
category: foundComponent.category,
features: foundComponent.features.length,
attributes: foundComponent.attributes.length,
methods: foundComponent.methods.length,
events: foundComponent.events.length,
gitlabUrl: foundComponent.filePath,
},
}, null, 2),
},
],
};
}
/**
* Get component framework examples (from examples repository)
*/
async getComponentFrameworkExamples(args) {
const { component, framework } = args;
if (!component || !component.trim()) {
throw new McpError(ErrorCode.InvalidParams, 'Component name cannot be empty');
}
if (!framework || !framework.trim()) {
throw new McpError(ErrorCode.InvalidParams, 'Framework must be specified (e.g., React, Angular)');
}
logger.api(`Getting framework examples for component: "${component}" framework: "${framework}"`);
logger.debug(`Total examples available: ${this.examples.length}`);
logger.debug(`React examples: ${this.examples.filter(e => e.framework === 'React').length}`);
logger.debug(`Angular examples: ${this.examples.filter(e => e.framework === 'Angular').length}`);
// Normalize component name (ensure it starts with ids-)
const normalizedComponent = component.startsWith('ids-') ? component : `ids-${component}`;
// Find examples for the component in the specified framework
let relevantExamples = this.examples.filter(example => {
const exampleName = example.name.toLowerCase();
const componentLower = normalizedComponent.toLowerCase();
const originalComponentLower = component.toLowerCase();
const frameworkMatch = example.framework.toLowerCase().includes(framework.toLowerCase());
return frameworkMatch && (exampleName.includes(componentLower) ||
exampleName.includes(originalComponentLower) ||
example.content.toLowerCase().includes(componentLower) ||
example.content.toLowerCase().includes(originalComponentLower));
});
logger.debug(`Found ${relevantExamples.length} relevant examples after first filter`);
// If no direct matches, try broader search
if (relevantExamples.length === 0) {
const componentBaseName = component.replace('ids-', '').replace('-', '');
relevantExamples = this.examples.filter(example => {
const exampleName = example.name.toLowerCase();
const frameworkMatch = example.framework.toLowerCase().includes(framework.toLowerCase());
return frameworkMatch && (exampleName.includes(componentBaseName) ||
example.content.toLowerCase().includes(componentBaseName));
});
}
logger.debug(`Final relevant examples count: ${relevantExamples.length}`);
if (relevantExamples.length === 0) {
// Check if component exists
const componentExists = this.components.find(c => c.name.toLowerCase() === normalizedComponent.toLowerCase() ||
c.name.toLowerCase() === component.toLowerCase());
// Check if framework exists
const availableFrameworks = this.frameworks.map(f => f.name);
const frameworkExists = availableFrameworks.some(f => f.toLowerCase().includes(framework.toLowerCase()));
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: `No ${framework} examples found for component "${component}"`,
componentExists: !!componentExists,
frameworkExists,
availableFrameworks,
suggestions: [
componentExists ? null : 'Use search_components to find available components',
frameworkExists ? null : `Available frameworks: ${availableFrameworks.join(', ')}`,
'Try get_component_readme_examples for basic usage examples',
'Use search_examples to find related examples',
].filter(Boolean),
totalExamplesAvailable: this.examples.length,
}, null, 2),
},
],
};
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
component: normalizedComponent,
framework,
source: 'Framework examples repository',
repository: 'infor-design/enterprise-wc-examples',
exampleCount: relevantExamples.length,
examples: relevantExamples.slice(0, 10).map(example => ({
name: example.name,
framework: example.framework,
description: example.description,
codePreview: example.content.substring(0, 400) + (example.content.length > 400 ? '...\n\n[See full example at GitLab URL]' : ''),
filePath: example.filePath,
setupInstructions: `See the ${framework} README for setup instructions: https://oxford.awsdev.infor.com/infor-design/enterprise-wc-examples/-/blob/development/${framework.toLowerCase()}-ids-wc/README.MD`,
})),
totalAvailable: relevantExamples.length,
note: relevantExamples.length > 10 ? `Showing first 10 of ${relevantExamples.length} available examples. Use get_framework_guide for more examples.` : undefined,
}, null, 2),
},
],
};
}
/**
* Get framework guide
*/
async getFrameworkGuide(args) {
const { framework } = args;
logger.api(`Getting framework guide for: "${framework}"`);
const foundFramework = this.frameworks.find(f => f.name.toLowerCase().includes(framework.toLowerCase()) ||
framework.toLowerCase().includes(f.name.toLowerCase()));
if (!foundFramework) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: `Framework "${framework}" not found`,
availableFrameworks: this.frameworks.map(f => f.name),
}, null, 2),
},
],
};
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
framework: foundFramework.name,
description: foundFramework.description,
exampleCount: foundFramework.exampleCount,
setupGuide: foundFramework.setupGuide.substring(0, 2000) + (foundFramework.setupGuide.length > 2000 ? '...' : ''),
readmePath: foundFramework.readmePath,
examples: foundFramework.examples.slice(0, 5).map(ex => ({
name: ex.name,
description: ex.description,
filePath: ex.filePath,
})),
}, null, 2),
},
],
};
}
/**
* List all frameworks
*/
async listFrameworks() {
logger.api('Listing all frameworks');
return {
content: [
{
type: 'text',
text: JSON.stringify({
totalFrameworks: this.frameworks.length,
totalExamples: this.examples.length,
frameworks: this.frameworks.map(framework => ({
name: framework.name,
description: framework.description,
exampleCount: framework.exampleCount,
readmePath: framework.readmePath,
})),
}, null, 2),
},
],
};
}
/**
* Search documentation
*/
async searchDocumentation(args) {
const { query, limit = 3 } = args;
logger.api(`Searching documentation for: "${query}"`);
const results = [];
const searchTerms = query.toLowerCase().split(' ');
for (const doc of this.documentation) {
let relevanceScore = 0;
for (const term of searchTerms) {
if (doc.name.toLowerCase().includes(term)) {
relevanceScore += 3;
}
if (doc.content.toLowerCase().includes(term)) {
relevanceScore += 1;
}
}
if (relevanceScore > 0) {
results.push({
component: doc.name,
content: doc.content.substring(0, 500) + (doc.content.length > 500 ? '...' : ''),
category: doc.category,
relevanceScore,
filePath: doc.filePath,
});
}
}
results.sort((a, b) => b.relevanceScore - a.relevanceScore);
const limitedResults = results.slice(0, limit);
return {
content: [
{
type: 'text',
text: JSON.stringify({
query,
totalResults: results.length,
results: limitedResults,
}, null, 2),
},
],
};
}
/**
* Get project overview
*/
async getProjectOverview() {
logger.api('Getting project overview');
const categories = [...new Set(this.components.map(c => c.category))];
const totalComponents = this.components.length;
const overview = this.documentation.find(d => d.name.toLowerCase().includes('readme') || d.name.toLowerCase().includes('overview'));
return {
content: [
{
type: 'text',
text: JSON.stringify({
project: 'IDS Enterprise Web Components',
description: 'Framework independent UI library consisting of CSS and JS that provides Infor product development teams, partners, and customers the tools to create user experiences.',
totalComponents,
categories,
keyFeatures: [
'Three themes, including WCAG 2.0 AAA compatible high-contrast theme',
'Responsive and Mobile Adaptable',
'Touch-friendly interactions',
'SVG-based iconography for high DPI screens',
'Built-in localization system',
'Built-in XSS exploit mitigation',
'Excellent test coverage',
'WAI-ARIA authoring practices compliance',
'Fully namespaced with ids- namespace',
'100+ Components',
'TypeScript support',
'ES Modules support'
],
browserSupport: 'Latest release and R-1 for browsers and OS versions',
installation: 'npm install --save ids-enterprise-wc@latest',
overview: overview?.content.substring(0, 1000) || 'No detailed overview available',
}, null, 2),
},
],
};
}
/**
* List all categories
*/
async listAllCategories() {
logger.api('Listing all categories');
const categories = [...new Set(this.components.map(c => c.category))];
const categoryStats = categories.map(category => ({
name: category,
componentCount: this.components.filter(c => c.category === category).length,
components: this.components
.filter(c => c.category === category)
.map(c => c.name)
.slice(0, 5), // Show first 5 components as examples
}));
return {
content: [
{
type: 'text',
text: JSON.stringify({
totalCategories: categories.length,
totalComponents: this.components.length,
categories: categoryStats,
}, null, 2),
},
],
};
}
/**
* Get development guidelines
*/
async getDevelopmentGuidelines(args) {
const { topic } = args;
logger.api(`Getting development guidelines for topic: "${topic || 'all'}"`);
const guidelines = {
general: [
'Always use sentence case for static text',
'Follow WAI-ARIA authoring practices with focus on accessibility',
'All interactive elements must be keyboard accessible',
'Use ARIA attributes appropriately',
'Ensure proper focus management'
],
testing: [
'Use Playwright for testing',
'Always use await where needed',
'Never use waitForTimeout or delay',
'Write comprehensive test coverage'
],
accessibility: [
'All interactive elements must be keyboard accessible',
'Use ARIA attributes appropriately',
'Ensure proper focus management',
'Follow WCAG 2.0 AAA guidelines',
'Test with screen readers'
],
theming: [
'Three themes available including high-contrast',
'Use CSS custom properties for theming',
'Support color-variant attribute',
'Ensure accessible color contrast ratios'
],
components: [
'Follow the Gold standard for making web components',
'Use ids- namespace for all components',
'Implement CSS and DOM encapsulation',
'Support TypeScript types',
'Include Visual Studio Code intellisense'
]
};
let relevantGuidelines = guidelines.general;
if (topic) {
const topicLower = topic.toLowerCase();
if (topicLower.includes('test')) {
relevantGuidelines = guidelines.testing;
}
else if (topicLower.includes('access')) {
relevantGuidelines = guidelines.accessibility;
}
else if (topicLower.includes('theme') || topicLower.includes('style')) {
relevantGuidelines = guidelines.theming;
}
else if (topicLower.includes('component')) {
relevantGuidelines = guidelines.components;
}
}
// Find relevant documentation
const relevantDocs = this.documentation.filter(doc => {
if (!topic)
return true;
return doc.name.toLowerCase().includes(topic.toLowerCase()) ||
doc.content.toLowerCase().includes(topic.toLowerCase());
});
return {
content: [
{
type: 'text',
text: JSON.stringify({
topic: topic || 'general',
guidelines: relevantGuidelines,
relatedDocumentation: relevantDocs.slice(0, 3).map(doc => ({
name: doc.name,
category: doc.category,
excerpt: doc.content.substring(0, 200) + '...',
gitlabUrl: doc.filePath,
})),
allTopics: Object.keys(guidelines),
}, null, 2),
},
],
};
}
}
//# sourceMappingURL=tool-handlers.js.map