# Directory Structure ``` ├── .gitignore ├── package-lock.json ├── package.json ├── README.md ├── src │ ├── index.ts │ └── utils │ ├── flowParser.ts │ └── repoHandlers.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` node_modules/ build/ ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # UIFlowchartCreator UIFlowchartCreator is an MCP (Model Context Protocol) server for creating UI flowcharts. This tool helps developers and designers visualize user interfaces and their interactions. ## GitHub Repository The source code for this project is available on GitHub: [https://github.com/umshere/uiflowchartcreator](https://github.com/umshere/uiflowchartcreator) ## Features - Generate UI flowcharts based on input specifications - Integrate with MCP-compatible systems - Easy-to-use API for flowchart creation ## Installation ```bash npm install uiflowchartcreator ``` ## Usage To use UIFlowchartCreator in your MCP-compatible system, add it to your MCP configuration: ```json { "mcpServers": { "uiflowchartcreator": { "command": "node", "args": ["path/to/uiflowchartcreator/build/index.js"], "env": {} } } } ``` For detailed usage instructions and API documentation, please refer to the source code and comments in `src/index.ts`. ## Contributing Contributions are welcome! Please feel free to submit a Pull Request. ## License This project is licensed under the ISC License. ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "es2020", "module": "es2020", "moduleResolution": "node", "esModuleInterop": true, "outDir": "./build", "strict": true, "skipLibCheck": true }, "include": ["src/**/*"] } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "uiflowchartcreator", "version": "1.0.3", "description": "MCP server for creating UI flowcharts", "main": "build/index.js", "type": "module", "bin": { "uiflowchartcreator": "build/index.js" }, "scripts": { "start": "node build/index.js", "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'" }, "keywords": [ "mcp", "ui", "flowchart", "generator", "modelcontextprotocol", "uiflowchart" ], "author": "", "license": "ISC", "files": [ "build", "README.md" ], "repository": { "type": "git", "url": "https://github.com/umshere/uiflowchartcreator.git" }, "homepage": "https://github.com/umshere/uiflowchartcreator#readme", "bugs": { "url": "https://github.com/umshere/uiflowchartcreator/issues" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.0.4", "axios": "^1.4.0" }, "devDependencies": { "@types/axios": "^0.14.0", "@types/node": "^20.4.1", "typescript": "^5.1.3" } } ``` -------------------------------------------------------------------------------- /src/utils/repoHandlers.ts: -------------------------------------------------------------------------------- ```typescript import axios from "axios"; import fs from "fs/promises"; import path from "path"; import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; export interface RepoContents { name: string; path: string; type: string; content?: string; download_url?: string; owner?: string; repo?: string; } export async function fetchGitHubRepoContents( owner: string, repo: string, repoPath: string = "" ): Promise<RepoContents[]> { console.log( `[MCP] Fetching GitHub repo contents for ${owner}/${repo}${ repoPath ? `/${repoPath}` : "" }` ); const githubToken = process.env.GITHUB_TOKEN; if (!githubToken) { throw new McpError( ErrorCode.InvalidRequest, "GitHub token is required. Set GITHUB_TOKEN environment variable." ); } try { const response = await axios.get( `https://api.github.com/repos/${owner}/${repo}/contents/${repoPath}`, { headers: { Accept: "application/vnd.github.v3+json", Authorization: `token ${githubToken}`, "User-Agent": "UIFlowChartCreator-MCP", }, } ); if (!response.data) { throw new McpError( ErrorCode.InvalidRequest, `No data returned from GitHub API for ${owner}/${repo}` ); } const excludeList = [ "node_modules", ".git", "dist", "build", ".vscode", ".idea", "test", "__tests__", ]; const excludeFiles = [ ".env", ".gitignore", "package-lock.json", "yarn.lock", ]; return response.data.filter((item: RepoContents) => { if (item.type === "dir" && excludeList.includes(item.name)) { return false; } if (item.type === "file" && excludeFiles.includes(item.name)) { return false; } return true; }); } catch (error) { if (axios.isAxiosError(error)) { throw new McpError( ErrorCode.InvalidRequest, `GitHub API error: ${error.response?.data?.message || error.message}` ); } throw error; } } export async function fetchLocalRepoContents( repoPath: string ): Promise<RepoContents[]> { console.log(`[MCP] Fetching local repo contents from ${repoPath}`); try { const contents: RepoContents[] = []; const items = await fs.readdir(repoPath, { withFileTypes: true }); const excludeList = [ "node_modules", ".git", "dist", "build", ".vscode", ".idea", "test", "__tests__", ]; const excludeFiles = [ ".env", ".gitignore", "package-lock.json", "yarn.lock", ]; for (const item of items) { if ( excludeList.includes(item.name) || (item.isFile() && excludeFiles.includes(item.name)) ) continue; const itemPath = path.join(repoPath, item.name); if (item.isDirectory()) { contents.push({ name: item.name, path: itemPath, type: "dir", }); } else if (item.isFile()) { const content = await fs.readFile(itemPath, "utf-8"); contents.push({ name: item.name, path: itemPath, type: "file", content, }); } } return contents; } catch (error) { throw new McpError( ErrorCode.InvalidRequest, `Failed to read local repository: ${ error instanceof Error ? error.message : String(error) }` ); } } ``` -------------------------------------------------------------------------------- /src/utils/flowParser.ts: -------------------------------------------------------------------------------- ```typescript import path from "path"; import axios from "axios"; import { RepoContents, fetchLocalRepoContents, fetchGitHubRepoContents, } from "./repoHandlers.js"; import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; export interface ComponentInfo { name: string; type: "page" | "layout" | "component"; filePath: string; imports: string[]; children: ComponentInfo[]; } export async function parseUIFlow( contents: RepoContents[], isLocal: boolean, fileExtensions: string[] = ["js", "jsx", "ts", "tsx"] ): Promise<string> { console.log( `[MCP] Parsing UI flow with extensions: ${fileExtensions.join(", ")}` ); const components: { [key: string]: ComponentInfo } = {}; async function processContents( currentContents: RepoContents[], currentPath: string = "" ) { for (const item of currentContents) { if ( item.type === "file" && fileExtensions.some((ext) => item.name.endsWith(`.${ext}`)) ) { let content: string; if (isLocal) { content = item.content || ""; } else { try { const response = await axios.get(item.download_url || ""); content = response.data; } catch (error) { console.warn( `[MCP] Failed to fetch content for ${item.name}: ${error}` ); continue; } } const componentName = item.name.split(".")[0]; const componentPath = path.join(currentPath, componentName); const componentType = getComponentType(componentPath); components[componentPath] = { name: componentName, type: componentType, filePath: path.join(currentPath, item.name), imports: [], children: [], }; // Analyze import statements const importMatches = content.match( /import\s+(\w+|\{[^}]+\})\s+from\s+['"]([^'"]+)['"]/g ); if (importMatches) { importMatches.forEach((match) => { const [, importedComponent, importPath] = match.match( /import\s+(\w+|\{[^}]+\})\s+from\s+['"]([^'"]+)['"]/ ) || []; if (importedComponent) { const cleanedImport = importedComponent .replace(/[{}]/g, "") .trim(); const resolvedPath = path.join( currentPath, path.dirname(importPath), cleanedImport ); components[componentPath].imports.push(resolvedPath); } }); } } else if (item.type === "dir") { const subContents = isLocal ? await fetchLocalRepoContents(item.path) : await fetchGitHubRepoContents( item.owner || "", item.repo || "", item.path ); await processContents(subContents, path.join(currentPath, item.name)); } } } await processContents(contents); // Build component hierarchy const rootComponents: ComponentInfo[] = []; Object.values(components).forEach((component) => { component.imports.forEach((importPath) => { if (components[importPath]) { components[importPath].children.push(component); } }); if (component.imports.length === 0) { rootComponents.push(component); } }); return JSON.stringify(rootComponents, null, 2); } function getComponentType( componentPath: string ): "page" | "layout" | "component" { // Special handling for App component if (componentPath.toLowerCase().includes("app")) { return "page"; } // Detect pages based on common patterns if ( componentPath.includes("pages") || componentPath.includes("views") || componentPath.toLowerCase().includes("meals") ) { return "page"; } // Detect layouts based on common patterns if ( componentPath.includes("layouts") || componentPath.toLowerCase().includes("header") ) { return "layout"; } // Default to component return "component"; } export function generateMermaidFlowchart(components: ComponentInfo[]): string { let chart = "flowchart TD\n"; // Create a map of all components for quick lookup const componentMap = new Map<string, ComponentInfo>(); components.forEach((component) => { componentMap.set(component.name, component); }); // Create nodes with proper styling and hierarchy const createNode = (component: ComponentInfo, depth: number = 0): string => { const nodeId = component.name.replace(/[^a-zA-Z0-9]/g, "_"); const indent = " ".repeat(depth); // Determine node style based on type let nodeStyle = ""; switch (component.type) { case "page": nodeStyle = "(( ))"; break; case "layout": nodeStyle = "{{ }}"; break; default: nodeStyle = "[/ /]"; } // Add node with proper indentation chart += `${indent}${nodeId}${nodeStyle}["${component.name} (${component.type})"]\n`; // Recursively process children component.children.forEach((child) => { const childComponent = componentMap.get(child.name); if (childComponent) { createNode(childComponent, depth + 1); } }); return nodeId; }; // Find root components (those with no parents) const rootComponents = components.filter( (component) => !components.some((c) => c.children.some((child) => child.name === component.name) ) ); // Start building the chart from root components rootComponents.forEach((component) => { createNode(component); }); // Create relationships with labels components.forEach((component) => { const parentId = component.name.replace(/[^a-zA-Z0-9]/g, "_"); component.children.forEach((child) => { const childId = child.name.replace(/[^a-zA-Z0-9]/g, "_"); const relationshipType = determineRelationshipType(component, child); chart += ` ${parentId} -->|${relationshipType}| ${childId}\n`; }); }); // Validate Mermaid.js syntax try { // Basic validation - check for required elements if (!chart.includes("flowchart TD")) { throw new Error("Missing flowchart declaration"); } if (!chart.match(/\[.*\]/)) { throw new Error("Missing node definitions"); } if (!chart.match(/-->|--/)) { throw new Error("Missing relationship definitions"); } } catch (error) { console.error("[MCP] Mermaid.js validation error:", error); throw new McpError( ErrorCode.InternalError, `Failed to generate valid Mermaid.js chart: ${error}` ); } return chart; } function determineRelationshipType( parent: ComponentInfo, child: ComponentInfo ): string { if (parent.type === "layout" && child.type === "page") { return "contains"; } if (parent.type === "page" && child.type === "component") { return "uses"; } if (parent.type === "component" && child.type === "component") { return "composes"; } return "relates to"; } ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import fs from "fs/promises"; import path from "path"; import { fetchLocalRepoContents, fetchGitHubRepoContents, RepoContents, } from "./utils/repoHandlers.js"; import { parseUIFlow, generateMermaidFlowchart } from "./utils/flowParser.js"; // Initialize MCP server with capabilities const server = new Server( { name: "uiflowchartcreator", version: "1.0.1", capabilities: { resources: { "ui-flow": { name: "UI Flow Resource", description: "Access generated UI flow diagrams", uriTemplate: "ui-flow://{owner}/{repo}", }, }, tools: { generate_ui_flow: { name: "generate_ui_flow", description: "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.", inputSchema: { type: "object", properties: { repoPath: { type: "string", description: "Path to local repository or empty string for GitHub repos", }, isLocal: { type: "boolean", description: "Whether to analyze a local repository (true) or GitHub repository (false)", }, owner: { type: "string", description: "GitHub repository owner (required if isLocal is false)", }, repo: { type: "string", description: "GitHub repository name (required if isLocal is false)", }, fileExtensions: { type: "array", items: { type: "string" }, description: "List of file extensions to analyze (e.g., ['js', 'jsx', 'ts', 'tsx'] for React, ['ts', 'html'] for Angular)", default: ["js", "jsx", "ts", "tsx"], }, }, required: ["repoPath", "isLocal"], additionalProperties: false, }, }, }, }, }, { capabilities: { resources: {}, tools: {}, }, } ); // List available tools server.setRequestHandler(ListToolsRequestSchema, async () => { console.log("[MCP] Listing available tools"); return { tools: [ { name: "generate_ui_flow", description: "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.", inputSchema: { type: "object", properties: { repoPath: { type: "string", description: "Path to local repository or empty string for GitHub repos", }, isLocal: { type: "boolean", description: "Whether to analyze a local repository (true) or GitHub repository (false)", }, owner: { type: "string", description: "GitHub repository owner (required if isLocal is false)", }, repo: { type: "string", description: "GitHub repository name (required if isLocal is false)", }, fileExtensions: { type: "array", items: { type: "string" }, description: "List of file extensions to analyze (e.g., ['js', 'jsx', 'ts', 'tsx'] for React, ['ts', 'html'] for Angular)", default: ["js", "jsx", "ts", "tsx"], }, }, required: ["repoPath", "isLocal"], additionalProperties: false, }, }, ], }; }); // Handle tool execution server.setRequestHandler(CallToolRequestSchema, async (request) => { console.log("[MCP] Received tool request:", request.params.name); if (request.params.name !== "generate_ui_flow") { throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}` ); } const args = request.params.arguments as { repoPath: string; isLocal: boolean; owner?: string; repo?: string; fileExtensions?: string[]; }; const { repoPath, isLocal, owner, repo, fileExtensions } = args; try { let contents: RepoContents[]; if (isLocal) { contents = await fetchLocalRepoContents(repoPath); } else { if (!owner || !repo) { throw new McpError( ErrorCode.InvalidParams, "Owner and repo are required for GitHub repositories" ); } contents = await fetchGitHubRepoContents(owner, repo); } const components = await parseUIFlow(contents, isLocal, fileExtensions); const mermaidChart = generateMermaidFlowchart(JSON.parse(components)); // Determine output path based on repository type const outputPath = isLocal ? path.join(repoPath, "userflo.md") : path.join(process.cwd(), "userflo.md"); const flowDescription = `# UI Flow Diagram\n\nThis document describes the UI flow of the application.\n\n`; const fullContent = flowDescription + "```mermaid\n" + mermaidChart + "\n```\n\n"; await fs.writeFile(outputPath, fullContent); console.log(`[MCP] UI flow saved to ${outputPath}`); return { content: [ { type: "text", text: mermaidChart, }, ], }; } catch (error) { if (error instanceof McpError) { throw error; } throw new McpError( ErrorCode.InternalError, `Failed to generate UI flow: ${ error instanceof Error ? error.message : String(error) }` ); } }); // Handle resource access server.setRequestHandler(ReadResourceRequestSchema, async (request) => { console.log("[MCP] Received resource request:", request.params.uri); const match = request.params.uri.match(/^ui-flow:\/\/([^\/]+)\/([^\/]+)$/); if (!match) { throw new McpError( ErrorCode.InvalidRequest, `Invalid resource URI format: ${request.params.uri}` ); } const [, owner, repo] = match; try { const contents = await fetchGitHubRepoContents(owner, repo); const uiFlowJson = await parseUIFlow(contents, false); return { contents: [ { uri: request.params.uri, mimeType: "application/json", text: uiFlowJson, }, ], }; } catch (error) { throw new McpError( ErrorCode.InternalError, `Failed to read UI flow resource: ${ error instanceof Error ? error.message : String(error) }` ); } }); async function run() { const transport = new StdioServerTransport(); await server.connect(transport); console.log("[MCP] UI Flow Chart Creator server running on stdio"); } run().catch((error) => { console.error("[MCP] Fatal error:", error); process.exit(1); }); // Export to make it a proper ES module export { server, run }; ```