mcp-rn-devtools
v0.5.0
Published
MCP server for React Native runtime debugging — zero-config access to Redux state, AsyncStorage, actions, logs, network, and profiling from your running app
Maintainers
Readme
mcp-rn-devtools
An MCP server that gives Claude (or any MCP host) real-time access to your running React Native app — including your Redux state and AsyncStorage, with zero app changes. Console logs, errors, network, state, storage, action log, navigation, performance profiling, and more.
Why this one
- True zero-config state access. A runtime agent injected over the Chrome DevTools Protocol walks the React fiber tree to discover your Redux store — no SDK, no middleware, no exposing the store on a global. Reading AsyncStorage works the same way (native module proxy). Install the server, ask Claude about your state.
- Headless. No desktop app to keep open, no toggle to remember. Works in fully autonomous agent workflows and CI.
- Secrets redacted by default. Tokens, passwords, auth headers, and JWT-shaped strings are masked server-side before anything reaches the LLM (
MCP_RN_NO_REDACT=1to opt out). - Built for agent loops.
clear_buffers→ reproduce → read.wait_for_logblocks until the app emits a matching log.get_state_diffshows exactly what changed between two moments.
How It Works
Claude / MCP Host
│ MCP (stdio)
▼
mcp-rn-devtools (server)
├── CDP WebSocket ──► RN App (Hermes / Metro) ← zero config
│ └── runtime agent (injected): Redux discovery,
│ AsyncStorage, action log, navigation
└── SDK WebSocket ◄── mcp-rn-devtools-sdk ← optional enhancerLayer 1 — CDP + runtime agent (zero config): Connects via Chrome DevTools Protocol through Metro's debugger proxy. Captures console logs, errors, warnings, and network requests, and injects a runtime agent that discovers Redux stores / React Navigation / React Query by walking the fiber tree, reads and writes AsyncStorage through the native module proxy, and records every dispatched action. Also provides JS evaluation, memory/CPU profiling, and source map resolution. No app changes needed.
Layer 2 — SDK (optional enhancer): Install mcp-rn-devtools-sdk for what the agent can't reach: Zustand/custom stores, MMKV, per-component render profiling, navigation timing, and a second capture channel that survives CDP drops.
Installation
With Claude Code
claude mcp add rn-devtools -- npx -y mcp-rn-devtoolsOr add .mcp.json to your project root (shared with your team):
{
"mcpServers": {
"rn-devtools": {
"command": "npx",
"args": ["-y", "mcp-rn-devtools"]
}
}
}With Claude Desktop
Add to your claude_desktop_config.json:
{
"mcpServers": {
"rn-devtools": {
"command": "npx",
"args": ["-y", "mcp-rn-devtools"]
}
}
}Manual
npm install -g mcp-rn-devtools
mcp-rn-devtools --versionQuick Start
- Start your React Native app — Metro must be running, Hermes engine (default since RN 0.70).
- Add the MCP server to your Claude config (see above).
- Ask Claude about your app:
- "What's in my Redux store right now?"
- "Read the AsyncStorage key persist:root"
- "Clear the buffers, I'll reproduce the bug — then show me what happened"
- "Dispatch auth/logout and show me the state diff"
- "Profile the CPU for 3 seconds and show me the hot functions"
Available Tools
Source legend: agent = zero config, injected via CDP. CDP = zero config, protocol-level. SDK = requires
mcp-rn-devtools-sdkin the app. Read-only tools are annotated withreadOnlyHintso MCP hosts can auto-allow them.
State & Actions
| Tool | Source | Description |
|------|--------|-------------|
| get_app_state | agent + SDK | Redux store state with dot-path access and depth control. Stores discovered automatically |
| get_state_diff | agent + SDK | What changed in the store since the last call (baseline → diff workflow) |
| get_action_log | agent + SDK | Dispatched actions with duration and changed slices — recorded automatically, no middleware |
| dispatch_action | agent | Dispatch a Redux action to reproduce states or trigger flows |
Storage
| Tool | Source | Description |
|------|--------|-------------|
| get_storage_keys | agent + SDK | List AsyncStorage (zero-config) or MMKV (SDK) keys with search |
| get_storage_value | agent + SDK | Read a storage value — secrets redacted by default |
Logging & Errors
| Tool | Source | Description |
|------|--------|-------------|
| get_console_logs | CDP + SDK | Console output with level filter and search; capped for the LLM by default, full=true for everything |
| get_errors | CDP + SDK | JS errors and exceptions with stack traces (5 frames by default, full=true for all) |
| get_warnings | CDP + SDK | LogBox warnings from console.warn, deduplicated across debugger reconnects |
| wait_for_log | CDP + SDK | Block until a log matching a pattern appears — synchronize with app activity |
Network
| Tool | Source | Description |
|------|--------|-------------|
| get_network_requests | CDP + SDK | HTTP requests; verbose adds headers/bodies with secrets redacted, capped unless full=true |
| get_failed_requests | CDP + SDK | Requests with status >= 400 or network errors, response bodies capped unless full=true |
Diagnostics
| Tool | Source | Description |
|------|--------|-------------|
| health_check | — | Verdict first — READY or BLOCKED: cause → fix — then version, debugger owner, stores, counts |
| list_targets | — | Targets on every Metro port (8081–8085) with who holds each one (→ this instance, ⊙ another instance, ✗ library runtime) |
| select_target | — | Pin a target by id (optionally with metro_port) or by app id prefix; takes it from the instance holding it; library runtimes (Reanimated) are refused |
| clear_buffers | — | Reset captured data before reproducing a scenario |
Navigation
| Tool | Source | Description |
|------|--------|-------------|
| get_navigation_state | agent + SDK | Current route and stack (React Navigation), discovered automatically |
| get_navigation_timing | SDK | Screen transition timing with per-route summary |
Memory & Performance
| Tool | Source | Description |
|------|--------|-------------|
| get_memory_usage | CDP | Current heap usage (used / total / percentage) |
| take_heap_snapshot | CDP | Heap snapshot summary — object count, top retainers by size |
| get_cpu_profile | CDP | CPU profile for N seconds — hot functions sorted by self time |
| force_gc | CDP | Trigger garbage collection, return before/after heap comparison |
| get_render_profile | SDK | Component render events — mount/update durations, slow renders |
Advanced
| Tool | Source | Description |
|------|--------|-------------|
| evaluate_js | CDP | Execute JavaScript in the app: global and globalThis both work, await_promise settles Promises in-app |
| resolve_source_location | CDP | Resolve bundled line:column to original source via Metro source maps |
Building on top of the core
The server exports its building blocks (ConnectionManager, SDKBridgeServer, the injected agent bridge, adb helpers) so tools can extend it in the same process — Hermes admits a single debugger, so extensions must share the CDP session rather than compete for it. Unrecognized SDK-channel messages are re-emitted as sdk-message events, and sendToClient() lets extensions answer.
The first product built this way is tapfix: a live QA loop for React Native (mark issues on-device or from a cockpit web UI, and an embedded coding agent fixes them on the fly). Its MCP server is a superset of this one.
Multiple sessions and multiple apps
Hermes admits one debugger per app instance, and every Claude Code session starts its own mcp-rn-devtools process. Ownership is settled per app on a device, not per machine:
- Sessions on different apps coexist. Two emulators, two apps, two sessions: each holds its own debugger. Instances register in
~/.mcp-rn-devtools/instances/and ask each other to yield through a per-instance control endpoint — only the one holding that app is asked. - The debugger follows the session in use for the same app. An instance attaches on its first tool call (
lazy, the default); if a sibling holds that app, it yields and gets it back on its own next tool call. An instance kicked by a sibling does not fight back. - A session attaches to its own app. The server reads the app id from the repo it runs in (
android/app/build.gradleapplicationId, the iOS bundle id, or Expo'sapp.json;MCP_RN_APPoverrides) and finds that app across Metro ports 8081–8085 — a session in the wms repo attaches to wms even while picking runs on 8081.select_targetalso takes anappid. - No orphans. The server exits when its MCP client goes away (stdin closes or the parent process dies).
health_checkshows the session app, who holds the debugger, the other instances and their apps, and who serves the SDK channel.list_targetsmarks targets held by other instances with ⊙.
Extensions that record continuously (tapfix) opt into connectMode: 'eager'. Instances older than 0.5 (no registry) are still asked to yield over the SDK port.
Secret Redaction
Every tool that outputs app data (state, storage values, network headers/bodies, action payloads) masks secrets server-side, before the data reaches the LLM:
- Values under sensitive keys (
token,password,authorization,apiKey,session,cookie, …) →[REDACTED] - JWT-shaped strings and
Bearer …tokens anywhere in text → masked
Redaction is a blocklist (defence in depth, not a guarantee) — audit what your app stores before pointing any LLM at it. Opt out with MCP_RN_NO_REDACT=1.
SDK Setup (optional)
The runtime agent covers Redux, AsyncStorage, actions, and navigation with zero config. Install the SDK only if you need Zustand/custom stores, MMKV, render profiling, or navigation timing:
npm install mcp-rn-devtools-sdk --save-devimport { RNDevtoolsProvider } from 'mcp-rn-devtools-sdk';
export default function App() {
return (
<RNDevtoolsProvider>
<YourApp />
</RNDevtoolsProvider>
);
}With Zustand / custom stores
const useAuthStore = create((set) => ({ /* ... */ }));
<RNDevtoolsProvider stateManagers={{ auth: useAuthStore }}>
<YourApp />
</RNDevtoolsProvider>State snapshots are pull-only: they're serialized only when a tool asks, so the SDK adds zero overhead while idle.
With MMKV
import { MMKV } from 'react-native-mmkv';
const storage = new MMKV();
<RNDevtoolsProvider mmkv={storage}>
<YourApp />
</RNDevtoolsProvider>Per-Component Render Profiling
import { RNDevtoolsProfiler } from 'mcp-rn-devtools-sdk';
<RNDevtoolsProfiler id="UserList">
<UserList />
</RNDevtoolsProfiler>Provider Props
| Prop | Type | Description |
|------|------|-------------|
| navigationRef | RefObject | React Navigation container ref for richer route tracking |
| stateManagers | Record<string, StateStore> | Zustand/custom stores for state inspection |
| reduxMiddlewares | DevtoolsMiddleware[] | Middlewares created via createDevtoolsMiddleware() |
| asyncStorage | AsyncStorageLike | AsyncStorage instance (also available zero-config via agent) |
| mmkv | MMKVLike | MMKV instance for storage reading |
| port | number | WebSocket port (default: 8098) |
| host | string | Dev machine host — auto-detected from the bundle URL (SourceCode.scriptURL), works on emulators, physical devices, and adb reverse |
The SDK automatically strips itself from production builds via
__DEV__checks — zero overhead in release.
Configuration
| Environment Variable | Default | Description |
|---------------------|---------|-------------|
| METRO_PORT | 8081 | Metro bundler port (when it has no app, ports 8082–8085 are checked too) |
| SDK_PORT | 8098 | SDK WebSocket port — also marks which instance owns the debugger |
| MCP_RN_CONNECT | lazy | lazy attaches on the first tool call; eager attaches at startup |
| MCP_RN_APP | from the repo | App id prefix(es) this session works on, comma-separated (overrides the cwd detection) |
| MCP_RN_STATE_DIR | ~/.mcp-rn-devtools/instances | Where instances register themselves for per-app ownership |
| MCP_RN_NO_REDACT | - | Disable secret redaction |
| MCP_RN_NO_UPDATE_CHECK | - | Skip the npm "newer version" lookup in health_check |
| MCP_RN_DEBUG | - | Enable debug logging |
Architecture Notes
- Target selection: RN 0.76+ (Fusebox) no longer advertises
vm: 'Hermes'— the server picks the main runtime byreactNative.capabilities.prefersFuseboxFrontendand skips secondary runtimes like Reanimated's. Legacy targets still work via thevmfield. - Kick-and-poll: CDP's
awaitPromisecan't resolve React Native's polyfilled Promises, so async in-app operations (AsyncStorage) fire a callback that writes to a result slot, which the server polls. - Clock skew: log/error entries carry a server-clock
receivedAt— device clocks can drift seconds from the host, which would break "wait for new logs" cuts. - Reconnection: exponential backoff, agent re-injected automatically after every bundle reload. A pinned target that keeps failing is unpinned after 3 attempts.
- Console replay:
Runtime.enablereplays the runtime's console backlog on every reconnect; entries are deduplicated by (timestamp, message) so warnings do not inflate. globalin the evaluate scope: Hermes exposesglobalThisbut notglobaltoRuntime.evaluate(Metro only passesglobalto module factories). Injected scripts resolve the global object themselves andevaluate_jsaliasesglobalfor the duration of the call.- Multi-Metro: a known session app is searched across ports 8081–8085 regardless of what the configured port serves. Without a session app: when nothing listens on the configured port and exactly one other port has an app, the server switches to it; a running Metro without an app is left alone (the app is about to appear there), and with several candidates it asks you to
select_target. - Target identity: ownership is keyed by
reactNative.logicalDeviceId(RN 0.73+: a stable hash of app + device, shared by the app's main and Reanimated runtimes), falling back to app id + device name. - Action log caveat: the agent wraps
store.dispatchat discovery time; components that captured a directdispatchreference before discovery bypass the log (rare — discovery runs at connect).
Compatibility
- React Native: 0.71+ (validated against 0.80 / bridgeless / Fusebox)
- Engine: Hermes (default since RN 0.70)
- Platforms: iOS, Android
- Node.js: 20+
- MCP Hosts: Claude Code, Claude Desktop, or any MCP-compatible client
Development
mise install # node 22 + pnpm 9 from .mise.toml (pnpm 10+ breaks the build)
pnpm install
pnpm build # shared → server → sdk
pnpm test # vitest, includes fake Metro/Hermes integration tests
pnpm typecheck && pnpm lintRelease: bump version in the three packages/*/package.json, push, and publish a GitHub Release — publish.yml runs CI and pnpm -r publish.
License
MIT
