/** * Vibn MCP (Model Context Protocol) Server * * Exposes Vibn project data, sessions, and capabilities to AI assistants * through a standardized protocol. */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { getAdminDb } from '@/lib/firebase/admin'; interface VibnResource { uri: string; name: string; description: string; mimeType: string; } class VibnMCPServer { private server: Server; constructor() { this.server = new Server( { name: 'vibn-mcp-server', version: '1.0.0', }, { capabilities: { resources: {}, tools: {}, prompts: {}, }, } ); this.setupHandlers(); } private setupHandlers() { // List available resources this.server.setRequestHandler(ListResourcesRequestSchema, async () => { return { resources: [ { uri: 'vibn://projects', name: 'Projects', description: 'List all user projects', mimeType: 'application/json', }, { uri: 'vibn://sessions', name: 'Coding Sessions', description: 'List all coding sessions', mimeType: 'application/json', }, { uri: 'vibn://conversations', name: 'AI Conversations', description: 'List all AI conversation history', mimeType: 'application/json', }, ] as VibnResource[], }; }); // Read a specific resource this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const { uri } = request.params; const db = getAdminDb(); try { if (uri === 'vibn://projects') { // Fetch all projects (would need user context in real implementation) const projectsSnapshot = await db .collection('projects') .orderBy('createdAt', 'desc') .limit(50) .get(); const projects = projectsSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data(), })); return { contents: [ { uri, mimeType: 'application/json', text: JSON.stringify(projects, null, 2), }, ], }; } if (uri.startsWith('vibn://projects/')) { const projectId = uri.replace('vibn://projects/', ''); const projectDoc = await db.collection('projects').doc(projectId).get(); if (!projectDoc.exists) { throw new Error(`Project ${projectId} not found`); } return { contents: [ { uri, mimeType: 'application/json', text: JSON.stringify({ id: projectDoc.id, ...projectDoc.data() }, null, 2), }, ], }; } if (uri.startsWith('vibn://sessions/')) { const projectId = uri.replace('vibn://sessions/', ''); const sessionsSnapshot = await db .collection('sessions') .where('projectId', '==', projectId) .orderBy('createdAt', 'desc') .limit(50) .get(); const sessions = sessionsSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data(), })); return { contents: [ { uri, mimeType: 'application/json', text: JSON.stringify(sessions, null, 2), }, ], }; } if (uri.startsWith('vibn://conversations/')) { const projectId = uri.replace('vibn://conversations/', ''); const conversationsSnapshot = await db .collection('projects') .doc(projectId) .collection('aiConversations') .orderBy('createdAt', 'asc') .limit(100) .get(); const conversations = conversationsSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data(), })); return { contents: [ { uri, mimeType: 'application/json', text: JSON.stringify(conversations, null, 2), }, ], }; } throw new Error(`Unknown resource: ${uri}`); } catch (error) { throw new Error(`Failed to read resource: ${error instanceof Error ? error.message : String(error)}`); } }); // List available tools this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'get_project_summary', description: 'Get a summary of a specific project including sessions, costs, and activity', inputSchema: { type: 'object', properties: { projectId: { type: 'string', description: 'The project ID', }, }, required: ['projectId'], }, }, { name: 'search_sessions', description: 'Search coding sessions by workspace path, date range, or project', inputSchema: { type: 'object', properties: { projectId: { type: 'string', description: 'Filter by project ID', }, workspacePath: { type: 'string', description: 'Filter by workspace path', }, startDate: { type: 'string', description: 'Start date (ISO format)', }, endDate: { type: 'string', description: 'End date (ISO format)', }, }, }, }, { name: 'get_conversation_context', description: 'Get the full AI conversation history for a project', inputSchema: { type: 'object', properties: { projectId: { type: 'string', description: 'The project ID', }, limit: { type: 'number', description: 'Maximum number of messages to return (default: 50)', }, }, required: ['projectId'], }, }, ], }; }); // Handle tool calls this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; const db = getAdminDb(); try { if (name === 'get_project_summary') { const { projectId } = args as { projectId: string }; const projectDoc = await db.collection('projects').doc(projectId).get(); if (!projectDoc.exists) { throw new Error(`Project ${projectId} not found`); } const project = { id: projectDoc.id, ...projectDoc.data() }; // Get sessions const sessionsSnapshot = await db .collection('sessions') .where('projectId', '==', projectId) .get(); const sessions = sessionsSnapshot.docs.map(doc => doc.data()); const totalCost = sessions.reduce((sum, s: any) => sum + (s.cost || 0), 0); const totalTokens = sessions.reduce((sum, s: any) => sum + (s.tokensUsed || 0), 0); const totalDuration = sessions.reduce((sum, s: any) => sum + (s.duration || 0), 0); const summary = { project, stats: { totalSessions: sessions.length, totalCost, totalTokens, totalDuration, }, recentSessions: sessions.slice(0, 5), }; return { content: [ { type: 'text', text: JSON.stringify(summary, null, 2), }, ], }; } if (name === 'search_sessions') { const { projectId, workspacePath, startDate, endDate } = args as { projectId?: string; workspacePath?: string; startDate?: string; endDate?: string; }; let query = db.collection('sessions'); if (projectId) { query = query.where('projectId', '==', projectId) as any; } if (workspacePath) { query = query.where('workspacePath', '==', workspacePath) as any; } const snapshot = await query.orderBy('createdAt', 'desc').limit(50).get(); const sessions = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data(), })); return { content: [ { type: 'text', text: JSON.stringify(sessions, null, 2), }, ], }; } if (name === 'get_conversation_context') { const { projectId, limit = 50 } = args as { projectId: string; limit?: number }; const conversationsSnapshot = await db .collection('projects') .doc(projectId) .collection('aiConversations') .orderBy('createdAt', 'asc') .limit(limit) .get(); const conversations = conversationsSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data(), })); return { content: [ { type: 'text', text: JSON.stringify(conversations, null, 2), }, ], }; } throw new Error(`Unknown tool: ${name}`); } catch (error) { throw new Error(`Tool execution failed: ${error instanceof Error ? error.message : String(error)}`); } }); } async start() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('Vibn MCP Server running on stdio'); } } // Start the server const server = new VibnMCPServer(); server.start().catch(console.error);