Files
vibn-frontend/app/api/mcp/route.ts

299 lines
8.6 KiB
TypeScript

/**
* Vibn MCP HTTP API
*
* Exposes MCP capabilities over HTTP for web-based AI assistants
*/
import { NextResponse } from 'next/server';
import { getAdminAuth, getAdminDb } from '@/lib/firebase/admin';
export async function POST(request: Request) {
try {
// Authenticate user
const authHeader = request.headers.get('Authorization');
if (!authHeader?.startsWith('Bearer ')) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const token = authHeader.split('Bearer ')[1];
const adminAuth = getAdminAuth();
const adminDb = getAdminDb();
let userId: string;
// Try MCP API key first (for ChatGPT integration)
if (token.startsWith('vibn_mcp_')) {
const mcpKeysSnapshot = await adminDb
.collection('mcpKeys')
.where('key', '==', token)
.limit(1)
.get();
if (mcpKeysSnapshot.empty) {
return NextResponse.json({ error: 'Invalid MCP API key' }, { status: 401 });
}
const keyDoc = mcpKeysSnapshot.docs[0];
userId = keyDoc.data().userId;
// Update last used timestamp
await keyDoc.ref.update({
lastUsed: new Date().toISOString(),
});
} else {
// Try Firebase ID token (for direct user access)
try {
const decodedToken = await adminAuth.verifyIdToken(token);
userId = decodedToken.uid;
} catch (error) {
return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
}
}
const body = await request.json();
const { action, params } = body;
// Handle different MCP actions
switch (action) {
case 'list_resources': {
return NextResponse.json({
resources: [
{
uri: `vibn://projects/${userId}`,
name: 'My Projects',
description: 'All your Vibn projects',
mimeType: 'application/json',
},
{
uri: `vibn://sessions/${userId}`,
name: 'My Sessions',
description: 'All your coding sessions',
mimeType: 'application/json',
},
],
});
}
case 'read_resource': {
const { uri } = params;
if (uri === `vibn://projects/${userId}`) {
const projectsSnapshot = await adminDb
.collection('projects')
.where('userId', '==', userId)
.orderBy('createdAt', 'desc')
.limit(50)
.get();
const projects = projectsSnapshot.docs.map(doc => ({
id: doc.id,
...doc.data(),
}));
return NextResponse.json({
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify(projects, null, 2),
},
],
});
}
if (uri.startsWith('vibn://projects/') && uri.split('/').length === 4) {
const projectId = uri.split('/')[3];
const projectDoc = await adminDb.collection('projects').doc(projectId).get();
if (!projectDoc.exists || projectDoc.data()?.userId !== userId) {
return NextResponse.json({ error: 'Project not found' }, { status: 404 });
}
return NextResponse.json({
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify({ id: projectDoc.id, ...projectDoc.data() }, null, 2),
},
],
});
}
if (uri === `vibn://sessions/${userId}`) {
const sessionsSnapshot = await adminDb
.collection('sessions')
.where('userId', '==', userId)
.orderBy('createdAt', 'desc')
.limit(50)
.get();
const sessions = sessionsSnapshot.docs.map(doc => ({
id: doc.id,
...doc.data(),
}));
return NextResponse.json({
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify(sessions, null, 2),
},
],
});
}
return NextResponse.json({ error: 'Unknown resource' }, { status: 404 });
}
case 'call_tool': {
const { name, arguments: args } = params;
if (name === 'get_project_summary') {
const { projectId } = args;
const projectDoc = await adminDb.collection('projects').doc(projectId).get();
if (!projectDoc.exists || projectDoc.data()?.userId !== userId) {
return NextResponse.json({ error: 'Project not found' }, { status: 404 });
}
const project = { id: projectDoc.id, ...projectDoc.data() };
const sessionsSnapshot = await adminDb
.collection('sessions')
.where('projectId', '==', projectId)
.where('userId', '==', userId)
.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 NextResponse.json({
content: [
{
type: 'text',
text: JSON.stringify(summary, null, 2),
},
],
});
}
if (name === 'search_sessions') {
const { projectId, workspacePath } = args;
let query = adminDb.collection('sessions').where('userId', '==', userId);
if (projectId) {
query = query.where('projectId', '==', projectId) as any;
}
if (workspacePath) {
query = query.where('workspacePath', '==', workspacePath) as any;
}
const snapshot = await (query as any).orderBy('createdAt', 'desc').limit(50).get();
const sessions = snapshot.docs.map((doc: any) => ({
id: doc.id,
...doc.data(),
}));
return NextResponse.json({
content: [
{
type: 'text',
text: JSON.stringify(sessions, null, 2),
},
],
});
}
if (name === 'get_conversation_context') {
const { projectId, limit = 50 } = args;
const projectDoc = await adminDb.collection('projects').doc(projectId).get();
if (!projectDoc.exists || projectDoc.data()?.userId !== userId) {
return NextResponse.json({ error: 'Project not found' }, { status: 404 });
}
const conversationsSnapshot = await adminDb
.collection('projects')
.doc(projectId)
.collection('aiConversations')
.orderBy('createdAt', 'asc')
.limit(limit)
.get();
const conversations = conversationsSnapshot.docs.map(doc => ({
id: doc.id,
...doc.data(),
}));
return NextResponse.json({
content: [
{
type: 'text',
text: JSON.stringify(conversations, null, 2),
},
],
});
}
return NextResponse.json({ error: 'Unknown tool' }, { status: 404 });
}
default:
return NextResponse.json({ error: 'Unknown action' }, { status: 400 });
}
} catch (error) {
console.error('MCP API error:', error);
return NextResponse.json(
{
error: 'Failed to process MCP request',
details: error instanceof Error ? error.message : String(error),
},
{ status: 500 }
);
}
}
// GET endpoint to list capabilities
export async function GET(request: Request) {
return NextResponse.json({
name: 'vibn-mcp-server',
version: '1.0.0',
capabilities: {
resources: {
supported: true,
endpoints: [
'vibn://projects/{userId}',
'vibn://projects/{userId}/{projectId}',
'vibn://sessions/{userId}',
],
},
tools: {
supported: true,
available: [
'get_project_summary',
'search_sessions',
'get_conversation_context',
],
},
},
documentation: 'https://vibnai.com/docs/mcp',
});
}