=== 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
361 lines
8.7 KiB
TypeScript
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,
|
|
};
|
|
}
|