Skip to content

feat: add MCP server for Claude Code integration - #1598

Merged
joshuayoes merged 29 commits into
infinitered:masterfrom
kbrandwijk:feat/mcp-server
Mar 30, 2026
Merged

feat: add MCP server for Claude Code integration#1598
joshuayoes merged 29 commits into
infinitered:masterfrom
kbrandwijk:feat/mcp-server

Conversation

@kbrandwijk

Copy link
Copy Markdown
Contributor

Please verify the following:

  • yarn build-and-test:local passes
  • I have added tests for any new features, if relevant
  • README.md (or relevant documentation) has been updated with your changes

Describe your PR

Adds a built-in MCP server to Reactotron, allowing AI coding assistants like Claude Code to read debug events and send commands to connected React Native / React apps.

What it does

New package: reactotron-mcp (lib/reactotron-mcp/)

A self-contained MCP server that receives the reactotron-core-server instance and exposes its data as MCP resources and tools over HTTP.

Resources (read-only debug data):

  • Timeline — last 500 events (logs, state changes, network, benchmarks, custom commands)
  • App state — latest cached Redux/MST snapshot
  • Network log — captured HTTP request/response pairs
  • Connected apps — with platform, version, and clientId
  • Benchmarks — performance benchmark results
  • State subscriptions — values at subscribed state paths
  • AsyncStorage — mutations (setItem, removeItem, etc.)

Tools (interact with running app):

  • dispatch_action — dispatch Redux actions with confirmation polling
  • request_state — request fresh state snapshot (1.5s timeout)
  • swap_state — hot-swap entire state tree
  • send_custom_command — trigger registered custom commands
  • list_custom_commands — discover available custom commands
  • show_overlay — image overlay with local file→base64 conversion and dimension extraction
  • subscribe_state / unsubscribe_state — watch state paths for changes
  • clear_timeline — clear MCP event buffer

Architecture

React Native app
    | WebSocket (port 9090, unchanged)
    v
Reactotron Desktop
    ├── relay server (reactotron-core-server, unchanged)
    └── MCP server (reactotron-mcp, HTTP on configurable port)
          ↑
Claude Code
  • No changes to the React Native app or relay protocol
  • MCP server reads directly from the server instance's connections and event emitter
  • Built with tsup (Node.js server, not a React Native lib)
  • Bundles @modelcontextprotocol/sdk (its CJS exports are broken)
  • Stateless per-request HTTP transport (required by MCP SDK)

Desktop app changes

  • "MCP" toggle button in the footer bar with green status indicator
  • MCP port configurable via electron-store (default: 4567, localhost only)
  • reactotron-mcp is NOT whitelisted in electron-webpack — loaded via Node.js at runtime

Multi-app support

When multiple apps are connected, resources include _meta hints guiding Claude to ask the user which app they're working on. Single-app connections are auto-selected.

Setup

claude mcp add --transport http reactotron http://localhost:4567/mcp

Tests

20 integration tests covering resources, tools, multi-app handling, and edge cases.

Documentation

Added docs/mcp.md with getting started guide, feature overview, and architecture docs.

kbrandwijk and others added 20 commits March 19, 2026 18:29
Add `reactotron-mcp` package that exposes Reactotron's debug data as an
MCP server. Claude Code connects directly via HTTP to read timeline events,
app state, network requests, and send commands to connected apps.

- New `lib/reactotron-mcp/` package with MCP resources and tools
- MCP toggle button in desktop app footer (green dot status indicator)
- Resources: timeline, state, network, apps, benchmarks
- Tools: dispatch_action, request_state, swap_state, send_custom_command
- Uses @modelcontextprotocol/sdk with StreamableHTTP transport
- No changes to reactotron-core-server or client packages
- Replace react-native-builder-bob with tsup (this is a Node.js server, not a React Native lib)
- Bundle @modelcontextprotocol/sdk into the output to avoid CJS/ESM resolution issues
- Use the new McpServer.registerResource/registerTool API instead of deprecated Server
- Use zod schemas for tool input validation
- Remove stale .d.ts files from src/
…acks state plugin

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tools:
- dispatch_action: polls for confirmation (state.action.complete)
- request_state: filters response by clientId, faster timeout
- send_custom_command: description guides to list_custom_commands first
- New: list_custom_commands — parses customCommand.register events
- New: clear_timeline — clears command buffer
- New: show_overlay — sends image overlay to app with file→base64
  conversion, PNG/JPEG/GIF dimension extraction, and all defaults
  matching the desktop app's behavior

Resources:
- Improved descriptions to guide Claude (read timeline first, etc.)
- State resource clarifies staleness, points to request_state tool

Also:
- Fix dynamic import("fs") crash in Electron renderer — use static imports
- Add file logging to /tmp/reactotron-mcp.log for debugging
- Add uncaughtException/unhandledRejection handlers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Resources:
- reactotron://state/subscriptions — shows active subscriptions and
  state.values.change events from subscribed paths
- reactotron://asyncstorage — shows all AsyncStorage mutations
  (setItem, removeItem, etc.) captured from the app

Tools:
- subscribe_state — subscribe to a state path via server.stateValuesSubscribe()
- unsubscribe_state — unsubscribe via server.stateValuesUnsubscribe()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add 20 integration tests covering MCP lifecycle, all resources,
  and all tools (using real relay server + mock app WebSocket)
- Fix zod v3/v4 type mismatch: add zod ^4.3.0 as direct dependency,
  import from zod/v4 (matching the MCP SDK's usage)
- Flatten dispatch_action schema (actionType/actionPayload instead
  of nested object) for zod v4 compatibility
- Remove unused ReactotronMcpOptions interface
- Remove process.on listener leak (was adding per start() call)
- Update CLAUDE.md to reflect current implementation status

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Security:
- Bind HTTP server to 127.0.0.1 only (was 0.0.0.0, exposed on LAN)
- Remove wildcard CORS headers (Claude Code doesn't need them)

Correctness:
- start() returns Promise, rejects on port bind failure
- mcpStatus only set to "started" after listen succeeds, "error" on failure
- Clean up MCP server on Electron window close (useEffect cleanup)
- Set started=true synchronously to prevent double-start race
- MCP port configurable via electron-store (mcpPort, default 4567)
- Fix opacity description (0.25 → 0.5 to match actual default)
- Add JPEG header magic byte validation in dimension parser
- Resource json() helper now passes through correct URI per MCP spec

Cleanup:
- Remove all file-based debug logging (/tmp/reactotron-mcp.log)
- Remove toolLog and appendFileSync imports
- Remove unused ReactotronMcpOptions interface

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Feature documentation belongs in docs/, not as a CLAUDE.md in the project root.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Match the style of existing Reactotron docs — Docusaurus frontmatter,
conversational tone, code examples, and practical usage guide.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
joshuayoes and others added 2 commits March 24, 2026 10:37
reactotron-mcp is an internal MCP server loaded at runtime by the
desktop app — it should not be published to npm. This marks it
private and updates the validation and release scripts to skip
private packages gracefully.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@joshuayoes

Copy link
Copy Markdown
Contributor

Pushed two commits to fix CI failures:

a0436ea9 — mark reactotron-mcp as private, skip private packages in CI

reactotron-mcp is an internal MCP server loaded at runtime by the desktop app — it's not published to npm. Marked it "private": true and updated two scripts:

  • scripts/package.validate.mjs — skips private packages during yarn package:validate (with log output so it's visible in CI)
  • scripts/release.artifacts.mjs — exits cleanly if a tagged package is private, instead of failing on yarn npm publish

0426ace2 — add missing MCP props to Footer storybook stories

The Footer component gained mcpStatus, mcpPort, and onToggleMcp props but the Storybook stories weren't updated.


Note: I used yarn ci:trust to push to a temp-ci-trusted-fork branch so CircleCI would run on the fork PR — standard process for reviewing community PRs.

I'll be testing things locally from here. Planning to tighten up error handling since the MCP client can send arbitrary payloads to the tools/resources, and we should validate defensively at that boundary.

Wraps all timeline commands in an error boundary via buildTimelineCommand.
If a command component throws during render (e.g. unexpected payload shape),
the boundary catches it and renders a RENDER ERROR timeline entry showing
the error message and raw payload, instead of crashing the desktop app.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@joshuayoes

joshuayoes commented Mar 24, 2026

Copy link
Copy Markdown
Contributor

Screenshot of new error component when Reactotron tries to render a bad timeline payload (which my testing with Claude did pretty quick)

Screenshot 2026-03-24 at 11 21 50 AM

joshuayoes and others added 3 commits March 24, 2026 13:45
Store custom command registrations in the Reactotron server so they
survive MCP server restarts. Previously list_custom_commands returned
empty if the MCP server started after apps connected, since the
registration events were missed. Now the server tracks commands per
clientId and the MCP tool reads from that instead of the event buffer.

Also adds three new custom commands to the example app (showAlert,
setAsyncStorage, getAsyncStorage) for testing MCP integration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add @unknown default case to Calendar.Identifier switch in
LocalizationModule.swift, which became non-exhaustive in newer SDKs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@joshuayoes

Copy link
Copy Markdown
Contributor

Four commits from integration testing:

12663f13 — Error boundary in buildTimelineCommand. Bad payloads show as "RENDER ERROR" with details instead of crashing the desktop app.

6d4a66ac — Persist custom commands in the server so list_custom_commands works regardless of MCP start timing. 3 new example app commands, 5 new tests (verified to fail without fix).

df996f9f — Patch expo-localization for Xcode 26.2 (non-exhaustive switch).

0aa814a4 — ESLint fix for promise param names in test file.

Tested all MCP tools/resources, multi-app targeting, dispatch round-trips, custom command flow, desktop timeline regression (37 events, 6 types, no render errors).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@joshuayoes joshuayoes left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking really good! Thank you for this contribution.

I'll get this released later this week after I record a cool demo video since this is a great new feature.

@kbrandwijk

Copy link
Copy Markdown
Contributor Author

This is looking really good! Thank you for this contribution.

I'll get this released later this week after I record a cool demo video since this is a great new feature.

Great to hear! Let me know when the video's up, curious to see what you've made!

@joshuayoes

Copy link
Copy Markdown
Contributor

We are going to do some internal testing this week first, so I'm planning to get this released Monday.

joshuayoes and others added 2 commits March 30, 2026 10:21
Updated the instructions in mcp.md to specify that the MCP server is off by default and needs to be toggled on before connecting with Claude Code. This enhances user understanding of the setup process.
Large apps were blowing past Claude Code's token limits — Tyler hit 3.3M
chars on request_state. This adds response size protection:

- Compact JSON (no pretty printing) saves 30-50% on nested data
- 800K character cap with truncation + actionable guidance messages
- Timeline returns summarized events (type/timestamp/preview) instead of
  full payloads, with a new timeline_by_type resource template for drill-down
- Network resource truncates request/response bodies to 500-char previews
- New request_state_keys tool for lightweight state tree exploration
- request_state description now strongly pushes toward using path param
- Stress test buttons added to example app LoggingScreen

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@joshuayoes

Copy link
Copy Markdown
Contributor

Response size protection for MCP server

Addresses feedback where a noisy app exceeded Claude Code's token limits (3.3M chars on request_state).

Changes:

  • Compact JSON (no pretty-printing) — 30-50% size reduction
  • 800K char cap with guidance messages telling Claude how to narrow the query
  • Timeline returns summarized events; new timeline_by_type resource template for drill-down
  • Network bodies truncated to 500-char previews
  • New request_state_keys tool for lightweight state tree exploration
  • request_state description pushes toward using path param

Docs: Clarified MCP is off by default, documented new tools, updated example conversation.

Tests: 43 passing (20 new), including integration tests for oversized state truncation.

@joshuayoes
joshuayoes merged commit 5aba55f into infinitered:master Mar 30, 2026
3 checks passed
@joshuayoes

Copy link
Copy Markdown
Contributor

Released in 3.9.0!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants