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

205 lines
5.9 KiB
TypeScript

// Generator Service — streams MCP server code generation via Claude
import Anthropic from '@anthropic-ai/sdk';
import { getSkill } from '../skills/loader';
import type {
PipelineEvent,
AnalysisResult,
ToolDefinition,
ServerBundle,
GeneratedFile,
} from '../types';
const MODEL = 'claude-sonnet-4-5-20250514';
const MAX_TOKENS = 16384;
/**
* Generate a complete MCP server from analysis results and tool configuration.
* Streams file-by-file as they are generated.
*/
export async function* generateServer(
analysis: AnalysisResult,
toolConfig: ToolDefinition[]
): AsyncGenerator<PipelineEvent> {
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
const systemPrompt = getSkill('builder');
yield { type: 'generate:progress', file: 'Initializing generation', percent: 0 };
const toolSummary = toolConfig.map((t) => ({
name: t.name,
description: t.description,
endpoint: t.endpoint,
method: t.method,
inputSchema: t.inputSchema,
annotations: t.annotations,
}));
try {
const stream = client.messages.stream({
model: MODEL,
max_tokens: MAX_TOKENS,
system: systemPrompt,
messages: [
{
role: 'user',
content: `Generate a complete MCP server for the "${analysis.service}" API.
## API Details
- Base URL: ${analysis.baseUrl}
- Auth: ${JSON.stringify(analysis.authFlow)}
- Rate Limits: ${JSON.stringify(analysis.rateLimits)}
## Tools to Implement (${toolConfig.length} total)
\`\`\`json
${JSON.stringify(toolSummary, null, 2)}
\`\`\`
## Requirements
1. Generate each file separately, wrapped in a code block with the file path as the language tag
2. Use this format for each file:
\`\`\`typescript // path: src/index.ts
...code...
\`\`\`
3. Include: package.json, tsconfig.json, src/index.ts (entry), src/tools/*.ts, src/auth.ts, src/client.ts
4. Follow MCP SDK best practices from your training
5. Add proper error handling, input validation, and rate limit respect
6. TypeScript strict mode throughout
Generate all files now.`,
},
],
});
let fullText = '';
const generatedFiles: GeneratedFile[] = [];
let lastFileCount = 0;
for await (const event of stream) {
if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') {
fullText += event.delta.text;
// Check for newly completed file blocks
const files = extractFiles(fullText);
if (files.length > lastFileCount) {
for (let i = lastFileCount; i < files.length; i++) {
const file = files[i];
generatedFiles.push(file);
yield {
type: 'generate:file_ready',
path: file.path,
content: file.content,
};
yield {
type: 'generate:progress',
file: file.path,
percent: Math.min(90, Math.floor((files.length / estimateFileCount(toolConfig.length)) * 90)),
};
}
lastFileCount = files.length;
}
}
}
// Final extraction pass (catch any files missed during streaming)
const finalFiles = extractFiles(fullText);
for (let i = lastFileCount; i < finalFiles.length; i++) {
const file = finalFiles[i];
generatedFiles.push(file);
yield {
type: 'generate:file_ready',
path: file.path,
content: file.content,
};
}
// Get token usage
const finalMessage = await stream.finalMessage();
yield { type: 'generate:progress', file: 'Finalizing bundle', percent: 95 };
// Build the server bundle
const packageJson = generatedFiles.find((f) => f.path.endsWith('package.json'));
const tsConfig = generatedFiles.find((f) => f.path.endsWith('tsconfig.json'));
const bundle: ServerBundle = {
files: generatedFiles,
packageJson: packageJson ? JSON.parse(packageJson.content) : buildDefaultPackageJson(analysis.service),
tsConfig: tsConfig ? JSON.parse(tsConfig.content) : buildDefaultTsConfig(),
entryPoint: 'src/index.ts',
toolCount: toolConfig.length,
};
yield { type: 'generate:complete', bundle };
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
yield {
type: 'error',
message: `Generation failed: ${msg}`,
recoverable: error instanceof Anthropic.RateLimitError,
};
}
}
/** Extract file blocks from streamed text. */
function extractFiles(text: string): GeneratedFile[] {
const files: GeneratedFile[] = [];
// Match: ```typescript // path: some/path.ts OR ```json // path: package.json
const regex = /```(\w+)\s*\/\/\s*path:\s*(\S+)\s*\n([\s\S]*?)```/g;
let match;
while ((match = regex.exec(text)) !== null) {
const lang = match[1] as GeneratedFile['language'];
const path = match[2].trim();
const content = match[3].trim();
files.push({
path,
content,
language: lang === 'json' ? 'json' : lang === 'markdown' ? 'markdown' : 'typescript',
});
}
return files;
}
function estimateFileCount(toolCount: number): number {
// base files (index, auth, client, package.json, tsconfig) + one file per tool
return 5 + toolCount;
}
function buildDefaultPackageJson(service: string): object {
const slug = service.toLowerCase().replace(/[^a-z0-9]+/g, '-');
return {
name: `@mcpengine/${slug}-mcp`,
version: '1.0.0',
type: 'module',
main: 'dist/index.js',
scripts: {
build: 'tsc',
start: 'node dist/index.js',
dev: 'tsx src/index.ts',
},
dependencies: {
'@modelcontextprotocol/sdk': '^1.12.1',
},
devDependencies: {
typescript: '^5.8.0',
tsx: '^4.19.0',
},
};
}
function buildDefaultTsConfig(): object {
return {
compilerOptions: {
target: 'ES2022',
module: 'Node16',
moduleResolution: 'Node16',
outDir: './dist',
rootDir: './src',
strict: true,
esModuleInterop: true,
declaration: true,
},
include: ['src/**/*'],
};
}