UNPKG

apple-dev-mcp

Version:

Complete Apple development guidance: Human Interface Guidelines (design) + Technical Documentation for iOS, macOS, watchOS, tvOS, and visionOS

496 lines 27.6 kB
/** * MCP Tools implementation for Apple HIG interactive functionality */ import { AppleContentAPIClient } from './services/apple-content-api-client.service.js'; import { StaticContentSearchService } from './services/static-content-search.service.js'; export class HIGToolProvider { _cache; appleContentAPIClient; staticContentSearch; constructor(cache, appleContentAPIClient) { this._cache = cache; this.appleContentAPIClient = appleContentAPIClient || new AppleContentAPIClient(cache); this.staticContentSearch = new StaticContentSearchService(); } /** * Search Human Interface Guidelines content by keywords/topics with input validation */ async searchHumanInterfaceGuidelines(args) { // Input validation if (!args || typeof args !== 'object') { throw new Error('Invalid arguments: expected object'); } const { query, platform } = args; const limit = 3; // Return top 3 results with full content // Validate required parameters if (typeof query !== 'string') { throw new Error('Invalid query: must be a string'); } // Handle empty/whitespace queries gracefully if (query.trim().length === 0) { return { results: [], total: 0, query: query.trim(), filters: { platform } }; } if (query.length > 100) { throw new Error('Query too long: maximum 100 characters allowed'); } // Validate optional parameters if (platform && !['iOS', 'macOS', 'watchOS', 'tvOS', 'visionOS', 'universal'].includes(platform)) { args.platform = 'universal'; } try { let results = []; // Use static content search as primary source (fast and reliable) try { results = await this.staticContentSearch.searchContent(query.trim(), args.platform, undefined, limit); // If static content search returns no results, fall back to minimal results if (results.length === 0) { results = this.getMinimalFallbackResults(query.trim(), platform, limit); } } catch { // Fall back to minimal hardcoded results results = this.getMinimalFallbackResults(query.trim(), platform, limit); } return { results: results.slice(0, limit), total: results.length, query: query.trim(), filters: { platform } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; throw new Error(`Search failed: ${errorMessage}`); } } /** * Minimal fallback search with hardcoded results (last resort only) */ getMinimalFallbackResults(query, platform, limit = 3) { const queryLower = query.toLowerCase(); const fallbackData = [ // Buttons & Touch Targets { keywords: ['button', 'btn', 'press', 'tap', 'click'], title: 'Buttons', platform: 'iOS', category: 'visual-design', url: 'https://developer.apple.com/design/human-interface-guidelines/buttons', snippet: 'Buttons initiate app-specific actions, have customizable backgrounds, and can include a title or an icon. Minimum touch target size is 44pt x 44pt.' }, { keywords: ['touch', 'targets', '44pt', 'minimum', 'size', 'accessibility'], title: 'Touch Targets & Accessibility', platform: 'iOS', category: 'foundations', url: 'https://developer.apple.com/design/human-interface-guidelines/accessibility', snippet: 'Interactive elements must be large enough for people to interact with easily. A minimum touch target size of 44pt x 44pt ensures accessibility.' }, // Navigation { keywords: ['navigation', 'nav', 'navigate', 'menu', 'bar'], title: 'Navigation Bars', platform: 'iOS', category: 'navigation', url: 'https://developer.apple.com/design/human-interface-guidelines/navigation-bars', snippet: 'A navigation bar appears at the top of an app screen, enabling navigation through a hierarchy of content.' }, { keywords: ['tab', 'tabs', 'bottom'], title: 'Tab Bars', platform: 'iOS', category: 'navigation', url: 'https://developer.apple.com/design/human-interface-guidelines/tab-bars', snippet: 'A tab bar appears at the bottom of an app screen and provides the ability to quickly switch between different sections of an app.' }, // Layout & Design { keywords: ['layout', 'grid', 'spacing', 'margin'], title: 'Layout', platform: 'universal', category: 'layout', url: 'https://developer.apple.com/design/human-interface-guidelines/layout', snippet: 'A consistent layout that adapts to various devices and contexts makes your app easier to use and helps people feel confident.' }, { keywords: ['color', 'colours', 'theme', 'dark', 'light'], title: 'Color', platform: 'universal', category: 'color-and-materials', url: 'https://developer.apple.com/design/human-interface-guidelines/color', snippet: 'Color can indicate interactivity, impart vitality, and provide visual continuity.' }, { keywords: ['typography', 'text', 'font', 'size'], title: 'Typography', platform: 'universal', category: 'typography', url: 'https://developer.apple.com/design/human-interface-guidelines/typography', snippet: 'Typography can help you clarify a hierarchy of information and make it easy for people to find what they\'re looking for.' }, // Accessibility & Contrast { keywords: ['accessibility', 'a11y', 'voiceover', 'accessible'], title: 'Accessibility', platform: 'universal', category: 'foundations', url: 'https://developer.apple.com/design/human-interface-guidelines/accessibility', snippet: 'People use Apple accessibility features to personalize how they interact with their devices in ways that work for them.' }, { keywords: ['contrast', 'color', 'wcag', 'visibility', 'readability'], title: 'Color Contrast & Accessibility', platform: 'universal', category: 'foundations', url: 'https://developer.apple.com/design/human-interface-guidelines/accessibility', snippet: 'Ensure sufficient color contrast for text and UI elements. Follow WCAG guidelines with minimum 4.5:1 contrast ratio for normal text.' }, // Custom Interface Patterns { keywords: ['custom', 'interface', 'patterns', 'design', 'user', 'expectations'], title: 'Custom Interface Patterns', platform: 'universal', category: 'foundations', url: 'https://developer.apple.com/design/human-interface-guidelines/', snippet: 'When creating custom interfaces, maintain consistency with platform conventions and user expectations to ensure familiarity and usability.' }, { keywords: ['user', 'interface', 'standards', 'guidelines', 'principles'], title: 'User Interface Standards', platform: 'universal', category: 'foundations', url: 'https://developer.apple.com/design/human-interface-guidelines/', snippet: 'Follow established interface standards and design principles to create intuitive, accessible, and consistent user experiences across Apple platforms.' }, // Visual Effects { keywords: ['gradients', 'materials', 'visual', 'effects'], title: 'Materials & Visual Effects', platform: 'universal', category: 'color-and-materials', url: 'https://developer.apple.com/design/human-interface-guidelines/materials', snippet: 'Use system materials and visual effects thoughtfully to create depth and hierarchy while maintaining clarity and performance.' }, // Input & Controls { keywords: ['input', 'field', 'form', 'text'], title: 'Text Fields', platform: 'iOS', category: 'selection-and-input', url: 'https://developer.apple.com/design/human-interface-guidelines/text-fields', snippet: 'A text field is a rectangular area in which people enter or edit small, specific pieces of text.' }, { keywords: ['picker', 'select', 'choose'], title: 'Pickers', platform: 'iOS', category: 'selection-and-input', url: 'https://developer.apple.com/design/human-interface-guidelines/pickers', snippet: 'A picker displays one or more scrollable lists of distinct values that people can choose from.' }, // Platform specific { keywords: ['vision', 'visionos', 'spatial', 'immersive', 'ar', 'vr'], title: 'Designing for visionOS', platform: 'visionOS', category: 'foundations', url: 'https://developer.apple.com/design/human-interface-guidelines/designing-for-visionos', snippet: 'visionOS brings together digital and physical worlds, creating opportunities for new types of immersive experiences.' }, { keywords: ['watch', 'watchos', 'complication', 'crown'], title: 'Designing for watchOS', platform: 'watchOS', category: 'foundations', url: 'https://developer.apple.com/design/human-interface-guidelines/designing-for-watchos', snippet: 'Apple Watch is a highly personal device that people wear on their wrist, making it instantly accessible.' } ]; const results = []; fallbackData.forEach((item, index) => { let relevanceScore = 0; // Check for keyword matches const hasKeywordMatch = item.keywords.some(keyword => queryLower.includes(keyword) || keyword.includes(queryLower)); if (hasKeywordMatch) { relevanceScore = 1.0; } // Check title match if (item.title.toLowerCase().includes(queryLower)) { relevanceScore = Math.max(relevanceScore, 0.8); } // Apply platform filter if (platform && platform !== 'universal' && item.platform !== platform && item.platform !== 'universal') { return; } if (relevanceScore > 0) { results.push({ id: `fallback-${index}`, title: item.title, url: item.url, platform: item.platform, relevanceScore, content: item.snippet, type: 'guideline' }); } }); // Sort by relevance score and return top results return results .sort((a, b) => b.relevanceScore - a.relevanceScore) .slice(0, limit); } /** * Get accessibility requirements for specific components * TODO: Future release - integrate with static content parsing * For now, users should search "accessibility" + component through regular HIG search */ // async getAccessibilityRequirements(args: { component: string; platform: string }): Promise<{ // component: string; // platform: string; // requirements: { // minimumTouchTarget: string; // contrastRatio: string; // voiceOverSupport: string[]; // keyboardNavigation: string[]; // wcagCompliance: string; // additionalGuidelines: string[]; // }; // }> { // const { component, platform } = args; // const componentLower = component.toLowerCase(); // const a11yRequirements = this.getAccessibilityDatabase(componentLower, platform); // return { // component, // platform, // requirements: a11yRequirements // }; // } /** * Search technical documentation using dynamic Apple API client */ async searchTechnicalDocumentation(args) { // Input validation if (!args || typeof args !== 'object') { throw new Error('Invalid arguments: expected object'); } const { query, framework, platform } = args; const maxResults = 20; // Use sensible default internally // Validate required parameters if (typeof query !== 'string') { throw new Error('Invalid query: must be a string'); } if (query.trim().length === 0) { return { results: [], total: 0, query: query.trim(), success: true }; } if (query.length > 100) { throw new Error('Query too long: maximum 100 characters allowed'); } try { let results = []; // Try fast, targeted API search with aggressive timeout try { const searchPromise = this.performFastAPISearch(query.trim(), { framework, platform, maxResults }); // Race condition: API search vs 15-second timeout (matching MightyDillah's approach) const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error('API search timeout')), 15000); }); results = await Promise.race([searchPromise, timeoutPromise]); } catch { // API failed or timed out - return empty results for now // This maintains the "no static content" principle results = []; } return { results: results.slice(0, maxResults), total: results.length, query: query.trim(), success: results.length > 0, error: results.length === 0 ? 'No results found. Try a more specific technical symbol like "UIButton" or "ScrollView".' : undefined }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return { results: [], total: 0, query: query.trim(), success: false, error: errorMessage }; } } /** * Perform fast, targeted API search with intelligent framework targeting */ async performFastAPISearch(query, options) { const { framework, platform, maxResults = 10 } = options; // If framework specified, search only that framework (faster) if (framework) { return await this.appleContentAPIClient.searchFramework(framework, query, { platform, maxResults: Math.min(maxResults, 5) // Limit to reduce API calls }); } // For general searches, use the improved global search with sequential framework processing const results = await this.appleContentAPIClient.searchGlobal(query, { platform, maxResults }); return results.slice(0, maxResults); } /** * Unified search across both HIG design guidelines and technical documentation * Phase 2: Enhanced search that combines design and implementation guidance */ async searchUnified(args) { const { query, platform } = args; // Use sensible defaults internally const includeDesign = true; const includeTechnical = true; const maxResults = 20; // Input validation if (!query || typeof query !== 'string' || query.trim().length === 0) { throw new Error('Invalid query: must be a non-empty string'); } if (query.length > 100) { throw new Error('Query too long: maximum 100 characters allowed'); } const sources = []; let designResults = []; let technicalResults = []; try { // Search design guidelines if requested if (includeDesign) { sources.push('design-guidelines'); try { const designSearch = await this.searchHumanInterfaceGuidelines({ query, platform }); designResults = designSearch.results; } catch { // Fall through to fallback } } // Search technical documentation if requested if (includeTechnical) { sources.push('technical-documentation'); try { const technicalSearch = await this.searchTechnicalDocumentation({ query, platform }); technicalResults = technicalSearch.results; } catch { // Fall through to fallback } } // Generate cross-references between design and technical content const crossReferences = this.generateCrossReferences(designResults, technicalResults, query); // Combine and rank results using unified scoring const unifiedResults = this.combineAndRankResults(designResults, technicalResults, crossReferences, maxResults); return { results: unifiedResults, designResults, technicalResults, total: unifiedResults.length, query: query.trim(), sources, crossReferences }; } catch (error) { throw new Error(`Unified search failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Generate cross-references between design guidelines and technical documentation */ generateCrossReferences(designResults, technicalResults, query) { const crossReferences = []; // Common UI component mappings const componentMappings = new Map([ // Buttons ['button', ['Button', 'UIButton', 'NSButton', 'SwiftUI.Button']], ['buttons', ['Button', 'UIButton', 'NSButton', 'SwiftUI.Button']], // Navigation ['navigation', ['NavigationView', 'UINavigationController', 'NSNavigationController', 'NavigationStack']], ['navigation bar', ['NavigationView', 'UINavigationBar', 'NSNavigationItem']], // Lists ['list', ['List', 'UITableView', 'NSTableView', 'UICollectionView']], ['table', ['UITableView', 'NSTableView', 'TableView']], // Text ['text', ['Text', 'UILabel', 'NSTextField', 'TextField']], ['label', ['Text', 'UILabel', 'NSTextField']], // Images ['image', ['Image', 'UIImageView', 'NSImageView']], ['icon', ['Image', 'UIImageView', 'NSImageView', 'SF Symbols']], // Controls ['picker', ['Picker', 'UIPickerView', 'NSPopUpButton']], ['slider', ['Slider', 'UISlider', 'NSSlider']], ['switch', ['Toggle', 'UISwitch', 'NSSwitch']], ['toggle', ['Toggle', 'UISwitch', 'NSSwitch']], // Layout ['stack', ['VStack', 'HStack', 'ZStack', 'UIStackView', 'NSStackView']], ['scroll', ['ScrollView', 'UIScrollView', 'NSScrollView']], // Sheets and Popups ['sheet', ['Sheet', 'UIModalPresentationStyle', 'NSModalSession']], ['alert', ['Alert', 'UIAlertController', 'NSAlert']], ['popup', ['Popover', 'UIPopoverController', 'NSPopover']] ]); // Extract key terms from query for mapping const queryTerms = query.toLowerCase().split(/\s+/).filter(term => term.length > 2); for (const designResult of designResults) { for (const technicalResult of technicalResults) { let relevance = 0; // Direct title matching const designTitle = designResult.title.toLowerCase(); const technicalTitle = technicalResult.title.toLowerCase(); // Check if design title contains technical symbol name or vice versa if (designTitle.includes(technicalTitle) || technicalTitle.includes(designTitle)) { relevance += 0.8; } // Component mapping-based relevance for (const [designTerm, technicalSymbols] of componentMappings) { if (designTitle.includes(designTerm)) { for (const symbol of technicalSymbols) { if (technicalTitle.includes(symbol.toLowerCase())) { relevance += 0.6; break; } } } } // Query term overlap between design and technical content for (const term of queryTerms) { if (designTitle.includes(term) && technicalTitle.includes(term)) { relevance += 0.3; } } // Platform consistency boost if (designResult.platform && technicalResult.platforms && typeof technicalResult.platforms === 'string') { const designPlatform = designResult.platform.toLowerCase(); const technicalPlatforms = technicalResult.platforms.toLowerCase(); if (technicalPlatforms.includes(designPlatform)) { relevance += 0.2; } } // Framework preference: slightly boost UIKit/AppKit over SwiftUI in cross-references for backward compatibility if (technicalResult.framework === 'UIKit' || technicalResult.framework === 'AppKit') { relevance += 0.1; } // Only include cross-references with meaningful relevance if (relevance >= 0.4) { const crossRefKey = `${designResult.title}:${technicalResult.title}`; // Avoid duplicate cross-references if (!crossReferences.some(ref => `${ref.designSection}:${ref.technicalSymbol}` === crossRefKey)) { crossReferences.push({ designSection: designResult.title, technicalSymbol: technicalResult.title, relevance: Math.round(relevance * 100) / 100 }); } } } } // Sort by relevance and limit to top cross-references return crossReferences .sort((a, b) => b.relevance - a.relevance) .slice(0, 10); } /** * Combine and rank design and technical results into unified search results */ combineAndRankResults(designResults, technicalResults, crossReferences, maxResults) { const unifiedResults = []; // Convert design results to unified format for (const result of designResults) { const hasCrossRef = crossReferences.some(ref => ref.designSection === result.title); const crossRefBoost = hasCrossRef ? 0.2 : 0; unifiedResults.push({ id: `design-${result.url}`, title: result.title, type: 'design', url: result.url, relevanceScore: result.relevanceScore + crossRefBoost, snippet: result.content || '', designContent: { platform: result.platform, category: result.category } }); } // Convert technical results to unified format for (const result of technicalResults) { const hasCrossRef = crossReferences.some(ref => ref.technicalSymbol === result.title); const crossRefBoost = hasCrossRef ? 0.2 : 0; unifiedResults.push({ id: `technical-${result.path}`, title: result.title, type: 'technical', url: result.url, relevanceScore: result.relevanceScore + crossRefBoost, snippet: result.description, technicalContent: { framework: result.framework, symbolKind: result.symbolKind || '', platforms: result.platforms ? [result.platforms] : [], abstract: result.description, codeExamples: [] } }); } // Create combined results for high-confidence cross-references const processedCombinations = new Set(); for (const crossRef of crossReferences.slice(0, 5)) { // Top 5 cross-references if (crossRef.relevance >= 0.6) { // Lower threshold for more combinations const designResult = designResults.find(r => r.title === crossRef.designSection); const technicalResult = technicalResults.find(r => r.title === crossRef.technicalSymbol); if (designResult && technicalResult) { const combinationKey = `${designResult.title}:${technicalResult.title}`; if (!processedCombinations.has(combinationKey)) { processedCombinations.add(combinationKey); // Create more concise snippet for combined results const designSnippet = (designResult.content || '').slice(0, 200); const techSnippet = technicalResult.description.slice(0, 200); unifiedResults.push({ id: `combined-${designResult.url.split('/').pop()}-${technicalResult.path.split('/').pop()}`, title: `${designResult.title} + ${technicalResult.title}`, type: 'combined', url: designResult.url, relevanceScore: (designResult.relevanceScore + technicalResult.relevanceScore) / 2 + (crossRef.relevance * 0.2), snippet: `Design: ${designSnippet}... | Implementation: ${techSnippet}...`, designContent: { platform: designResult.platform, category: designResult.category }, technicalContent: { framework: technicalResult.framework, symbolKind: technicalResult.symbolKind || '', platforms: technicalResult.platforms ? [technicalResult.platforms] : [], abstract: technicalResult.description, codeExamples: [] }, combinedGuidance: { designPrinciples: [designSnippet], implementationSteps: [techSnippet], crossPlatformConsiderations: technicalResult.platforms ? [technicalResult.platforms] : [], accessibilityNotes: [`Ensure ${designResult.title} follows accessibility guidelines`] } }); } } } } // Sort by relevance score and return top results return unifiedResults .sort((a, b) => b.relevanceScore - a.relevanceScore) .slice(0, maxResults); } } //# sourceMappingURL=tools.js.map