UNPKG

apple-hig-mcp

Version:

High-performance MCP server providing instant access to Apple's Human Interface Guidelines via hybrid static/dynamic content delivery

406 lines • 18.8 kB
#!/usr/bin/env node /** * Apple Human Interface Guidelines MCP Server * * A Model Context Protocol server that provides up-to-date access to Apple's * Human Interface Guidelines with comprehensive design system coverage. * * @version 1.0.0 * @author Tanner Maasen * @license MIT */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { ListResourcesRequestSchema, ReadResourceRequestSchema, ListToolsRequestSchema, CallToolRequestSchema, ErrorCode, McpError, } from '@modelcontextprotocol/sdk/types.js'; import { HIGCache } from './cache.js'; import { CrawleeHIGService } from './services/crawlee-hig.service.js'; import { HIGResourceProvider } from './resources.js'; import { HIGToolProvider } from './tools.js'; import { HIGStaticContentProvider } from './static-content.js'; class AppleHIGMCPServer { server; cache; crawleeService; staticContentProvider; resourceProvider; toolProvider; useStaticContent = false; constructor() { this.server = new Server({ name: 'Apple Human Interface Guidelines', version: '1.0.7', description: 'Comprehensive access to Apple\'s design guidelines for iOS, macOS, watchOS, tvOS, and visionOS development', }, { capabilities: { resources: {}, tools: {}, }, }); } /** * Initialize the server asynchronously */ async initialize() { // Validate environment await this.validateEnvironment(); // Initialize static content if available await this.initializeStaticContent(); // Initialize components this.cache = new HIGCache(3600); // 1 hour default TTL this.crawleeService = new CrawleeHIGService(this.cache); this.staticContentProvider = new HIGStaticContentProvider(); this.resourceProvider = new HIGResourceProvider(this.crawleeService, this.cache, this.staticContentProvider); this.toolProvider = new HIGToolProvider(this.crawleeService, this.cache, this.resourceProvider, this.staticContentProvider); this.setupHandlers(); } /** * Validate runtime environment and configuration */ async validateEnvironment() { const requiredNodeVersion = '18.0.0'; const currentVersion = process.version.slice(1); // Remove 'v' prefix if (this.compareVersions(currentVersion, requiredNodeVersion) < 0) { throw new Error(`Node.js ${requiredNodeVersion} or higher is required. Current version: ${process.version}`); } // Validate dependencies are available try { await import('@crawlee/playwright'); await import('playwright'); await import('node-cache'); } catch { throw new Error(`Missing required dependencies. Run 'npm install' to install dependencies.`); } } /** * Compare semantic versions */ compareVersions(version1, version2) { const v1Parts = version1.split('.').map(Number); const v2Parts = version2.split('.').map(Number); for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) { const v1Part = v1Parts[i] || 0; const v2Part = v2Parts[i] || 0; if (v1Part > v2Part) return 1; if (v1Part < v2Part) return -1; } return 0; } /** * Initialize static content provider if available */ async initializeStaticContent() { try { const isAvailable = await this.staticContentProvider.isAvailable(); if (isAvailable) { await this.staticContentProvider.initialize(); this.useStaticContent = true; // Check if content is stale const isStale = this.staticContentProvider.isContentStale(); const metadata = this.staticContentProvider.getMetadata(); if (process.env.NODE_ENV === 'development') { console.log('āœ… Static HIG content initialized'); console.log(`šŸ“Š Content: ${metadata?.totalSections || 0} sections`); console.log(`šŸ“… Last updated: ${metadata?.lastUpdated ? new Date(metadata.lastUpdated).toLocaleDateString() : 'unknown'}`); if (isStale) { console.log('āš ļø Content is stale (>6 months old). Consider running content generation.'); } } } else { if (process.env.NODE_ENV === 'development') { console.log('ā„¹ļø Static content not available. Using live scraping fallback.'); } } } catch (error) { if (process.env.NODE_ENV === 'development') { console.warn('āš ļø Failed to initialize static content:', error); console.log('ā„¹ļø Falling back to live scraping.'); } } } /** * Set up MCP request handlers with comprehensive error handling */ setupHandlers() { // Resource handlers this.server.setRequestHandler(ListResourcesRequestSchema, async () => { try { const startTime = Date.now(); const resources = await this.resourceProvider.listResources(); const duration = Date.now() - startTime; // Log performance metrics (disabled in production to avoid console pollution) if (process.env.NODE_ENV === 'development') { console.log(`[AppleHIGMCP] Listed ${resources.length} resources in ${duration}ms`); } return { resources: resources.map(resource => ({ uri: resource.uri, name: resource.name, description: resource.description, mimeType: resource.mimeType, })) }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; if (process.env.NODE_ENV === 'development') { console.error('[AppleHIGMCP] Failed to list resources:', error); } throw new McpError(ErrorCode.InternalError, `Failed to list resources: ${errorMessage}`); } }); this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { try { const { uri } = request.params; const startTime = Date.now(); // Validate URI format if (!uri || typeof uri !== 'string') { throw new McpError(ErrorCode.InvalidRequest, 'Invalid or missing URI parameter'); } if (!uri.startsWith('hig://')) { throw new McpError(ErrorCode.InvalidRequest, `Unsupported URI scheme. Expected 'hig://', got: ${uri}`); } const resource = await this.resourceProvider.getResource(uri); const duration = Date.now() - startTime; if (!resource) { throw new McpError(ErrorCode.InvalidRequest, `Resource not found: ${uri}`); } if (process.env.NODE_ENV === 'development') { console.log(`[AppleHIGMCP] Read resource ${uri} in ${duration}ms`); } return { contents: [{ uri: resource.uri, mimeType: resource.mimeType, text: resource.content, }] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; if (process.env.NODE_ENV === 'development') { console.error(`[AppleHIGMCP] Failed to read resource ${request.params.uri}:`, error); } if (error instanceof McpError) { throw error; } throw new McpError(ErrorCode.InternalError, `Failed to read resource: ${errorMessage}`); } }); // Tool handlers this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'search_guidelines', description: 'Search Apple Human Interface Guidelines by keywords, with optional platform and category filters', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query (keywords, component names, design concepts)', }, platform: { type: 'string', enum: ['iOS', 'macOS', 'watchOS', 'tvOS', 'visionOS', 'universal'], description: 'Filter by Apple platform', }, category: { type: 'string', enum: [ 'foundations', 'layout', 'navigation', 'presentation', 'selection-and-input', 'status', 'system-capabilities', 'visual-design', 'icons-and-images', 'color-and-materials', 'typography', 'motion', 'technologies' ], description: 'Filter by HIG category', }, limit: { type: 'number', minimum: 1, maximum: 50, default: 10, description: 'Maximum number of results to return', }, }, required: ['query'], }, }, { name: 'get_component_spec', description: 'Get detailed specifications and guidelines for a specific UI component', inputSchema: { type: 'object', properties: { componentName: { type: 'string', description: 'Name of the UI component (e.g., "Button", "Navigation Bar", "Tab Bar")', }, platform: { type: 'string', enum: ['iOS', 'macOS', 'watchOS', 'tvOS', 'visionOS', 'universal'], description: 'Target platform for the component', }, }, required: ['componentName'], }, }, { name: 'get_design_tokens', description: 'Get design system values (colors, spacing, typography) for specific components', inputSchema: { type: 'object', properties: { component: { type: 'string', description: 'Component name (e.g., "Button", "Navigation Bar", "Tab Bar")', }, platform: { type: 'string', enum: ['iOS', 'macOS', 'watchOS', 'tvOS', 'visionOS'], description: 'Target platform', }, tokenType: { type: 'string', enum: ['colors', 'spacing', 'typography', 'dimensions', 'all'], description: 'Type of design tokens to retrieve', default: 'all' } }, required: ['component', 'platform'], }, }, { name: 'get_accessibility_requirements', description: 'Get accessibility requirements and guidelines for specific components', inputSchema: { type: 'object', properties: { component: { type: 'string', description: 'Component name (e.g., "Button", "Navigation Bar", "Tab Bar")', }, platform: { type: 'string', enum: ['iOS', 'macOS', 'watchOS', 'tvOS', 'visionOS'], description: 'Target platform', }, }, required: ['component', 'platform'], }, }, ], }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const { name, arguments: args } = request.params; const startTime = Date.now(); // Validate tool name if (!name || typeof name !== 'string') { throw new McpError(ErrorCode.InvalidRequest, 'Invalid or missing tool name'); } // Validate arguments if (args && typeof args !== 'object') { throw new McpError(ErrorCode.InvalidRequest, 'Tool arguments must be an object'); } let result; switch (name) { case 'search_guidelines': { result = await this.toolProvider.searchGuidelines(args); break; } case 'get_component_spec': { result = await this.toolProvider.getComponentSpec(args); break; } case 'get_design_tokens': { result = await this.toolProvider.getDesignTokens(args); break; } case 'get_accessibility_requirements': { result = await this.toolProvider.getAccessibilityRequirements(args); break; } default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } const duration = Date.now() - startTime; if (process.env.NODE_ENV === 'development') { console.log(`[AppleHIGMCP] Tool '${name}' executed in ${duration}ms`); } return { content: [{ type: 'text', text: JSON.stringify(result, null, 2), }], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; if (process.env.NODE_ENV === 'development') { console.error(`[AppleHIGMCP] Tool call failed for ${request.params.name}:`, error); } if (error instanceof McpError) { throw error; } throw new McpError(ErrorCode.InternalError, `Tool execution failed: ${errorMessage}`); } }); } /** * Start the MCP server with proper error handling and logging */ async run() { try { // Development-only startup logging if (process.env.NODE_ENV === 'development') { console.log('šŸŽ Apple Human Interface Guidelines MCP Server starting...'); console.log('šŸ“– Providing up-to-date access to Apple design guidelines'); console.log('ā„¹ļø This server respects Apple\'s content and provides fair use access'); console.log('ā„¹ļø for educational and development purposes.'); console.log(''); } // Initialize the server components await this.initialize(); const transport = new StdioServerTransport(); await this.server.connect(transport); if (process.env.NODE_ENV === 'development') { console.log(`šŸš€ Apple HIG MCP Server is ready! (Mode: ${this.useStaticContent ? 'Static Content' : 'Live Scraping'})`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; // Always log startup errors console.error('šŸ’„ Failed to start Apple HIG MCP Server:', errorMessage); if (process.env.NODE_ENV === 'development') { console.error('Full error details:', error); } process.exit(1); } } } // Handle graceful shutdown process.on('SIGINT', async () => { // console.log('\nšŸ›‘ Shutting down Apple HIG MCP Server...'); process.exit(0); }); process.on('SIGTERM', async () => { // console.log('\nšŸ›‘ Shutting down Apple HIG MCP Server...'); process.exit(0); }); // Start the server if (import.meta.url === `file://${process.argv[1]}`) { // Always start the server regardless of arguments const server = new AppleHIGMCPServer(); server.run().catch((error) => { console.error('šŸ’„ Failed to start Apple HIG MCP Server:', error); process.exit(1); }); } //# sourceMappingURL=server.js.map