Jake Shore 96e52666c5 MCPEngine full sync — studio scaffold, factory v2, server updates, state.json — 2026-02-12
=== NEW ===
- studio/ — MCPEngine Studio scaffold (Next.js monorepo, build plan)
- docs/FACTORY-V2.md — Factory v2 architecture doc
- docs/CALENDLY_MCP_BUILD_SUMMARY.md — Calendly MCP build report

=== UPDATED SERVERS ===
- fieldedge: Added jobs-tools, UI build script, main entry update
- lightspeed: Updated main + server entry points
- squarespace: Added collection-browser + page-manager apps
- toast: Added main + server entry points

=== INFRA ===
- infra/command-center/state.json — Updated pipeline state
- infra/command-center/FACTORY-V2.md — Factory v2 operator playbook
2026-02-12 17:58:33 -05:00

161 lines
4.6 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@clerk/nextjs/server';
import { db, projects, users, tools, apps, apiKeys, deployments } from '@mcpengine/db';
import { eq, and } from 'drizzle-orm';
type RouteContext = { params: Promise<{ id: string }> };
// ── GET /api/projects/[id] — project detail with tools + apps ───────────────
export async function GET(req: NextRequest, context: RouteContext) {
try {
const { userId: clerkId } = await auth();
if (!clerkId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const user = await db.query.users.findFirst({
where: eq(users.clerkId, clerkId),
});
if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}
const { id } = await context.params;
const project = await db.query.projects.findFirst({
where: and(eq(projects.id, id), eq(projects.userId, user.id)),
with: {
tools: true,
apps: true,
deployments: {
orderBy: (d: any, { desc }: any) => [desc(d.createdAt)],
limit: 5,
},
},
});
if (!project) {
return NextResponse.json({ error: 'Project not found' }, { status: 404 });
}
return NextResponse.json({ data: project });
} catch (error) {
console.error('[GET /api/projects/[id]]', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 },
);
}
}
// ── PATCH /api/projects/[id] — update project ──────────────────────────────
export async function PATCH(req: NextRequest, context: RouteContext) {
try {
const { userId: clerkId } = await auth();
if (!clerkId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const user = await db.query.users.findFirst({
where: eq(users.clerkId, clerkId),
});
if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}
const { id } = await context.params;
// Verify ownership
const existing = await db.query.projects.findFirst({
where: and(eq(projects.id, id), eq(projects.userId, user.id)),
});
if (!existing) {
return NextResponse.json({ error: 'Project not found' }, { status: 404 });
}
const body = await req.json();
// Whitelist updatable fields
const allowedFields = [
'name',
'description',
'status',
'specUrl',
'specRaw',
'analysis',
'toolConfig',
'appConfig',
'authConfig',
'serverBundle',
] as const;
const updates: Record<string, unknown> = { updatedAt: new Date() };
for (const field of allowedFields) {
if (body[field] !== undefined) {
updates[field] = body[field];
}
}
const [updated] = (await db
.update(projects)
.set(updates)
.where(and(eq(projects.id, id), eq(projects.userId, user.id)))
.returning()) as any[];
return NextResponse.json({ data: updated });
} catch (error) {
console.error('[PATCH /api/projects/[id]]', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 },
);
}
}
// ── DELETE /api/projects/[id] — delete project + cascade ────────────────────
export async function DELETE(req: NextRequest, context: RouteContext) {
try {
const { userId: clerkId } = await auth();
if (!clerkId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const user = await db.query.users.findFirst({
where: eq(users.clerkId, clerkId),
});
if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}
const { id } = await context.params;
// Verify ownership
const existing = await db.query.projects.findFirst({
where: and(eq(projects.id, id), eq(projects.userId, user.id)),
});
if (!existing) {
return NextResponse.json({ error: 'Project not found' }, { status: 404 });
}
// Delete non-cascade relations first
await db.delete(deployments).where(eq(deployments.projectId, id));
// tools, apps, apiKeys cascade automatically via FK onDelete
await db
.delete(projects)
.where(and(eq(projects.id, id), eq(projects.userId, user.id)));
return NextResponse.json({ success: true }, { status: 200 });
} catch (error) {
console.error('[DELETE /api/projects/[id]]', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 },
);
}
}