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

361 lines
8.7 KiB
TypeScript

/**
* MCPEngine Studio — Deploy as Download
*
* Creates a zip-ready directory structure with all generated files,
* package.json, tsconfig, README, Dockerfile, and .env.example.
*/
import { promises as fs } from 'fs';
import path from 'path';
import os from 'os';
import type {
ServerBundle,
DeployResult,
GeneratedFile,
} from '@mcpengine/ai-pipeline/types';
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export interface DownloadProgress {
step: string;
message: string;
percent: number;
level: 'info' | 'success' | 'warning' | 'error';
}
// ---------------------------------------------------------------------------
// Template files
// ---------------------------------------------------------------------------
function generateReadme(bundle: ServerBundle, slug: string): string {
const toolFiles = bundle.files.filter((f) => f.path.includes('tools/'));
const toolList = toolFiles
.map((f) => `- \`${f.path.replace(/^.*tools\//, '').replace(/\.ts$/, '')}\``)
.join('\n');
return `# ${slug} — MCP Server
Generated by [MCPEngine Studio](https://mcpengine.ai)
## Quick Start
\`\`\`bash
# Install dependencies
npm install
# Set up environment variables
cp .env.example .env
# Edit .env with your API keys
# Build
npm run build
# Start the server
npm start
\`\`\`
## Tools (${bundle.toolCount})
${toolList || '_(No tools found)_'}
## Development
\`\`\`bash
# Run in development mode with auto-reload
npm run dev
# Run tests
npm test
# Type check
npm run typecheck
\`\`\`
## Docker
\`\`\`bash
# Build the Docker image
docker build -t ${slug}-mcp .
# Run the container
docker run -p 3000:3000 --env-file .env ${slug}-mcp
\`\`\`
## Add to Claude Desktop
Add this to your Claude Desktop config (\`~/Library/Application Support/Claude/claude_desktop_config.json\`):
\`\`\`json
{
"mcpServers": {
"${slug}": {
"command": "node",
"args": ["${slug}/dist/index.js"],
"env": {}
}
}
}
\`\`\`
## License
MIT
`;
}
function generateDockerfile(slug: string): string {
return `# ${slug} MCP Server
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/package*.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \\
CMD wget -q --spider http://localhost:3000/health || exit 1
CMD ["node", "dist/index.js"]
`;
}
function generateDockerignore(): string {
return `node_modules
dist
.env
.git
.gitignore
*.md
Dockerfile
.dockerignore
.mcpengine-output
`;
}
function generateEnvExample(bundle: ServerBundle): string {
const lines = [
'# MCPEngine Server Environment Variables',
'# Copy this file to .env and fill in your values',
'',
'# Server Configuration',
'PORT=3000',
'NODE_ENV=production',
'',
'# API Authentication',
'API_KEY=your_api_key_here',
'API_BASE_URL=https://api.example.com',
'',
'# Optional: OAuth2 (if required by the target API)',
'# OAUTH_CLIENT_ID=',
'# OAUTH_CLIENT_SECRET=',
'# OAUTH_TOKEN_URL=',
'',
];
return lines.join('\n');
}
function generateTsConfig(): string {
return JSON.stringify(
{
compilerOptions: {
target: 'ES2022',
module: 'NodeNext',
moduleResolution: 'NodeNext',
lib: ['ES2022'],
outDir: './dist',
rootDir: './src',
strict: true,
esModuleInterop: true,
skipLibCheck: true,
forceConsistentCasingInFileNames: true,
resolveJsonModule: true,
declaration: true,
declarationMap: true,
sourceMap: true,
},
include: ['src/**/*'],
exclude: ['node_modules', 'dist'],
},
null,
2,
);
}
function generatePackageJson(bundle: ServerBundle, slug: string): string {
const pkg = {
name: `@mcpengine/${slug}`,
version: '1.0.0',
description: `MCP Server generated by MCPEngine Studio`,
type: 'module',
main: 'dist/index.js',
types: 'dist/index.d.ts',
scripts: {
build: 'tsc',
dev: 'tsx watch src/index.ts',
start: 'node dist/index.js',
typecheck: 'tsc --noEmit',
test: 'vitest run',
},
dependencies: {
'@modelcontextprotocol/sdk': '^1.0.0',
zod: '^3.23.0',
},
devDependencies: {
typescript: '^5.5.0',
tsx: '^4.16.0',
vitest: '^2.0.0',
'@types/node': '^20.14.0',
},
engines: {
node: '>=20',
},
license: 'MIT',
// Merge with bundle's package.json if present
...(typeof bundle.packageJson === 'object' ? bundle.packageJson : {}),
};
// Ensure name/version stay correct
pkg.name = `@mcpengine/${slug}`;
return JSON.stringify(pkg, null, 2);
}
function generateGitignore(): string {
return `node_modules/
dist/
.env
.env.local
*.log
.DS_Store
.mcpengine-output/
`;
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
export async function* deployAsDownload(
bundle: ServerBundle,
slug?: string,
): AsyncGenerator<DownloadProgress, DeployResult, void> {
const serverSlug = slug ?? 'mcp-server';
const deployId = crypto.randomUUID();
const startedAt = new Date().toISOString();
const logs: string[] = [];
const log = (msg: string) => {
logs.push(`[${new Date().toISOString()}] ${msg}`);
};
// ── Step 1: Create temp directory ──────────────────────────────────────
yield {
step: 'package',
message: 'Creating project structure…',
percent: 10,
level: 'info',
};
const tmpDir = path.join(os.tmpdir(), `mcpengine-download-${deployId}`);
const projectDir = path.join(tmpDir, serverSlug);
const srcDir = path.join(projectDir, 'src');
await fs.mkdir(srcDir, { recursive: true });
log(`Created project directory: ${projectDir}`);
// ── Step 2: Write source files ─────────────────────────────────────────
yield {
step: 'package',
message: `Writing ${bundle.files.length} source files…`,
percent: 30,
level: 'info',
};
for (const file of bundle.files) {
const filePath = path.join(srcDir, file.path);
await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, file.content, 'utf-8');
log(`Wrote ${file.path}`);
}
yield {
step: 'package',
message: `${bundle.files.length} source files written`,
percent: 50,
level: 'success',
};
// ── Step 3: Write config / meta files ──────────────────────────────────
yield {
step: 'package',
message: 'Generating project configuration…',
percent: 60,
level: 'info',
};
const configFiles: { name: string; content: string }[] = [
{ name: 'package.json', content: generatePackageJson(bundle, serverSlug) },
{ name: 'tsconfig.json', content: generateTsConfig() },
{ name: 'README.md', content: generateReadme(bundle, serverSlug) },
{ name: 'Dockerfile', content: generateDockerfile(serverSlug) },
{ name: '.dockerignore', content: generateDockerignore() },
{ name: '.env.example', content: generateEnvExample(bundle) },
{ name: '.gitignore', content: generateGitignore() },
];
for (const cf of configFiles) {
await fs.writeFile(path.join(projectDir, cf.name), cf.content, 'utf-8');
log(`Wrote ${cf.name}`);
}
yield {
step: 'package',
message: 'Project configuration complete',
percent: 80,
level: 'success',
};
// ── Step 4: Calculate stats ────────────────────────────────────────────
yield {
step: 'verify',
message: 'Verifying download package…',
percent: 90,
level: 'info',
};
const totalFiles = bundle.files.length + configFiles.length;
log(`Download package ready: ${totalFiles} files in ${projectDir}`);
yield {
step: 'verify',
message: `Package ready (${totalFiles} files)`,
percent: 100,
level: 'success',
};
// ── Return result ──────────────────────────────────────────────────────
return {
id: deployId,
target: 'download',
status: 'live',
url: projectDir,
endpoint: projectDir,
logs,
createdAt: startedAt,
};
}