@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
JavaScript
/**
* 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