Skip to main content
Glama
README.md
<div align="center">

# react-profiler-mcp

**Give your AI agent real React performance data. Get specific fixes, not guesses.**

[![CI](https://github.com/UmarHassanKhan929/react-profiler-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/YOUR_USERNAME/react-profiler-mcp/actions)
[![npm collector](https://img.shields.io/npm/v/@react-profiler-mcp/react-collector?label=%40react-profiler-mcp%2Freact-collector&color=cb3837)](https://www.npmjs.com/package/@react-profiler-mcp/react-collector)
[![npm server](https://img.shields.io/npm/v/@react-profiler-mcp/mcp-server?label=%40react-profiler-mcp%2Fmcp-server&color=cb3837)](https://www.npmjs.com/package/@react-profiler-mcp/mcp-server)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![MCP Compatible](https://img.shields.io/badge/MCP-compatible-8a2be2)](https://modelcontextprotocol.io)

<br />

```
"Which components are making my app slow, and how do I fix them?"
```

*— a question your AI agent can now answer with real data*

</div>

---

## The problem

React DevTools Profiler gives you raw render timings. But staring at a flame graph and knowing *exactly what to change in your code* are two different things. Most developers either ignore the data or spend hours guessing.

**react-profiler-mcp streams live profiler data from your app directly to your AI agent** — so instead of a flame graph you can't read, you get this:

```
ProductList re-renders 47× on this page, averaging 68ms per render.

The root cause is onAddToCart being recreated on every parent render,
which breaks React.memo() on the child. Fix:

  const onAddToCart = useCallback((id) => {
    dispatch({ type: 'ADD', id });
  }, [dispatch]);

That alone should drop renders from 47 to 3.
```

---

## How it works

```
Your React app                      AI agent
─────────────────                   ────────────────────────
<ProfilerBridge>    →  POST  →    HTTP ingest (:8787)
  records every                          │
  render commit                    in-memory store
                                         │
                                   MCP stdio server
                                         │
                              "analyze_performance" tool
                                         │
                              Cursor / Claude / Copilot
                              reads real samples and
                              gives you targeted fixes
```

One local Node process handles both sides. The browser POSTs render samples over loopback; your editor connects to the same process over MCP stdio.

---

## Works with any MCP-compatible agent

| Editor / Agent | Setup |
|----------------|-------|
| **Cursor** | Add to `.cursor/mcp.json` |
| **Claude Desktop** | Add to `claude_desktop_config.json` |
| **Windsurf** | Add to MCP settings |
| **GitHub Copilot** (VS Code) | Add to `.vscode/mcp.json` |
| **Gemini CLI** | Add to `~/.gemini/settings.json` |
| Any MCP client | Same config, same server |

---

## Quickstart

### 1. Install the collector

```bash
npm install @react-profiler-mcp/react-collector
```

### 2. Wrap your app

```tsx
import { ProfilerBridge } from '@react-profiler-mcp/react-collector';

export function App() {
  return (
    <ProfilerBridge
      ingestUrl="http://127.0.0.1:8787/v1/profile-samples"
      sessionId="my-app"
      profilerId="main-shell"
    >
      <YourApp />
    </ProfilerBridge>
  );
}
```

### 3. Add the MCP server to your editor

**Cursor / Windsurf / VS Code** — add to your project's `.cursor/mcp.json` or `.vscode/mcp.json`:

```json
{
  "mcpServers": {
    "react-profiler": {
      "command": "npx",
      "args": ["-y", "@react-profiler-mcp/mcp-server"]
    }
  }
}
```

<details>
<summary>Claude Desktop</summary>

`~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or
`%APPDATA%\Claude\claude_desktop_config.json` (Windows):

```json
{
  "mcpServers": {
    "react-profiler": {
      "command": "npx",
      "args": ["-y", "@react-profiler-mcp/mcp-server"]
    }
  }
}
```

</details>

<details>
<summary>Gemini CLI</summary>

`~/.gemini/settings.json`:

```json
{
  "mcpServers": {
    "react-profiler": {
      "command": "npx",
      "args": ["-y", "@react-profiler-mcp/mcp-server"]
    }
  }
}
```

</details>

### 4. Use your app, then ask

Open your React app and interact with it normally for 30–60 seconds. Then in your AI agent's chat:

```
Analyze my React app's performance. Which components are
the worst offenders and what exactly should I change?
```

The agent calls the profiler tools, reads the real samples from your session, and gives you specific fixes — component names, line-level suggestions, and why each change helps.

---

## Next.js

`ProfilerBridge` uses client hooks. Keep it inside a Client Component:

```tsx
'use client';

import { ProfilerBridge } from '@react-profiler-mcp/react-collector';

const ingestUrl =
  process.env.NEXT_PUBLIC_PROFILER_INGEST_URL ?? 'http://127.0.0.1:8787/v1/profile-samples';

export default function Page() {
  return (
    <ProfilerBridge ingestUrl={ingestUrl} sessionId="my-next-app" profilerId="page-root">
      {/* page content */}
    </ProfilerBridge>
  );
}
```

Add to `next.config.js`:

```js
const nextConfig = {
  transpilePackages: ['@react-profiler-mcp/react-collector'],
};
export default nextConfig;
```

---

## ProfilerBridge props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `ingestUrl` | `string` | — | **Required.** Full URL to POST samples to. |
| `sessionId` | `string` | *(omit)* | If set, sent as `X-React-Profiler-Session`. If omitted, the server stores samples under **`default`** — match that in MCP tools like `session_summary`, or set a custom id here and pass the same string as `sessionId` in tools. |
| `profilerId` | `string` | `"react-profiler-mcp-root"` | React `<Profiler id={...}>`. Becomes `componentName` in ingest/MCP. Prefer **distinct** ids per subtree you care about (`"checkout-form"`, `"data-table"`). |
| `enabled` | `boolean` | `true` | Set `false` to disable profiling and HTTP traffic (e.g. production). |

---

## MCP tools

The agent can call these tools once connected (names match the server in `packages/mcp-server/src/mcp/server.ts`):

| Tool | What it returns |
|------|----------------|
| `list_sessions` | All ingest buckets: `sessionId`, counts, timestamps |
| `get_profiler_data` | Raw sample tail (ids, phases, durations); optional `sessionId` defaults to **most recently updated** session |
| `get_component_summary` | Per **Profiler id** (`profilerId`) stats, sortable |
| `get_slow_renders` | Renders over `thresholdMs` (default 16ms) |
| `analyze_performance` | Composite report: offenders, re-renders, heuristic suggestions |
| `clear_data` | Clears all in-memory sessions |
| `session_summary` | Aggregate stats for one session (`sessionId` defaults to `default` if omitted) |
| `list_recent_samples` | Recent raw rows for citations |
| `explain_jank` | Heuristic jank signals over a time window |
| `suggest_fixes` | Ranked remediation ideas tied to captured labels |

---

## ⚠️ One process rule

Run **either** a manually started server **or** the one your editor spawns — not both. Two processes means two separate in-memory stores. MCP will connect to one; the browser posts to the other. Data never meets.

**stdio MCP:** Your editor usually **spawns** the server (`npx` / `node …`) and owns stdin/stdout for the protocol. You generally **do not** attach MCP to a server you already started in a separate terminal (that process’s stdio is tied to the shell). For logs, rely on **stderr** from the editor-spawned process, or run a **second** terminal only for HTTP debugging (accepting that MCP in the editor will use a **different** store unless you use a single process — see [docs/MCP_USAGE.md](docs/MCP_USAGE.md)).

**Quick check** — the process logs the ingest URL on **stderr** (stdout is reserved for MCP when an editor spawns it):

```bash
npx -y @react-profiler-mcp/mcp-server
```

---

## Notes

- **Production:** React’s `<Profiler>` still runs `onRender` in normal production builds (with some overhead). For shipped apps, set **`enabled={false}`** (or omit `ProfilerBridge`) unless you deliberately want field metrics.
- **In-memory store.** Data resets when the server restarts. This is intentional — it's a dev tool, not a database.
- **Loopback only.** The ingest listener binds to **`127.0.0.1`** (see `packages/mcp-server/src/index.ts`). Override port with env **`PORT`**; do not expose the port publicly.

---

## Packages

| Package | npm | Description |
|---------|-----|-------------|
| `@react-profiler-mcp/react-collector` | [![npm](https://img.shields.io/npm/v/@react-profiler-mcp/react-collector)](https://npmjs.com/package/@react-profiler-mcp/react-collector) | React component — goes in your UI bundle |
| `@react-profiler-mcp/mcp-server` | [![npm](https://img.shields.io/npm/v/@react-profiler-mcp/mcp-server)](https://npmjs.com/package/@react-profiler-mcp/mcp-server) | Local server — HTTP ingest + MCP stdio |

---

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md). Issues and PRs welcome.

To run locally:

```bash
git clone https://github.com/YOUR_USERNAME/react-profiler-mcp.git
cd react-profiler-mcp
npm install
npm run build
node packages/mcp-server/dist/index.js
```

Then in another terminal:

```bash
npm run dev -w @react-profiler-mcp/demo
```

Full contributor guide: [docs/LOCAL_DEVELOPMENT.md](docs/LOCAL_DEVELOPMENT.md)

---

## License

MIT — see [LICENSE](LICENSE).