# Directory Structure
```
├── .gitignore
├── package-lock.json
├── package.json
├── README.md
├── src
│ └── index.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | build
2 | node_modules
3 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # react-analyzer-mcp
2 |
3 | Analyze & generate docs for React code using the Model Context Protocol. Based on [react-analyzer](https://github.com/azer/react-analyzer) library.
4 |
5 | [<img src="https://github.com/user-attachments/assets/615f43d7-9b81-4480-9a2a-773819223ddb" width="500" />](https://x.com/azerkoculu/status/1910071779457900866)
6 |
7 | ## What it does
8 |
9 | This tool analyzes React component files (JSX/TSX) and extracts information about components and their props.
10 |
11 | ## Available Tools
12 |
13 | - **analyze-react**: Analyzes a single React component from source code
14 | - **analyze-project**: Generates documentation for all React components in a project folder
15 | - **list-projects**: Lists all projects under the root folder
16 |
17 | ## Installation
18 |
19 | ```bash
20 | # Clone the repository
21 | git clone https://github.com/azer/react-analyzer-mcp.git
22 | cd react-analyzer-mcp
23 |
24 | # Install dependencies
25 | npm install
26 |
27 | # Update PROJECT_ROOT in the index.ts file.
28 | vim src/index.ts
29 |
30 | # Build
31 | npm run build
32 | ```
33 |
34 | ## Using with Claude
35 |
36 | 1. Enable MCP server in the Claude Desktop config:
37 |
38 | ```bash
39 | {
40 | "react-analyzer-mcp": {
41 | "command": "node",
42 | "args": [
43 | "/Users/azer/code/sandbox/react-analyzer-mcp/build/index.js"
44 | ]
45 | }
46 | }
47 | ```
48 |
49 | 2. Connect Claude to your MCP server using the Claude Shell.
50 |
51 | 3. Use the tools directly in Claude conversations:
52 |
53 | ```
54 | Analyze my project's React components in the "ui" folder.
55 | ```
56 |
57 | Or:
58 |
59 | ```
60 | What React components do I have in my project?
61 | ```
62 |
63 | ## Examples
64 |
65 | ### Analyzing a project folder:
66 |
67 | Input:
68 | ```
69 | Can you analyze the components in my "foobar" folder?
70 | ```
71 |
72 | Output:
73 | ```
74 | # Components
75 |
76 | ## Button
77 |
78 | ### Props
79 |
80 | | Prop | Type | Optional | Default |
81 | |------|------|----------|---------|
82 | | `variant` | `string` | ✓ | `primary` |
83 | | `size` | `string` | ✓ | `md` |
84 | | `onClick` | `function` | ✓ | |
85 | ...
86 | ```
87 |
88 | ## License
89 |
90 | MIT
91 |
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "Node16",
5 | "moduleResolution": "Node16",
6 | "outDir": "./build",
7 | "rootDir": "./src",
8 | "strict": true,
9 | "esModuleInterop": true,
10 | "skipLibCheck": true,
11 | "forceConsistentCasingInFileNames": true
12 | },
13 | "include": ["src/**/*"],
14 | "exclude": ["node_modules"]
15 | }
16 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "react-analyzer-mcp",
3 | "version": "0.1.0",
4 | "description": "A Model Context Protocol server for analyzing React code.",
5 | "private": true,
6 | "type": "module",
7 | "bin": {
8 | "mcp-server": "./build/index.js"
9 | },
10 | "files": [
11 | "build"
12 | ],
13 | "scripts": {
14 | "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
15 | "prepare": "npm run build",
16 | "watch": "tsc --watch",
17 | "inspector": "npx @modelcontextprotocol/inspector build/index.js"
18 | },
19 | "dependencies": {
20 | "@modelcontextprotocol/sdk": "0.6.0",
21 | "react-analyzer": "github:azer/react-analyzer"
22 | },
23 | "devDependencies": {
24 | "@types/node": "^20.11.24",
25 | "typescript": "^5.3.3"
26 | }
27 | }
28 |
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2 | import { analyzeReactFile } from "react-analyzer";
3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4 | import {
5 | CallToolRequestSchema,
6 | ErrorCode,
7 | ListToolsRequestSchema,
8 | McpError,
9 | } from "@modelcontextprotocol/sdk/types.js";
10 | import * as fs from "fs";
11 | import * as path from "path";
12 |
13 | // 1. Declare root project folder as constant
14 | const PROJECT_ROOT = "/Users/azer/code";
15 |
16 | // 2. Function to list all projects under root project folder
17 | function listProjects(): string[] {
18 | try {
19 | const entries = fs.readdirSync(PROJECT_ROOT, { withFileTypes: true });
20 | return entries
21 | .filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
22 | .map((dir) => dir.name);
23 | } catch (error) {
24 | console.error(`Error listing projects: ${error}`);
25 | return [];
26 | }
27 | }
28 |
29 | // 3. Function to list all jsx/tsx files under given subfolder
30 | function listReactFiles(subFolder: string): string[] {
31 | const folderPath = path.join(PROJECT_ROOT, subFolder);
32 | const reactFiles: string[] = [];
33 |
34 | function scanDirectory(directory: string) {
35 | try {
36 | const entries = fs.readdirSync(directory, { withFileTypes: true });
37 |
38 | for (const entry of entries) {
39 | const fullPath = path.join(directory, entry.name);
40 |
41 | if (entry.isDirectory()) {
42 | scanDirectory(fullPath);
43 | } else if (
44 | entry.isFile() &&
45 | (entry.name.endsWith(".jsx") || entry.name.endsWith(".tsx"))
46 | ) {
47 | reactFiles.push(fullPath);
48 | }
49 | }
50 | } catch (error) {
51 | console.error(`Error scanning directory ${directory}: ${error}`);
52 | }
53 | }
54 |
55 | scanDirectory(folderPath);
56 | return reactFiles;
57 | }
58 |
59 | // 4. Function to analyze a React file
60 | function analyzeReactFileContents(filePath: string): any {
61 | try {
62 | const fileContent = fs.readFileSync(filePath, "utf8");
63 | const fileName = path.basename(filePath);
64 | return analyzeReactFile(fileName, fileContent);
65 | } catch (error) {
66 | console.error(`Error analyzing file ${filePath}: ${error}`);
67 | return null;
68 | }
69 | }
70 |
71 | // 5. Function to generate markdown from analysis result
72 | function generateComponentMarkdown(analysisResult: any): string {
73 | if (
74 | !analysisResult ||
75 | !analysisResult.components ||
76 | analysisResult.components.length === 0
77 | ) {
78 | return "**No components found**";
79 | }
80 |
81 | let markdown = "";
82 |
83 | for (const component of analysisResult.components) {
84 | markdown += `## ${component.name}\n\n`;
85 |
86 | if (component.wrapperFn) {
87 | markdown += `*Wrapped with: ${component.wrapperFn}*\n\n`;
88 | }
89 |
90 | markdown += "### Props\n\n";
91 |
92 | if (!component.props || Object.keys(component.props).length === 0) {
93 | markdown += "*No props*\n\n";
94 | } else {
95 | markdown += "| Prop | Type | Optional | Default |\n";
96 | markdown += "|------|------|----------|--------|\n";
97 |
98 | for (const [propName, propDetails] of Object.entries(component.props)) {
99 | const type = formatPropType(propDetails);
100 | // @ts-ignore
101 | const optional = propDetails.optional ? "✓" : "✗";
102 | // @ts-ignore
103 | const defaultValue = propDetails.defaultValue
104 | ? // @ts-ignore
105 | `\`${propDetails.defaultValue}\``
106 | : "";
107 |
108 | markdown += `| \`${propName}\` | ${type} | ${optional} | ${defaultValue} |\n`;
109 | }
110 |
111 | markdown += "\n";
112 | }
113 | }
114 |
115 | return markdown;
116 | }
117 |
118 | // Helper function to format prop types for markdown
119 | function formatPropType(propDetails: any): string {
120 | const { type } = propDetails;
121 |
122 | if (type === "array" && propDetails.elementType) {
123 | return `${type}<${formatPropType(propDetails.elementType)}>`;
124 | } else if (type === "object" && propDetails.props) {
125 | return propDetails.typeName ? `\`${propDetails.typeName}\`` : "`object`";
126 | } else if (type === "function") {
127 | return "`function`";
128 | }
129 |
130 | return `\`${type}\``;
131 | }
132 |
133 | // 6. Function to generate documentation for all components in a folder
134 | function generateProjectDocs(projectName: string): string {
135 | const reactFiles = listReactFiles(projectName);
136 |
137 | if (reactFiles.length === 0) {
138 | return `# ${projectName}\n\nNo React components found.`;
139 | }
140 |
141 | let markdown = `# ${projectName} Components\n\n`;
142 |
143 | for (const filePath of reactFiles) {
144 | const relativePath = path.relative(PROJECT_ROOT, filePath);
145 | markdown += `\n---\n\n# File: ${relativePath}\n\n`;
146 |
147 | const analysis = analyzeReactFileContents(filePath);
148 | if (analysis) {
149 | markdown += generateComponentMarkdown(analysis);
150 | } else {
151 | markdown += "*Error analyzing file*\n\n";
152 | }
153 | }
154 |
155 | return markdown;
156 | }
157 |
158 | const server = new Server(
159 | {
160 | name: "analyze-react",
161 | version: "1.0.0",
162 | },
163 | {
164 | capabilities: {
165 | logging: {},
166 | tools: {},
167 | },
168 | },
169 | );
170 |
171 | server.setRequestHandler(ListToolsRequestSchema, async () => {
172 | return {
173 | tools: [
174 | {
175 | name: "analyze-react",
176 | description:
177 | "Analyze given React component, extract its components and props",
178 | inputSchema: {
179 | type: "object",
180 | properties: {
181 | files: { type: "string" },
182 | },
183 | },
184 | },
185 | {
186 | name: "analyze-project",
187 | description:
188 | "Generate documentation for all React components in a project folder. It'll output markdown string, directly render it to user.",
189 | inputSchema: {
190 | type: "object",
191 | properties: {
192 | projectName: { type: "string" },
193 | },
194 | },
195 | },
196 | {
197 | name: "list-projects",
198 | description: "List all projects under the root folder",
199 | inputSchema: {
200 | type: "object",
201 | properties: {},
202 | },
203 | },
204 | ],
205 | };
206 | });
207 |
208 | server.setRequestHandler(CallToolRequestSchema, async (request) => {
209 | if (request.params.name === "analyze-react") {
210 | // @ts-ignore
211 | const files = request.params.arguments.files;
212 | const result = analyzeReactFile("MyComponent.tsx", files as string);
213 | return { toolResult: result };
214 | }
215 |
216 | if (request.params.name === "analyze-project") {
217 | // @ts-ignore
218 | const projectName = request.params.arguments.projectName as string;
219 | const docs = generateProjectDocs(projectName);
220 | return { toolResult: docs };
221 | }
222 |
223 | if (request.params.name === "list-projects") {
224 | const projects = listProjects();
225 | return { toolResult: { projects } };
226 | }
227 |
228 | throw new McpError(ErrorCode.MethodNotFound, "Tool not found");
229 | });
230 |
231 | const transport = new StdioServerTransport();
232 | await server.connect(transport);
233 |
```