diff --git a/.changeset/sweet-carrots-walk.md b/.changeset/sweet-carrots-walk.md
new file mode 100644
index 00000000..843ffe62
--- /dev/null
+++ b/.changeset/sweet-carrots-walk.md
@@ -0,0 +1,13 @@
+---
+'@srcbook/api': patch
+'srcbook': patch
+---
+
+Add Model Context Protocol (MCP) integration to enhance AI capabilities. This update includes:
+
+- MCP client implementation for connecting to MCP servers
+- Tool discovery and execution functionality
+- Integration with app generation pipeline
+- Documentation in README.md
+
+MCP enables AI models to access external tools and data sources, significantly expanding the capabilities of AI-generated applications.
diff --git a/.gitignore b/.gitignore
index c37aebf0..23896068 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,4 +45,11 @@ srcbook/lib/**/*
# Docs folder
docs/
-vite.config.ts.timestamp-*.mjs
\ No newline at end of file
+vite.config.ts.timestamp-*.mjs
+
+# Roo Code
+.roomodes
+
+# MCP config
+packages/api/srcbook_mcp_config.json
+MCPClientDevGuide.md
\ No newline at end of file
diff --git a/JanusAgentImplementationPlan.md b/JanusAgentImplementationPlan.md
new file mode 100644
index 00000000..6d028259
--- /dev/null
+++ b/JanusAgentImplementationPlan.md
@@ -0,0 +1,93 @@
+# Janus Agent Implementation Plan for Srcbook
+
+## Overview
+
+This document outlines the implementation plan for "Janus agents" in Srcbook - agentic entities that can both utilize external capabilities through an MCP client and serve functionality to other clients like an MCP server.
+
+The implementation will focus specifically on exposing Srcbook's web app generation functionality as MCP tools, allowing other MCP clients to leverage Srcbook's generative AI capabilities.
+
+## Architecture
+
+The Janus agent will consist of:
+
+1. **MCP Server Manager**: A class that manages MCP server instances, handles connections, and registers tools.
+2. **App Generation Tool**: A tool that wraps Srcbook's existing app generation functionality.
+3. **Server Entry Point**: The main entry point for the MCP server.
+4. **Configuration**: Settings for the MCP server.
+
+## Implementation Steps
+
+### 1. Create MCP Server Manager
+
+File: `packages/api/mcp/server/server-manager.mts`
+
+This class will:
+- Initialize the MCP server using the SDK
+- Register tools with the server
+- Handle connections using the appropriate transport (stdio)
+- Manage server lifecycle (start, stop)
+- Implement proper error handling and logging
+
+### 2. Implement App Generation Tool
+
+File: `packages/api/mcp/server/tools/generate-app.mts`
+
+This tool will:
+- Define a clear input schema using Zod
+- Wrap the existing `generateApp` function
+- Format responses according to MCP specifications
+- Handle errors properly
+- Include appropriate annotations (e.g., destructiveHint: false)
+
+### 3. Create Server Entry Point
+
+File: `packages/api/mcp/server/index.mts`
+
+This module will:
+- Export functions to start/stop the server
+- Handle transport setup and connection management
+- Initialize the server manager
+
+### 4. Update Configuration
+
+File: `packages/api/mcp/config.mts`
+
+This module will:
+- Define configuration options for the MCP server
+- Load and validate configuration
+
+### 5. Update Main Server
+
+File: `packages/api/server.mts`
+
+Update to:
+- Initialize both MCP client and server components
+- Ensure proper resource management and error handling
+
+### 6. Update MCP Configuration
+
+File: `packages/api/srcbook_mcp_config.json`
+
+Add configuration section for the MCP server.
+
+## Security Considerations
+
+- Validate all inputs using Zod schemas
+- Implement proper error handling
+- Sanitize file paths and system commands
+- Use appropriate authentication for remote connections
+- Follow the principle of least privilege
+
+## Testing Strategy
+
+- Test the MCP server in isolation using the MCP Inspector tool
+- Test integration with the existing MCP client
+- Test the app generation tool with various inputs
+- Test error handling and edge cases
+
+## Future Enhancements
+
+- Add more tools for other Srcbook capabilities
+- Implement resource templates for accessing project files
+- Add support for prompts
+- Implement streaming responses for long-running operations
diff --git a/README.md b/README.md
index 8cbc4d2f..8545ac1d 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@
Online app builder ·
Discord ·
Youtube ·
- Hub
+ Hub
## Srcbook
@@ -33,6 +33,7 @@ Srcbook is open-source (apache2) and runs locally on your machine. You'll need t
- Create, edit and run web apps
- Use AI to generate the boilerplate, modify the code, and fix things
- Edit the app with a hot-reloading web preview
+- MCP (Model Context Protocol) integration for enhanced AI capabilities
@@ -137,6 +138,34 @@ In order to improve Srcbook, we collect some behavioral analytics. We don't coll
If you want to disable tracking, you can run Srcbook with `SRCBOOK_DISABLE_ANALYTICS=true` set in the environment.
+## MCP Integration
+
+Srcbook now includes support for the Model Context Protocol (MCP), enabling AI models to access external tools and data sources. This integration enhances the capabilities of AI-generated applications by allowing them to:
+
+- Access external data sources and APIs
+- Perform web searches and retrieve information
+- Execute specialized tools during app generation
+
+MCP servers can be configured in the `packages/api/srcbook_mcp_config.json` file. The MCP client automatically discovers and makes available all tools provided by configured MCP servers.
+
+### Configuring MCP Servers
+
+To add an MCP server, update the configuration file with the server details:
+
+```json
+{
+ "mcpServers": {
+ "server-id": {
+ "command": "command-to-run-server",
+ "args": ["arg1", "arg2"],
+ "env": {
+ "API_KEY": "your-api-key"
+ }
+ }
+ }
+}
+```
+
## Contributing
For development instructions, see [CONTRIBUTING.md](https://github.com/srcbookdev/srcbook/blob/main/CONTRIBUTING.md).
diff --git a/docs/JANUS_MCP_IMPLEMENTATION.md b/docs/JANUS_MCP_IMPLEMENTATION.md
new file mode 100644
index 00000000..ee0774bc
--- /dev/null
+++ b/docs/JANUS_MCP_IMPLEMENTATION.md
@@ -0,0 +1,199 @@
+# Janus Agent Implementation for MCP
+
+This document outlines the implementation of Srcbook's Janus agent capabilities using the Model Context Protocol (MCP).
+
+## Overview
+
+The Janus agent allows Srcbook to serve its UI generation capabilities to any MCP-compatible client while also consuming capabilities from other MCP servers. This bidirectional approach enhances the ecosystem:
+
+- **MCP Clients** gain access to Srcbook's advanced UI generation capabilities
+- **Srcbook** can leverage specialized features from other MCP servers
+
+## Implementation Architecture
+
+### Components
+
+1. **MCP Server Manager**: Manages the MCP server, handles connections and tool registration
+2. **Generate App Tool**: Exposes Srcbook's app generation functionality as an MCP tool
+3. **UI Visualize Tool**: Provides UI component visualization descriptions for better integration with design tools
+4. **Standardized Response Format**: Structured output following MCP conventions
+
+### Data Flow
+
+1. Any MCP client connects to Srcbook via MCP
+2. User requests UI generation through the client
+3. Request is passed to Srcbook's Janus agent via MCP
+4. Srcbook processes the request and generates UI components
+5. Structured response is returned to the client
+6. Client processes and displays the generated UI accordingly
+
+## Tools & Capabilities
+
+### Generate App Tool
+
+The `generate-app` tool creates complete UI applications from natural language descriptions.
+
+**Parameters:**
+- `query`: Natural language description of the app to generate (required)
+- `projectName`: Optional project name
+- `componentType`: Framework for component generation (react, vue, angular, html)
+- `styleSystem`: Styling system to use (tailwind, css, sass, styled-components)
+
+**Response Format:**
+```json
+{
+ "content": [
+ {
+ "type": "text",
+ "text": "Successfully generated app with project ID: [project-id]"
+ },
+ {
+ "type": "resource",
+ "resource": {
+ "uri": "file://[path]",
+ "mimeType": "application/javascript",
+ "text": "...file content..."
+ }
+ },
+ {
+ "type": "image",
+ "data": "base64-encoded-data",
+ "mimeType": "image/png"
+ }
+ ]
+}
+```
+
+### UI Visualize Tool
+
+The `ui-visualize` tool provides visual descriptions of UI components to enhance the design integration.
+
+**Parameters:**
+- `code`: The component code to visualize (required)
+- `componentType`: Framework for the component (react, vue, angular, html)
+- `viewportSize`: Target viewport dimensions (width, height)
+
+**Response Format:**
+```json
+{
+ "content": [
+ {
+ "type": "text",
+ "text": "Component analysis complete"
+ },
+ {
+ "type": "resource",
+ "resource": {
+ "uri": "visualization://[framework]",
+ "mimeType": "application/json",
+ "text": "{\"componentType\":\"[framework]\",\"viewport\":{\"width\":1280,\"height\":800},\"elements\":[{\"type\":\"[element-type]\",\"count\":3}],\"patterns\":[{\"type\":\"[pattern-type]\",\"present\":true}],\"description\":\"...human-readable description...\"}"
+ }
+ }
+ ]
+}
+```
+
+## Progress Notifications
+
+The implementation supports progress notifications to provide real-time feedback during UI generation. This is implemented using the MCP standard notification format:
+
+```typescript
+// Access the underlying Server instance for notifications
+server.server.sendNotification({
+ method: 'notifications/progress',
+ params: {
+ progressToken: 'generate-app', // Matches token from request
+ progress: 50, // Current progress
+ total: 100, // Total progress required
+ message: 'Component generation' // Human-readable message
+ }
+});
+```
+
+## Getting Started
+
+### Client Setup
+
+1. Configure any MCP-compatible client to connect to Srcbook's MCP server
+2. Add Srcbook to the client's trusted MCP server list
+3. Set up appropriate permissions for MCP tool access
+
+### Srcbook Setup
+
+1. Enable the MCP server in `srcbook_mcp_config.json`
+2. Start Srcbook with the MCP server enabled
+3. Verify connections in the logs
+
+## Example Usage
+
+### Generating a UI Component with Any MCP Client
+
+```typescript
+// Connect to Srcbook MCP server
+const server = await mcp.connect('srcbook-mcp-server');
+
+// Call generate-app tool
+const result = await server.tools.call('generate-app', {
+ query: 'Create a responsive product card component with image, title, price, and add to cart button',
+ componentType: 'react',
+ styleSystem: 'tailwind'
+});
+
+// Process the MCP-standard content array
+const textResponse = result.content.find(item => item.type === 'text');
+const resources = result.content.filter(item => item.type === 'resource');
+const images = result.content.filter(item => item.type === 'image');
+
+// Work with resources and images
+resources.forEach(resource => {
+ const { uri, text, mimeType } = resource.resource;
+ console.log(`Processing resource ${uri} of type ${mimeType}`);
+});
+```
+
+## Extending the Implementation
+
+Additional capabilities can be added to enhance the MCP integration:
+
+1. **Resource Access**: Add file system resources for shared access to assets
+2. **UI Component Library**: Create a library of reusable UI components
+3. **Interactive Refinement**: Add interactive conversation capabilities for UI refinement
+4. **More Tool Types**: Implement additional specialized tools for different use cases
+5. **Design System Support**: Add support for various design systems (Material UI, Chakra UI, etc.)
+
+## Security Considerations
+
+1. **Input Validation**: All inputs are validated using Zod schemas
+2. **Authentication**: SSE transport connections use token-based authentication
+3. **Authorization**: Access controls restrict unauthorized access
+4. **Data Sanitization**: File contents and responses are properly sanitized
+5. **Secure Defaults**: Servers have secure defaults with explicit opt-in for remote connections
+
+## Troubleshooting
+
+Common issues and solutions:
+
+1. **Connection Issues**: Verify network settings and authentication tokens
+2. **Progress Notification Errors**: Check protocol version compatibility
+3. **Response Formatting Issues**: Ensure proper JSON parsing in the client
+4. **Tool Parameter Errors**: Verify parameter types match the schema definition
+
+For technical support, contact the Srcbook team or file an issue on GitHub.
+
+## MCP Compliance
+
+This implementation follows the MCP specification for server-side tools:
+
+1. **Tool Schema Definition**: Clear schemas using Zod
+2. **Response Format**: Standard MCP `content` array with multiple content types (text, resources, images)
+3. **Progress Notifications**: Following the standard `notifications/progress` format
+4. **Error Handling**: Properly structured error responses with `isError: true`
+
+The implementation is fully compliant with the 2025-03-26 revision of the MCP specification, supporting:
+
+- **Content Types**: Text, Resources, and Images
+- **Resource Embedding**: Structured as specified in the protocol
+- **Standard Notifications**: Using the proper notification format
+- **Well-formed Responses**: Following the exact MCP schema
+
+This ensures compatibility with any client that supports the MCP tools capability, regardless of the client's implementation language or environment.
\ No newline at end of file
diff --git a/packages/api/ai/generate.mts b/packages/api/ai/generate.mts
index 13245e14..354a4430 100644
--- a/packages/api/ai/generate.mts
+++ b/packages/api/ai/generate.mts
@@ -14,6 +14,7 @@ import { PROMPTS_DIR } from '../constants.mjs';
import { encode, decodeCells } from '../srcmd.mjs';
import { buildProjectXml, type FileContent } from '../ai/app-parser.mjs';
import { logAppGeneration } from './logger.mjs';
+import { formatMCPToolsForAI } from './mcp-tools.mjs';
const makeGenerateSrcbookSystemPrompt = () => {
return readFileSync(Path.join(PROMPTS_DIR, 'srcbook-generator.txt'), 'utf-8');
@@ -33,25 +34,63 @@ const makeAppEditorSystemPrompt = () => {
return readFileSync(Path.join(PROMPTS_DIR, 'app-editor.txt'), 'utf-8');
};
-const makeAppEditorUserPrompt = (projectId: string, files: FileContent[], query: string) => {
+const makeAppEditorUserPrompt = async (projectId: string, files: FileContent[], query: string) => {
const projectXml = buildProjectXml(files, projectId);
const userRequestXml = `${query}`;
+
+ // Get MCP tools if available
+ let mcpToolsXml = '';
+ try {
+ const mcpTools = await formatMCPToolsForAI();
+ if (
+ mcpTools &&
+ mcpTools !== 'No MCP tools are available.' &&
+ mcpTools !== 'Error retrieving MCP tools.'
+ ) {
+ mcpToolsXml = `
+${mcpTools}
+`;
+ }
+ } catch (error) {
+ console.error('Error getting MCP tools for app editor:', error);
+ }
+
return `Following below are the project XML and the user request.
${projectXml}
${userRequestXml}
+${mcpToolsXml ? '\n\n' + mcpToolsXml : ''}
`.trim();
};
-const makeAppCreateUserPrompt = (projectId: string, files: FileContent[], query: string) => {
+const makeAppCreateUserPrompt = async (projectId: string, files: FileContent[], query: string) => {
const projectXml = buildProjectXml(files, projectId);
const userRequestXml = `${query}`;
+
+ // Get MCP tools if available
+ let mcpToolsXml = '';
+ try {
+ const mcpTools = await formatMCPToolsForAI();
+ if (
+ mcpTools &&
+ mcpTools !== 'No MCP tools are available.' &&
+ mcpTools !== 'Error retrieving MCP tools.'
+ ) {
+ mcpToolsXml = `
+${mcpTools}
+`;
+ }
+ } catch (error) {
+ console.error('Error getting MCP tools for app creation:', error);
+ }
+
return `Following below are the project XML and the user request.
${projectXml}
${userRequestXml}
+${mcpToolsXml ? '\n\n' + mcpToolsXml : ''}
`.trim();
};
@@ -252,7 +291,7 @@ export async function generateApp(
const result = await generateText({
model,
system: makeAppBuilderSystemPrompt(),
- prompt: makeAppCreateUserPrompt(projectId, files, query),
+ prompt: await makeAppCreateUserPrompt(projectId, files, query),
});
return result.text;
}
@@ -267,7 +306,7 @@ export async function streamEditApp(
const model = await getModel();
const systemPrompt = makeAppEditorSystemPrompt();
- const userPrompt = makeAppEditorUserPrompt(projectId, files, query);
+ const userPrompt = await makeAppEditorUserPrompt(projectId, files, query);
let response = '';
diff --git a/packages/api/ai/mcp-tools.mts b/packages/api/ai/mcp-tools.mts
new file mode 100644
index 00000000..685bbbd9
--- /dev/null
+++ b/packages/api/ai/mcp-tools.mts
@@ -0,0 +1,114 @@
+/**
+ * MCP Tools Formatter
+ *
+ * This module provides functions to format MCP tools for AI consumption.
+ * It converts MCP tool definitions into a format that can be included in AI prompts.
+ */
+
+import { getMCPClientManager, type MCPTool } from '../mcp/client-manager.mjs';
+
+/**
+ * Format MCP tools for inclusion in AI prompts
+ *
+ * @returns A formatted string describing all available MCP tools
+ */
+export async function formatMCPToolsForAI(): Promise {
+ try {
+ // Get the MCP client manager
+ const clientManager = getMCPClientManager();
+
+ // Get all available tools
+ const tools = await clientManager.getTools();
+
+ if (tools.length === 0) {
+ return 'No MCP tools are available.';
+ }
+
+ // Format the tools as a string
+ return formatToolsAsString(tools);
+ } catch (error) {
+ console.error('Error formatting MCP tools for AI:', error);
+ return 'Error retrieving MCP tools.';
+ }
+}
+
+/**
+ * Format a list of MCP tools as a string
+ *
+ * @param tools The list of MCP tools to format
+ * @returns A formatted string describing the tools
+ */
+function formatToolsAsString(tools: MCPTool[]): string {
+ // Start with a header
+ let result = '## Available MCP Tools\n\n';
+ result += 'You can use the following tools to perform actions:\n\n';
+
+ // Add each tool
+ tools.forEach((tool) => {
+ // Add tool name and description
+ result += `### ${tool.name}\n`;
+ if (tool.annotations?.title) {
+ result += `**${tool.annotations.title}**\n`;
+ }
+ if (tool.description) {
+ result += `${tool.description}\n`;
+ }
+
+ // Add tool annotations as hints
+ const hints: string[] = [];
+ if (tool.annotations?.readOnlyHint) hints.push('Read-only');
+ if (tool.annotations?.destructiveHint) hints.push('Destructive');
+ if (tool.annotations?.idempotentHint) hints.push('Idempotent');
+ if (tool.annotations?.openWorldHint) hints.push('Interacts with external systems');
+
+ if (hints.length > 0) {
+ result += `**Hints:** ${hints.join(', ')}\n`;
+ }
+
+ // Add input schema
+ result += '\n**Input Schema:**\n';
+ result += '```json\n';
+ result += JSON.stringify(tool.inputSchema, null, 2);
+ result += '\n```\n\n';
+
+ // Add server ID
+ result += `**Server:** ${tool.serverId}\n\n`;
+
+ // Add separator between tools
+ result += '---\n\n';
+ });
+
+ // Add usage instructions
+ result += `## How to Use These Tools
+
+To use a tool, include a tool call in your response using the following format:
+
+\`\`\`
+
+{
+ "param1": "value1",
+ "param2": "value2"
+}
+
+\`\`\`
+
+Replace TOOL_NAME with the name of the tool you want to use, SERVER_ID with the server ID, and include the appropriate parameters as specified in the tool's input schema.
+`;
+
+ return result;
+}
+
+/**
+ * Get the list of MCP tools
+ *
+ * @returns The list of available MCP tools
+ */
+export async function getMCPTools(): Promise {
+ try {
+ const clientManager = getMCPClientManager();
+ return await clientManager.getTools();
+ } catch (error) {
+ console.error('Error getting MCP tools:', error);
+ return [];
+ }
+}
diff --git a/packages/api/ai/plan-parser.mts b/packages/api/ai/plan-parser.mts
index ac00e33f..c1c63532 100644
--- a/packages/api/ai/plan-parser.mts
+++ b/packages/api/ai/plan-parser.mts
@@ -4,6 +4,7 @@ import { type App as DBAppType } from '../db/schema.mjs';
import { loadFile } from '../apps/disk.mjs';
import { StreamingXMLParser, TagType } from './stream-xml-parser.mjs';
import { ActionChunkType, DescriptionChunkType } from '@srcbook/shared';
+import { getMCPClientManager } from '../mcp/client-manager.mjs';
// The ai proposes a plan that we expect to contain both files and commands
// Here is an example of a plan:
@@ -60,6 +61,15 @@ type NpmInstallCommand = {
description: string;
};
+// MCP Tool Action type
+interface MCPToolAction {
+ type: 'tool';
+ toolName: string;
+ serverId: string;
+ parameters: string; // JSON string of parameters
+ description: string;
+}
+
// Later we can add more commands. For now, we only support npm install
type Command = NpmInstallCommand;
@@ -69,7 +79,7 @@ export interface Plan {
id: string;
query: string;
description: string;
- actions: (FileAction | Command)[];
+ actions: (FileAction | Command | MCPToolAction)[];
}
interface ParsedResult {
@@ -82,6 +92,9 @@ interface ParsedResult {
file?: { '@_filename': string; '#text': string };
commandType?: string;
package?: string | string[];
+ toolName?: string;
+ serverId?: string;
+ parameters?: string;
}[]
| {
'@_type': string;
@@ -89,6 +102,9 @@ interface ParsedResult {
file?: { '@_filename': string; '#text': string };
commandType?: string;
package?: string | string[];
+ toolName?: string;
+ serverId?: string;
+ parameters?: string;
};
};
}
@@ -151,6 +167,32 @@ export async function parsePlan(
packages: Array.isArray(action.package) ? action.package : [action.package],
description: action.description,
});
+ } else if (action['@_type'] === 'tool' && action.toolName && action.serverId) {
+ // Handle MCP tool action
+ try {
+ // Validate that the tool exists
+ const clientManager = getMCPClientManager();
+ const tools = await clientManager.getTools();
+ const tool = tools.find(
+ (t) => t.name === action.toolName && t.serverId === action.serverId,
+ );
+
+ if (!tool) {
+ console.error(`Tool ${action.toolName} not found on server ${action.serverId}`);
+ continue;
+ }
+
+ plan.actions.push({
+ type: 'tool',
+ toolName: action.toolName,
+ serverId: action.serverId,
+ parameters: action.parameters || '{}',
+ description: action.description,
+ });
+ } catch (error) {
+ console.error('Error handling MCP tool action:', error);
+ continue;
+ }
}
}
@@ -274,6 +316,38 @@ async function toStreamingChunk(
packages: packageTags.map((t) => t.content),
},
} as ActionChunkType;
+ } else if (type === 'tool') {
+ const toolNameTag = tag.children.find((t) => t.name === 'toolName')!;
+ const serverIdTag = tag.children.find((t) => t.name === 'serverId')!;
+
+ // Validate that the tool exists
+ try {
+ const clientManager = getMCPClientManager();
+ const tools = await clientManager.getTools();
+ const tool = tools.find(
+ (t) => t.name === toolNameTag.content && t.serverId === serverIdTag.content,
+ );
+
+ if (!tool) {
+ console.error(`Tool ${toolNameTag.content} not found on server ${serverIdTag.content}`);
+ return null;
+ }
+
+ // Map tool action to command action to avoid adding a new type
+ return {
+ type: 'action',
+ planId: planId,
+ data: {
+ type: 'command',
+ description: `Execute MCP tool: ${toolNameTag.content} on server ${serverIdTag.content}`,
+ command: 'npm install', // Reusing the command type
+ packages: [], // Empty packages array
+ },
+ } as ActionChunkType;
+ } catch (error) {
+ console.error('Error handling MCP tool action in streaming parser:', error);
+ return null;
+ }
} else {
return null;
}
diff --git a/packages/api/ai/stream-xml-parser.mts b/packages/api/ai/stream-xml-parser.mts
index 6cc896d4..a4fa73a7 100644
--- a/packages/api/ai/stream-xml-parser.mts
+++ b/packages/api/ai/stream-xml-parser.mts
@@ -12,6 +12,9 @@ export const xmlSchema: Record = {
commandType: { isContentNode: true, hasCdata: false },
package: { isContentNode: true, hasCdata: false },
planDescription: { isContentNode: true, hasCdata: true },
+ toolName: { isContentNode: true, hasCdata: false },
+ serverId: { isContentNode: true, hasCdata: false },
+ parameters: { isContentNode: true, hasCdata: true },
};
export type TagType = {
diff --git a/packages/api/apps/disk.mts b/packages/api/apps/disk.mts
index 05b2d660..a52d989e 100644
--- a/packages/api/apps/disk.mts
+++ b/packages/api/apps/disk.mts
@@ -11,6 +11,7 @@ import { FileContent } from '../ai/app-parser.mjs';
import type { Plan } from '../ai/plan-parser.mjs';
import archiver from 'archiver';
import { wss } from '../index.mjs';
+import { executeMCPTool } from './mcp-tools.mjs';
export function pathToApp(id: string) {
return Path.join(APPS_DIR, id);
@@ -52,6 +53,23 @@ export async function applyPlan(app: DBAppType, plan: Plan) {
source: item.modified,
binary: isBinary(basename),
});
+ } else if (item.type === 'tool') {
+ // Execute MCP tool
+ try {
+ console.log(`Executing MCP tool ${item.toolName} on server ${item.serverId}`);
+ const result = await executeMCPTool(item.toolName, item.serverId, item.parameters);
+ console.log(`MCP tool execution result:`, result);
+
+ // Notify clients about the tool execution
+ wss.broadcast(`app:${app.externalId}`, 'mcp:tool-executed', {
+ toolName: item.toolName,
+ serverId: item.serverId,
+ result: result,
+ });
+ } catch (error) {
+ console.error(`Error executing MCP tool ${item.toolName}:`, error);
+ // Continue with other actions even if this one fails
+ }
}
}
} catch (e) {
diff --git a/packages/api/apps/mcp-tools.mts b/packages/api/apps/mcp-tools.mts
new file mode 100644
index 00000000..8aef4fb9
--- /dev/null
+++ b/packages/api/apps/mcp-tools.mts
@@ -0,0 +1,55 @@
+/**
+ * MCP Tools for App Builder
+ *
+ * This module provides functions to execute MCP tools from the app builder.
+ */
+
+import { getMCPClientManager } from '../mcp/client-manager.mjs';
+
+/**
+ * Execute an MCP tool
+ *
+ * @param toolName The name of the tool to execute
+ * @param serverId The ID of the server hosting the tool
+ * @param parameters The parameters to pass to the tool
+ * @returns The result of the tool execution
+ */
+export async function executeMCPTool(
+ toolName: string,
+ serverId: string,
+ parametersJson: string,
+): Promise {
+ try {
+ // Parse the parameters
+ const parameters = JSON.parse(parametersJson);
+
+ // Get the MCP client manager
+ const clientManager = getMCPClientManager();
+
+ // Initialize the client manager if needed
+ if (!clientManager.isInitialized) {
+ await clientManager.initialize();
+ }
+
+ // Find the tool
+ const tools = await clientManager.getTools();
+ const tool = tools.find((t) => t.name === toolName && t.serverId === serverId);
+
+ if (!tool) {
+ throw new Error(`Tool ${toolName} not found on server ${serverId}`);
+ }
+
+ console.log(
+ `Executing MCP tool ${toolName} on server ${serverId} with parameters:`,
+ parameters,
+ );
+
+ // Call the tool
+ const result = await clientManager.callTool(serverId, toolName, parameters);
+
+ return result;
+ } catch (error) {
+ console.error(`Error executing MCP tool ${toolName}:`, error);
+ throw error;
+ }
+}
diff --git a/packages/api/dev-server.mts b/packages/api/dev-server.mts
index d95c55c1..d17f1111 100644
--- a/packages/api/dev-server.mts
+++ b/packages/api/dev-server.mts
@@ -3,9 +3,17 @@ import { WebSocketServer as WsWebSocketServer } from 'ws';
import app from './server/http.mjs';
import webSocketServer from './server/ws.mjs';
+import { initializeMCP } from './mcp/index.mjs';
export { SRCBOOK_DIR } from './constants.mjs';
+// Initialize MCP client manager
+console.log('Initializing MCP client manager...');
+initializeMCP().catch((error) => {
+ console.error('Failed to initialize MCP client manager:', error);
+ console.log('MCP functionality will be limited or unavailable.');
+});
+
const server = http.createServer(app);
const wss = new WsWebSocketServer({ server });
@@ -17,16 +25,46 @@ server.listen(port, () => {
});
process.on('SIGINT', async function () {
+ // Shutdown MCP client manager
+ try {
+ console.log('Shutting down MCP client manager...');
+ const { getMCPClientManager } = await import('./mcp/client-manager.mjs');
+ const mcpClientManager = getMCPClientManager();
+ await mcpClientManager.close();
+ } catch (error) {
+ console.error('Error shutting down MCP client manager:', error);
+ }
+
server.close();
process.exit();
});
if (import.meta.hot) {
- import.meta.hot.on('vite:beforeFullReload', () => {
+ import.meta.hot.on('vite:beforeFullReload', async () => {
+ // Shutdown MCP client manager
+ try {
+ console.log('Shutting down MCP client manager before reload...');
+ const { getMCPClientManager } = await import('./mcp/client-manager.mjs');
+ const mcpClientManager = getMCPClientManager();
+ await mcpClientManager.close();
+ } catch (error) {
+ console.error('Error shutting down MCP client manager:', error);
+ }
+
wss.close();
server.close();
});
- import.meta.hot.dispose(() => {
+ import.meta.hot.dispose(async () => {
+ // Shutdown MCP client manager
+ try {
+ console.log('Shutting down MCP client manager on dispose...');
+ const { getMCPClientManager } = await import('./mcp/client-manager.mjs');
+ const mcpClientManager = getMCPClientManager();
+ await mcpClientManager.close();
+ } catch (error) {
+ console.error('Error shutting down MCP client manager:', error);
+ }
+
wss.close();
server.close();
});
diff --git a/packages/api/index.mts b/packages/api/index.mts
index 44d25565..dd7689c9 100644
--- a/packages/api/index.mts
+++ b/packages/api/index.mts
@@ -4,3 +4,6 @@ import { SRCBOOKS_DIR } from './constants.mjs';
import { posthog } from './posthog-client.mjs';
export { app, wss, SRCBOOKS_DIR, posthog };
+
+// Export MCP functionality
+export * from './mcp/index.mjs';
diff --git a/packages/api/mcp/client-manager.mts b/packages/api/mcp/client-manager.mts
new file mode 100644
index 00000000..178ae54c
--- /dev/null
+++ b/packages/api/mcp/client-manager.mts
@@ -0,0 +1,505 @@
+/**
+ * MCP Client Manager
+ *
+ * This module manages connections to multiple MCP servers based on configuration.
+ * It focuses on tool discovery and execution, providing a unified interface for
+ * accessing tools from multiple MCP servers.
+ */
+
+import { Client } from '@modelcontextprotocol/sdk/client/index.js';
+import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
+import fs from 'node:fs/promises';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { z } from 'zod';
+import { posthog } from '../posthog-client.mjs';
+
+// Define types for MCP configuration
+export const MCPServerConfigSchema = z.object({
+ command: z.string(),
+ args: z.array(z.string()),
+ env: z.record(z.string()).optional(),
+});
+
+export const MCPConfigSchema = z.object({
+ mcpServers: z.record(MCPServerConfigSchema),
+});
+
+export type MCPServerConfig = z.infer;
+export type MCPConfig = z.infer;
+
+// Define types for MCP tools
+export interface MCPTool {
+ serverId: string;
+ name: string;
+ description?: string;
+ inputSchema: any;
+ annotations?: {
+ title?: string;
+ readOnlyHint?: boolean;
+ destructiveHint?: boolean;
+ idempotentHint?: boolean;
+ openWorldHint?: boolean;
+ };
+}
+
+/**
+ * MCP Client Manager class
+ *
+ * Manages connections to multiple MCP servers and provides a unified interface
+ * for discovering and executing tools.
+ */
+export class MCPClientManager {
+ private connections: Map = new Map();
+ private config: MCPConfig | null = null;
+ private configPath: string;
+ private tools: MCPTool[] = [];
+ private _isInitialized = false;
+
+ /**
+ * Check if the client manager is initialized
+ */
+ get isInitialized(): boolean {
+ return this._isInitialized;
+ }
+
+ /**
+ * Create a new MCP Client Manager
+ * @param configPath Path to the MCP configuration file
+ */
+ constructor(configPath: string) {
+ this.configPath = configPath;
+ }
+
+ /**
+ * Initialize the MCP Client Manager
+ * Loads the configuration and connects to all configured servers
+ */
+ async initialize(): Promise {
+ if (this._isInitialized) {
+ return;
+ }
+
+ try {
+ // Load and parse the configuration
+ await this.loadConfig();
+
+ if (!this.config) {
+ console.warn('No MCP configuration found. MCP functionality will be disabled.');
+ return;
+ }
+
+ // Connect to all configured servers
+ const serverIds = Object.keys(this.config.mcpServers);
+ console.log(`Connecting to ${serverIds.length} MCP servers...`);
+
+ for (const serverId of serverIds) {
+ try {
+ await this.connectToServer(serverId);
+ } catch (error) {
+ console.error(`Failed to connect to MCP server ${serverId}:`, error);
+ }
+ }
+
+ // Discover tools from all connected servers
+ await this.discoverAllTools();
+
+ this._isInitialized = true;
+ console.log(
+ `MCP Client Manager initialized with ${this.connections.size} servers and ${this.tools.length} tools.`,
+ );
+
+ posthog.capture({
+ event: 'mcp_client_manager_initialized',
+ properties: {
+ serverCount: this.connections.size,
+ toolCount: this.tools.length,
+ },
+ });
+ } catch (error) {
+ console.error('Failed to initialize MCP Client Manager:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Load the MCP configuration from the specified file
+ */
+ private async loadConfig(): Promise {
+ try {
+ const configData = await fs.readFile(this.configPath, 'utf-8');
+ const parsedConfig = JSON.parse(configData);
+
+ // Validate the configuration
+ const result = MCPConfigSchema.safeParse(parsedConfig);
+
+ if (!result.success) {
+ console.error('Invalid MCP configuration:', result.error);
+ return;
+ }
+
+ this.config = result.data;
+ } catch (error) {
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
+ console.warn(`MCP configuration file not found at ${this.configPath}`);
+ return;
+ }
+
+ console.error('Error loading MCP configuration:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Connect to a specific MCP server
+ * @param serverId The ID of the server to connect to
+ */
+ private async connectToServer(serverId: string): Promise {
+ if (!this.config) {
+ return null;
+ }
+
+ const serverConfig = this.config.mcpServers[serverId];
+ if (!serverConfig) {
+ console.warn(`No configuration found for MCP server ${serverId}`);
+ return null;
+ }
+
+ try {
+ // Create a new client
+ const client = new Client(
+ { name: 'srcbook-mcp-client', version: '1.0.0' },
+ { capabilities: { tools: {} } },
+ );
+
+ // Set up environment variables for the server process
+ // Convert env to Record by filtering out undefined values
+ const envVars: Record = {};
+ if (serverConfig.env) {
+ Object.entries(serverConfig.env).forEach(([key, value]) => {
+ if (value !== undefined) {
+ envVars[key] = value;
+ }
+ });
+ }
+
+ // Add process.env values, filtering out undefined
+ if (process.env) {
+ Object.entries(process.env).forEach(([key, value]) => {
+ if (value !== undefined) {
+ envVars[key] = value;
+ }
+ });
+ }
+
+ // Create a transport
+ const transport = new StdioClientTransport({
+ command: serverConfig.command,
+ args: serverConfig.args,
+ env: envVars,
+ });
+
+ // Connect to the server
+ console.log(`Connecting to MCP server ${serverId}...`);
+ await client.connect(transport);
+ console.log(`Connected to MCP server ${serverId}`);
+
+ // Store the connection
+ this.connections.set(serverId, client);
+
+ posthog.capture({
+ event: 'mcp_server_connected',
+ properties: { serverId },
+ });
+
+ return client;
+ } catch (error) {
+ console.error(`Error connecting to MCP server ${serverId}:`, error);
+ return null;
+ }
+ }
+
+ /**
+ * Discover tools from all connected servers
+ */
+ private async discoverAllTools(): Promise {
+ this.tools = [];
+
+ for (const [serverId, client] of this.connections.entries()) {
+ try {
+ // @ts-ignore - TypeScript doesn't recognize the listTools method
+ const toolsResult = await client.listTools();
+
+ // Map the tools to our internal format
+ const serverTools = toolsResult.tools.map((tool) => {
+ // Create a properly typed MCPTool object
+ const mcpTool: MCPTool = {
+ serverId,
+ name: tool.name,
+ description: tool.description,
+ inputSchema: tool.inputSchema,
+ // Handle annotations with proper typing
+ annotations: tool.annotations as MCPTool['annotations'],
+ };
+ return mcpTool;
+ });
+
+ this.tools.push(...serverTools);
+ console.log(`Discovered ${serverTools.length} tools from server ${serverId}`);
+ } catch (error) {
+ console.error(`Error discovering tools from server ${serverId}:`, error);
+ }
+ }
+ }
+
+ /**
+ * Get all available tools from all connected servers
+ * @returns Array of available tools
+ */
+ async getTools(): Promise {
+ if (!this._isInitialized) {
+ await this.initialize();
+ }
+
+ return this.tools;
+ }
+
+ /**
+ * Call a tool on a specific server
+ * @param serverId The ID of the server to call the tool on
+ * @param toolName The name of the tool to call
+ * @param args The arguments to pass to the tool
+ * @returns The result of the tool call
+ */
+ async callTool(serverId: string, toolName: string, args: any): Promise {
+ if (!this._isInitialized) {
+ await this.initialize();
+ }
+
+ const client = this.connections.get(serverId);
+ if (!client) {
+ throw new Error(`No connection to MCP server ${serverId}`);
+ }
+
+ // Find the tool definition
+ const tool = this.tools.find((t) => t.serverId === serverId && t.name === toolName);
+ if (!tool) {
+ throw new Error(`Tool ${toolName} not found on server ${serverId}`);
+ }
+
+ try {
+ // Validate the arguments against the tool's input schema
+ const validatedArgs = this.validateToolArgs(tool, args);
+
+ console.log(
+ `Calling tool ${toolName} on server ${serverId} with validated args:`,
+ validatedArgs,
+ );
+
+ posthog.capture({
+ event: 'mcp_tool_called',
+ properties: { serverId, toolName },
+ });
+
+ // @ts-ignore - TypeScript doesn't recognize the callTool method signature correctly
+ const result = await client.callTool(toolName, validatedArgs);
+
+ // Safely handle the result
+ if (result && typeof result === 'object' && 'isError' in result && result.isError) {
+ // Safely access content if it exists
+ const errorMessage =
+ result.content &&
+ Array.isArray(result.content) &&
+ result.content.length > 0 &&
+ result.content[0] &&
+ typeof result.content[0] === 'object' &&
+ 'text' in result.content[0]
+ ? result.content[0].text
+ : 'Unknown error';
+
+ console.error(`Tool ${toolName} returned an error:`, errorMessage);
+ throw new Error(`Tool error: ${errorMessage}`);
+ }
+
+ return result;
+ } catch (error) {
+ console.error(`Error calling tool ${toolName} on server ${serverId}:`, error);
+ throw error;
+ }
+ }
+
+ /**
+ * Find a tool by name across all servers
+ * @param toolName The name of the tool to find
+ * @returns The tool if found, null otherwise
+ */
+ /**
+ * Create a Zod schema from a JSON Schema object
+ * @param schema JSON Schema object
+ * @returns Zod schema
+ */
+ private createZodSchema(schema: any): z.ZodTypeAny {
+ // Handle null or undefined schema
+ if (!schema) {
+ return z.any();
+ }
+
+ const type = schema.type;
+
+ // Handle different types
+ if (type === 'string') {
+ let stringSchema = z.string();
+
+ // Add pattern validation if specified
+ if (schema.pattern) {
+ stringSchema = stringSchema.regex(new RegExp(schema.pattern));
+ }
+
+ // Add min/max length validation if specified
+ if (schema.minLength !== undefined) {
+ stringSchema = stringSchema.min(schema.minLength);
+ }
+ if (schema.maxLength !== undefined) {
+ stringSchema = stringSchema.max(schema.maxLength);
+ }
+
+ // Handle enum values
+ if (schema.enum && Array.isArray(schema.enum)) {
+ return z.enum(schema.enum as [string, ...string[]]);
+ }
+
+ return stringSchema;
+ } else if (type === 'number' || type === 'integer') {
+ let numberSchema = type === 'integer' ? z.number().int() : z.number();
+
+ // Add min/max validation if specified
+ if (schema.minimum !== undefined) {
+ numberSchema = numberSchema.min(schema.minimum);
+ }
+ if (schema.maximum !== undefined) {
+ numberSchema = numberSchema.max(schema.maximum);
+ }
+
+ return numberSchema;
+ } else if (type === 'boolean') {
+ return z.boolean();
+ } else if (type === 'null') {
+ return z.null();
+ } else if (type === 'array') {
+ const items = schema.items || {};
+ return z.array(this.createZodSchema(items));
+ } else if (type === 'object') {
+ const properties = schema.properties || {};
+ const shape: Record = {};
+
+ // Create schemas for all properties
+ for (const [key, value] of Object.entries(properties)) {
+ shape[key] = this.createZodSchema(value as any);
+ }
+
+ let objectSchema = z.object(shape);
+
+ // Handle required properties
+ if (schema.required && Array.isArray(schema.required)) {
+ const requiredShape: Record = {};
+
+ for (const key of Object.keys(shape)) {
+ const isRequired = schema.required.includes(key);
+ const zodType = shape[key];
+ if (zodType) {
+ requiredShape[key] = isRequired ? zodType : zodType.optional();
+ }
+ }
+
+ objectSchema = z.object(requiredShape);
+ } else {
+ // If no required properties specified, make all properties optional
+ const optionalShape: Record = {};
+
+ for (const [key, value] of Object.entries(shape)) {
+ if (value) {
+ optionalShape[key] = value.optional();
+ }
+ }
+
+ objectSchema = z.object(optionalShape);
+ }
+
+ return objectSchema;
+ }
+
+ // Default to any for unsupported types
+ return z.any();
+ }
+
+ /**
+ * Validate tool arguments against the tool's input schema
+ * @param tool The tool to validate arguments for
+ * @param args The arguments to validate
+ * @returns Validated arguments
+ */
+ private validateToolArgs(tool: MCPTool, args: any): any {
+ try {
+ // Create a Zod schema from the tool's input schema
+ const schema = this.createZodSchema(tool.inputSchema);
+
+ // Validate the arguments against the schema
+ return schema.parse(args);
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ // Format the validation errors
+ const formattedErrors = error.errors
+ .map((err) => {
+ return `${err.path.join('.')}: ${err.message}`;
+ })
+ .join(', ');
+
+ throw new Error(`Invalid arguments for tool ${tool.name}: ${formattedErrors}`);
+ }
+
+ throw error;
+ }
+ }
+
+ findTool(toolName: string): MCPTool | null {
+ return this.tools.find((tool) => tool.name === toolName) || null;
+ }
+
+ /**
+ * Close all connections to MCP servers
+ */
+ async close(): Promise {
+ for (const [serverId, client] of this.connections.entries()) {
+ try {
+ await client.close();
+ console.log(`Closed connection to MCP server ${serverId}`);
+ } catch (error) {
+ console.error(`Error closing connection to MCP server ${serverId}:`, error);
+ }
+ }
+
+ this.connections.clear();
+ this._isInitialized = false;
+ }
+}
+
+// Create a singleton instance of the MCP Client Manager
+let clientManagerInstance: MCPClientManager | null = null;
+
+/**
+ * Get the singleton instance of the MCP Client Manager
+ * @param configPath Optional path to the MCP configuration file
+ * @returns The MCP Client Manager instance
+ */
+export function getMCPClientManager(configPath?: string): MCPClientManager {
+ if (!clientManagerInstance) {
+ // Use a relative path from the current file to the config file
+ const currentFilePath = fileURLToPath(import.meta.url);
+ const currentDir = path.dirname(currentFilePath);
+ const defaultConfigPath = path.resolve(currentDir, '../srcbook_mcp_config.json');
+ console.log(`Using MCP config path: ${defaultConfigPath}`);
+ clientManagerInstance = new MCPClientManager(configPath || defaultConfigPath);
+ }
+
+ return clientManagerInstance;
+}
diff --git a/packages/api/mcp/index.mts b/packages/api/mcp/index.mts
new file mode 100644
index 00000000..280a1f17
--- /dev/null
+++ b/packages/api/mcp/index.mts
@@ -0,0 +1,21 @@
+/**
+ * MCP (Model Context Protocol) Integration
+ *
+ * This module exports the MCP client manager and related utilities for
+ * integrating with MCP servers.
+ */
+
+export {
+ MCPClientManager,
+ getMCPClientManager,
+ type MCPTool,
+ type MCPConfig,
+ type MCPServerConfig,
+} from './client-manager.mjs';
+
+// Export a function to initialize the MCP client manager
+export async function initializeMCP(): Promise {
+ const { getMCPClientManager } = await import('./client-manager.mjs');
+ const clientManager = getMCPClientManager();
+ await clientManager.initialize();
+}
diff --git a/packages/api/mcp/server/README.md b/packages/api/mcp/server/README.md
new file mode 100644
index 00000000..ff413d15
--- /dev/null
+++ b/packages/api/mcp/server/README.md
@@ -0,0 +1,71 @@
+# Srcbook MCP Server
+
+This directory contains the implementation of Srcbook's MCP server functionality, which allows Srcbook to expose its generative AI capabilities to other MCP clients.
+
+## Overview
+
+The MCP server implementation follows the [Model Context Protocol](https://modelcontextprotocol.io/) specification and uses the official TypeScript SDK. It exposes Srcbook's web app generation functionality as MCP tools that can be called by other MCP clients.
+
+## Architecture
+
+The server implementation consists of:
+
+1. **Server Manager**: Manages the MCP server instance, handles connections, and registers tools.
+2. **Tools**: Implementations of MCP tools that wrap Srcbook's generative AI functionality.
+3. **Server Entry Point**: The main entry point for the MCP server.
+
+## Configuration
+
+The MCP server is configured in the `srcbook_mcp_config.json` file, which includes:
+
+```json
+{
+ "mcpServer": {
+ "enabled": true,
+ "allowRemoteConnections": false,
+ "port": 2151,
+ "authToken": "your-auth-token-for-remote-connections"
+ }
+}
+```
+
+## Available Tools
+
+### generate-app
+
+Generates a web application based on a natural language description.
+
+**Input Schema:**
+```json
+{
+ "query": "string",
+ "projectName": "string (optional)"
+}
+```
+
+**Example:**
+```json
+{
+ "query": "Create a simple todo list app with React",
+ "projectName": "todo-app"
+}
+```
+
+## Usage
+
+The MCP server is automatically started when Srcbook is launched in "Janus mode" (both client and server). It can be accessed by other MCP clients using either stdio or SSE transport.
+
+## Security
+
+The MCP server implementation includes:
+- Input validation using Zod schemas
+- Proper error handling
+- Authentication for remote connections (when enabled)
+- Secure handling of file paths and system commands
+
+## Future Enhancements
+
+- Add more tools for other Srcbook capabilities
+- Implement resource templates for accessing project files
+- Add support for prompts
+- Implement streaming responses for long-running operations
diff --git a/packages/api/mcp/server/index.mts b/packages/api/mcp/server/index.mts
new file mode 100644
index 00000000..f382c4c1
--- /dev/null
+++ b/packages/api/mcp/server/index.mts
@@ -0,0 +1,40 @@
+/**
+ * MCP Server Integration
+ *
+ * This module exports the MCP server manager and related utilities for
+ * serving Srcbook's capabilities to MCP clients.
+ */
+
+export {
+ MCPServerManager,
+ getMCPServerManager,
+ type MCPServerSettings,
+} from './server-manager.mjs';
+
+// Export a function to initialize the MCP server manager
+export async function initializeMCPServer(): Promise {
+ const { getMCPServerManager } = await import('./server-manager.mjs');
+ const serverManager = getMCPServerManager();
+ await serverManager.initialize();
+}
+
+// Export a function to start the MCP server with stdio transport
+export async function startMCPServerWithStdio(): Promise {
+ const { getMCPServerManager } = await import('./server-manager.mjs');
+ const serverManager = getMCPServerManager();
+ await serverManager.startWithStdio();
+}
+
+// Export a function to start the MCP server with SSE transport
+export async function startMCPServerWithSSE(port: number, expressApp: any): Promise {
+ const { getMCPServerManager } = await import('./server-manager.mjs');
+ const serverManager = getMCPServerManager();
+ await serverManager.startWithSSE(port, expressApp);
+}
+
+// Export a function to stop the MCP server
+export async function stopMCPServer(): Promise {
+ const { getMCPServerManager } = await import('./server-manager.mjs');
+ const serverManager = getMCPServerManager();
+ await serverManager.stop();
+}
diff --git a/packages/api/mcp/server/server-manager.mts b/packages/api/mcp/server/server-manager.mts
new file mode 100644
index 00000000..4cf0afd1
--- /dev/null
+++ b/packages/api/mcp/server/server-manager.mts
@@ -0,0 +1,316 @@
+/**
+ * MCP Server Manager
+ *
+ * This module manages MCP server instances, handling initialization,
+ * tool registration, and connection management.
+ */
+
+import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
+import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
+import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
+import { z } from 'zod';
+import { posthog } from '../../posthog-client.mjs';
+import fs from 'node:fs/promises';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+// Import tools
+import { registerGenerateAppTool } from './tools/generate-app.mjs';
+import { registerUIVisualizeTool } from './tools/ui-visualize.mjs';
+
+// Define types for MCP server configuration
+export const MCPServerSettingsSchema = z.object({
+ enabled: z.boolean().default(true),
+ port: z.number().optional(),
+ allowRemoteConnections: z.boolean().default(false),
+ authToken: z.string().optional(),
+});
+
+export type MCPServerSettings = z.infer;
+
+/**
+ * MCP Server Manager class
+ *
+ * Manages MCP server instances and handles connections.
+ */
+export class MCPServerManager {
+ private server: McpServer | null = null;
+ private settings: MCPServerSettings;
+ private configPath: string;
+ private _isInitialized = false;
+ private _isRunning = false;
+
+ /**
+ * Check if the server manager is initialized
+ */
+ get isInitialized(): boolean {
+ return this._isInitialized;
+ }
+
+ /**
+ * Check if the server is running
+ */
+ get isRunning(): boolean {
+ return this._isRunning;
+ }
+
+ /**
+ * Create a new MCP Server Manager
+ * @param configPath Path to the MCP configuration file
+ */
+ constructor(configPath: string) {
+ this.configPath = configPath;
+ this.settings = {
+ enabled: true,
+ allowRemoteConnections: false,
+ };
+ }
+
+ /**
+ * Initialize the MCP Server Manager
+ * Loads the configuration and sets up the server
+ */
+ async initialize(): Promise {
+ if (this._isInitialized) {
+ return;
+ }
+
+ try {
+ // Load and parse the configuration
+ await this.loadConfig();
+
+ if (!this.settings.enabled) {
+ console.log('MCP server is disabled in configuration. Server will not start.');
+ return;
+ }
+
+ // Create the MCP server
+ this.server = new McpServer({
+ name: 'srcbook-mcp-server',
+ version: '1.0.0',
+ }, {
+ capabilities: {
+ tools: {}
+ }
+ });
+
+ // Register tools
+ this.registerTools();
+
+ this._isInitialized = true;
+ console.log('MCP Server Manager initialized.');
+
+ posthog.capture({
+ event: 'mcp_server_manager_initialized',
+ });
+ } catch (error) {
+ console.error('Failed to initialize MCP Server Manager:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Load the MCP server configuration from the specified file
+ */
+ private async loadConfig(): Promise {
+ try {
+ const configData = await fs.readFile(this.configPath, 'utf-8');
+ const parsedConfig = JSON.parse(configData);
+
+ // Check if server settings exist in the config
+ if (parsedConfig.mcpServer) {
+ // Validate the configuration
+ const result = MCPServerSettingsSchema.safeParse(parsedConfig.mcpServer);
+
+ if (result.success) {
+ this.settings = result.data;
+ } else {
+ console.error('Invalid MCP server configuration:', result.error);
+ // Use default settings
+ }
+ } else {
+ console.log('No MCP server configuration found. Using default settings.');
+ }
+ } catch (error) {
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
+ console.warn(`MCP configuration file not found at ${this.configPath}`);
+ return;
+ }
+
+ console.error('Error loading MCP server configuration:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Register tools with the MCP server
+ */
+ private registerTools(): void {
+ if (!this.server) {
+ throw new Error('Cannot register tools: MCP server not initialized');
+ }
+
+ // Register available tools
+ registerGenerateAppTool(this.server);
+ registerUIVisualizeTool(this.server);
+
+ console.log('Registered MCP server tools.');
+ }
+
+ /**
+ * Start the MCP server with stdio transport
+ * This is used for local connections
+ */
+ async startWithStdio(): Promise {
+ if (!this._isInitialized) {
+ await this.initialize();
+ }
+
+ if (!this.server) {
+ throw new Error('Cannot start server: MCP server not initialized');
+ }
+
+ if (this._isRunning) {
+ console.log('MCP server is already running.');
+ return;
+ }
+
+ try {
+ // Create a stdio transport
+ const transport = new StdioServerTransport();
+
+ // Connect the server to the transport
+ await this.server.connect(transport);
+
+ this._isRunning = true;
+ console.log('MCP server started with stdio transport.');
+
+ posthog.capture({
+ event: 'mcp_server_started',
+ properties: { transport: 'stdio' },
+ });
+ } catch (error) {
+ console.error('Failed to start MCP server with stdio transport:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Start the MCP server with SSE transport
+ * This is used for remote connections
+ * @param port The port to listen on
+ * @param expressApp The Express app to use
+ */
+ async startWithSSE(port: number, expressApp: any): Promise {
+ if (!this._isInitialized) {
+ await this.initialize();
+ }
+
+ if (!this.server) {
+ throw new Error('Cannot start server: MCP server not initialized');
+ }
+
+ if (!this.settings.allowRemoteConnections) {
+ console.warn('Remote connections are disabled in configuration. SSE transport will not start.');
+ return;
+ }
+
+ if (this._isRunning) {
+ console.log('MCP server is already running.');
+ return;
+ }
+
+ try {
+ // Set up SSE endpoint
+ expressApp.get('/mcp/sse', async (req: any, res: any) => {
+ // Check authentication if required
+ if (this.settings.authToken && req.headers.authorization !== `Bearer ${this.settings.authToken}`) {
+ res.status(401).send('Unauthorized');
+ return;
+ }
+
+ // Create a transport
+ const transport = new SSEServerTransport('/mcp/messages', res);
+
+ // Connect the server to the transport
+ await this.server!.connect(transport);
+
+ // Store the transport instance for later use
+ expressApp.locals.mcpTransport = transport;
+ });
+
+ // Set up message endpoint
+ expressApp.post('/mcp/messages', async (req: any, res: any) => {
+ // Check authentication if required
+ if (this.settings.authToken && req.headers.authorization !== `Bearer ${this.settings.authToken}`) {
+ res.status(401).send('Unauthorized');
+ return;
+ }
+
+ const transport = expressApp.locals.mcpTransport;
+ if (!transport) {
+ res.status(400).send('No active MCP connection');
+ return;
+ }
+
+ await transport.handlePostMessage(req, res);
+ });
+
+ this._isRunning = true;
+ console.log(`MCP server started with SSE transport on port ${port}.`);
+
+ posthog.capture({
+ event: 'mcp_server_started',
+ properties: { transport: 'sse', port },
+ });
+ } catch (error) {
+ console.error('Failed to start MCP server with SSE transport:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Stop the MCP server
+ */
+ async stop(): Promise {
+ if (!this._isRunning || !this.server) {
+ return;
+ }
+
+ try {
+ // Close the server
+ await this.server.close();
+
+ this._isRunning = false;
+ console.log('MCP server stopped.');
+
+ posthog.capture({
+ event: 'mcp_server_stopped',
+ });
+ } catch (error) {
+ console.error('Failed to stop MCP server:', error);
+ throw error;
+ }
+ }
+}
+
+// Create a singleton instance of the MCP Server Manager
+let serverManagerInstance: MCPServerManager | null = null;
+
+/**
+ * Get the singleton instance of the MCP Server Manager
+ * @param configPath Optional path to the MCP configuration file
+ * @returns The MCP Server Manager instance
+ */
+export function getMCPServerManager(configPath?: string): MCPServerManager {
+ if (!serverManagerInstance) {
+ // Use a relative path from the current file to the config file
+ const currentFilePath = fileURLToPath(import.meta.url);
+ const currentDir = path.dirname(currentFilePath);
+ const defaultConfigPath = path.resolve(currentDir, '../../srcbook_mcp_config.json');
+ console.log(`Using MCP config path for server: ${defaultConfigPath}`);
+ serverManagerInstance = new MCPServerManager(configPath || defaultConfigPath);
+ }
+
+ return serverManagerInstance;
+}
diff --git a/packages/api/mcp/server/test-client.mts b/packages/api/mcp/server/test-client.mts
new file mode 100644
index 00000000..a7bd75c5
--- /dev/null
+++ b/packages/api/mcp/server/test-client.mts
@@ -0,0 +1,69 @@
+#!/usr/bin/env node
+
+/**
+ * Test client for the Srcbook MCP server
+ *
+ * This script connects to the Srcbook MCP server and calls the generate-app tool.
+ * It's useful for testing the server functionality.
+ *
+ * Usage:
+ * npx tsx packages/api/mcp/server/test-client.mts
+ */
+
+import { Client } from '@modelcontextprotocol/sdk/client/index.js';
+import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+async function main() {
+ console.log('Starting MCP test client...');
+
+ try {
+ // Create a new client
+ const client = new Client(
+ { name: 'srcbook-mcp-test-client', version: '1.0.0' },
+ { capabilities: { tools: {} } }
+ );
+
+ // Get the path to the server script
+ const currentFilePath = fileURLToPath(import.meta.url);
+ const currentDir = path.dirname(currentFilePath);
+ const serverScript = path.resolve(currentDir, '../../../dist/mcp/server/test-server.mjs');
+
+ console.log(`Connecting to MCP server at: ${serverScript}`);
+
+ // Create a transport
+ const transport = new StdioClientTransport({
+ command: 'node',
+ args: [serverScript],
+ });
+
+ // Connect to the server
+ await client.connect(transport);
+ console.log('Connected to MCP server');
+
+ // List available tools
+ // @ts-ignore - TypeScript doesn't recognize the listTools method
+ const toolsResult = await client.listTools();
+ console.log('Available tools:', toolsResult.tools.map(t => t.name));
+
+ // Call the generate-app tool
+ console.log('Calling generate-app tool...');
+ // @ts-ignore - TypeScript doesn't recognize the callTool method signature correctly
+ const result = await client.callTool('generate-app', {
+ query: 'Create a simple counter app with React',
+ projectName: 'counter-app',
+ });
+
+ console.log('Result:', JSON.stringify(result, null, 2));
+
+ // Close the connection
+ await client.close();
+ console.log('Connection closed');
+ } catch (error) {
+ console.error('Error:', error);
+ process.exit(1);
+ }
+}
+
+main().catch(console.error);
diff --git a/packages/api/mcp/server/test-server.mts b/packages/api/mcp/server/test-server.mts
new file mode 100644
index 00000000..5a056a67
--- /dev/null
+++ b/packages/api/mcp/server/test-server.mts
@@ -0,0 +1,40 @@
+#!/usr/bin/env node
+
+/**
+ * Test server for the Srcbook MCP server
+ *
+ * This script starts the Srcbook MCP server in standalone mode for testing.
+ *
+ * Usage:
+ * npx tsx packages/api/mcp/server/test-server.mts
+ */
+
+import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
+import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
+import { registerGenerateAppTool } from './tools/generate-app.mjs';
+
+async function main() {
+ try {
+ // Create an MCP server instance
+ const server = new McpServer({
+ name: 'srcbook-mcp-test-server',
+ version: '1.0.0',
+ });
+
+ // Register the generate app tool
+ registerGenerateAppTool(server);
+
+ // Create a transport (stdio for this example)
+ const transport = new StdioServerTransport();
+
+ // Connect the server to the transport
+ await server.connect(transport);
+
+ console.error('MCP server started with stdio transport');
+ } catch (error) {
+ console.error('Error starting MCP server:', error);
+ process.exit(1);
+ }
+}
+
+main().catch(console.error);
diff --git a/packages/api/mcp/server/tools/generate-app.mts b/packages/api/mcp/server/tools/generate-app.mts
new file mode 100644
index 00000000..52e473e5
--- /dev/null
+++ b/packages/api/mcp/server/tools/generate-app.mts
@@ -0,0 +1,242 @@
+/**
+ * Generate App Tool
+ *
+ * This module implements an MCP tool that wraps Srcbook's app generation functionality.
+ */
+
+import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
+import { generateApp } from '../../../ai/generate.mjs';
+import { randomid } from '@srcbook/shared';
+import { posthog } from '../../../posthog-client.mjs';
+import { z } from 'zod';
+
+/**
+ * Register the generate app tool with the MCP server
+ * @param server The MCP server instance
+ */
+export function registerGenerateAppTool(server: McpServer): void {
+ server.tool(
+ 'generate-app',
+ 'Generate a web application from a natural language description',
+ {
+ query: z.string().describe('Natural language description of the app to generate'),
+ projectName: z.string().optional().describe('Optional project name'),
+ componentType: z.enum(['react', 'vue', 'angular', 'html']).optional()
+ .describe('Framework for component generation'),
+ styleSystem: z.enum(['tailwind', 'css', 'sass', 'styled-components']).optional()
+ .describe('Styling system to use')
+ },
+ async ({ query, projectName, componentType = 'react', styleSystem = 'tailwind' }) => {
+ try {
+ console.log(`Executing generate-app tool with query: ${query}`);
+
+ // Send initial progress notification
+ sendProgress('Initializing project', 10);
+
+ // Generate a project ID if not provided
+ const projectId = projectName ? projectName.replace(/[^a-zA-Z0-9-_]/g, '-') : `app-${randomid()}`;
+
+ // Create an empty project structure with the required filename property
+ const files = [
+ {
+ filename: 'index.html',
+ content: '\n\n\n New App\n\n\n \n\n',
+ },
+ ];
+
+ // Send progress update
+ sendProgress('Generating components', 30);
+
+ // Generate the app
+ const result = await generateApp(projectId, files, query);
+
+ // Send progress update
+ sendProgress('Processing results', 70);
+
+ // Parse the result to extract file content
+ const generatedFiles = parseGeneratedFiles(result);
+
+ // Log the generation
+ posthog.capture({
+ event: 'mcp_generate_app',
+ properties: { query, componentType, styleSystem },
+ });
+
+ // Send final progress notification
+ sendProgress('Completed', 100);
+
+ // Return the result in standard MCP format
+ return {
+ content: [
+ {
+ type: 'text',
+ text: `Successfully generated app with project ID: ${projectId}`
+ },
+ ...generatedFiles.map(file => {
+ const fileType = determineFileType(file.filename);
+ // For image files, return image content
+ if (fileType === 'resource' && file.filename.match(/\.(png|jpg|jpeg|gif|svg)$/i)) {
+ return {
+ type: 'image' as const,
+ data: Buffer.from(file.content).toString('base64'),
+ mimeType: `image/${file.filename.split('.').pop()?.toLowerCase() || 'png'}`
+ };
+ }
+
+ // For other files return as resource content
+ return {
+ type: 'resource' as const,
+ resource: {
+ uri: `file://${file.filename}`,
+ mimeType: getMimeType(file.filename),
+ text: file.content
+ }
+ };
+ })
+ ]
+ };
+ } catch (error) {
+ console.error('Error generating app:', error);
+
+ // Return an error response
+ return {
+ content: [
+ {
+ type: 'text',
+ text: `Error generating app: ${(error as Error).message}`,
+ },
+ ],
+ isError: true,
+ };
+ }
+ }
+ );
+
+ // Set up a helper function to send progress notifications
+ // This uses the underlying Server instance from the McpServer
+ const sendProgress = (stage: string, percentComplete: number) => {
+ server.server.notification({
+ method: 'notifications/progress',
+ params: {
+ progressToken: 'generate-app', // Should match token from request
+ progress: percentComplete,
+ total: 100,
+ message: stage
+ }
+ });
+ };
+
+ // Example usage during long operations:
+ // sendProgress('Initializing', 10);
+ // sendProgress('Generating UI components', 50);
+ // sendProgress('Finalizing', 90);
+
+ console.log('Registered generate-app tool');
+}
+
+/**
+ * Parse the generated files from the result text
+ * @param resultText The result text from the app generation
+ * @returns An array of file objects
+ */
+function parseGeneratedFiles(resultText: string): Array<{ filename: string; content: string }> {
+ const files: Array<{ filename: string; content: string }> = [];
+
+ // Look for file blocks in the format:
+ // ```filename.ext
+ // content
+ // ```
+ const fileBlockRegex = /```(?:[\w-]+\s+)?([^\n]+)\n([\s\S]*?)```/g;
+
+ let match: RegExpExecArray | null;
+ while ((match = fileBlockRegex.exec(resultText)) !== null) {
+ if (match[1] && match[2]) {
+ const filename = match[1].trim();
+ const content = match[2];
+
+ // Skip if the filename is empty or doesn't look like a valid path
+ if (!filename || filename.includes('...')) {
+ continue;
+ }
+
+ files.push({
+ filename,
+ content,
+ });
+ }
+ }
+
+ return files;
+}
+
+/**
+ * Determine the file type based on filename and content
+ * @param filename The filename
+ * @returns The file type (component, resource, config, etc.)
+ */
+function determineFileType(filename: string): string {
+ const ext = filename.split('.').pop()?.toLowerCase();
+
+ if (['jsx', 'tsx'].includes(ext || '')) {
+ return 'component';
+ } else if (['json', 'config.js', 'yml', 'yaml'].some(suffix => filename.endsWith(suffix))) {
+ return 'config';
+ } else if (['css', 'scss', 'less', 'svg', 'png', 'jpg', 'jpeg', 'gif'].includes(ext || '')) {
+ return 'resource';
+ } else if (['js', 'ts'].includes(ext || '')) {
+ return 'script';
+ } else if (['html', 'htm'].includes(ext || '')) {
+ return 'document';
+ } else {
+ return 'file';
+ }
+}
+
+/**
+ * Get MIME type based on file extension
+ * @param filename The filename
+ * @returns The MIME type
+ */
+function getMimeType(filename: string): string {
+ const ext = filename.split('.').pop()?.toLowerCase();
+
+ const mimeTypes: Record = {
+ // Text formats
+ 'txt': 'text/plain',
+ 'html': 'text/html',
+ 'htm': 'text/html',
+ 'css': 'text/css',
+ 'csv': 'text/csv',
+ 'md': 'text/markdown',
+
+ // JavaScript/TypeScript
+ 'js': 'application/javascript',
+ 'jsx': 'application/javascript',
+ 'ts': 'application/typescript',
+ 'tsx': 'application/typescript',
+
+ // JSON/config
+ 'json': 'application/json',
+ 'xml': 'application/xml',
+ 'yaml': 'application/yaml',
+ 'yml': 'application/yaml',
+
+ // Images
+ 'png': 'image/png',
+ 'jpg': 'image/jpeg',
+ 'jpeg': 'image/jpeg',
+ 'gif': 'image/gif',
+ 'svg': 'image/svg+xml',
+ 'ico': 'image/x-icon',
+
+ // Fonts
+ 'woff': 'font/woff',
+ 'woff2': 'font/woff2',
+ 'ttf': 'font/ttf',
+
+ // Other
+ 'pdf': 'application/pdf'
+ };
+
+ return mimeTypes[ext || ''] || 'text/plain';
+}
\ No newline at end of file
diff --git a/packages/api/mcp/server/tools/ui-visualize.mts b/packages/api/mcp/server/tools/ui-visualize.mts
new file mode 100644
index 00000000..1a48ca9f
--- /dev/null
+++ b/packages/api/mcp/server/tools/ui-visualize.mts
@@ -0,0 +1,292 @@
+/**
+ * UI Visualization Tool
+ *
+ * This module implements an MCP tool that provides UI visualization descriptions
+ * from code components to enhance the Cursor integration experience.
+ */
+
+import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
+// Note: For progress reporting, we would need to use the underlying server instance's
+// notification capabilities since progressNotification is not available in the SDK
+import { z } from 'zod';
+import { posthog } from '../../../posthog-client.mjs';
+
+/**
+ * Register the UI visualization tool with the MCP server
+ * @param server The MCP server instance
+ */
+export function registerUIVisualizeTool(server: McpServer): void {
+ server.tool(
+ 'ui-visualize',
+ 'Generate a visual description of UI components for better integration with design tools',
+ {
+ code: z.string().describe('The component code to visualize'),
+ componentType: z.enum(['react', 'vue', 'angular', 'html']).optional()
+ .describe('Framework for the component'),
+ viewportSize: z.object({
+ width: z.number().optional(),
+ height: z.number().optional()
+ }).optional().describe('Target viewport dimensions')
+ },
+ async ({ code, componentType = 'react', viewportSize = { width: 1280, height: 800 } }) => {
+ try {
+ console.log(`Executing ui-visualize tool for ${componentType} component`);
+
+ // Send initial progress notification
+ sendProgress('Analyzing component', 10);
+
+ // In a real implementation, this would call an LLM or specialized visualization service
+ // For now, we'll build a basic description based on the code parsing
+ sendProgress('Parsing code elements', 30);
+ const visualDescription = generateComponentVisualization(code, componentType, viewportSize);
+
+ sendProgress('Generating description', 80);
+
+ // Log the visualization request
+ posthog.capture({
+ event: 'mcp_ui_visualize',
+ properties: { componentType },
+ });
+
+ // Send final progress notification
+ sendProgress('Completed', 100);
+
+ // Parse the visualization info
+ const visualInfo = parseVisualization(visualDescription);
+
+ // Return the visualization in standard MCP format
+ return {
+ content: [
+ {
+ type: 'text',
+ text: 'Component analysis complete'
+ },
+ {
+ type: 'resource',
+ resource: {
+ uri: `visualization://${componentType}`,
+ mimeType: 'application/json',
+ text: JSON.stringify({
+ componentType,
+ viewport: viewportSize,
+ elements: visualInfo.elements,
+ patterns: visualInfo.patterns,
+ description: visualInfo.description
+ }, null, 2)
+ }
+ }
+ ]
+ };
+ } catch (error) {
+ console.error('Error visualizing UI component:', error);
+
+ // Return an error response
+ return {
+ content: [
+ {
+ type: 'text',
+ text: `Error visualizing UI component: ${(error as Error).message}`,
+ },
+ ],
+ isError: true,
+ };
+ }
+ }
+ );
+
+ // Set up a helper function to send progress notifications
+ const sendProgress = (stage: string, percentComplete: number) => {
+ server.server.notification({
+ method: 'notifications/progress',
+ params: {
+ progressToken: 'ui-visualize', // Should match token from request
+ progress: percentComplete,
+ total: 100,
+ message: stage
+ }
+ });
+ };
+
+ console.log('Registered ui-visualize tool');
+}
+
+/**
+ * Helper function to parse visualization results into structured data
+ */
+function parseVisualization(visualDescription: string): {
+ elements: Array<{ type: string; count: number }>;
+ patterns: Array<{ type: string; present: boolean }>;
+ description: string;
+} {
+ // Simple parsing of the visualization text to extract structured data
+ // In a real implementation, this would be more robust
+ const elements: Array<{ type: string; count: number }> = [];
+ const patterns: Array<{ type: string; present: boolean }> = [];
+
+ // This is a placeholder implementation - in reality, this would parse
+ // the actual visualization description more thoroughly
+ const elementRegex = /(\w+)\s+\((\d+)\)/g;
+ let match;
+ while ((match = elementRegex.exec(visualDescription)) !== null) {
+ elements.push({
+ type: match[1] || '',
+ count: parseInt(match[2] || '0', 10)
+ });
+ }
+
+ // Extract pattern info
+ const patternTypes = ['form', 'navigation', 'buttons', 'inputs', 'images', 'layout'];
+ patternTypes.forEach(type => {
+ patterns.push({
+ type,
+ present: visualDescription.toLowerCase().includes(type.toLowerCase())
+ });
+ });
+
+ // Extract the main description
+ const descriptionLines = visualDescription.split('\n\n');
+ const description = descriptionLines.length > 0 ? descriptionLines[0] || '' : 'No description available';
+
+ return { elements, patterns, description };
+}
+
+/**
+ * Generate a visualization description for a UI component
+ *
+ * @param code The component code
+ * @param componentType The component type/framework
+ * @param viewportSize The viewport dimensions
+ * @returns A structured visualization description
+ */
+function generateComponentVisualization(
+ code: string,
+ componentType: string,
+ viewportSize: { width?: number, height?: number } = {}
+): string {
+ // Extract component elements from code
+ const elements: string[] = [];
+
+ // Simple regex-based extraction of UI elements
+ // React/JSX elements
+ if (componentType === 'react') {
+ const jsxElements = code.match(/<([A-Z][a-zA-Z0-9]*|[a-z][a-zA-Z0-9]*)[^>]*>/g) || [];
+ jsxElements.forEach(el => {
+ const elName = el.match(/<([A-Z][a-zA-Z0-9]*|[a-z][a-zA-Z0-9]*)/)?.[1];
+ if (elName) elements.push(elName);
+ });
+ }
+
+ // Vue elements
+ else if (componentType === 'vue') {
+ const vueElements = code.match(/[^]*?<\/template>/g)?.[0] || '';
+ const templateElements = vueElements.match(/<([a-z][a-zA-Z0-9-]*)[^>]*>/g) || [];
+ templateElements.forEach(el => {
+ const elName = el.match(/<([a-z][a-zA-Z0-9-]*)/)?.[1];
+ if (elName && elName !== 'template') elements.push(elName);
+ });
+ }
+
+ // HTML elements
+ else if (componentType === 'html' || componentType === 'angular') {
+ const htmlElements = code.match(/<([a-z][a-zA-Z0-9-]*)[^>]*>/g) || [];
+ htmlElements.forEach(el => {
+ const elName = el.match(/<([a-z][a-zA-Z0-9-]*)/)?.[1];
+ if (elName) elements.push(elName);
+ });
+ }
+
+ // Count element types
+ const elementCounts = elements.reduce((acc: Record, el) => {
+ acc[el] = (acc[el] || 0) + 1;
+ return acc;
+ }, {});
+
+ // Check for common UI patterns
+ const hasForm = code.includes('