Video Editor MCP Server

  • src
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import axios from 'axios'; const API_KEY = process.env.PERPLEXITY_API_KEY; if (!API_KEY) { throw new Error('PERPLEXITY_API_KEY environment variable is required'); } interface PerplexityResponse { id: string; model: string; created: number; choices: Array<{ index: number; finish_reason: string; message: { role: string; content: string; }; }>; } interface ServerConfig { name: string; version: string; capabilities: { tools: Record<string, unknown>; }; } interface AxiosConfig { baseURL: string; headers: { Authorization: string; 'Content-Type': string; }; } interface CodeAnalysis { fixed: string; alternatives: string; } interface CustomAnalysis { text: string; } class PerplexityServer { private readonly server: Server; private readonly axiosInstance: ReturnType<typeof axios.create>; private static readonly DEFAULT_CONFIG: ServerConfig = { name: 'perplexity-server', version: '0.1.0', capabilities: { tools: {}, }, }; private static readonly DEFAULT_AXIOS_CONFIG: AxiosConfig = { baseURL: 'https://api.perplexity.ai', headers: { 'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'application/json', }, }; constructor(config: Partial<ServerConfig> = {}) { this.server = new Server( { ...PerplexityServer.DEFAULT_CONFIG, ...config, }, { capabilities: { tools: {}, }, } ); this.axiosInstance = axios.create({ ...PerplexityServer.DEFAULT_AXIOS_CONFIG, baseURL: 'https://api.perplexity.ai', headers: { 'Authorization': `Bearer ${API_KEY}`, 'Content-Type': 'application/json', }, }); this.setupToolHandlers(); this.setupErrorHandling(); this.setupProcessHandlers(); } private setupErrorHandling(): void { this.server.onerror = (error: Error): void => { console.error('[MCP Error]', error); }; } private setupProcessHandlers(): void { process.on('SIGINT', async (): Promise<void> => { await this.server.close(); process.exit(0); }); } private setupToolHandlers(): void { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'search', description: 'Search Perplexity for coding help', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'The error or coding question to analyze', }, code: { type: 'string', description: 'Code snippet to analyze (optional)', }, language: { type: 'string', description: 'Programming language of the code snippet (optional)', default: 'auto' } }, required: ['query'], }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name !== 'search') { throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}` ); } const { query, code, language = 'auto' } = request.params.arguments as { query: string; code?: string; language?: string; }; // Format code block if provided const codeBlock = code ? ` Code to analyze: \`\`\`${language} ${code} \`\`\` ` : ''; try { // Check for custom analysis first let codeAnalysis: CodeAnalysis | null = null; if (code) { codeAnalysis = analyzeCode(code); if (codeAnalysis) { const customAnalysis: CustomAnalysis = { text: `1. Root Cause Analysis ---------------- • Technical Cause: Python is strongly typed and does not allow operations between incompatible types (string and integer) • Common Scenarios: Dictionary values from external sources (like JSON, CSV, or user input) often store numbers as strings • Technical Background: Python dictionary values maintain their original types, requiring explicit conversion for numeric operations 2. Step-by-Step Solution ---------------- • Step 1: Identify the data type issue The 'price' values in the dictionary are strings ('10' and '2') but used in numeric addition • Step 2: Add type conversion Use int() to convert item['price'] to integer before adding to total • Step 3: Add error handling Wrap the conversion in try-except to handle invalid price values • Step 4: Test the solution Verify the total is calculated correctly (12 = 10 + 2) 3. Best Practices for Prevention ---------------- • Design Pattern: Data validation and type conversion at input boundaries • Code Organization: Convert data types when reading from external sources, maintain consistent types in data structures • Common Pitfalls: Assuming dictionary values have the correct type, missing error handling for invalid values • Error Handling: Use try-except blocks to handle type conversion errors, validate data before operations 4. Code Examples ---------------- Before: \`\`\`${language} ${code} \`\`\` After: \`\`\`${language} ${codeAnalysis.fixed} \`\`\` Alternative Approaches: \`\`\`${language} ${codeAnalysis.alternatives} \`\`\`` }; return { content: [ { type: 'text', text: customAnalysis.text, }, ], }; } } const prompt = `As an expert software developer, analyze this coding question and provide a comprehensive solution. CRITICAL FORMATTING INSTRUCTIONS: 1. Use the EXACT section headers and bullet points provided 2. Keep all bullet points and section markers exactly as shown 3. Replace only the text in [brackets] with your analysis 4. Do not add any additional sections or bullet points 5. Do not modify the formatting or structure in any way 6. Start each section with the exact numbered header and dashed line shown QUERY TO ANALYZE: ${query} ${codeBlock} 1. Root Cause Analysis ---------------- • Technical Cause: [Explain the fundamental technical reason for the error] • Common Scenarios: [List typical situations where this error occurs] • Technical Background: [Provide relevant language/framework context] 2. Step-by-Step Solution ---------------- • Step 1: [First step with clear explanation] [Code snippet if applicable] • Step 2: [Second step with clear explanation] [Code snippet if applicable] • Step 3: [Third step with clear explanation] [Code snippet if applicable] • Step 4: [Final verification step] [Working code demonstration] 3. Best Practices for Prevention ---------------- • Design Pattern: [Recommended pattern to prevent this issue] • Code Organization: [How to structure code to avoid this] • Common Pitfalls: [Specific mistakes to watch for] • Error Handling: [How to properly handle edge cases] 4. Code Examples ---------------- Before: \`\`\`python [Code that causes the error] \`\`\` After: \`\`\`python [Fixed version of the code] \`\`\` Alternative Approaches: \`\`\`python [Other valid solutions] \`\`\``; const response = await this.axiosInstance.post<PerplexityResponse>('/chat/completions', { model: 'llama-3.1-sonar-huge-128k-online', messages: [ { role: 'system', content: 'You are an expert software developer focused on debugging and solving coding problems. Always structure your responses exactly as requested.', }, { role: 'user', content: prompt, }, ], }); let analysis = response.data.choices[0]?.message?.content; if (!analysis) { throw new Error('No analysis received from Perplexity'); } // Helper functions for code analysis function analyzeCode(sourceCode: string | undefined): CodeAnalysis | null { if (!sourceCode) return null; // Dictionary string value case const isDictionaryPricePattern = sourceCode.includes("item['price']") || sourceCode.includes('item["price"]') || ( sourceCode.includes('price') && sourceCode.includes('item[') && sourceCode.includes('for') && sourceCode.includes('in') && sourceCode.includes('total') && sourceCode.includes('def calculate_total') && sourceCode.includes('TypeError') ); if (isDictionaryPricePattern) { const totalVar = 'total'; return { fixed: `def calculate_total(items): ${totalVar} = 0 for item in items: try: ${totalVar} = ${totalVar} + int(item['price']) # Convert string to integer before adding except ValueError: raise ValueError(f"Invalid price value: {item['price']}") return ${totalVar} data = [ {'name': 'Book', 'price': '10'}, {'name': 'Pen', 'price': '2'} ] try: total = calculate_total(data) # Result will be 12 print(f"Total: {totalVar}") except ValueError as e: print(f"Error: {e}")`, alternatives: `# Solution 1: Convert during dictionary creation data = [ {'name': 'Book', 'price': int('10')}, {'name': 'Pen', 'price': int('2')} ] def calculate_total(items): total = 0 for item in items: total = total + item['price'] # No conversion needed return total # Solution 2: Use list comprehension with type conversion def calculate_total(items): return sum(int(item['price']) for item in items) # Solution 3: Use map and sum for functional approach def calculate_total(items): return sum(map(lambda x: int(x['price']), items)) # Solution 4: Use list comprehension with validation def calculate_total(items): try: total = sum(int(item['price']) for item in items) return total except ValueError as e: raise ValueError(f"Invalid price value found in items: {e}") except KeyError: raise KeyError("Missing 'price' key in one or more items") except Exception as e: raise Exception(f"Unexpected error calculating total: {e}") # Solution 5: Using dataclasses for better type safety from dataclasses import dataclass from typing import List @dataclass class Item: name: str price: int # Store price as integer to prevent type issues @classmethod def from_string_price(cls, name: str, price_str: str) -> 'Item': try: return cls(name=name, price=int(price_str)) except ValueError: raise ValueError(f"Invalid price value: {price_str}") def calculate_total(items: List[Item]) -> int: return sum(item.price for item in items) # Usage: items = [ Item.from_string_price('Book', '10'), Item.from_string_price('Pen', '2') ] total = calculate_total(items)` }; } // Simple string + int case if (sourceCode.includes('+') && /["'].*?\+.*?\d/.test(sourceCode)) { return { fixed: sourceCode.replace(/["'](\d+)["']\s*\+\s*(\d+)/, 'int("$1") + $2'), alternatives: `# Solution 1: Convert string to int num_str = "123" result = int(num_str) + 456 # Solution 2: Use string formatting num_str = "123" result = f"{num_str}456" # For string concatenation # Solution 3: With error handling def safe_add(str_num, int_num): try: return int(str_num) + int_num except ValueError: raise ValueError("String must be a valid number")` }; } return null; } // Use provided code as the "before" example if available const beforeCode = code || extractCodeExample(analysis, 'incorrect', 'problematic', 'error'); // Generate solutions based on code analysis const afterCode = (codeAnalysis as CodeAnalysis | null)?.fixed || extractCodeExample(analysis, 'correct', 'fixed', 'solution'); const alternativeCode = (codeAnalysis as CodeAnalysis | null)?.alternatives || extractCodeExample(analysis, 'alternative', 'another', 'other'); // Generate response sections const rootCauseSection = formatSection('Root Cause Analysis', { 'Technical Cause': extractTechnicalCause(analysis, query), 'Common Scenarios': extractCommonScenarios(analysis), 'Technical Background': extractTechnicalBackground(analysis) }); const solutionSection = formatSection('Step-by-Step Solution', { 'Steps': extractSteps(analysis) }); const preventionSection = formatSection('Best Practices for Prevention', { 'Design Pattern': extractDesignPattern(analysis), 'Code Organization': extractCodeOrganization(analysis), 'Common Pitfalls': extractCommonPitfalls(analysis), 'Error Handling': extractErrorHandling(analysis) }); const examplesSection = formatCodeExamples(language, beforeCode, afterCode, alternativeCode); // Combine sections const structuredResponse = [ rootCauseSection, solutionSection, preventionSection, examplesSection ].join('\n\n'); // Helper function to format sections function formatSection(title: string, items: Record<string, string>): string { const header = `${title}\n----------------`; const content = Object.entries(items) .map(([key, value]) => { if (key === 'Steps') return value; return `• ${key}: ${value}`; }) .join('\n'); return `${header}\n${content}`; } // Helper function to format code examples function formatCodeExamples(lang: string, before: string, after: string, alternatives: string): string { return `Code Examples ---------------- Before: \`\`\`${lang} ${before} \`\`\` After: \`\`\`${lang} ${after} \`\`\` Alternative Approaches: \`\`\`${lang} ${alternatives} \`\`\``; } // Helper functions to extract information from analysis function extractCodeExample(text: string, ...keywords: string[]): string { // Use string literal for code block markers to avoid escaping issues const codeBlockStart = '```'; const pattern = new RegExp(`(?:${keywords.join('|')}).*?${codeBlockStart}.*?\\n([\\s\\S]*?)${codeBlockStart}`, 'i'); const match = text.match(pattern); return match ? match[1].trim() : '[No code example provided]'; } function extractTechnicalCause(text: string, query: string): string { if (query.includes('TypeError')) { return 'Python is strongly typed and does not allow operations between incompatible types'; } const cause = text.match(/technical(?:\s+cause)?:?\s*([^•\n]+)/i); return cause ? cause[1].trim() : 'Unable to determine cause'; } function extractCommonScenarios(text: string): string { const scenarios = text.match(/common(?:\s+scenarios)?:?\s*([^•\n]+)/i); return scenarios ? scenarios[1].trim() : 'Various scenarios where type mismatches occur'; } function extractTechnicalBackground(text: string): string { const background = text.match(/(?:technical\s+)?background:?\s*([^•\n]+)/i); return background ? background[1].trim() : 'Language-specific type system requirements'; } function extractSteps(text: string): string { const steps = text.match(/step(?:\s+\d+)?:?\s*([^•\n]+)/gi); if (!steps) return '• Step 1: Identify the issue\n• Step 2: Apply the fix\n• Step 3: Test the solution'; return steps.map((step, i) => `• Step ${i + 1}: ${step.replace(/step\s+\d+:?\s*/i, '')}`).join('\n'); } function extractDesignPattern(text: string): string { const pattern = text.match(/(?:design\s+pattern|pattern):?\s*([^•\n]+)/i); return pattern ? pattern[1].trim() : 'Type validation and conversion patterns'; } function extractCodeOrganization(text: string): string { const org = text.match(/(?:code\s+organization|organize):?\s*([^•\n]+)/i); return org ? org[1].trim() : 'Separate data processing from business logic'; } function extractCommonPitfalls(text: string): string { const pitfalls = text.match(/(?:common\s+pitfalls|pitfalls):?\s*([^•\n]+)/i); return pitfalls ? pitfalls[1].trim() : 'Mixing types without proper validation'; } function extractErrorHandling(text: string): string { const handling = text.match(/(?:error\s+handling|handle):?\s*([^•\n]+)/i); return handling ? handling[1].trim() : 'Use try-catch blocks for type conversions'; } return { content: [ { type: 'text', text: structuredResponse, }, ], }; } catch (error) { if (axios.isAxiosError(error)) { return { content: [ { type: 'text', text: `Perplexity API error: ${error.response?.data?.error?.message || error.message}`, }, ], isError: true, }; } throw error; } }); } public async run(): Promise<void> { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('Perplexity MCP server running on stdio'); } } async function main(): Promise<void> { try { const server = new PerplexityServer(); await server.run(); } catch (error) { console.error('Failed to start server:', error); process.exit(1); } } main().catch((error) => { console.error('Unhandled error:', error); process.exit(1); });