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

204 lines
6.2 KiB
TypeScript

/**
* MCPEngine Studio — Deploy API Route
*
* POST /api/deploy
* Accepts { projectId, target, config }
* Loads project bundle from DB, runs deploy pipeline,
* streams progress via SSE, saves deployment record on completion.
*/
import { NextRequest, NextResponse } from 'next/server';
import { deploy } from '@/lib/deploy';
import type {
DeployTarget,
DeployConfig,
ServerBundle,
} from '@mcpengine/ai-pipeline/types';
// ---------------------------------------------------------------------------
// POST /api/deploy — Start deployment (SSE stream)
// ---------------------------------------------------------------------------
export async function POST(req: NextRequest) {
try {
const body = await req.json();
const { projectId, target, config } = body as {
projectId: string;
target: DeployTarget;
config?: Partial<DeployConfig>;
};
if (!projectId) {
return NextResponse.json(
{ error: 'projectId is required' },
{ status: 400 },
);
}
if (!target) {
return NextResponse.json(
{ error: 'target is required' },
{ status: 400 },
);
}
// ── Load project from DB ─────────────────────────────────────────────
// TODO: Replace with real DB query using drizzle
// import { db } from '@/lib/db';
// import { projects, deployments } from '@mcpengine/db/schema';
// import { eq } from 'drizzle-orm';
// const project = await db.query.projects.findFirst({
// where: eq(projects.id, projectId),
// });
// For now, create a mock bundle if no DB yet
const bundle: ServerBundle = await loadProjectBundle(projectId);
if (!bundle || !bundle.files.length) {
return NextResponse.json(
{ error: 'Project has no server bundle. Generate code first.' },
{ status: 400 },
);
}
// ── Build deploy config ──────────────────────────────────────────────
const deployConfig: DeployConfig = {
target,
slug: config?.slug ?? projectId.slice(0, 12),
envVars: config?.envVars ?? {},
customDomain: config?.customDomain,
};
// ── Stream deploy events via SSE ─────────────────────────────────────
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
try {
const pipeline = deploy(bundle, deployConfig);
let deployResult;
for await (const event of pipeline) {
const sseData = `data: ${JSON.stringify(event)}\n\n`;
controller.enqueue(encoder.encode(sseData));
if (event.type === 'complete' && event.result) {
deployResult = event.result;
}
}
// ── Save deployment record to DB ─────────────────────────────
if (deployResult) {
await saveDeploymentRecord(projectId, deployResult);
// Send final event with saved record
const finalEvent = {
type: 'saved',
message: 'Deployment record saved',
percent: 100,
level: 'success',
timestamp: new Date().toISOString(),
result: deployResult,
};
controller.enqueue(
encoder.encode(`data: ${JSON.stringify(finalEvent)}\n\n`),
);
}
controller.close();
} catch (error) {
const errMsg =
error instanceof Error ? error.message : 'Unknown error';
const errorEvent = {
type: 'error',
message: errMsg,
percent: 0,
level: 'error',
timestamp: new Date().toISOString(),
};
controller.enqueue(
encoder.encode(`data: ${JSON.stringify(errorEvent)}\n\n`),
);
controller.close();
}
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
'X-Accel-Buffering': 'no',
},
});
} catch (error) {
console.error('Deploy route error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 },
);
}
}
// ---------------------------------------------------------------------------
// Helpers — TODO: Replace with real DB operations
// ---------------------------------------------------------------------------
async function loadProjectBundle(projectId: string): Promise<ServerBundle> {
// TODO: Real implementation:
// const project = await db.query.projects.findFirst({
// where: eq(projects.id, projectId),
// });
// return project?.serverBundle as ServerBundle;
// Mock bundle for development
return {
files: [
{
path: 'index.ts',
content: `
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
const server = new Server({ name: 'mcp-server', version: '1.0.0' }, {
capabilities: { tools: {} },
});
export const listTools = () => [];
export const callTool = async (name, args) => ({ content: [{ type: 'text', text: 'OK' }] });
`,
language: 'typescript',
},
],
packageJson: { name: 'mcp-server', version: '1.0.0' },
tsConfig: {},
entryPoint: 'index.ts',
toolCount: 0,
};
}
async function saveDeploymentRecord(
projectId: string,
result: any,
): Promise<void> {
// TODO: Real implementation:
// await db.insert(deployments).values({
// projectId,
// userId: currentUser.id, // from auth context
// target: result.target,
// status: result.status,
// url: result.url,
// endpoint: result.endpoint,
// logs: result.logs,
// });
console.log('[deploy] Saved deployment record:', {
projectId,
deployId: result.id,
target: result.target,
status: result.status,
url: result.url,
});
}