=== 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
205 lines
5.9 KiB
TypeScript
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/**/*'],
|
|
};
|
|
}
|