Skip to main content
Glama

UI/UX MCP Server

by willem4130
components.tsโ€ข14.5 kB
import { z } from 'zod'; const ComponentCreateSchema = z.object({ name: z.string(), type: z.enum(['react', 'vue', 'svelte', 'web-component']), props: z.array(z.object({ name: z.string(), type: z.string(), required: z.boolean().optional(), default: z.any().optional() })).optional(), styles: z.record(z.any()).optional(), accessibility: z.object({ role: z.string().optional(), ariaLabel: z.string().optional(), ariaDescribedBy: z.string().optional(), tabIndex: z.number().optional() }).optional() }); const ComponentAnalyzeSchema = z.object({ code: z.string(), checks: z.array(z.enum(['performance', 'accessibility', 'best-practices', 'seo'])).optional() }); export class ComponentTools { constructor() {} async create(args: any) { const params = ComponentCreateSchema.parse(args); try { let componentCode: string; switch (params.type) { case 'react': componentCode = this.generateReactComponent(params); break; case 'vue': componentCode = this.generateVueComponent(params); break; case 'svelte': componentCode = this.generateSvelteComponent(params); break; case 'web-component': componentCode = this.generateWebComponent(params); break; default: throw new Error(`Unsupported component type: ${params.type}`); } const tests = this.generateTests(params); const storybook = this.generateStorybookStory(params); const documentation = this.generateDocumentation(params); return { content: [ { type: 'text', text: JSON.stringify({ component: { name: params.name, type: params.type, code: componentCode, tests, storybook, documentation }, files: { component: `${params.name}.${this.getFileExtension(params.type)}`, test: `${params.name}.test.${this.getFileExtension(params.type)}`, story: `${params.name}.stories.${this.getFileExtension(params.type)}`, docs: `${params.name}.md` } }, null, 2) } ] }; } catch (error: any) { return { content: [ { type: 'text', text: `Error creating component: ${error.message}` } ], isError: true }; } } async analyze(args: any) { const params = ComponentAnalyzeSchema.parse(args); const checks = params.checks || ['performance', 'accessibility', 'best-practices']; try { const results: any = { summary: { score: 0, issues: [], suggestions: [] }, checks: {} }; if (checks.includes('performance')) { results.checks.performance = this.analyzePerformance(params.code); } if (checks.includes('accessibility')) { results.checks.accessibility = this.analyzeAccessibility(params.code); } if (checks.includes('best-practices')) { results.checks.bestPractices = this.analyzeBestPractices(params.code); } if (checks.includes('seo')) { results.checks.seo = this.analyzeSEO(params.code); } // Calculate overall score const scores = Object.values(results.checks).map((check: any) => check.score); results.summary.score = scores.reduce((a, b) => a + b, 0) / scores.length; // Collect all issues and suggestions Object.values(results.checks).forEach((check: any) => { results.summary.issues.push(...(check.issues || [])); results.summary.suggestions.push(...(check.suggestions || [])); }); return { content: [ { type: 'text', text: JSON.stringify(results, null, 2) } ] }; } catch (error: any) { return { content: [ { type: 'text', text: `Error analyzing component: ${error.message}` } ], isError: true }; } } private generateReactComponent(params: any): string { const props = params.props || []; const propTypes = props.map((p: any) => ` ${p.name}${p.required ? '' : '?'}: ${this.mapTypeToTS(p.type)};`).join('\n'); const defaultProps = props.filter((p: any) => p.default !== undefined); return `import React from 'react'; interface ${params.name}Props { ${propTypes} } export const ${params.name}: React.FC<${params.name}Props> = ({ ${props.map((p: any) => p.name + (p.default ? ` = ${JSON.stringify(p.default)}` : '')).join(',\n ')} }) => { return ( <div className="${params.name.toLowerCase()}" ${params.accessibility?.role ? `role="${params.accessibility.role}"` : ''} ${params.accessibility?.ariaLabel ? `aria-label="${params.accessibility.ariaLabel}"` : ''} ${params.accessibility?.tabIndex !== undefined ? `tabIndex={${params.accessibility.tabIndex}}` : ''} > {/* Component content */} </div> ); }; ${params.name}.displayName = '${params.name}';`; } private generateVueComponent(params: any): string { const props = params.props || []; return `<template> <div class="${params.name.toLowerCase()}" ${params.accessibility?.role ? `:role="${params.accessibility.role}"` : ''} ${params.accessibility?.ariaLabel ? `:aria-label="${params.accessibility.ariaLabel}"` : ''} > <!-- Component content --> </div> </template> <script setup lang="ts"> interface Props { ${props.map((p: any) => `${p.name}${p.required ? '' : '?'}: ${this.mapTypeToTS(p.type)}`).join(';\n ')}; } const props = withDefaults(defineProps<Props>(), { ${props.filter((p: any) => p.default).map((p: any) => `${p.name}: ${JSON.stringify(p.default)}`).join(',\n ')} }); </script> <style scoped> .${params.name.toLowerCase()} { /* Component styles */ } </style>`; } private generateSvelteComponent(params: any): string { const props = params.props || []; return `<script lang="ts"> ${props.map((p: any) => `export let ${p.name}${p.type ? `: ${this.mapTypeToTS(p.type)}` : ''}${p.default ? ` = ${JSON.stringify(p.default)}` : ''};`).join('\n ')} </script> <div class="${params.name.toLowerCase()}" ${params.accessibility?.role ? `role="${params.accessibility.role}"` : ''} ${params.accessibility?.ariaLabel ? `aria-label="${params.accessibility.ariaLabel}"` : ''} > <!-- Component content --> </div> <style> .${params.name.toLowerCase()} { /* Component styles */ } </style>`; } private generateWebComponent(params: any): string { return `class ${params.name} extends HTMLElement { static get observedAttributes() { return [${(params.props || []).map((p: any) => `'${p.name}'`).join(', ')}]; } constructor() { super(); this.attachShadow({ mode: 'open' }); } connectedCallback() { this.render(); } attributeChangedCallback(name, oldValue, newValue) { if (oldValue !== newValue) { this.render(); } } render() { this.shadowRoot.innerHTML = \` <style> :host { display: block; } </style> <div ${params.accessibility?.role ? `role="${params.accessibility.role}"` : ''}> <!-- Component content --> </div> \`; } } customElements.define('${params.name.toLowerCase()}-component', ${params.name});`; } private generateTests(params: any): string { return `import { render, screen } from '@testing-library/react'; import { ${params.name} } from './${params.name}'; describe('${params.name}', () => { it('renders without crashing', () => { render(<${params.name} />); }); ${(params.props || []).filter((p: any) => p.required).map((p: any) => ` it('renders with required prop ${p.name}', () => { render(<${params.name} ${p.name}={${this.getTestValue(p.type)}} />); });`).join('')} ${params.accessibility?.role ? ` it('has correct ARIA role', () => { render(<${params.name} />); expect(screen.getByRole('${params.accessibility.role}')).toBeInTheDocument(); });` : ''} });`; } private generateStorybookStory(params: any): string { return `import type { Meta, StoryObj } from '@storybook/react'; import { ${params.name} } from './${params.name}'; const meta: Meta<typeof ${params.name}> = { title: 'Components/${params.name}', component: ${params.name}, parameters: { layout: 'centered', }, tags: ['autodocs'], }; export default meta; type Story = StoryObj<typeof meta>; export const Default: Story = { args: { ${(params.props || []).map((p: any) => `${p.name}: ${JSON.stringify(p.default || this.getTestValue(p.type))}`).join(',\n ')} }, };`; } private generateDocumentation(params: any): string { return `# ${params.name} ## Overview ${params.name} component for ${params.type} applications. ## Props ${(params.props || []).length > 0 ? ` | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| ${params.props.map((p: any) => `| ${p.name} | ${p.type} | ${p.required ? 'Yes' : 'No'} | ${p.default || '-'} | - |`).join('\n')} ` : 'No props defined.'} ## Accessibility ${params.accessibility ? ` - Role: ${params.accessibility.role || 'Not specified'} - ARIA Label: ${params.accessibility.ariaLabel || 'Not specified'} - Tab Index: ${params.accessibility.tabIndex !== undefined ? params.accessibility.tabIndex : 'Default'} ` : 'No accessibility attributes specified.'} ## Usage \`\`\`${this.getFileExtension(params.type)} // Import the component import { ${params.name} } from './${params.name}'; // Use in your application <${params.name} ${(params.props || []).filter((p: any) => p.required).map((p: any) => `${p.name}={...}`).join(' ')} /> \`\`\``; } private analyzePerformance(code: string): any { const issues = []; const suggestions = []; // Check for common performance issues if (code.includes('componentDidUpdate') && !code.includes('shouldComponentUpdate')) { issues.push('Missing shouldComponentUpdate optimization'); } if (code.match(/useState.*map/)) { suggestions.push('Consider using useMemo for expensive computations'); } if (code.includes('addEventListener') && !code.includes('removeEventListener')) { issues.push('Event listener not cleaned up'); } return { score: Math.max(100 - issues.length * 20, 0), issues, suggestions, metrics: { renderComplexity: 'Low', reRenderRisk: issues.length > 0 ? 'High' : 'Low', memoryLeakRisk: code.includes('addEventListener') ? 'Medium' : 'Low' } }; } private analyzeAccessibility(code: string): any { const issues = []; const suggestions = []; // Check for accessibility issues if (code.includes('<img') && !code.includes('alt=')) { issues.push('Images missing alt text'); } if (code.includes('<button') && !code.includes('aria-label') && !code.includes('>')) { suggestions.push('Consider adding aria-label to buttons'); } if (code.includes('onClick') && !code.includes('onKeyDown')) { suggestions.push('Add keyboard support for click handlers'); } if (!code.includes('role=') && !code.includes('aria-')) { suggestions.push('Consider adding ARIA attributes for better screen reader support'); } return { score: Math.max(100 - issues.length * 25, 0), issues, suggestions, wcagCompliance: { 'A': issues.length === 0, 'AA': issues.length === 0 && suggestions.length < 2, 'AAA': issues.length === 0 && suggestions.length === 0 } }; } private analyzeBestPractices(code: string): any { const issues = []; const suggestions = []; // Check for best practices if (code.includes('var ')) { issues.push('Using var instead of const/let'); } if (code.includes('==') && !code.includes('===')) { issues.push('Using loose equality instead of strict equality'); } if (!code.includes('PropTypes') && !code.includes('interface') && !code.includes('type')) { suggestions.push('Add type checking with PropTypes or TypeScript'); } if (code.length > 500 && !code.includes('function') && !code.includes('const')) { suggestions.push('Consider breaking down into smaller components'); } return { score: Math.max(100 - issues.length * 15, 0), issues, suggestions, codeQuality: { maintainability: issues.length === 0 ? 'High' : 'Medium', readability: code.length < 300 ? 'High' : 'Medium', testability: 'Medium' } }; } private analyzeSEO(code: string): any { const issues = []; const suggestions = []; // Check for SEO considerations if (code.includes('<h1') && code.split('<h1').length > 2) { issues.push('Multiple H1 tags detected'); } if (!code.includes('meta') && code.includes('head')) { suggestions.push('Add meta tags for better SEO'); } if (!code.includes('semantic') && (code.includes('<div>') || code.includes('<span>'))) { suggestions.push('Use semantic HTML elements'); } return { score: 100 - issues.length * 30, issues, suggestions, seoReadiness: issues.length === 0 ? 'Good' : 'Needs Improvement' }; } private mapTypeToTS(type: string): string { const typeMap: any = { 'string': 'string', 'number': 'number', 'boolean': 'boolean', 'array': 'any[]', 'object': 'Record<string, any>', 'function': '(...args: any[]) => any', 'any': 'any' }; return typeMap[type.toLowerCase()] || 'any'; } private getFileExtension(type: string): string { const extensions: any = { 'react': 'tsx', 'vue': 'vue', 'svelte': 'svelte', 'web-component': 'js' }; return extensions[type] || 'js'; } private getTestValue(type: string): any { const values: any = { 'string': '"test"', 'number': '42', 'boolean': 'true', 'array': '[]', 'object': '{}', 'function': '() => {}' }; return values[type.toLowerCase()] || 'null'; } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/willem4130/ui-ux-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server