VIBN Frontend for Coolify deployment
This commit is contained in:
368
lib/mcp/server.ts
Normal file
368
lib/mcp/server.ts
Normal file
@@ -0,0 +1,368 @@
|
||||
/**
|
||||
* 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);
|
||||
|
||||
Reference in New Issue
Block a user