mcp-apple-notes

  • src
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, ErrorCode, McpError, Tool, CallToolResult, } from "@modelcontextprotocol/sdk/types.js"; // @ts-ignore import { getSubtitles } from 'youtube-captions-scraper'; // Define tool configurations const TOOLS: Tool[] = [ { name: "get_transcript", description: "Extract transcript from a YouTube video URL or ID", inputSchema: { type: "object", properties: { url: { type: "string", description: "YouTube video URL or ID" }, lang: { type: "string", description: "Language code for transcript (e.g., 'ko', 'en')", default: "en" } }, required: ["url", "lang"] } } ]; interface TranscriptLine { text: string; start: number; dur: number; } class YouTubeTranscriptExtractor { /** * Extracts YouTube video ID from various URL formats or direct ID input */ extractYoutubeId(input: string): string { if (!input) { throw new McpError( ErrorCode.InvalidParams, 'YouTube URL or ID is required' ); } // Handle URL formats try { const url = new URL(input); if (url.hostname === 'youtu.be') { return url.pathname.slice(1); } else if (url.hostname.includes('youtube.com')) { const videoId = url.searchParams.get('v'); if (!videoId) { throw new McpError( ErrorCode.InvalidParams, `Invalid YouTube URL: ${input}` ); } return videoId; } } catch (error) { // Not a URL, check if it's a direct video ID if (!/^[a-zA-Z0-9_-]{11}$/.test(input)) { throw new McpError( ErrorCode.InvalidParams, `Invalid YouTube video ID: ${input}` ); } return input; } throw new McpError( ErrorCode.InvalidParams, `Could not extract video ID from: ${input}` ); } /** * Retrieves transcript for a given video ID and language */ async getTranscript(videoId: string, lang: string): Promise<string> { try { const transcript = await getSubtitles({ videoID: videoId, lang: lang, }); return this.formatTranscript(transcript); } catch (error) { console.error('Failed to fetch transcript:', error); throw new McpError( ErrorCode.InternalError, `Failed to retrieve transcript: ${(error as Error).message}` ); } } /** * Formats transcript lines into readable text */ private formatTranscript(transcript: TranscriptLine[]): string { return transcript .map(line => line.text.trim()) .filter(text => text.length > 0) .join(' '); } } class TranscriptServer { private extractor: YouTubeTranscriptExtractor; private server: Server; constructor() { this.extractor = new YouTubeTranscriptExtractor(); this.server = new Server( { name: "mcp-servers-youtube-transcript", version: "0.1.0", }, { capabilities: { tools: {}, }, } ); this.setupHandlers(); this.setupErrorHandling(); } private setupErrorHandling(): void { this.server.onerror = (error) => { console.error("[MCP Error]", error); }; process.on('SIGINT', async () => { await this.stop(); process.exit(0); }); } private setupHandlers(): void { // List available tools this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS })); // Handle tool calls this.server.setRequestHandler(CallToolRequestSchema, async (request) => this.handleToolCall(request.params.name, request.params.arguments ?? {}) ); } /** * Handles tool call requests */ private async handleToolCall(name: string, args: any): Promise<{ toolResult: CallToolResult }> { switch (name) { case "get_transcript": { const { url: input, lang = "en" } = args; if (!input || typeof input !== 'string') { throw new McpError( ErrorCode.InvalidParams, 'URL parameter is required and must be a string' ); } if (lang && typeof lang !== 'string') { throw new McpError( ErrorCode.InvalidParams, 'Language code must be a string' ); } try { const videoId = this.extractor.extractYoutubeId(input); console.error(`Processing transcript for video: ${videoId}`); const transcript = await this.extractor.getTranscript(videoId, lang); console.error(`Successfully extracted transcript (${transcript.length} chars)`); return { toolResult: { content: [{ type: "text", text: transcript, metadata: { videoId, language: lang, timestamp: new Date().toISOString(), charCount: transcript.length } }], isError: false } }; } catch (error) { console.error('Transcript extraction failed:', error); if (error instanceof McpError) { throw error; } throw new McpError( ErrorCode.InternalError, `Failed to process transcript: ${(error as Error).message}` ); } } default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${name}` ); } } /** * Starts the server */ async start(): Promise<void> { const transport = new StdioServerTransport(); await this.server.connect(transport); } /** * Stops the server */ async stop(): Promise<void> { try { await this.server.close(); } catch (error) { console.error('Error while stopping server:', error); } } } // Main execution async function main() { const server = new TranscriptServer(); try { await server.start(); } catch (error) { console.error("Server failed to start:", error); process.exit(1); } } main().catch((error) => { console.error("Fatal server error:", error); process.exit(1); });