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