# Directory Structure
```
├── .gitignore
├── package-lock.json
├── package.json
├── README.md
├── src
│ └── index.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
build
node_modules
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# react-analyzer-mcp
Analyze & generate docs for React code using the Model Context Protocol. Based on [react-analyzer](https://github.com/azer/react-analyzer) library.
[<img src="https://github.com/user-attachments/assets/615f43d7-9b81-4480-9a2a-773819223ddb" width="500" />](https://x.com/azerkoculu/status/1910071779457900866)
## What it does
This tool analyzes React component files (JSX/TSX) and extracts information about components and their props.
## Available Tools
- **analyze-react**: Analyzes a single React component from source code
- **analyze-project**: Generates documentation for all React components in a project folder
- **list-projects**: Lists all projects under the root folder
## Installation
```bash
# Clone the repository
git clone https://github.com/azer/react-analyzer-mcp.git
cd react-analyzer-mcp
# Install dependencies
npm install
# Update PROJECT_ROOT in the index.ts file.
vim src/index.ts
# Build
npm run build
```
## Using with Claude
1. Enable MCP server in the Claude Desktop config:
```bash
{
"react-analyzer-mcp": {
"command": "node",
"args": [
"/Users/azer/code/sandbox/react-analyzer-mcp/build/index.js"
]
}
}
```
2. Connect Claude to your MCP server using the Claude Shell.
3. Use the tools directly in Claude conversations:
```
Analyze my project's React components in the "ui" folder.
```
Or:
```
What React components do I have in my project?
```
## Examples
### Analyzing a project folder:
Input:
```
Can you analyze the components in my "foobar" folder?
```
Output:
```
# Components
## Button
### Props
| Prop | Type | Optional | Default |
|------|------|----------|---------|
| `variant` | `string` | ✓ | `primary` |
| `size` | `string` | ✓ | `md` |
| `onClick` | `function` | ✓ | |
...
```
## License
MIT
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "react-analyzer-mcp",
"version": "0.1.0",
"description": "A Model Context Protocol server for analyzing React code.",
"private": true,
"type": "module",
"bin": {
"mcp-server": "./build/index.js"
},
"files": [
"build"
],
"scripts": {
"build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
"prepare": "npm run build",
"watch": "tsc --watch",
"inspector": "npx @modelcontextprotocol/inspector build/index.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "0.6.0",
"react-analyzer": "github:azer/react-analyzer"
},
"devDependencies": {
"@types/node": "^20.11.24",
"typescript": "^5.3.3"
}
}
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { analyzeReactFile } from "react-analyzer";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
} from "@modelcontextprotocol/sdk/types.js";
import * as fs from "fs";
import * as path from "path";
// 1. Declare root project folder as constant
const PROJECT_ROOT = "/Users/azer/code";
// 2. Function to list all projects under root project folder
function listProjects(): string[] {
try {
const entries = fs.readdirSync(PROJECT_ROOT, { withFileTypes: true });
return entries
.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
.map((dir) => dir.name);
} catch (error) {
console.error(`Error listing projects: ${error}`);
return [];
}
}
// 3. Function to list all jsx/tsx files under given subfolder
function listReactFiles(subFolder: string): string[] {
const folderPath = path.join(PROJECT_ROOT, subFolder);
const reactFiles: string[] = [];
function scanDirectory(directory: string) {
try {
const entries = fs.readdirSync(directory, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(directory, entry.name);
if (entry.isDirectory()) {
scanDirectory(fullPath);
} else if (
entry.isFile() &&
(entry.name.endsWith(".jsx") || entry.name.endsWith(".tsx"))
) {
reactFiles.push(fullPath);
}
}
} catch (error) {
console.error(`Error scanning directory ${directory}: ${error}`);
}
}
scanDirectory(folderPath);
return reactFiles;
}
// 4. Function to analyze a React file
function analyzeReactFileContents(filePath: string): any {
try {
const fileContent = fs.readFileSync(filePath, "utf8");
const fileName = path.basename(filePath);
return analyzeReactFile(fileName, fileContent);
} catch (error) {
console.error(`Error analyzing file ${filePath}: ${error}`);
return null;
}
}
// 5. Function to generate markdown from analysis result
function generateComponentMarkdown(analysisResult: any): string {
if (
!analysisResult ||
!analysisResult.components ||
analysisResult.components.length === 0
) {
return "**No components found**";
}
let markdown = "";
for (const component of analysisResult.components) {
markdown += `## ${component.name}\n\n`;
if (component.wrapperFn) {
markdown += `*Wrapped with: ${component.wrapperFn}*\n\n`;
}
markdown += "### Props\n\n";
if (!component.props || Object.keys(component.props).length === 0) {
markdown += "*No props*\n\n";
} else {
markdown += "| Prop | Type | Optional | Default |\n";
markdown += "|------|------|----------|--------|\n";
for (const [propName, propDetails] of Object.entries(component.props)) {
const type = formatPropType(propDetails);
// @ts-ignore
const optional = propDetails.optional ? "✓" : "✗";
// @ts-ignore
const defaultValue = propDetails.defaultValue
? // @ts-ignore
`\`${propDetails.defaultValue}\``
: "";
markdown += `| \`${propName}\` | ${type} | ${optional} | ${defaultValue} |\n`;
}
markdown += "\n";
}
}
return markdown;
}
// Helper function to format prop types for markdown
function formatPropType(propDetails: any): string {
const { type } = propDetails;
if (type === "array" && propDetails.elementType) {
return `${type}<${formatPropType(propDetails.elementType)}>`;
} else if (type === "object" && propDetails.props) {
return propDetails.typeName ? `\`${propDetails.typeName}\`` : "`object`";
} else if (type === "function") {
return "`function`";
}
return `\`${type}\``;
}
// 6. Function to generate documentation for all components in a folder
function generateProjectDocs(projectName: string): string {
const reactFiles = listReactFiles(projectName);
if (reactFiles.length === 0) {
return `# ${projectName}\n\nNo React components found.`;
}
let markdown = `# ${projectName} Components\n\n`;
for (const filePath of reactFiles) {
const relativePath = path.relative(PROJECT_ROOT, filePath);
markdown += `\n---\n\n# File: ${relativePath}\n\n`;
const analysis = analyzeReactFileContents(filePath);
if (analysis) {
markdown += generateComponentMarkdown(analysis);
} else {
markdown += "*Error analyzing file*\n\n";
}
}
return markdown;
}
const server = new Server(
{
name: "analyze-react",
version: "1.0.0",
},
{
capabilities: {
logging: {},
tools: {},
},
},
);
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "analyze-react",
description:
"Analyze given React component, extract its components and props",
inputSchema: {
type: "object",
properties: {
files: { type: "string" },
},
},
},
{
name: "analyze-project",
description:
"Generate documentation for all React components in a project folder. It'll output markdown string, directly render it to user.",
inputSchema: {
type: "object",
properties: {
projectName: { type: "string" },
},
},
},
{
name: "list-projects",
description: "List all projects under the root folder",
inputSchema: {
type: "object",
properties: {},
},
},
],
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "analyze-react") {
// @ts-ignore
const files = request.params.arguments.files;
const result = analyzeReactFile("MyComponent.tsx", files as string);
return { toolResult: result };
}
if (request.params.name === "analyze-project") {
// @ts-ignore
const projectName = request.params.arguments.projectName as string;
const docs = generateProjectDocs(projectName);
return { toolResult: docs };
}
if (request.params.name === "list-projects") {
const projects = listProjects();
return { toolResult: { projects } };
}
throw new McpError(ErrorCode.MethodNotFound, "Tool not found");
});
const transport = new StdioServerTransport();
await server.connect(transport);
```