@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
201 lines • 6.38 kB
JavaScript
/**
* WWDC Data Source Manager
* Supports fetching data from GitHub or local file system
*/
import fs from 'fs/promises';
import path from 'path';
import { httpClient } from './http-client.js';
import { logger } from './logger.js';
import { wwdcDataCache } from './cache.js';
import { WWDC_DATA_SOURCE_CONFIG, WWDC_CONFIG } from './constants.js';
// Data source configuration (using constants)
export const DATA_SOURCE_CONFIG = {
github: WWDC_DATA_SOURCE_CONFIG.github,
local: {
// Use relative path from project root
dataDir: path.join(process.cwd(), WWDC_DATA_SOURCE_CONFIG.local.dataDir),
},
};
/**
* Get data source type
*/
export async function getDataSourceType() {
// Use local data in development environment
if (process.env.NODE_ENV === 'development') {
return 'local';
}
// Check if local data directory exists
try {
const localDataDir = DATA_SOURCE_CONFIG.local.dataDir;
await fs.access(localDataDir);
return 'local';
}
catch (error) {
logger.debug('Local data directory not accessible, using GitHub');
}
// Default to GitHub
return 'github';
}
/**
* Fetch file content from GitHub
*/
async function fetchFromGitHub(filePath) {
const url = `${DATA_SOURCE_CONFIG.github.baseUrl}/${filePath}`;
logger.debug(`Fetching from GitHub: ${url}`);
try {
const response = await httpClient.get(url);
return await response.text();
}
catch (error) {
logger.error(`Failed to fetch from GitHub: ${url}`, error);
throw new Error(`Failed to fetch ${filePath} from GitHub: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Fetch file content from local file system
*/
async function fetchFromLocal(filePath) {
const fullPath = path.join(DATA_SOURCE_CONFIG.local.dataDir, filePath);
logger.debug(`Reading from local: ${fullPath}`);
try {
return await fs.readFile(fullPath, 'utf-8');
}
catch (error) {
logger.error(`Failed to read local file: ${fullPath}`, error);
throw new Error(`Failed to read ${filePath} from local: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Generic data fetching function
*/
async function fetchData(filePath, dataSource) {
const cacheKey = `data:${filePath}`;
// Check cache
const cached = wwdcDataCache.get(cacheKey);
if (cached) {
logger.debug(`Cache hit for: ${filePath}`);
return cached;
}
// Determine data source
const source = dataSource || await getDataSourceType();
// Fetch data
let data;
if (source === 'github') {
data = await fetchFromGitHub(filePath);
}
else {
data = await fetchFromLocal(filePath);
}
// Cache data with TTL from constants
wwdcDataCache.set(cacheKey, data, WWDC_CONFIG.CACHE_TTL);
return data;
}
/**
* Load global metadata
*/
export async function loadGlobalMetadata(dataSource) {
try {
const data = await fetchData(WWDC_DATA_SOURCE_CONFIG.filePaths.globalMetadata, dataSource);
return JSON.parse(data);
}
catch (error) {
logger.error('Failed to load global metadata', error);
throw new Error(`Failed to load global metadata: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Load topic index
*/
export async function loadTopicIndex(topicId, dataSource) {
try {
const data = await fetchData(WWDC_DATA_SOURCE_CONFIG.filePaths.topicIndex(topicId), dataSource);
return JSON.parse(data);
}
catch (error) {
logger.error(`Failed to load topic index: ${topicId}`, error);
throw new Error(`Failed to load topic index ${topicId}: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Load year index
*/
export async function loadYearIndex(year, dataSource) {
try {
const data = await fetchData(WWDC_DATA_SOURCE_CONFIG.filePaths.yearIndex(year), dataSource);
return JSON.parse(data);
}
catch (error) {
logger.error(`Failed to load year index: ${year}`, error);
throw new Error(`Failed to load year index ${year}: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Load video data
*/
export async function loadVideoData(videoFile, dataSource) {
try {
const data = await fetchData(videoFile, dataSource);
return JSON.parse(data);
}
catch (error) {
logger.error(`Failed to load video data: ${videoFile}`, error);
throw new Error(`Failed to load video data ${videoFile}: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Batch load video data
*/
export async function loadVideosData(videoFiles, dataSource) {
const videos = [];
// Use Promise.allSettled to handle partial failures
const results = await Promise.allSettled(videoFiles.map(file => loadVideoData(file, dataSource)));
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
videos.push(result.value);
}
else {
logger.warn(`Failed to load video data: ${videoFiles[index]}`, result.reason);
}
});
return videos;
}
/**
* Get all available topics
*/
export async function getAvailableTopics(dataSource) {
try {
const metadata = await loadGlobalMetadata(dataSource);
return metadata.topics.map(topic => topic.id);
}
catch (error) {
logger.error('Failed to get available topics', error);
throw new Error(`Failed to get available topics: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Get all available years
*/
export async function getAvailableYears(dataSource) {
try {
const metadata = await loadGlobalMetadata(dataSource);
return metadata.years;
}
catch (error) {
logger.error('Failed to get available years', error);
throw new Error(`Failed to get available years: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Clear cache
*/
export function clearDataCache() {
wwdcDataCache.clear();
logger.info('WWDC data cache cleared');
}
/**
* Get cache statistics
*/
export function getDataCacheStats() {
return wwdcDataCache.getStats();
}
//# sourceMappingURL=wwdc-data-source.js.map