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
JavaScript
#!/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