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

171 lines
4.5 KiB
TypeScript

// Designer Service — streams MCP app UI design via Claude
import Anthropic from '@anthropic-ai/sdk';
import { getSkill } from '../skills/loader';
import type {
PipelineEvent,
ToolDefinition,
AppPattern,
AppBundle,
} from '../types';
const MODEL = 'claude-sonnet-4-5-20250514';
const MAX_TOKENS = 8192;
export interface AppDesignConfig {
name: string;
pattern: AppPattern;
widgets: string[];
}
/**
* Design an MCP app UI for the given tools and configuration.
* Streams progress and yields the final AppBundle.
*/
export async function* designApp(
tools: ToolDefinition[],
appConfig: AppDesignConfig
): AsyncGenerator<PipelineEvent> {
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
const systemPrompt = getSkill('designer');
yield {
type: 'design:progress',
app: appConfig.name,
percent: 0,
};
const toolDescriptions = tools.map((t) => ({
name: t.name,
description: t.description,
inputSchema: t.inputSchema,
outputSchema: t.outputSchema,
}));
try {
const stream = client.messages.stream({
model: MODEL,
max_tokens: MAX_TOKENS,
system: systemPrompt,
messages: [
{
role: 'user',
content: `Design an MCP App with the following requirements:
## App Configuration
- **Name**: ${appConfig.name}
- **Pattern**: ${appConfig.pattern}
- **Widgets**: ${appConfig.widgets.join(', ')}
## Available MCP Tools
\`\`\`json
${JSON.stringify(toolDescriptions, null, 2)}
\`\`\`
## Requirements
1. Generate a single self-contained HTML file with embedded CSS and JavaScript
2. Use the "${appConfig.pattern}" layout pattern
3. Include the following widgets: ${appConfig.widgets.join(', ')}
4. Each widget should bind to the appropriate MCP tool(s) via window.mcpRequest()
5. The app must call tools via the MCP Apps bridge: window.mcpRequest(toolName, params)
6. Include responsive design, clean modern UI (Tailwind CDN is fine)
7. Include loading states, error handling, and empty states
8. Wrap the complete HTML in a \`\`\`html code block
## Tool Binding Format
For each widget, specify which tool it uses as a data-mcp-tool attribute.
Generate the complete app now.`,
},
],
});
let fullText = '';
for await (const event of stream) {
if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') {
fullText += event.delta.text;
const percent = Math.min(85, Math.floor((fullText.length / 3000) * 85));
// Yield progress at intervals
if (percent % 10 < 2) {
yield {
type: 'design:progress',
app: appConfig.name,
percent,
};
}
}
}
const finalMessage = await stream.finalMessage();
yield { type: 'design:progress', app: appConfig.name, percent: 90 };
// Extract the HTML from the response
const html = extractHtml(fullText);
const toolBindings = extractToolBindings(fullText, tools);
if (html) {
const bundle: AppBundle = {
id: crypto.randomUUID(),
name: appConfig.name,
pattern: appConfig.pattern,
html,
toolBindings,
};
yield { type: 'design:complete', bundle };
} else {
yield {
type: 'error',
message: 'Failed to extract HTML app from Claude response',
recoverable: true,
};
}
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
yield {
type: 'error',
message: `App design failed: ${msg}`,
recoverable: error instanceof Anthropic.RateLimitError,
};
}
}
function extractHtml(text: string): string | null {
const htmlMatch = text.match(/```html\s*\n([\s\S]*?)\n```/);
return htmlMatch ? htmlMatch[1].trim() : null;
}
function extractToolBindings(
text: string,
tools: ToolDefinition[]
): Record<string, string> {
const bindings: Record<string, string> = {};
// Look for data-mcp-tool attributes or mcpRequest calls
for (const tool of tools) {
if (text.includes(tool.name)) {
// Map widget/component name to tool name
const widgetMatch = text.match(
new RegExp(`(?:data-mcp-tool|mcpRequest)\\s*[=(]\\s*['"]${tool.name}['"]`, 'i')
);
if (widgetMatch) {
bindings[tool.name] = tool.name;
}
}
}
// Fallback: just map all mentioned tools
if (Object.keys(bindings).length === 0) {
for (const tool of tools) {
if (text.includes(tool.name)) {
bindings[tool.name] = tool.name;
}
}
}
return bindings;
}