#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, Tool, } from '@modelcontextprotocol/sdk/types.js'; import { JarvisController, JarvisConfig } from './jarvis-controller.js'; import { VoiceSettings, ChunkingOptions } from './types/audio.js'; import { JarvisIntegrationHub } from './integrations/integration-hub.js'; import { ServiceConnectorManager } from './integrations/service-connectors.js'; // Global JARVIS instance let jarvisInstance: JarvisController | null = null; let integrationHub: JarvisIntegrationHub | null = null; let serviceManager: ServiceConnectorManager | null = null; // MCP Server setup const server = new Server( { name: '@chinchillaenterprises/mcp-ai-assistant', version: '2.0.0', }, { capabilities: { tools: {}, }, } ); // Define available tools const tools: Tool[] = [ { name: 'jarvis_daily_briefing', description: 'Get a comprehensive daily briefing from Gmail, Slack, Calendar, and other integrated services. JARVIS will read it aloud.', inputSchema: { type: 'object', properties: { includeEmails: { type: 'boolean', description: 'Include email summary', default: true }, includeSlack: { type: 'boolean', description: 'Include Slack summary', default: true }, includeCalendar: { type: 'boolean', description: 'Include calendar events', default: true }, includeNews: { type: 'boolean', description: 'Include news headlines', default: true } } } }, { name: 'jarvis_read_emails', description: 'Have JARVIS read and summarize emails from Gmail based on search criteria.', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Gmail search query (e.g., "is:unread", "from:boss", "is:important")', default: 'is:unread' }, limit: { type: 'number', description: 'Maximum number of emails to summarize', default: 10 }, speak: { type: 'boolean', description: 'Have JARVIS speak the summary', default: true } } } }, { name: 'jarvis_read_slack', description: 'Have JARVIS read and summarize Slack messages from specific channels or mentions.', inputSchema: { type: 'object', properties: { channel: { type: 'string', description: 'Slack channel name (e.g., "general", "development")', default: 'general' }, limit: { type: 'number', description: 'Number of messages to read', default: 20 }, onlyMentions: { type: 'boolean', description: 'Only read messages where you are mentioned', default: false }, speak: { type: 'boolean', description: 'Have JARVIS speak the summary', default: true } } } }, { name: 'jarvis_intelligent_summary', description: 'Get an AI-powered intelligent summary of all your communications and activities.', inputSchema: { type: 'object', properties: { timeframe: { type: 'string', enum: ['today', 'week', 'month'], description: 'Timeframe for the summary', default: 'today' }, focus: { type: 'string', description: 'Specific area to focus on (e.g., "development", "marketing", "urgent")', }, actionItems: { type: 'boolean', description: 'Include action items and todos', default: true } } } }, { name: 'jarvis_monitor_services', description: 'Start monitoring Gmail, Slack, and other services for important messages and alert you.', inputSchema: { type: 'object', properties: { emailKeywords: { type: 'array', items: { type: 'string' }, description: 'Keywords to watch for in emails', default: ['urgent', 'important', 'ASAP'] }, slackKeywords: { type: 'array', items: { type: 'string' }, description: 'Keywords to watch for in Slack', default: ['@here', '@channel', 'urgent'] }, checkInterval: { type: 'number', description: 'Check interval in minutes', default: 15 } } } }, { name: 'elevenlabs_auto_read_response', description: 'Automatically convert and read Claude\'s response using JARVIS text-to-speech with chunked delivery, pause/resume controls, and human-like pacing.', inputSchema: { type: 'object', properties: { text: { type: 'string', description: 'The full text response to read aloud' }, voice: { type: 'string', description: 'Voice to use (george, sarah, aria, laura, charlie)', default: 'george' }, speed: { type: 'number', description: 'Speech speed multiplier (0.5 to 2.0)', default: 1.0 }, autoPlay: { type: 'boolean', description: 'Automatically start playback after processing', default: true }, chunking: { type: 'object', description: 'Text chunking options for natural speech flow', properties: { maxSentencesPerChunk: { type: 'number', description: 'Maximum sentences per chunk (default: 3)', default: 3 }, respectParagraphs: { type: 'boolean', description: 'Break at paragraph boundaries', default: true } } } }, required: ['text'] } }, { name: 'jarvis_control', description: 'Control JARVIS playback with commands like pause, resume, stop, rewind, skip, speed change, etc.', inputSchema: { type: 'object', properties: { command: { type: 'string', enum: ['pause', 'resume', 'stop', 'rewind', 'skip', 'replay', 'speed', 'seek'], description: 'Control command to execute' }, speed: { type: 'number', description: 'New speed for speed command (0.5 to 2.0)' }, chunkIndex: { type: 'number', description: 'Chunk index for seek command' } }, required: ['command'] } }, { name: 'jarvis_status', description: 'Get current JARVIS playback status, progress, and available controls.', inputSchema: { type: 'object', properties: {}, additionalProperties: false } }, { name: 'jarvis_configure', description: 'Configure JARVIS voice settings, chunking options, and control server settings.', inputSchema: { type: 'object', properties: { voice: { type: 'string', description: 'Voice to use (george, sarah, aria, laura, charlie)' }, speed: { type: 'number', description: 'Default speech speed (0.5 to 2.0)' }, voiceSettings: { type: 'object', description: 'Advanced voice settings', properties: { stability: { type: 'number', description: 'Voice stability (0 to 1)' }, similarityBoost: { type: 'number', description: 'Voice similarity boost (0 to 1)' }, style: { type: 'number', description: 'Voice style (0 to 1)' }, useSpeakerBoost: { type: 'boolean', description: 'Enable speaker boost' } } }, chunking: { type: 'object', description: 'Text chunking configuration', properties: { maxSentencesPerChunk: { type: 'number', description: 'Maximum sentences per chunk' }, respectParagraphs: { type: 'boolean', description: 'Break at paragraph boundaries' }, detectCodeBlocks: { type: 'boolean', description: 'Detect and handle code blocks specially' } } } } } } ]; // Initialize JARVIS if not already done async function ensureJarvisInitialized(): Promise { if (!jarvisInstance) { console.log('šŸ¤– Initializing JARVIS Controller...'); const config: JarvisConfig = { voice: 'george', speed: 1.0, autoStartServer: true, controlServer: { port: 3456, enableCors: true, logRequests: false // Reduce noise in MCP context } }; jarvisInstance = new JarvisController(config); await jarvisInstance.startControlServer(); // Initialize integration hub and service manager integrationHub = new JarvisIntegrationHub(jarvisInstance); // Create a wrapper to call MCP tools from within JARVIS const mcpInvoke = async (tool: string, args: any) => { // This would need to be connected to actual MCP tool invocation // For now, we'll use mock data from the connectors return { content: [{ type: 'text', text: 'Mock response' }] }; }; serviceManager = new ServiceConnectorManager(mcpInvoke); console.log('āœ… JARVIS Controller ready'); console.log('šŸ”Œ Integration Hub initialized'); console.log(`šŸŽ® Control server: http://localhost:${jarvisInstance.getControlServerPort()}`); } return jarvisInstance; } // List available tools server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools }; }); // Handle tool calls server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'elevenlabs_auto_read_response': { const jarvis = await ensureJarvisInitialized(); const { text, voice, speed, autoPlay, chunking } = args as { text: string; voice?: string; speed?: number; autoPlay?: boolean; chunking?: Partial; }; console.log(`šŸ—£ļø JARVIS reading response (${text.length} chars)`); const options: { voice?: string; speed?: number; chunking?: Partial; waitForCompletion?: boolean; } = { waitForCompletion: false // Don't block MCP response }; if (voice !== undefined) { options.voice = voice; } if (speed !== undefined) { options.speed = speed; } if (chunking !== undefined) { options.chunking = chunking; } const playbackState = await jarvis.speakText(text, options); const controlPort = jarvis.getControlServerPort(); const progress = jarvis.getProgress(); return { content: [ { type: 'text', text: `āœ… JARVIS is now speaking your response!\n\n` + `šŸŽ™ļø Voice: ${voice || 'george'}\n` + `⚔ Speed: ${speed || 1.0}x\n` + `šŸ“ Chunks: ${progress.totalChunks}\n` + `šŸŽ® Control server: http://localhost:${controlPort}\n\n` + `**Available controls:**\n` + `• curl -X POST http://localhost:${controlPort}/pause\n` + `• curl -X POST http://localhost:${controlPort}/resume\n` + `• curl -X POST http://localhost:${controlPort}/stop\n` + `• curl -X POST http://localhost:${controlPort}/rewind\n` + `• curl -X POST http://localhost:${controlPort}/skip\n` + `• curl -X POST http://localhost:${controlPort}/speed -d '{"speed": 1.5}'\n\n` + `Use the jarvis_control tool for easier control from Claude.` } ] }; } case 'jarvis_control': { const jarvis = await ensureJarvisInitialized(); const { command, speed, chunkIndex } = args as { command: string; speed?: number; chunkIndex?: number; }; let result: any; let message: string; switch (command) { case 'pause': jarvis.pause(); message = 'āøļø Playback paused'; break; case 'resume': jarvis.resume(); message = 'ā–¶ļø Playback resumed'; break; case 'stop': jarvis.stop(); message = 'ā¹ļø Playback stopped'; break; case 'rewind': result = jarvis.rewind(); message = result ? 'āŖ Rewound to previous segment' : 'āŒ Cannot rewind further'; break; case 'skip': result = jarvis.skipForward(); message = result ? 'ā© Skipped to next segment' : 'āŒ No more segments to skip'; break; case 'replay': result = jarvis.replayCurrentSegment(); message = result ? 'šŸ”„ Replaying current segment' : 'āŒ No segment to replay'; break; case 'speed': if (!speed) { throw new Error('Speed value required for speed command'); } jarvis.changeSpeed(speed); message = `⚔ Speed changed to ${speed}x`; break; case 'seek': if (chunkIndex === undefined) { throw new Error('Chunk index required for seek command'); } result = jarvis.seekToChunk(chunkIndex); message = result ? `šŸŽÆ Seeked to chunk ${chunkIndex}` : `āŒ Invalid chunk index: ${chunkIndex}`; break; default: throw new Error(`Unknown command: ${command}`); } const progress = jarvis.getProgress(); return { content: [ { type: 'text', text: `${message}\n\n` + `**Current status:**\n` + `• Chunk: ${progress.currentChunk + 1}/${progress.totalChunks}\n` + `• Progress: ${progress.completionPercentage.toFixed(1)}%\n` + `• Time remaining: ~${Math.round(progress.estimatedTimeRemaining)}s` } ] }; } case 'jarvis_status': { const jarvis = await ensureJarvisInitialized(); const playbackState = jarvis.getPlaybackState(); const progress = jarvis.getProgress(); const segments = jarvis.getSegmentsSummary(); const voiceSettings = jarvis.getVoiceSettings(); return { content: [ { type: 'text', text: `šŸ¤– **JARVIS Status Report**\n\n` + `**Playback:**\n` + `• Status: ${playbackState.status}\n` + `• Playing: ${playbackState.isPlaying ? 'āœ…' : 'āŒ'}\n` + `• Paused: ${playbackState.isPaused ? 'āœ…' : 'āŒ'}\n\n` + `**Progress:**\n` + `• Current chunk: ${progress.currentChunk + 1}/${progress.totalChunks}\n` + `• Completion: ${progress.completionPercentage.toFixed(1)}%\n` + `• Time remaining: ~${Math.round(progress.estimatedTimeRemaining)}s\n\n` + `**Voice Settings:**\n` + `• Voice: ${voiceSettings.voice}\n` + `• Speed: ${voiceSettings.speed}x\n` + `• Stability: ${voiceSettings.stability}\n` + `• Similarity: ${voiceSettings.similarityBoost}\n\n` + `**Control Server:**\n` + `• Running: ${jarvis.isControlServerRunning() ? 'āœ…' : 'āŒ'}\n` + `• Port: ${jarvis.getControlServerPort()}\n` + `• Endpoint: http://localhost:${jarvis.getControlServerPort()}\n\n` + `**Segments:** ${segments.length} loaded\n` + segments.slice(0, 5).map((s, i) => `${i + 1}. ${s.isCurrent ? 'ā–¶ļø' : s.status === 'played' ? 'āœ…' : 'ā³'} ${s.text}` ).join('\n') + (segments.length > 5 ? `\n... and ${segments.length - 5} more` : '') } ] }; } case 'jarvis_configure': { const jarvis = await ensureJarvisInitialized(); const { voice, speed, voiceSettings, chunking } = args as { voice?: string; speed?: number; voiceSettings?: Partial; chunking?: Partial; }; let changes: string[] = []; if (voice) { jarvis.changeVoice(voice); changes.push(`šŸŽ™ļø Voice: ${voice}`); } if (speed) { jarvis.changeSpeed(speed); changes.push(`⚔ Speed: ${speed}x`); } if (voiceSettings) { jarvis.updateVoiceSettings(voiceSettings); changes.push(`šŸŽ›ļø Voice settings updated`); } const updatedSettings = jarvis.getVoiceSettings(); return { content: [ { type: 'text', text: `šŸ”§ **JARVIS Configuration Updated**\n\n` + `**Changes made:**\n${changes.join('\n')}\n\n` + `**Current settings:**\n` + `• Voice: ${updatedSettings.voice}\n` + `• Speed: ${updatedSettings.speed}x\n` + `• Stability: ${updatedSettings.stability}\n` + `• Similarity Boost: ${updatedSettings.similarityBoost}\n` + `• Style: ${updatedSettings.style}\n` + `• Speaker Boost: ${updatedSettings.useSpeakerBoost ? 'Enabled' : 'Disabled'}\n\n` + `**Available voices:** ${jarvis.getAvailableVoices().join(', ')}` } ] }; } case 'jarvis_daily_briefing': { await ensureJarvisInitialized(); const { includeEmails, includeSlack, includeCalendar, includeNews } = args as { includeEmails?: boolean; includeSlack?: boolean; includeCalendar?: boolean; includeNews?: boolean; }; console.log('šŸ“… JARVIS preparing daily briefing...'); if (!integrationHub) { throw new Error('Integration hub not initialized'); } const briefing = await integrationHub.getDailyBriefing(); return { content: [ { type: 'text', text: `šŸ¤– **JARVIS Daily Briefing**\n\n${briefing}\n\n*JARVIS is now reading your briefing aloud...*` } ] }; } case 'jarvis_read_emails': { await ensureJarvisInitialized(); const { query, limit, speak } = args as { query?: string; limit?: number; speak?: boolean; }; console.log(`šŸ“§ JARVIS reading emails: ${query || 'is:unread'}`); if (!integrationHub) { throw new Error('Integration hub not initialized'); } const emailSummary = await integrationHub.readEmails(query || 'is:unread', speak !== false); return { content: [ { type: 'text', text: `šŸ“§ **Email Summary**\n${emailSummary}` } ] }; } case 'jarvis_read_slack': { await ensureJarvisInitialized(); const { channel, limit, onlyMentions, speak } = args as { channel?: string; limit?: number; onlyMentions?: boolean; speak?: boolean; }; console.log(`šŸ’¬ JARVIS reading Slack: ${channel || 'general'}`); if (!integrationHub) { throw new Error('Integration hub not initialized'); } const slackSummary = await integrationHub.readSlackChannel( channel || 'general', limit || 20, speak !== false ); return { content: [ { type: 'text', text: `šŸ’¬ **Slack Summary**\n${slackSummary}` } ] }; } case 'jarvis_intelligent_summary': { await ensureJarvisInitialized(); const { timeframe, focus, actionItems } = args as { timeframe?: 'today' | 'week' | 'month'; focus?: string; actionItems?: boolean; }; console.log(`🧠 JARVIS generating intelligent summary for ${timeframe || 'today'}...`); if (!integrationHub) { throw new Error('Integration hub not initialized'); } const summary = await integrationHub.getIntelligentSummary(timeframe || 'today'); return { content: [ { type: 'text', text: summary } ] }; } case 'jarvis_monitor_services': { await ensureJarvisInitialized(); const { emailKeywords, slackKeywords, checkInterval } = args as { emailKeywords?: string[]; slackKeywords?: string[]; checkInterval?: number; }; console.log(`šŸ” JARVIS starting service monitoring...`); if (!integrationHub) { throw new Error('Integration hub not initialized'); } const monitoringConfig: { emailKeywords?: string[]; slackKeywords?: string[]; checkInterval?: number; } = {}; if (emailKeywords) { monitoringConfig.emailKeywords = emailKeywords; } if (slackKeywords) { monitoringConfig.slackKeywords = slackKeywords; } if (checkInterval !== undefined) { monitoringConfig.checkInterval = checkInterval; } await integrationHub.startMonitoring(monitoringConfig); return { content: [ { type: 'text', text: `šŸ” **JARVIS Monitoring Active**\n\n` + `Monitoring services every ${checkInterval || 15} minutes:\n` + `• Email keywords: ${emailKeywords?.join(', ') || 'urgent, important, ASAP'}\n` + `• Slack keywords: ${slackKeywords?.join(', ') || '@here, @channel, urgent'}\n\n` + `JARVIS will alert you when important messages arrive.` } ] }; } default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { console.error(`āŒ Tool error (${name}):`, error); return { content: [ { type: 'text', text: `āŒ **JARVIS Error**\n\n` + `Tool: ${name}\n` + `Error: ${error instanceof Error ? error.message : 'Unknown error'}\n\n` + `Please check that ElevenLabs API key is properly configured.` } ], isError: true }; } }); // Handle graceful shutdown process.on('SIGINT', async () => { console.log('\nšŸ›‘ Shutting down JARVIS MCP server...'); if (jarvisInstance) { jarvisInstance.cleanup(); } process.exit(0); }); process.on('SIGTERM', async () => { console.log('\nšŸ›‘ JARVIS MCP server terminated'); if (jarvisInstance) { jarvisInstance.cleanup(); } process.exit(0); }); // Start the server async function main() { const transport = new StdioServerTransport(); console.error('šŸ¤– Starting JARVIS MCP Server v2.0.0...'); console.error('šŸŽ™ļø Enhanced with chunked speech, playback controls, and human-like delivery'); console.error('šŸ”§ Tools available: elevenlabs_auto_read_response, jarvis_control, jarvis_status, jarvis_configure'); await server.connect(transport); } // Handle unhandled errors process.on('unhandledRejection', (reason, promise) => { console.error('āŒ Unhandled Rejection at:', promise, 'reason:', reason); }); process.on('uncaughtException', (error) => { console.error('āŒ Uncaught Exception:', error); process.exit(1); }); if (import.meta.url === `file://${process.argv[1]}`) { main().catch((error) => { console.error('āŒ Failed to start JARVIS MCP server:', error); process.exit(1); }); }