VIBN Frontend for Coolify deployment
This commit is contained in:
126
app/api/mcp/generate-key/route.ts
Normal file
126
app/api/mcp/generate-key/route.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Generate a long-lived MCP API key for ChatGPT integration
|
||||
*/
|
||||
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getAdminAuth, getAdminDb } from '@/lib/firebase/admin';
|
||||
import { randomBytes } from 'crypto';
|
||||
|
||||
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 idToken = authHeader.split('Bearer ')[1];
|
||||
const adminAuth = getAdminAuth();
|
||||
const adminDb = getAdminDb();
|
||||
|
||||
let userId: string;
|
||||
try {
|
||||
const decodedToken = await adminAuth.verifyIdToken(idToken);
|
||||
userId = decodedToken.uid;
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Check if user already has an MCP key
|
||||
const mcpKeysRef = adminDb.collection('mcpKeys');
|
||||
const existingKey = await mcpKeysRef
|
||||
.where('userId', '==', userId)
|
||||
.limit(1)
|
||||
.get();
|
||||
|
||||
if (!existingKey.empty) {
|
||||
// Return existing key
|
||||
const keyDoc = existingKey.docs[0];
|
||||
const keyData = keyDoc.data();
|
||||
|
||||
return NextResponse.json({
|
||||
apiKey: keyData.key,
|
||||
createdAt: keyData.createdAt,
|
||||
message: 'Using existing MCP API key',
|
||||
});
|
||||
}
|
||||
|
||||
// Generate new API key
|
||||
const apiKey = `vibn_mcp_${randomBytes(32).toString('hex')}`;
|
||||
|
||||
// Store in Firestore
|
||||
await mcpKeysRef.add({
|
||||
userId,
|
||||
key: apiKey,
|
||||
type: 'mcp',
|
||||
createdAt: new Date().toISOString(),
|
||||
lastUsed: null,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
apiKey,
|
||||
createdAt: new Date().toISOString(),
|
||||
message: 'MCP API key generated successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error generating MCP key:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to generate MCP key',
|
||||
details: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE endpoint to revoke MCP key
|
||||
export async function DELETE(request: Request) {
|
||||
try {
|
||||
const authHeader = request.headers.get('Authorization');
|
||||
if (!authHeader?.startsWith('Bearer ')) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const idToken = authHeader.split('Bearer ')[1];
|
||||
const adminAuth = getAdminAuth();
|
||||
const adminDb = getAdminDb();
|
||||
|
||||
let userId: string;
|
||||
try {
|
||||
const decodedToken = await adminAuth.verifyIdToken(idToken);
|
||||
userId = decodedToken.uid;
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Delete user's MCP key
|
||||
const mcpKeysRef = adminDb.collection('mcpKeys');
|
||||
const existingKey = await mcpKeysRef
|
||||
.where('userId', '==', userId)
|
||||
.get();
|
||||
|
||||
if (existingKey.empty) {
|
||||
return NextResponse.json({ message: 'No MCP key to delete' });
|
||||
}
|
||||
|
||||
// Delete all keys for this user
|
||||
const batch = adminDb.batch();
|
||||
existingKey.docs.forEach(doc => {
|
||||
batch.delete(doc.ref);
|
||||
});
|
||||
await batch.commit();
|
||||
|
||||
return NextResponse.json({ message: 'MCP key deleted successfully' });
|
||||
} catch (error) {
|
||||
console.error('Error deleting MCP key:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to delete MCP key',
|
||||
details: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
298
app/api/mcp/route.ts
Normal file
298
app/api/mcp/route.ts
Normal file
@@ -0,0 +1,298 @@
|
||||
/**
|
||||
* 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',
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user