# Directory Structure ``` ├── .gitignore ├── package-lock.json ├── package.json ├── README.md ├── src │ ├── index.ts │ └── utils │ ├── flowParser.ts │ └── repoHandlers.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | node_modules/ 2 | build/ 3 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # UIFlowchartCreator 2 | 3 | UIFlowchartCreator is an MCP (Model Context Protocol) server for creating UI flowcharts. This tool helps developers and designers visualize user interfaces and their interactions. 4 | 5 | ## GitHub Repository 6 | 7 | The source code for this project is available on GitHub: 8 | [https://github.com/umshere/uiflowchartcreator](https://github.com/umshere/uiflowchartcreator) 9 | 10 | ## Features 11 | 12 | - Generate UI flowcharts based on input specifications 13 | - Integrate with MCP-compatible systems 14 | - Easy-to-use API for flowchart creation 15 | 16 | ## Installation 17 | 18 | ```bash 19 | npm install uiflowchartcreator 20 | ``` 21 | 22 | ## Usage 23 | 24 | To use UIFlowchartCreator in your MCP-compatible system, add it to your MCP configuration: 25 | 26 | ```json 27 | { 28 | "mcpServers": { 29 | "uiflowchartcreator": { 30 | "command": "node", 31 | "args": ["path/to/uiflowchartcreator/build/index.js"], 32 | "env": {} 33 | } 34 | } 35 | } 36 | ``` 37 | 38 | For detailed usage instructions and API documentation, please refer to the source code and comments in `src/index.ts`. 39 | 40 | ## Contributing 41 | 42 | Contributions are welcome! Please feel free to submit a Pull Request. 43 | 44 | ## License 45 | 46 | This project is licensed under the ISC License. 47 | ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "es2020", 5 | "moduleResolution": "node", 6 | "esModuleInterop": true, 7 | "outDir": "./build", 8 | "strict": true, 9 | "skipLibCheck": true 10 | }, 11 | "include": ["src/**/*"] 12 | } 13 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "uiflowchartcreator", 3 | "version": "1.0.3", 4 | "description": "MCP server for creating UI flowcharts", 5 | "main": "build/index.js", 6 | "type": "module", 7 | "bin": { 8 | "uiflowchartcreator": "build/index.js" 9 | }, 10 | "scripts": { 11 | "start": "node build/index.js", 12 | "build": "echo 'Running TypeScript compiler...' && tsc && echo 'TypeScript compilation complete. Setting file permissions...' && node -e \"require('fs').chmodSync('build/index.js', '755')\" && echo 'Build completed successfully'" 13 | }, 14 | "keywords": [ 15 | "mcp", 16 | "ui", 17 | "flowchart", 18 | "generator", 19 | "modelcontextprotocol", 20 | "uiflowchart" 21 | ], 22 | "author": "", 23 | "license": "ISC", 24 | "files": [ 25 | "build", 26 | "README.md" 27 | ], 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/umshere/uiflowchartcreator.git" 31 | }, 32 | "homepage": "https://github.com/umshere/uiflowchartcreator#readme", 33 | "bugs": { 34 | "url": "https://github.com/umshere/uiflowchartcreator/issues" 35 | }, 36 | "dependencies": { 37 | "@modelcontextprotocol/sdk": "^1.0.4", 38 | "axios": "^1.4.0" 39 | }, 40 | "devDependencies": { 41 | "@types/axios": "^0.14.0", 42 | "@types/node": "^20.4.1", 43 | "typescript": "^5.1.3" 44 | } 45 | } ``` -------------------------------------------------------------------------------- /src/utils/repoHandlers.ts: -------------------------------------------------------------------------------- ```typescript 1 | import axios from "axios"; 2 | import fs from "fs/promises"; 3 | import path from "path"; 4 | import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; 5 | 6 | export interface RepoContents { 7 | name: string; 8 | path: string; 9 | type: string; 10 | content?: string; 11 | download_url?: string; 12 | owner?: string; 13 | repo?: string; 14 | } 15 | 16 | export async function fetchGitHubRepoContents( 17 | owner: string, 18 | repo: string, 19 | repoPath: string = "" 20 | ): Promise<RepoContents[]> { 21 | console.log( 22 | `[MCP] Fetching GitHub repo contents for ${owner}/${repo}${ 23 | repoPath ? `/${repoPath}` : "" 24 | }` 25 | ); 26 | 27 | const githubToken = process.env.GITHUB_TOKEN; 28 | if (!githubToken) { 29 | throw new McpError( 30 | ErrorCode.InvalidRequest, 31 | "GitHub token is required. Set GITHUB_TOKEN environment variable." 32 | ); 33 | } 34 | 35 | try { 36 | const response = await axios.get( 37 | `https://api.github.com/repos/${owner}/${repo}/contents/${repoPath}`, 38 | { 39 | headers: { 40 | Accept: "application/vnd.github.v3+json", 41 | Authorization: `token ${githubToken}`, 42 | "User-Agent": "UIFlowChartCreator-MCP", 43 | }, 44 | } 45 | ); 46 | 47 | if (!response.data) { 48 | throw new McpError( 49 | ErrorCode.InvalidRequest, 50 | `No data returned from GitHub API for ${owner}/${repo}` 51 | ); 52 | } 53 | 54 | const excludeList = [ 55 | "node_modules", 56 | ".git", 57 | "dist", 58 | "build", 59 | ".vscode", 60 | ".idea", 61 | "test", 62 | "__tests__", 63 | ]; 64 | 65 | const excludeFiles = [ 66 | ".env", 67 | ".gitignore", 68 | "package-lock.json", 69 | "yarn.lock", 70 | ]; 71 | 72 | return response.data.filter((item: RepoContents) => { 73 | if (item.type === "dir" && excludeList.includes(item.name)) { 74 | return false; 75 | } 76 | if (item.type === "file" && excludeFiles.includes(item.name)) { 77 | return false; 78 | } 79 | return true; 80 | }); 81 | } catch (error) { 82 | if (axios.isAxiosError(error)) { 83 | throw new McpError( 84 | ErrorCode.InvalidRequest, 85 | `GitHub API error: ${error.response?.data?.message || error.message}` 86 | ); 87 | } 88 | throw error; 89 | } 90 | } 91 | 92 | export async function fetchLocalRepoContents( 93 | repoPath: string 94 | ): Promise<RepoContents[]> { 95 | console.log(`[MCP] Fetching local repo contents from ${repoPath}`); 96 | 97 | try { 98 | const contents: RepoContents[] = []; 99 | const items = await fs.readdir(repoPath, { withFileTypes: true }); 100 | 101 | const excludeList = [ 102 | "node_modules", 103 | ".git", 104 | "dist", 105 | "build", 106 | ".vscode", 107 | ".idea", 108 | "test", 109 | "__tests__", 110 | ]; 111 | 112 | const excludeFiles = [ 113 | ".env", 114 | ".gitignore", 115 | "package-lock.json", 116 | "yarn.lock", 117 | ]; 118 | 119 | for (const item of items) { 120 | if ( 121 | excludeList.includes(item.name) || 122 | (item.isFile() && excludeFiles.includes(item.name)) 123 | ) 124 | continue; 125 | 126 | const itemPath = path.join(repoPath, item.name); 127 | if (item.isDirectory()) { 128 | contents.push({ 129 | name: item.name, 130 | path: itemPath, 131 | type: "dir", 132 | }); 133 | } else if (item.isFile()) { 134 | const content = await fs.readFile(itemPath, "utf-8"); 135 | contents.push({ 136 | name: item.name, 137 | path: itemPath, 138 | type: "file", 139 | content, 140 | }); 141 | } 142 | } 143 | 144 | return contents; 145 | } catch (error) { 146 | throw new McpError( 147 | ErrorCode.InvalidRequest, 148 | `Failed to read local repository: ${ 149 | error instanceof Error ? error.message : String(error) 150 | }` 151 | ); 152 | } 153 | } 154 | ``` -------------------------------------------------------------------------------- /src/utils/flowParser.ts: -------------------------------------------------------------------------------- ```typescript 1 | import path from "path"; 2 | import axios from "axios"; 3 | import { 4 | RepoContents, 5 | fetchLocalRepoContents, 6 | fetchGitHubRepoContents, 7 | } from "./repoHandlers.js"; 8 | import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; 9 | 10 | export interface ComponentInfo { 11 | name: string; 12 | type: "page" | "layout" | "component"; 13 | filePath: string; 14 | imports: string[]; 15 | children: ComponentInfo[]; 16 | } 17 | 18 | export async function parseUIFlow( 19 | contents: RepoContents[], 20 | isLocal: boolean, 21 | fileExtensions: string[] = ["js", "jsx", "ts", "tsx"] 22 | ): Promise<string> { 23 | console.log( 24 | `[MCP] Parsing UI flow with extensions: ${fileExtensions.join(", ")}` 25 | ); 26 | 27 | const components: { [key: string]: ComponentInfo } = {}; 28 | 29 | async function processContents( 30 | currentContents: RepoContents[], 31 | currentPath: string = "" 32 | ) { 33 | for (const item of currentContents) { 34 | if ( 35 | item.type === "file" && 36 | fileExtensions.some((ext) => item.name.endsWith(`.${ext}`)) 37 | ) { 38 | let content: string; 39 | if (isLocal) { 40 | content = item.content || ""; 41 | } else { 42 | try { 43 | const response = await axios.get(item.download_url || ""); 44 | content = response.data; 45 | } catch (error) { 46 | console.warn( 47 | `[MCP] Failed to fetch content for ${item.name}: ${error}` 48 | ); 49 | continue; 50 | } 51 | } 52 | 53 | const componentName = item.name.split(".")[0]; 54 | const componentPath = path.join(currentPath, componentName); 55 | const componentType = getComponentType(componentPath); 56 | 57 | components[componentPath] = { 58 | name: componentName, 59 | type: componentType, 60 | filePath: path.join(currentPath, item.name), 61 | imports: [], 62 | children: [], 63 | }; 64 | 65 | // Analyze import statements 66 | const importMatches = content.match( 67 | /import\s+(\w+|\{[^}]+\})\s+from\s+['"]([^'"]+)['"]/g 68 | ); 69 | if (importMatches) { 70 | importMatches.forEach((match) => { 71 | const [, importedComponent, importPath] = 72 | match.match( 73 | /import\s+(\w+|\{[^}]+\})\s+from\s+['"]([^'"]+)['"]/ 74 | ) || []; 75 | if (importedComponent) { 76 | const cleanedImport = importedComponent 77 | .replace(/[{}]/g, "") 78 | .trim(); 79 | const resolvedPath = path.join( 80 | currentPath, 81 | path.dirname(importPath), 82 | cleanedImport 83 | ); 84 | components[componentPath].imports.push(resolvedPath); 85 | } 86 | }); 87 | } 88 | } else if (item.type === "dir") { 89 | const subContents = isLocal 90 | ? await fetchLocalRepoContents(item.path) 91 | : await fetchGitHubRepoContents( 92 | item.owner || "", 93 | item.repo || "", 94 | item.path 95 | ); 96 | 97 | await processContents(subContents, path.join(currentPath, item.name)); 98 | } 99 | } 100 | } 101 | 102 | await processContents(contents); 103 | 104 | // Build component hierarchy 105 | const rootComponents: ComponentInfo[] = []; 106 | Object.values(components).forEach((component) => { 107 | component.imports.forEach((importPath) => { 108 | if (components[importPath]) { 109 | components[importPath].children.push(component); 110 | } 111 | }); 112 | if (component.imports.length === 0) { 113 | rootComponents.push(component); 114 | } 115 | }); 116 | 117 | return JSON.stringify(rootComponents, null, 2); 118 | } 119 | 120 | function getComponentType( 121 | componentPath: string 122 | ): "page" | "layout" | "component" { 123 | // Special handling for App component 124 | if (componentPath.toLowerCase().includes("app")) { 125 | return "page"; 126 | } 127 | 128 | // Detect pages based on common patterns 129 | if ( 130 | componentPath.includes("pages") || 131 | componentPath.includes("views") || 132 | componentPath.toLowerCase().includes("meals") 133 | ) { 134 | return "page"; 135 | } 136 | 137 | // Detect layouts based on common patterns 138 | if ( 139 | componentPath.includes("layouts") || 140 | componentPath.toLowerCase().includes("header") 141 | ) { 142 | return "layout"; 143 | } 144 | 145 | // Default to component 146 | return "component"; 147 | } 148 | 149 | export function generateMermaidFlowchart(components: ComponentInfo[]): string { 150 | let chart = "flowchart TD\n"; 151 | 152 | // Create a map of all components for quick lookup 153 | const componentMap = new Map<string, ComponentInfo>(); 154 | components.forEach((component) => { 155 | componentMap.set(component.name, component); 156 | }); 157 | 158 | // Create nodes with proper styling and hierarchy 159 | const createNode = (component: ComponentInfo, depth: number = 0): string => { 160 | const nodeId = component.name.replace(/[^a-zA-Z0-9]/g, "_"); 161 | const indent = " ".repeat(depth); 162 | 163 | // Determine node style based on type 164 | let nodeStyle = ""; 165 | switch (component.type) { 166 | case "page": 167 | nodeStyle = "(( ))"; 168 | break; 169 | case "layout": 170 | nodeStyle = "{{ }}"; 171 | break; 172 | default: 173 | nodeStyle = "[/ /]"; 174 | } 175 | 176 | // Add node with proper indentation 177 | chart += `${indent}${nodeId}${nodeStyle}["${component.name} (${component.type})"]\n`; 178 | 179 | // Recursively process children 180 | component.children.forEach((child) => { 181 | const childComponent = componentMap.get(child.name); 182 | if (childComponent) { 183 | createNode(childComponent, depth + 1); 184 | } 185 | }); 186 | 187 | return nodeId; 188 | }; 189 | 190 | // Find root components (those with no parents) 191 | const rootComponents = components.filter( 192 | (component) => 193 | !components.some((c) => 194 | c.children.some((child) => child.name === component.name) 195 | ) 196 | ); 197 | 198 | // Start building the chart from root components 199 | rootComponents.forEach((component) => { 200 | createNode(component); 201 | }); 202 | 203 | // Create relationships with labels 204 | components.forEach((component) => { 205 | const parentId = component.name.replace(/[^a-zA-Z0-9]/g, "_"); 206 | 207 | component.children.forEach((child) => { 208 | const childId = child.name.replace(/[^a-zA-Z0-9]/g, "_"); 209 | const relationshipType = determineRelationshipType(component, child); 210 | chart += ` ${parentId} -->|${relationshipType}| ${childId}\n`; 211 | }); 212 | }); 213 | 214 | // Validate Mermaid.js syntax 215 | try { 216 | // Basic validation - check for required elements 217 | if (!chart.includes("flowchart TD")) { 218 | throw new Error("Missing flowchart declaration"); 219 | } 220 | if (!chart.match(/\[.*\]/)) { 221 | throw new Error("Missing node definitions"); 222 | } 223 | if (!chart.match(/-->|--/)) { 224 | throw new Error("Missing relationship definitions"); 225 | } 226 | } catch (error) { 227 | console.error("[MCP] Mermaid.js validation error:", error); 228 | throw new McpError( 229 | ErrorCode.InternalError, 230 | `Failed to generate valid Mermaid.js chart: ${error}` 231 | ); 232 | } 233 | 234 | return chart; 235 | } 236 | 237 | function determineRelationshipType( 238 | parent: ComponentInfo, 239 | child: ComponentInfo 240 | ): string { 241 | if (parent.type === "layout" && child.type === "page") { 242 | return "contains"; 243 | } 244 | if (parent.type === "page" && child.type === "component") { 245 | return "uses"; 246 | } 247 | if (parent.type === "component" && child.type === "component") { 248 | return "composes"; 249 | } 250 | return "relates to"; 251 | } 252 | ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 4 | import { 5 | CallToolRequestSchema, 6 | ErrorCode, 7 | ListToolsRequestSchema, 8 | McpError, 9 | ReadResourceRequestSchema, 10 | } from "@modelcontextprotocol/sdk/types.js"; 11 | import fs from "fs/promises"; 12 | import path from "path"; 13 | import { 14 | fetchLocalRepoContents, 15 | fetchGitHubRepoContents, 16 | RepoContents, 17 | } from "./utils/repoHandlers.js"; 18 | import { parseUIFlow, generateMermaidFlowchart } from "./utils/flowParser.js"; 19 | 20 | // Initialize MCP server with capabilities 21 | const server = new Server( 22 | { 23 | name: "uiflowchartcreator", 24 | version: "1.0.1", 25 | capabilities: { 26 | resources: { 27 | "ui-flow": { 28 | name: "UI Flow Resource", 29 | description: "Access generated UI flow diagrams", 30 | uriTemplate: "ui-flow://{owner}/{repo}", 31 | }, 32 | }, 33 | tools: { 34 | generate_ui_flow: { 35 | name: "generate_ui_flow", 36 | description: 37 | "Generate a UI flow diagram by analyzing React/Angular repositories. This tool scans the codebase to identify components, their relationships, and the overall UI structure.", 38 | inputSchema: { 39 | type: "object", 40 | properties: { 41 | repoPath: { 42 | type: "string", 43 | description: 44 | "Path to local repository or empty string for GitHub repos", 45 | }, 46 | isLocal: { 47 | type: "boolean", 48 | description: 49 | "Whether to analyze a local repository (true) or GitHub repository (false)", 50 | }, 51 | owner: { 52 | type: "string", 53 | description: 54 | "GitHub repository owner (required if isLocal is false)", 55 | }, 56 | repo: { 57 | type: "string", 58 | description: 59 | "GitHub repository name (required if isLocal is false)", 60 | }, 61 | fileExtensions: { 62 | type: "array", 63 | items: { type: "string" }, 64 | description: 65 | "List of file extensions to analyze (e.g., ['js', 'jsx', 'ts', 'tsx'] for React, ['ts', 'html'] for Angular)", 66 | default: ["js", "jsx", "ts", "tsx"], 67 | }, 68 | }, 69 | required: ["repoPath", "isLocal"], 70 | additionalProperties: false, 71 | }, 72 | }, 73 | }, 74 | }, 75 | }, 76 | { 77 | capabilities: { 78 | resources: {}, 79 | tools: {}, 80 | }, 81 | } 82 | ); 83 | 84 | // List available tools 85 | server.setRequestHandler(ListToolsRequestSchema, async () => { 86 | console.log("[MCP] Listing available tools"); 87 | return { 88 | tools: [ 89 | { 90 | name: "generate_ui_flow", 91 | description: 92 | "Generate a UI flow diagram by analyzing React/Angular repositories. This tool scans the codebase to identify components, their relationships, and the overall UI structure.", 93 | inputSchema: { 94 | type: "object", 95 | properties: { 96 | repoPath: { 97 | type: "string", 98 | description: 99 | "Path to local repository or empty string for GitHub repos", 100 | }, 101 | isLocal: { 102 | type: "boolean", 103 | description: 104 | "Whether to analyze a local repository (true) or GitHub repository (false)", 105 | }, 106 | owner: { 107 | type: "string", 108 | description: 109 | "GitHub repository owner (required if isLocal is false)", 110 | }, 111 | repo: { 112 | type: "string", 113 | description: 114 | "GitHub repository name (required if isLocal is false)", 115 | }, 116 | fileExtensions: { 117 | type: "array", 118 | items: { type: "string" }, 119 | description: 120 | "List of file extensions to analyze (e.g., ['js', 'jsx', 'ts', 'tsx'] for React, ['ts', 'html'] for Angular)", 121 | default: ["js", "jsx", "ts", "tsx"], 122 | }, 123 | }, 124 | required: ["repoPath", "isLocal"], 125 | additionalProperties: false, 126 | }, 127 | }, 128 | ], 129 | }; 130 | }); 131 | 132 | // Handle tool execution 133 | server.setRequestHandler(CallToolRequestSchema, async (request) => { 134 | console.log("[MCP] Received tool request:", request.params.name); 135 | 136 | if (request.params.name !== "generate_ui_flow") { 137 | throw new McpError( 138 | ErrorCode.MethodNotFound, 139 | `Unknown tool: ${request.params.name}` 140 | ); 141 | } 142 | 143 | const args = request.params.arguments as { 144 | repoPath: string; 145 | isLocal: boolean; 146 | owner?: string; 147 | repo?: string; 148 | fileExtensions?: string[]; 149 | }; 150 | const { repoPath, isLocal, owner, repo, fileExtensions } = args; 151 | 152 | try { 153 | let contents: RepoContents[]; 154 | if (isLocal) { 155 | contents = await fetchLocalRepoContents(repoPath); 156 | } else { 157 | if (!owner || !repo) { 158 | throw new McpError( 159 | ErrorCode.InvalidParams, 160 | "Owner and repo are required for GitHub repositories" 161 | ); 162 | } 163 | contents = await fetchGitHubRepoContents(owner, repo); 164 | } 165 | 166 | const components = await parseUIFlow(contents, isLocal, fileExtensions); 167 | const mermaidChart = generateMermaidFlowchart(JSON.parse(components)); 168 | 169 | // Determine output path based on repository type 170 | const outputPath = isLocal 171 | ? path.join(repoPath, "userflo.md") 172 | : path.join(process.cwd(), "userflo.md"); 173 | const flowDescription = `# UI Flow Diagram\n\nThis document describes the UI flow of the application.\n\n`; 174 | const fullContent = 175 | flowDescription + "```mermaid\n" + mermaidChart + "\n```\n\n"; 176 | 177 | await fs.writeFile(outputPath, fullContent); 178 | console.log(`[MCP] UI flow saved to ${outputPath}`); 179 | 180 | return { 181 | content: [ 182 | { 183 | type: "text", 184 | text: mermaidChart, 185 | }, 186 | ], 187 | }; 188 | } catch (error) { 189 | if (error instanceof McpError) { 190 | throw error; 191 | } 192 | throw new McpError( 193 | ErrorCode.InternalError, 194 | `Failed to generate UI flow: ${ 195 | error instanceof Error ? error.message : String(error) 196 | }` 197 | ); 198 | } 199 | }); 200 | 201 | // Handle resource access 202 | server.setRequestHandler(ReadResourceRequestSchema, async (request) => { 203 | console.log("[MCP] Received resource request:", request.params.uri); 204 | 205 | const match = request.params.uri.match(/^ui-flow:\/\/([^\/]+)\/([^\/]+)$/); 206 | if (!match) { 207 | throw new McpError( 208 | ErrorCode.InvalidRequest, 209 | `Invalid resource URI format: ${request.params.uri}` 210 | ); 211 | } 212 | 213 | const [, owner, repo] = match; 214 | try { 215 | const contents = await fetchGitHubRepoContents(owner, repo); 216 | const uiFlowJson = await parseUIFlow(contents, false); 217 | 218 | return { 219 | contents: [ 220 | { 221 | uri: request.params.uri, 222 | mimeType: "application/json", 223 | text: uiFlowJson, 224 | }, 225 | ], 226 | }; 227 | } catch (error) { 228 | throw new McpError( 229 | ErrorCode.InternalError, 230 | `Failed to read UI flow resource: ${ 231 | error instanceof Error ? error.message : String(error) 232 | }` 233 | ); 234 | } 235 | }); 236 | 237 | async function run() { 238 | const transport = new StdioServerTransport(); 239 | await server.connect(transport); 240 | console.log("[MCP] UI Flow Chart Creator server running on stdio"); 241 | } 242 | 243 | run().catch((error) => { 244 | console.error("[MCP] Fatal error:", error); 245 | process.exit(1); 246 | }); 247 | 248 | // Export to make it a proper ES module 249 | export { server, run }; 250 | ```