UNPKG

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
/** * 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