apple-docs-mcp-server
Version:
🍎 MCP server for semantic search through Apple developer documentation, WWDC transcripts, and code examples. 16K+ documents indexed for AI agents.
683 lines (598 loc) • 24.7 kB
JavaScript
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import pLimit from 'p-limit';
import dotenv from 'dotenv';
import path from 'path';
import { fileURLToPath } from 'url';
import { AppleSearchEngine } from './search.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Try loading .env from multiple locations
function loadEnvironment() {
const envPaths = [
// 1. Current working directory (user's project)
path.join(process.cwd(), '.env'),
// 2. Package directory (our location)
path.join(__dirname, '.env'),
// 3. Parent directory (if installed as dependency)
path.join(process.cwd(), '..', '.env'),
// 4. Project root (common location)
path.join(process.cwd(), '..', '..', '.env')
];
for (const envPath of envPaths) {
try {
dotenv.config({ path: envPath, override: false });
} catch (e) {
// Silently continue to next path
}
}
// Also load from default location
dotenv.config({ override: false });
}
// Load environment variables
loadEnvironment();
const SERVER_INFO = {
name: 'apple-docs-mcp-server',
version: '1.0.0'
};
const CAPABILITIES = {
capabilities: {
tools: {}
}
};
class AppleSearchMCPServer {
constructor() {
this.server = new Server(SERVER_INFO, CAPABILITIES);
this.searchEngine = null;
this.initPromise = null;
this.sqliteQueue = pLimit(1); // Queue to prevent SQLite race conditions
this.setupErrorHandling();
}
setupErrorHandling() {
process.on('SIGINT', async () => {
console.error('[apple-docs-mcp] Received SIGINT, shutting down server...');
await this.cleanup();
process.exit(0);
});
process.on('SIGTERM', async () => {
console.error('[apple-docs-mcp] Received SIGTERM, shutting down server...');
await this.cleanup();
process.exit(0);
});
}
async cleanup() {
if (this.searchEngine) {
console.error('[apple-docs-mcp] Shutting down search engine...');
this.searchEngine.close();
}
}
// Lazy engine initialization with timeout
async initializeEngine() {
if (this.searchEngine) {
return this.searchEngine;
}
if (this.initPromise) {
return this.initPromise;
}
this.initPromise = this.withTimeout(async () => {
console.error('[apple-docs-mcp] Initializing search engine...');
this.searchEngine = new AppleSearchEngine();
await this.searchEngine.init(true); // With OpenAI API
console.error('[apple-docs-mcp] Search engine successfully initialized');
return this.searchEngine;
}, 30000, 'Engine initialization');
return this.initPromise;
}
// Timeout wrapper
async withTimeout(operation, timeoutMs, operationName) {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(`Operation timeout: ${operationName} (${timeoutMs}ms)`));
}, timeoutMs);
});
try {
return await Promise.race([operation(), timeoutPromise]);
} catch (error) {
console.error(`[apple-docs-mcp] Error in ${operationName}:`, error);
throw this.convertToMcpError(error);
}
}
// Convert errors to proper MCP format
convertToMcpError(error) {
if (error.message.includes('401')) {
return new Error('Invalid OpenAI API key');
} else if (error.message.includes('429')) {
return new Error('OpenAI API rate limit exceeded. Please try again later');
} else if (error.message.includes('ETIMEDOUT')) {
return new Error('OpenAI connection timeout');
}
return error;
}
setupTools() {
// Tool 1: Document search (preview)
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === 'search_docs') {
return this.handleSearchDocs(args);
} else if (name === 'get_doc') {
return this.handleGetDoc(args);
} else if (name === 'get_stats') {
return this.handleGetStats(args);
} else if (name === 'get_code_examples') {
return this.handleGetCodeExamples(args);
} else {
throw new Error(`Unknown tool: ${name}`);
}
});
// APPLE DOCUMENTATION SEARCH TOOLS - Complete workflow for iOS/macOS development
// 🎯 STRATEGY: search_docs (explore) → get_doc (analyze) → iterate (refine)
// 📚 DATABASE: 16,253 Apple docs with semantic search via text-embedding-3-large
// 🔄 WORKFLOW: Broad queries → narrow focus → deep analysis → implementation ready
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'search_docs',
description: `🔍 SEMANTIC SEARCH through 16,253 Apple documentation pages with intelligent relevance scoring.
SEARCH STRATEGY:
• BROAD→NARROW: Start with general concepts ("SwiftUI animation") then narrow down ("SwiftUI keyframe animator")
• ITERATE: If results aren't specific enough, reformulate with more precise terms
• EXPLORE: Use limit 8-10 for initial exploration, limit 3-5 for focused searches
• 🔗 RELATED MODE: Set includeRelated=true to auto-discover connected topics
QUERY EXAMPLES:
✅ GOOD: "SwiftUI custom transition animation", "Core Data CloudKit conflict resolution"
✅ GOOD: "NSFetchRequest predicate performance", "UIKit navigation controller delegate"
✅ WITH RELATED: "SwiftUI Button" + includeRelated=true → 3 main + 5 related (ButtonStyle, UIButton, design guidelines) = comprehensive topic coverage
❌ AVOID: Single words like "Button" or "Core Data" - too broad
❌ AVOID: Non-Apple terms like "React" or "Android"
RELEVANCE SCORES:
• 60%+: Highly relevant, likely contains what you need
• 50-59%: Good match, worth investigating
• 40-49%: Partial match, may have related information
• <40%: Weak match, consider refining search
🔗 ENHANCED BY DEFAULT: Every search includes 3-6 intelligently related documents
• Semantic similarity using existing embeddings (NO extra OpenAI API calls!)
• Intelligent relationship classification: 🔄 Migration, 🆕 Alternatives, ⚡ Performance, 📋 Examples
• Smart threshold adaptation based on main result quality (75-85% similarity scores)
• Framework-aware connections: SceneKit→RealityKit, UIKit→SwiftUI, Core Image→Metal
• Quality-first approach: vector-based semantic matching replaces keyword search
• Use includeRelated=false to disable and get only main search results
WORKFLOW: Use search_docs for exploration → get_doc for detailed analysis → repeat with refined queries.`,
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'SEARCH QUERY: Use 2-4 specific keywords. Examples: "SwiftUI custom animation", "Core Data fetch performance", "UIKit navigation delegate methods"'
},
limit: {
type: 'number',
description: 'RESULT COUNT: 8-10 for exploration, 3-5 for focused search. More results = broader overview.',
default: 10
},
minSimilarity: {
type: 'number',
description: 'RELEVANCE THRESHOLD: 0.3 (default) for broad search, 0.5+ for highly specific results. Lower = more results.',
default: 0.3
},
includeContent: {
type: 'boolean',
description: 'CONTENT PREVIEW: Always true for decision making. Provides snippet to evaluate relevance.',
default: true
},
maxContentChars: {
type: 'number',
description: 'SNIPPET SIZE: 150-200 for quick scan, 300+ for detailed preview. Balance overview vs detail.',
default: 200
},
includeRelated: {
type: 'boolean',
description: '🔗 VECTOR-BASED AUTO-DISCOVER: Find semantically related documents using embeddings similarity (NOW ENABLED BY DEFAULT). Adds 3-6 intelligently classified docs (🔄 Migration guides, 🆕 Modern alternatives, ⚡ Performance tips, 📋 Code examples). Set to false to show only main results.',
default: true
},
showCodePreview: {
type: 'boolean',
description: '💻 CODE PREVIEW: Show preview of first Swift code block in each result. Perfect for AI agents to quickly see if document contains useful code examples. Use with get_code_examples(docId) for full code extraction.',
default: false
}
},
required: ['query']
}
},
{
name: 'get_doc',
description: `📖 DEEP DIVE: Get complete document content with unlimited size - perfect for thorough analysis.
CONTENT RICHNESS:
• FULL TEXT: Complete Apple documentation (up to 18K+ characters)
• CODE BLOCKS: Multiple Swift examples with syntax highlighting
• METADATA: Content length, code block count, document type
• STRUCTURE: Sections, discussions, usage examples
USAGE PATTERNS:
• SINGLE DOC: Pass string ID for one document
• BATCH ANALYSIS: Pass array of up to 10 IDs for comparison
• FOLLOW-UP: Use after search_docs to get complete details of promising results
WHAT YOU GET:
• title: Document title
• url: Direct link to Apple Developer docs
• content: Complete text with markdown formatting
• contentLength: Size in characters for analysis
• codeBlocks: Number of code examples
STRATEGY: Get full documents when search_docs snippets look promising but lack detail. No size limits - get everything you need for implementation.`,
inputSchema: {
type: 'object',
properties: {
id: {
oneOf: [
{
type: 'string',
description: 'SINGLE DOCUMENT: ID from search_docs result for focused analysis of one topic'
},
{
type: 'array',
items: { type: 'string' },
maxItems: 10,
description: 'BATCH ANALYSIS: Up to 10 IDs for comparing related documents or getting comprehensive coverage'
}
]
}
},
required: ['id']
}
},
{
name: 'get_stats',
description: `📊 DATABASE OVERVIEW: Quick health check and scope understanding of the Apple documentation database.
DATABASE SCALE:
• 16,253 total documents from Apple Developer documentation
• text-embedding-3-large model (3072 dimensions) for semantic search
• Covers iOS, macOS, watchOS, tvOS, visionOS platforms
WHAT YOU GET:
• totalDocuments: Exact count of available docs
• model: AI model used for semantic search quality
• dimensions: Vector dimensions for search precision
• sampleTitles: Random titles to understand content types
WHEN TO USE:
• First interaction: Understand database scope
• Debugging: Verify system is working correctly
• Context setting: Know what's available before searching
• Health check: Confirm database connection
Quick reference: This database contains comprehensive Apple platform documentation with semantic search capabilities.`,
inputSchema: {
type: 'object',
properties: {},
additionalProperties: false
}
},
{
name: 'get_code_examples',
description: `🔗 CONTEXTUAL CODE EXTRACTION: Extract code examples from a specific document you already found.
WORKFLOW INTEGRATION:
• Use after search_docs: See codeBlocks: 5 → get_code_examples(doc_id) → get those 5 examples
• Perfect for AI agents: Found interesting document → extract its code without re-searching
• Contextual approach: Work with specific documents rather than broad searches
WHAT YOU GET:
• All code examples from the specified document
• Full context and explanations around each code block
• Categorization and complexity analysis
• Same rich metadata as extract_code_examples
USAGE PATTERN:
1. search_docs("SwiftUI navigation") → find relevant documents
2. See result shows "codeBlocks: 7"
3. get_code_examples(document_id) → get all 7 examples with context
PERFECT FOR: AI agents who want to drill down into specific documents after initial search.`,
inputSchema: {
type: 'object',
properties: {
docId: {
type: 'string',
description: 'DOCUMENT ID: The id field from search_docs results. Extract code examples from this specific document.'
}
},
required: ['docId']
}
}
]
};
});
}
// Document search handler
async handleSearchDocs(args) {
const {
query,
limit = 10,
minSimilarity = 0.3,
includeContent = true,
maxContentChars = 300,
includeRelated = true, // 🆕 NOW ENABLED BY DEFAULT
showCodePreview = false // 🆕 CONTEXTUAL CODE PREVIEW
} = args;
if (!query || typeof query !== 'string') {
throw new Error('Parameter query is required and must be a string');
}
// 🔧 Related documents enabled by default (handles undefined as true)
const shouldIncludeRelated = includeRelated === undefined ? true : (includeRelated !== false && includeRelated !== 'false');
return this.sqliteQueue(async () => {
const engine = await this.initializeEngine();
// 1. Main search (as before)
const results = await this.withTimeout(
() => engine.search(query, limit, minSimilarity),
30000,
`Search: "${query}"`
);
const formattedResults = results.map(result => {
// 🧹 APPLY CONTENT CLEANER - Universal content enhancement
const cleanedContent = engine.enhanceContentQuality(result.content);
const baseResult = {
id: result.id,
title: result.title,
url: result.url,
similarity: Math.round(result.similarity * 10000) / 100, // Percentage with 2 decimals
snippet: includeContent ?
(cleanedContent.length > maxContentChars ?
cleanedContent.substring(0, maxContentChars) + '...' :
cleanedContent
) : null
};
// Add code preview if requested and document has code (using cleaned content)
if (showCodePreview && cleanedContent && cleanedContent.includes('```swift')) {
const swiftCodeMatch = cleanedContent.match(/```swift\s*\n([\s\S]*?)\n?\s*```/);
if (swiftCodeMatch) {
// 🧹 Apply additional code cleaning specifically for preview
let codePreview = swiftCodeMatch[1].trim();
// For short code previews, use aggressive comma-to-newline replacement
if (codePreview.length < 200 && codePreview.includes(',')) {
codePreview = codePreview
.replace(/{\s*,/g, '{\n ') // Fix opening braces
.replace(/,\s*}/g, '\n}') // Fix closing braces
.replace(/,\s+/g, ',\n ') // Replace comma+space with comma+newline+indent
.replace(/}\s*,\s*\./g, '\n}.'); // Fix closing brace + method call
} else {
codePreview = engine.attemptCodeFormatFix(codePreview); // Use complex logic for longer code
}
baseResult.codePreview = codePreview.length > 150 ?
codePreview.substring(0, 150) + '...' :
codePreview;
}
}
// Add compatibility info if available
if (result.compatibility) {
const compat = result.compatibility;
baseResult.compatibility = {};
// Platform information
if (compat.platforms && compat.platforms.count > 0) {
baseResult.compatibility.platforms = {
supported: compat.platforms.supported,
scope: compat.platforms.universal ? '🌐 Universal' :
compat.platforms.mobile ? '📱 Mobile' :
compat.platforms.desktop ? '🖥 Desktop' :
compat.platforms.spatial ? '👓 Spatial' : '🎯 Limited'
};
}
// Technology information
if (compat.technologies && compat.technologies.primary) {
baseResult.compatibility.technologies = {
primary: compat.technologies.primary,
modern: compat.technologies.isModern ? '✅ Modern (SwiftUI)' :
compat.technologies.isLegacy ? '⚠️ Legacy (UIKit)' : null
};
}
// Requirements
if (compat.requirements && Object.keys(compat.requirements).length > 0) {
baseResult.compatibility.requirements = compat.requirements;
}
// Limitations
if (compat.limitations && compat.limitations.length > 0) {
baseResult.compatibility.limitations = compat.limitations;
}
// Deprecation warning
if (compat.deprecation) {
baseResult.deprecationWarning = {
status: compat.deprecation.status === 'deprecated' ? '⚠️ Deprecated API' :
compat.deprecation.status === 'superseded' ? '📄 Has Modern Alternative' :
'🔄 Migration Available',
confidence: compat.deprecation.confidence,
evidence: compat.deprecation.evidence
};
}
}
return baseResult;
});
let response = {
query: query,
total: results.length,
results: formattedResults
};
// 2. 🆕 RELATED DOCUMENTS (only if requested)
if (shouldIncludeRelated && results.length > 0) {
try {
const relatedDocs = await this.withTimeout(
() => engine.findRelatedDocuments(results.slice(0, 3), query),
15000,
`Finding related docs for: "${query}"`
);
if (relatedDocs.length > 0) {
response.relatedDocuments = relatedDocs.map(doc => {
// 🧹 CLEAN RELATED DOCUMENTS TOO - Consistent quality everywhere
const cleanedRelatedContent = engine.enhanceContentQuality(doc.content);
return {
id: doc.id,
title: doc.title,
url: doc.url,
similarity: Math.round(doc.similarity * 10000) / 100,
relationship: doc.relationship,
snippet: includeContent ?
(cleanedRelatedContent.length > 150 ?
cleanedRelatedContent.substring(0, 150) + '...' :
cleanedRelatedContent
) : null
};
});
response.coverage = 'extended'; // Flag that this is extended search
response.totalWithRelated = results.length + relatedDocs.length;
}
} catch (error) {
console.error('Error finding related documents:', error);
response.relatedError = 'Failed to find related documents';
}
}
return {
content: [
{
type: 'text',
text: JSON.stringify(response, null, 2)
}
]
};
});
}
// Document retrieval handler
async handleGetDoc(args) {
const { id } = args;
if (!id) {
throw new Error('Parameter id is required');
}
// Handle MCP's conversion of arrays to JSON strings
// MCP SDK converts ["id1", "id2"] to "[\"id1\", \"id2\"]" string
let processedId = id;
if (typeof id === 'string' && id.startsWith('[') && id.endsWith(']')) {
try {
processedId = JSON.parse(id);
} catch (e) {
// Keep original id if parsing fails
}
}
return this.sqliteQueue(async () => {
const engine = await this.initializeEngine();
let documents;
let isArray = false;
if (Array.isArray(processedId)) {
if (processedId.length === 0) {
throw new Error('Array of IDs cannot be empty');
}
if (processedId.length > 10) {
throw new Error('Maximum 10 documents at once');
}
isArray = true;
documents = await this.withTimeout(
() => engine.getDocuments(processedId),
30000,
`Retrieving documents: ${processedId.join(', ')}`
);
} else {
documents = await this.withTimeout(
() => engine.getDocument(processedId),
30000,
`Retrieving document: ${processedId}`
);
documents = documents ? [documents] : [];
}
if (!documents || documents.length === 0) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: 'Documents not found',
requested_ids: Array.isArray(processedId) ? processedId : [processedId],
found: 0
}, null, 2)
}
]
};
}
const enrichedDocs = documents.map(doc => {
// 🧹 CLEAN FULL DOCUMENTS TOO - Complete content enhancement
const cleanedContent = engine.enhanceContentQuality(doc.content);
const codeBlocks = (cleanedContent.match(/```[\s\S]*?```/g) || []).length;
return {
id: doc.id,
title: doc.title,
url: doc.url,
type: doc.type || null,
content: cleanedContent, // Return cleaned content
contentLength: cleanedContent.length,
codeBlocks: codeBlocks
};
});
return {
content: [
{
type: 'text',
text: JSON.stringify({
found: documents.length,
documents: enrichedDocs
}, null, 2)
}
]
};
});
}
// Statistics handler
async handleGetStats(args) {
return this.sqliteQueue(async () => {
const engine = await this.initializeEngine();
const stats = await this.withTimeout(
() => engine.getStats(),
10000,
'Retrieving statistics'
);
return {
content: [
{
type: 'text',
text: JSON.stringify({
database: 'embeddings.db',
totalDocuments: stats.totalDocuments,
model: 'text-embedding-3-large',
dimensions: 3072,
sampleTitles: stats.sampleTitles
}, null, 2)
}
]
};
});
}
// Contextual code examples extraction from specific document
async handleGetCodeExamples(args) {
const { docId } = args;
if (!docId || typeof docId !== 'string') {
throw new Error('Parameter docId is required and must be a string');
}
return this.sqliteQueue(async () => {
const engine = await this.initializeEngine();
const examples = await this.withTimeout(
() => engine.extractCodeFromDocument(docId),
15000, // Faster than full search since working with one document
`Extract code from document: ${docId}`
);
return {
content: [
{
type: 'text',
text: JSON.stringify({
docId: docId,
total: examples.length,
examples: examples
}, null, 2)
}
]
};
});
}
async run() {
console.error('[apple-docs-mcp] Starting MCP server...');
// Register handlers before connection
this.setupTools();
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('[apple-docs-mcp] MCP server started and ready');
}
}
// Start server
const server = new AppleSearchMCPServer();
server.run().catch(error => {
console.error('[apple-docs-mcp] Critical startup error:', error);
process.exit(1);
});