#
tokens: 4923/50000 7/7 files
lines: off (toggle) GitHub
raw markdown copy
# 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 };

```