# 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 |
```