Files
vibn-frontend/lib/mcp/server.ts

369 lines
10 KiB
TypeScript

/**
* 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);