UNPKG

@kimsungwhee/apple-docs-mcp

Version:

MCP server for Apple Developer Documentation - Search iOS/macOS/SwiftUI/UIKit docs, WWDC videos, Swift/Objective-C APIs & code examples in Claude, Cursor & AI assistants

734 lines 31.4 kB
/** * WWDC Video MCP Tool Handlers */ import { logger } from '../../utils/logger.js'; import { loadGlobalMetadata, loadTopicIndex, loadYearIndex, loadVideoData, loadVideosData, } from '../../utils/wwdc-data-source.js'; /** * List WWDC videos */ export async function handleListWWDCVideos(year, topic, hasCode, limit = 50) { try { // Load metadata const metadata = await loadGlobalMetadata(); let allVideos = []; if (topic && topic.includes('-')) { // If topic looks like a topic ID, try to use topic index try { const topicIndex = await loadTopicIndex(topic); // Filter by year const videosToLoad = year && year !== 'all' ? topicIndex.videos.filter(v => v.year === year) : topicIndex.videos; // Load video data const videoFiles = videosToLoad.map(v => v.dataFile); const videos = await loadVideosData(videoFiles); allVideos = videos.map(v => ({ ...v, year: v.year })); } catch (error) { logger.warn(`Failed to load topic index for ${topic}, will search by keyword instead`); // Fall through to load by year and filter by keyword } } if (allVideos.length === 0 && year && year !== 'all') { // If year is specified, use year index const yearIndex = await loadYearIndex(year); // 加载视频数据 const videoFiles = yearIndex.videos.map(v => v.dataFile); const videos = await loadVideosData(videoFiles); allVideos = videos.map(v => ({ ...v, year: v.year })); } else if (allVideos.length === 0) { // Load all videos - through year indices const yearsToLoad = metadata.years; for (const y of yearsToLoad) { try { const yearIndex = await loadYearIndex(y); const videoFiles = yearIndex.videos.map(v => v.dataFile); const videos = await loadVideosData(videoFiles); const videosWithYear = videos.map(v => ({ ...v, year: y })); allVideos.push(...videosWithYear); } catch (error) { logger.warn(`Failed to load year ${y}:`, error); } } } // Apply filters let filteredVideos = allVideos; // Topic filter (if not already filtered through topic index) if (topic && allVideos.length > 0) { // If we loaded videos but didn't use topic index, filter by keyword const topicLower = topic.toLowerCase(); const wasFilteredByTopicIndex = topic.includes('-') && allVideos.length > 0; if (!wasFilteredByTopicIndex) { filteredVideos = filteredVideos.filter(v => v.topics.some(t => t.toLowerCase().includes(topicLower)) || v.title.toLowerCase().includes(topicLower)); } } // Code filter if (hasCode !== undefined) { filteredVideos = filteredVideos.filter(v => v.hasCode === hasCode); } // Apply limit const limitedVideos = filteredVideos.slice(0, limit); // Format output return formatVideoList(limitedVideos, year, topic, hasCode); } catch (error) { logger.error('Failed to list WWDC videos:', error); const errorMessage = error instanceof Error ? error.message : String(error); return `Error: Failed to list WWDC videos: ${errorMessage}`; } } /** * Search WWDC content */ export async function handleSearchWWDCContent(query, searchIn = 'both', year, language, limit = 20) { try { const metadata = await loadGlobalMetadata(); const queryLower = query.toLowerCase(); const results = []; // Determine years to search const yearsToSearch = year ? [year] : metadata.years; // Search each year for (const y of yearsToSearch) { try { const yearIndex = await loadYearIndex(y); // Pre-filter: only load videos that might contain search content const potentialVideos = yearIndex.videos.filter(v => { // Basic title and topic matching const titleMatch = v.title?.toLowerCase().includes(queryLower) || false; const topicMatch = v.topics?.some(t => t.toLowerCase().includes(queryLower)) || false; return titleMatch || topicMatch || (searchIn === 'code' || searchIn === 'both') && v.hasCode || (searchIn === 'transcript' || searchIn === 'both') && v.hasTranscript; }); if (potentialVideos.length === 0) { continue; } // 加载视频数据 const videoFiles = potentialVideos.map(v => v.dataFile); const videos = await loadVideosData(videoFiles); // Search each video for (const video of videos) { const matches = []; // Search transcript if ((searchIn === 'transcript' || searchIn === 'both') && video.transcript) { const transcriptMatches = searchInTranscript(video.transcript.fullText, queryLower); matches.push(...transcriptMatches.map(m => ({ type: 'transcript', context: m.context, timestamp: m.timestamp, }))); } // Search code if ((searchIn === 'code' || searchIn === 'both') && video.codeExamples) { const codeMatches = searchInCode(video.codeExamples, queryLower, language); matches.push(...codeMatches.map(m => ({ type: 'code', context: m.context, timestamp: m.timestamp, }))); } if (matches.length > 0) { results.push({ video: { ...video, year: y }, matches: matches.slice(0, 3), // Max 3 matches per video }); } } } catch (error) { logger.warn(`Failed to search year ${y}:`, error); } } // Sort by match count results.sort((a, b) => b.matches.length - a.matches.length); // Apply limit const limitedResults = results.slice(0, limit); return formatSearchResults(limitedResults, query, searchIn); } catch (error) { logger.error('Failed to search WWDC content:', error); const errorMessage = error instanceof Error ? error.message : String(error); return `Error: Failed to search WWDC content: ${errorMessage}`; } } /** * Get WWDC video details */ export async function handleGetWWDCVideo(year, videoId, includeTranscript = true, includeCode = true) { try { // Load video data directly const videoFile = `videos/${year}-${videoId}.json`; const video = await loadVideoData(videoFile); return formatVideoDetail(video, includeTranscript, includeCode); } catch (error) { logger.error('Failed to get WWDC video:', error); const errorMessage = error instanceof Error ? error.message : String(error); return `Error: Failed to get WWDC video: ${errorMessage}`; } } /** * Get WWDC code examples */ export async function handleGetWWDCCodeExamples(framework, topic, year, language, limit = 30) { try { const metadata = await loadGlobalMetadata(); const codeExamples = []; // Determine years to search const yearsToSearch = year ? [year] : metadata.years; for (const y of yearsToSearch) { try { const yearIndex = await loadYearIndex(y); // Pre-filter: only load videos with code const videosWithCode = yearIndex.videos.filter(v => v.hasCode); // Topic filter let filteredVideos = videosWithCode; if (topic) { if (topic.includes('-')) { // If it's a standard topic ID, use topic index directly try { const topicIndex = await loadTopicIndex(topic); const topicVideoIds = new Set(topicIndex.videos.map(v => v.id)); filteredVideos = videosWithCode.filter(v => topicVideoIds.has(v.id)); } catch (error) { // If topic index doesn't exist, fallback to string matching const topicLower = topic.toLowerCase(); filteredVideos = videosWithCode.filter(v => v.topics.some(t => t.toLowerCase().includes(topicLower)) || v.title.toLowerCase().includes(topicLower)); } } else { // Search by name const topicLower = topic.toLowerCase(); filteredVideos = videosWithCode.filter(v => v.topics.some(t => t.toLowerCase().includes(topicLower)) || v.title.toLowerCase().includes(topicLower)); } } if (filteredVideos.length === 0) { continue; } // 加载视频数据 const videoFiles = filteredVideos.map(v => v.dataFile); const videos = await loadVideosData(videoFiles); // Extract code examples for (const video of videos) { if (!video.codeExamples || video.codeExamples.length === 0) { continue; } for (const example of video.codeExamples) { // Language filter if (language && example.language.toLowerCase() !== language.toLowerCase()) { continue; } // Framework filter (search in code) if (framework && !example.code.toLowerCase().includes(framework.toLowerCase())) { continue; } codeExamples.push({ code: example.code, language: example.language, title: example.title, timestamp: example.timestamp, videoTitle: video.title, videoUrl: video.url, year: y, }); } } } catch (error) { logger.warn(`Failed to search code examples for year ${y}:`, error); } } // Limit result count const limitedExamples = codeExamples.slice(0, limit); return formatCodeExamples(limitedExamples, framework, topic, language); } catch (error) { logger.error('Failed to get WWDC code examples:', error); const errorMessage = error instanceof Error ? error.message : String(error); return `Error: Failed to get WWDC code examples: ${errorMessage}`; } } /** * Search in transcript */ function searchInTranscript(fullText, query) { const matches = []; const lines = fullText.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (line.toLowerCase().includes(query)) { // Get context (one line before and after) const context = [ lines[i - 1] || '', line, lines[i + 1] || '', ].filter(l => l.trim()).join(' ... '); matches.push({ context }); } } return matches; } /** * Search in code */ function searchInCode(codeExamples, query, language) { const matches = []; for (const example of codeExamples) { // Language filter if (language && example.language.toLowerCase() !== language.toLowerCase()) { continue; } if (example.code.toLowerCase().includes(query)) { // Extract code snippet containing the query const lines = example.code.split('\n'); const matchingLines = lines.filter(line => line.toLowerCase().includes(query)); matches.push({ context: `[${example.language}] ${example.title || ''}: ${matchingLines[0]}`, timestamp: example.timestamp, }); } } return matches; } /** * Format video list */ function formatVideoList(videos, year, topic, hasCode) { if (videos.length === 0) { return 'No WWDC videos found matching the criteria.'; } let content = '# WWDC Video List\n\n'; // Filter conditions const filters = []; if (year && year !== 'all') { filters.push(`Year: ${year}`); } if (topic) { filters.push(`Topic: ${topic}`); } if (hasCode !== undefined) { filters.push(`Has Code: ${hasCode ? 'Yes' : 'No'}`); } if (filters.length > 0) { content += `**Filter Conditions:** ${filters.join(', ')}\n\n`; } content += `**Found ${videos.length} videos**\n\n`; // Group by year const videosByYear = videos.reduce((acc, video) => { if (!acc[video.year]) { acc[video.year] = []; } acc[video.year].push(video); return acc; }, {}); // Format each year Object.keys(videosByYear) .sort((a, b) => parseInt(b) - parseInt(a)) .forEach(y => { content += `## WWDC${y}\n\n`; videosByYear[y].forEach(video => { content += `### [${video.title}](${video.url})\n`; const metadata = []; if (video.duration) { metadata.push(`Duration: ${video.duration}`); } if (video.speakers && video.speakers.length > 0) { metadata.push(`Speakers: ${video.speakers.join(', ')}`); } if (video.hasTranscript) { metadata.push('Transcript'); } if (video.hasCode) { metadata.push('Code Examples'); } if (metadata.length > 0) { content += `*${metadata.join(' | ')}*\n`; } if (video.topics.length > 0) { content += `**Topics:** ${video.topics.join(', ')}\n`; } content += '\n'; }); }); return content; } /** * Format search results */ function formatSearchResults(results, query, searchIn) { if (results.length === 0) { return `No ${searchIn === 'code' ? 'code' : searchIn === 'transcript' ? 'transcript' : 'content'} found containing "${query}".`; } let content = '# WWDC Content Search Results\n\n'; content += `**Search Query:** "${query}"\n`; content += `**Search Scope:** ${searchIn === 'code' ? 'Code' : searchIn === 'transcript' ? 'Transcript' : 'All Content'}\n`; content += `**Found ${results.length} related videos**\n\n`; results.forEach(result => { content += `## [${result.video.title}](${result.video.url})\n`; content += `*WWDC${result.video.year} | ${result.matches.length} matches*\n\n`; result.matches.forEach(match => { content += `**${match.type === 'code' ? 'Code' : 'Transcript'}**`; if (match.timestamp) { content += ` (${match.timestamp})`; } content += '\n'; content += `> ${match.context}\n\n`; }); }); return content; } /** * Format video details */ function formatVideoDetail(video, includeTranscript, includeCode) { let content = `# ${video.title}\n\n`; content += `**WWDC${video.year}** | [Watch Video](${video.url})\n\n`; // Basic information if (video.duration) { content += `**Duration:** ${video.duration}\n`; } if (video.speakers && video.speakers.length > 0) { content += `**Speakers:** ${video.speakers.join(', ')}\n`; } if (video.topics.length > 0) { content += `**Topics:** ${video.topics.join(', ')}\n`; } // Resource links if (video.resources.hdVideo || video.resources.sdVideo || video.resources.resourceLinks) { content += '\n**Resources:**\n'; if (video.resources.hdVideo) { content += `- [HD Video](${video.resources.hdVideo})\n`; } if (video.resources.sdVideo) { content += `- [SD Video](${video.resources.sdVideo})\n`; } if (video.resources.resourceLinks && video.resources.resourceLinks.length > 0) { video.resources.resourceLinks.forEach(link => { content += `- [${link.title}](${link.url})\n`; }); } } // Chapters if (video.chapters && video.chapters.length > 0) { content += '\n## Chapters\n\n'; video.chapters.forEach(chapter => { content += `- **${chapter.timestamp}** ${chapter.title}\n`; }); } // Transcript if (includeTranscript && video.transcript) { content += '\n## Transcript\n\n'; // If there are timestamped segments, use them if (video.transcript.segments.length > 0) { video.transcript.segments.forEach(segment => { content += `**${segment.timestamp}**\n`; content += `${segment.text}\n\n`; }); } else { // Show full transcript text content += video.transcript.fullText; } } // Code examples if (includeCode && video.codeExamples && video.codeExamples.length > 0) { content += '\n## Code Examples\n\n'; video.codeExamples.forEach((example, index) => { if (example.title) { content += `### ${example.title}`; } else { content += `### Code Example ${index + 1}`; } if (example.timestamp) { content += ` (${example.timestamp})`; } content += '\n\n'; content += `\`\`\`${example.language}\n`; content += example.code; content += '\n\`\`\`\n\n'; if (example.context) { content += `*${example.context}*\n\n`; } }); } // Related videos if (video.relatedVideos && video.relatedVideos.length > 0) { content += '\n## Related Videos\n\n'; video.relatedVideos.forEach(related => { content += `- [${related.title}](${related.url}) (WWDC${related.year})\n`; }); } return content; } /** * Format code examples */ function formatCodeExamples(examples, framework, topic, language) { if (examples.length === 0) { return 'No code examples found matching the criteria.'; } let content = '# WWDC Code Examples\n\n'; // Filter conditions const filters = []; if (framework) { filters.push(`Framework: ${framework}`); } if (topic) { filters.push(`Topic: ${topic}`); } if (language) { filters.push(`Language: ${language}`); } if (filters.length > 0) { content += `**Filter Conditions:** ${filters.join(', ')}\n\n`; } content += `**Found ${examples.length} code examples**\n\n`; // Group by language const examplesByLanguage = examples.reduce((acc, ex) => { if (!acc[ex.language]) { acc[ex.language] = []; } acc[ex.language].push(ex); return acc; }, {}); Object.keys(examplesByLanguage).forEach(lang => { content += `## ${lang.charAt(0).toUpperCase() + lang.slice(1)}\n\n`; examplesByLanguage[lang].forEach(example => { content += `### ${example.title || 'Code Example'}\n`; content += `*From: [${example.videoTitle}](${example.videoUrl}) (WWDC${example.year})*`; if (example.timestamp) { content += ` *@ ${example.timestamp}*`; } content += '\n\n'; content += `\`\`\`${example.language}\n`; content += example.code; content += '\n\`\`\`\n\n'; }); }); return content; } /** * Browse WWDC topics */ export async function handleBrowseWWDCTopics(topicId, includeVideos = true, year, limit = 20) { try { const metadata = await loadGlobalMetadata(); if (!topicId) { // List all available topics let content = '# WWDC Topics\n\n'; content += `Found ${metadata.topics.length} topics:\n\n`; metadata.topics.forEach(topic => { content += `## [${topic.name}](${topic.url})\n`; content += `**Topic ID:** ${topic.id}\n`; // Show video count for this topic const topicStats = metadata.statistics.byTopic[topic.id]; if (topicStats) { content += `**Videos:** ${topicStats}\n`; } content += '\n'; }); return content; } // Browse specific topic const topic = metadata.topics.find(t => t.id === topicId); if (!topic) { return `Topic "${topicId}" not found. Available topics: ${metadata.topics.map(t => t.id).join(', ')}`; } let content = `# ${topic.name}\n\n`; content += `**Topic ID:** ${topic.id}\n`; content += `**URL:** [${topic.url}](${topic.url})\n\n`; if (includeVideos) { try { const topicIndex = await loadTopicIndex(topicId); // Filter by year if specified let videosToShow = topicIndex.videos; if (year && year !== 'all') { videosToShow = videosToShow.filter(v => v.year === year); } // Apply limit videosToShow = videosToShow.slice(0, limit); content += `## Videos (${videosToShow.length}${videosToShow.length === limit ? '+' : ''})\n\n`; if (videosToShow.length === 0) { content += 'No videos found for this topic.\n'; } else { // Group by year const videosByYear = videosToShow.reduce((acc, video) => { if (!acc[video.year]) { acc[video.year] = []; } acc[video.year].push(video); return acc; }, {}); Object.keys(videosByYear) .sort((a, b) => parseInt(b) - parseInt(a)) .forEach(y => { content += `### WWDC${y}\n\n`; videosByYear[y].forEach(video => { content += `- [${video.title}](${video.url})`; const features = []; if (video.hasTranscript) { features.push('Transcript'); } if (video.hasCode) { features.push('Code'); } if (features.length > 0) { content += ` | ${features.join(' | ')}`; } content += '\n'; }); content += '\n'; }); } } catch (error) { content += `Error loading videos for topic: ${error instanceof Error ? error.message : String(error)}\n`; } } return content; } catch (error) { logger.error('Failed to browse WWDC topics:', error); const errorMessage = error instanceof Error ? error.message : String(error); return `Error: Failed to browse WWDC topics: ${errorMessage}`; } } /** * Find related WWDC videos */ export async function handleFindRelatedWWDCVideos(videoId, year, includeExplicitRelated = true, includeTopicRelated = true, includeYearRelated = false, limit = 15) { try { // Load the source video const videoFile = `videos/${year}-${videoId}.json`; const sourceVideo = await loadVideoData(videoFile); let content = `# Related Videos for "${sourceVideo.title}"\n\n`; content += `**Source:** [${sourceVideo.title}](${sourceVideo.url}) (WWDC${year})\n\n`; const relatedVideos = []; // 1. Explicit related videos from video metadata if (includeExplicitRelated && sourceVideo.relatedVideos) { for (const related of sourceVideo.relatedVideos) { try { const relatedVideoFile = `videos/${related.year}-${related.id}.json`; const relatedVideo = await loadVideoData(relatedVideoFile); relatedVideos.push({ video: { ...relatedVideo, year: related.year }, relationship: 'Explicitly related', score: 10, }); } catch (error) { logger.warn(`Failed to load related video ${related.year}-${related.id}:`, error); } } } // 2. Topic-related videos if (includeTopicRelated && sourceVideo.topics && sourceVideo.topics.length > 0) { for (const topic of sourceVideo.topics) { try { // Try to find topic by name mapping const metadata = await loadGlobalMetadata(); const topicEntry = metadata.topics.find(t => t.name.toLowerCase() === topic.toLowerCase() || t.id.toLowerCase().includes(topic.toLowerCase().replace(/\s+/g, '-'))); if (topicEntry) { const topicIndex = await loadTopicIndex(topicEntry.id); // Get videos from same topic (excluding source video) const topicVideos = topicIndex.videos.filter(v => v.id !== videoId); // Load video data for scoring const videoFiles = topicVideos.slice(0, 10).map(v => v.dataFile); // Limit to avoid too many requests const videos = await loadVideosData(videoFiles); for (const video of videos) { // Skip if already added if (relatedVideos.find(r => r.video.id === video.id && r.video.year === video.year)) { continue; } // Calculate similarity score based on shared topics const sharedTopics = (sourceVideo.topics || []).filter(t => video.topics?.some(vt => vt.toLowerCase() === t.toLowerCase()) || false); const score = sharedTopics.length * 2; relatedVideos.push({ video: { ...video, year: video.year }, relationship: `Same topic: ${topicEntry.name}`, score, }); } } } catch (error) { logger.warn(`Failed to find topic-related videos for topic ${topic}:`, error); } } } // 3. Year-related videos (same year, similar topics) if (includeYearRelated) { try { const yearIndex = await loadYearIndex(year); // Get videos from same year with overlapping topics const yearVideos = yearIndex.videos.filter(v => v.id !== videoId && v.topics.some(t => sourceVideo.topics?.some(st => st.toLowerCase() === t.toLowerCase()) || false)); // Load a sample of videos const videoFiles = yearVideos.slice(0, 10).map(v => v.dataFile); const videos = await loadVideosData(videoFiles); for (const video of videos) { // Skip if already added if (relatedVideos.find(r => r.video.id === video.id && r.video.year === video.year)) { continue; } const sharedTopics = (sourceVideo.topics || []).filter(t => video.topics?.some(vt => vt.toLowerCase() === t.toLowerCase()) || false); const score = sharedTopics.length; relatedVideos.push({ video: { ...video, year: video.year }, relationship: `Same year, shared topics: ${sharedTopics.join(', ')}`, score, }); } } catch (error) { logger.warn('Failed to find year-related videos:', error); } } // Sort by score (descending) and apply limit relatedVideos.sort((a, b) => b.score - a.score); const limitedResults = relatedVideos.slice(0, limit); if (limitedResults.length === 0) { content += 'No related videos found.\n'; } else { content += `## Related Videos (${limitedResults.length})\n\n`; limitedResults.forEach(result => { content += `### [${result.video.title}](${result.video.url})\n`; content += `*WWDC${result.video.year} | ${result.relationship}*\n\n`; const features = []; if (result.video.duration) { features.push(`Duration: ${result.video.duration}`); } if (result.video.hasTranscript) { features.push('Transcript'); } if (result.video.hasCode) { features.push('Code'); } if (features.length > 0) { content += `${features.join(' | ')}\n`; } if (result.video.topics.length > 0) { content += `**Topics:** ${result.video.topics.join(', ')}\n`; } content += '\n'; }); } return content; } catch (error) { logger.error('Failed to find related WWDC videos:', error); const errorMessage = error instanceof Error ? error.message : String(error); return `Error: Failed to find related WWDC videos: ${errorMessage}`; } } //# sourceMappingURL=wwdc-handlers.js.map