UNPKG

summarai-mcp

Version:

MCP server for YouTube video summarization and information retrieval using SummarAI API

538 lines (533 loc) 23.7 kB
#!/usr/bin/env node "use strict"; const { Server } = require('@modelcontextprotocol/sdk/server/index.js'); const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js'); const { CallToolRequestSchema, ListToolsRequestSchema, } = require('@modelcontextprotocol/sdk/types.js'); const axios = require('axios'); // Get API configuration from environment variables const API_KEY = process.env.API_KEY; const YOUTUBE_VIDEO_SUMMARY_API_URL = process.env.YOUTUBE_VIDEO_SUMMARY_API_URL || 'https://summarai-sale-python-backend.onrender.com/api/youtube/summarize'; const YOUTUBE_VIDEO_INFO_API_URL = process.env.YOUTUBE_VIDEO_INFO_API_URL || 'https://summarai-sale-python-backend.onrender.com/api/youtube/info'; class SummarAIMCPServer { constructor() { this.server = new Server({ name: 'summarai-mcp', version: '1.0.8', }, { capabilities: { tools: {}, }, }); this.setupToolHandlers(); // Error handling this.server.onerror = (error) => console.error('[MCP Error]', error); process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); } setupToolHandlers() { // List available tools this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'summarize_youtube_video', description: 'Summarize a YouTube video using SummarAI API. Provide a YouTube URL and optionally a custom prompt for the summarization.', inputSchema: { type: 'object', properties: { youtube_video_url: { type: 'string', description: 'The YouTube video URL to summarize (must be a valid YouTube URL)', }, custom_prompt: { type: 'string', description: 'Optional custom prompt for the summarization. If not provided, a default prompt will be used.', }, }, required: ['youtube_video_url'], }, }, { name: 'get_youtube_video_info', description: 'Get information about a YouTube video including title, video ID, transcript availability, and available languages.', inputSchema: { type: 'object', properties: { youtube_video_url: { type: 'string', description: 'The YouTube video URL to get information for (must be a valid YouTube URL)', }, }, required: ['youtube_video_url'], }, }, ], }; }); // Handle tool calls this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; if (name === 'summarize_youtube_video') { // Validate and convert args to proper type if (!args || typeof args !== 'object') { throw new Error('Invalid arguments provided'); } const params = args; // Validate required parameters if (!params.youtube_video_url || typeof params.youtube_video_url !== 'string') { throw new Error('youtube_video_url is required and must be a string'); } const validatedParams = { youtube_video_url: params.youtube_video_url, custom_prompt: typeof params.custom_prompt === 'string' ? params.custom_prompt : undefined, }; return this.handleSummarizeVideo(validatedParams); } if (name === 'get_youtube_video_info') { // Validate and convert args to proper type if (!args || typeof args !== 'object') { throw new Error('Invalid arguments provided'); } const params = args; // Validate required parameters if (!params.youtube_video_url || typeof params.youtube_video_url !== 'string') { throw new Error('youtube_video_url is required and must be a string'); } const validatedParams = { youtube_video_url: params.youtube_video_url, }; return this.handleGetVideoInfo(validatedParams); } throw new Error(`Unknown tool: ${name}`); }); } extractYouTubeUrlFromRedirect(url) { try { const urlObj = new URL(url); // Check if it's a Google redirect URL if (urlObj.hostname.includes('google.com') || urlObj.hostname.includes('google.')) { const searchParams = urlObj.searchParams; // Look for the 'url' parameter which contains the actual YouTube URL if (searchParams.has('url')) { let extractedUrl = decodeURIComponent(searchParams.get('url')); // Additional decoding in case of double encoding if (extractedUrl.includes('%')) { extractedUrl = decodeURIComponent(extractedUrl); } console.error(`[DEBUG] Extracted YouTube URL from Google redirect: ${extractedUrl}`); return extractedUrl; } // Sometimes the URL might be in 'q' parameter else if (searchParams.has('q')) { let extractedUrl = decodeURIComponent(searchParams.get('q')); if (extractedUrl.includes('youtube.com') || extractedUrl.includes('youtu.be')) { if (extractedUrl.includes('%')) { extractedUrl = decodeURIComponent(extractedUrl); } console.error(`[DEBUG] Extracted YouTube URL from Google 'q' parameter: ${extractedUrl}`); return extractedUrl; } } // Check for other Google service redirects (like from Gmail, Google Docs, etc.) else if (urlObj.pathname.includes('/url')) { for (const param of ['target', 'dest']) { if (searchParams.has(param)) { let extractedUrl = decodeURIComponent(searchParams.get(param)); if (extractedUrl.includes('youtube.com') || extractedUrl.includes('youtu.be')) { if (extractedUrl.includes('%')) { extractedUrl = decodeURIComponent(extractedUrl); } console.error(`[DEBUG] Extracted YouTube URL from Google '${param}' parameter: ${extractedUrl}`); return extractedUrl; } } } } } // Check for other common redirect patterns (shortened URLs) else if (['t.co', 'bit.ly', 'tinyurl.com', 'short.link', 'ow.ly', 'is.gd'].some(domain => urlObj.hostname.includes(domain))) { console.error(`[DEBUG] Detected shortened URL: ${url}`); // For shortened URLs, we return as-is since we can't follow redirects in this context // The backend API will handle the redirect following return url; } // If it's not a redirect URL, return the original URL return url; } catch (error) { console.error(`[DEBUG] Error extracting URL from redirect: ${error}`); return url; } } extractYouTubeVideoId(url) { try { console.error(`[DEBUG] Processing URL for video ID extraction: ${url}`); // First, try to extract the actual YouTube URL from potential redirects const actualUrl = this.extractYouTubeUrlFromRedirect(url); console.error(`[DEBUG] URL after redirect extraction: ${actualUrl}`); // Parse the URL to handle various formats const urlObj = new URL(actualUrl); // Handle youtube.com URLs if (urlObj.hostname === 'www.youtube.com' || urlObj.hostname === 'youtube.com') { // Standard YouTube URL: https://www.youtube.com/watch?v=VIDEO_ID if (urlObj.pathname === '/watch' || urlObj.pathname.startsWith('/watch')) { const videoId = urlObj.searchParams.get('v'); if (videoId && /^[a-zA-Z0-9_-]{11}$/.test(videoId)) { console.error(`[DEBUG] Extracted video ID from youtube.com/watch: ${videoId}`); return videoId; } } // Handle youtube.com/embed URLs else if (urlObj.pathname.startsWith('/embed/')) { const pathParts = urlObj.pathname.split('/embed/'); if (pathParts.length > 1) { let videoId = pathParts[1]; // Clean the video ID (remove any additional parameters) videoId = videoId.replace(/[^a-zA-Z0-9_-].*/, ''); if (videoId.length === 11 && /^[a-zA-Z0-9_-]{11}$/.test(videoId)) { console.error(`[DEBUG] Extracted video ID from youtube.com/embed: ${videoId}`); return videoId; } } } // Handle youtube.com/v/ URLs else if (urlObj.pathname.startsWith('/v/')) { const pathParts = urlObj.pathname.split('/v/'); if (pathParts.length > 1) { let videoId = pathParts[1]; videoId = videoId.replace(/[^a-zA-Z0-9_-].*/, ''); if (videoId.length === 11 && /^[a-zA-Z0-9_-]{11}$/.test(videoId)) { console.error(`[DEBUG] Extracted video ID from youtube.com/v/: ${videoId}`); return videoId; } } } } // Handle youtu.be URLs else if (urlObj.hostname === 'youtu.be') { // Short YouTube URL: https://youtu.be/VIDEO_ID let videoId = urlObj.pathname.substring(1); // Remove leading slash // Clean the video ID (remove any additional parameters) videoId = videoId.replace(/[^a-zA-Z0-9_-].*/, ''); if (videoId.length === 11 && /^[a-zA-Z0-9_-]{11}$/.test(videoId)) { console.error(`[DEBUG] Extracted video ID from youtu.be: ${videoId}`); return videoId; } } // Fallback: try regex patterns for various YouTube URL formats const patterns = [ /(?:v=|\/watch\?v=)([a-zA-Z0-9_-]{11})/, // Standard YouTube URL /(?:youtu\.be\/)([a-zA-Z0-9_-]{11})/, // Shortened YouTube URL /(?:embed\/)([a-zA-Z0-9_-]{11})/, // Embedded URLs /(?:\/v\/)([a-zA-Z0-9_-]{11})/, // /v/ URLs /(?:watch%3Fv%3D)([a-zA-Z0-9_-]{11})/, // URL encoded /(?:&v=|%26v%3D)([a-zA-Z0-9_-]{11})/ // Additional parameter formats ]; for (const pattern of patterns) { const match = actualUrl.match(pattern); if (match && match[1]) { const videoId = match[1]; console.error(`[DEBUG] Extracted video ID using regex pattern: ${videoId}`); return videoId; } } console.error(`[DEBUG] Could not extract video ID from URL: ${actualUrl}`); return null; } catch (error) { console.error(`[DEBUG] Error extracting video ID: ${error}`); return null; } } validateYouTubeUrl(url) { console.error(`[DEBUG] Validating YouTube URL: ${url}`); const videoId = this.extractYouTubeVideoId(url); if (!videoId) { console.error(`[DEBUG] Invalid or missing video ID`); return { isValid: false, error: 'Invalid YouTube URL format. Please provide a valid YouTube video URL.' }; } // Return a clean, standard YouTube URL const cleanUrl = `https://www.youtube.com/watch?v=${videoId}`; console.error(`[DEBUG] Valid YouTube URL with video ID: ${videoId}`); return { isValid: true, cleanUrl }; } async handleSummarizeVideo(params) { const { youtube_video_url, custom_prompt } = params; // Validate API key if (!API_KEY) { return { content: [ { type: 'text', text: 'Error: API_KEY environment variable is not set. Please configure your API key.', }, ], isError: true, }; } // Validate and clean YouTube URL const urlValidation = this.validateYouTubeUrl(youtube_video_url); if (!urlValidation.isValid) { return { content: [ { type: 'text', text: `Error: ${urlValidation.error}`, }, ], isError: true, }; } const cleanYouTubeUrl = urlValidation.cleanUrl; try { // Prepare request data using the clean URL const requestData = { url: cleanYouTubeUrl, custom_prompt: custom_prompt || 'Summarize this YouTube video transcript:', }; // Make API call const response = await axios.post(YOUTUBE_VIDEO_SUMMARY_API_URL, requestData, { headers: { 'Content-Type': 'application/json', 'x-api-key': API_KEY, }, timeout: 120000, // 2 minutes timeout }); const { data: responseData } = response; if (!responseData.success) { return { content: [ { type: 'text', text: `Error: ${responseData.error || 'Failed to summarize video'}`, }, ], isError: true, }; } if (!responseData.data) { return { content: [ { type: 'text', text: 'Error: No data received from the API', }, ], isError: true, }; } // Format the successful response const { video_id, title, transcript_length, summary } = responseData.data; const resultText = `# YouTube Video Summary **Video Title:** ${title} **Video ID:** ${video_id} **Transcript Length:** ${transcript_length} characters ## Summary ${summary}`; return { content: [ { type: 'text', text: resultText, }, ], }; } catch (error) { let errorMessage = 'Unknown error occurred'; if (axios.isAxiosError(error)) { if (error.response) { // Server responded with error status const status = error.response.status; const data = error.response.data; if (status === 401) { errorMessage = 'Authentication failed. Please check your API key.'; } else if (status === 400) { errorMessage = `Bad request: ${data?.detail || data?.error || 'Invalid request parameters'}`; } else if (status === 429) { errorMessage = 'Rate limit exceeded. Please try again later.'; } else if (status >= 500) { errorMessage = 'Server error. The service may be temporarily unavailable.'; } else { errorMessage = `HTTP ${status}: ${data?.detail || data?.error || error.message}`; } } else if (error.request) { // Network error errorMessage = 'Network error: Unable to connect to the API. Please check your internet connection and API URL.'; } else { errorMessage = `Request error: ${error.message}`; } } else if (error instanceof Error) { errorMessage = error.message; } else { errorMessage = String(error); } return { content: [ { type: 'text', text: `Error: ${errorMessage}`, }, ], isError: true, }; } } async handleGetVideoInfo(params) { const { youtube_video_url } = params; // Validate API key if (!API_KEY) { return { content: [ { type: 'text', text: 'Error: API_KEY environment variable is not set. Please configure your API key.', }, ], isError: true, }; } // Validate and clean YouTube URL const urlValidation = this.validateYouTubeUrl(youtube_video_url); if (!urlValidation.isValid) { return { content: [ { type: 'text', text: `Error: ${urlValidation.error}`, }, ], isError: true, }; } const cleanYouTubeUrl = urlValidation.cleanUrl; try { // Prepare request data using the clean URL const requestData = { url: cleanYouTubeUrl, }; // Make API call const response = await axios.post(YOUTUBE_VIDEO_INFO_API_URL, requestData, { headers: { 'Content-Type': 'application/json', 'x-api-key': API_KEY, }, timeout: 30000, // 30 seconds timeout }); const { data: responseData } = response; if (!responseData.success) { return { content: [ { type: 'text', text: `Error: ${responseData.error || 'Failed to get video information'}`, }, ], isError: true, }; } if (!responseData.data) { return { content: [ { type: 'text', text: 'Error: No data received from the API', }, ], isError: true, }; } // Format the successful response const { video_id, title, transcript_available, available_languages } = responseData.data; const languagesList = available_languages && available_languages.length > 0 ? available_languages.join(', ') : 'None'; const resultText = `# YouTube Video Information **Video Title:** ${title} **Video ID:** ${video_id} **Transcript Available:** ${transcript_available ? 'Yes' : 'No'} **Available Languages:** ${languagesList} ${transcript_available ? '✅ This video has transcripts available and can be summarized.' : '❌ This video does not have transcripts available and cannot be summarized.'}`; return { content: [ { type: 'text', text: resultText, }, ], }; } catch (error) { let errorMessage = 'Unknown error occurred'; if (axios.isAxiosError(error)) { if (error.response) { // Server responded with error status const status = error.response.status; const data = error.response.data; if (status === 401) { errorMessage = 'Authentication failed. Please check your API key.'; } else if (status === 400) { errorMessage = `Bad request: ${data?.detail || data?.error || 'Invalid request parameters'}`; } else if (status === 429) { errorMessage = 'Rate limit exceeded. Please try again later.'; } else if (status >= 500) { errorMessage = 'Server error. The service may be temporarily unavailable.'; } else { errorMessage = `HTTP ${status}: ${data?.detail || data?.error || error.message}`; } } else if (error.request) { // Network error errorMessage = 'Network error: Unable to connect to the API. Please check your internet connection and API URL.'; } else { errorMessage = `Request error: ${error.message}`; } } else if (error instanceof Error) { errorMessage = error.message; } else { errorMessage = String(error); } return { content: [ { type: 'text', text: `Error: ${errorMessage}`, }, ], isError: true, }; } } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('SummarAI MCP server running on stdio'); } } module.exports = { SummarAIMCPServer }; //# sourceMappingURL=server.js.map