@vitrion/expo-state-mcp
v1.2.0
Published
MCP server + in-app HTTP bridge for Expo SQLite and Zustand — works with Cursor, Claude, OpenAI Codex (CLI + IDE), and any MCP client (dev-only)
Maintainers
Readme
expo-state-mcp
Inspect and mutate a running Expo app's live
expo-sqlitedatabase andzustandstores from any MCP client (Cursor, Claude, OpenAI Codex, …) — dev-only, zero production footprint.
One package (@vitrion/expo-state-mcp): Metro resolves the react-native export (TCP bridge in the app); Node runs the expo-state-mcp CLI for your MCP host.
MCP client ←stdio→ CLI ←HTTP→ bridge (in the RN app)Expo app setup
1. Install
yarn add -D @vitrion/expo-state-mcpreact-native-tcp-socket is included as a dependency of this package. Rebuild the native app once (expo run:ios / expo run:android) so autolinking picks it up.
For a local checkout next to your app:
yarn add -D file:../expo-state-mcpRun yarn build in this repo first (dist/ is not committed). Ensure Metro resolves package.json exports (Expo SDK 54+ / Metro 0.82+ usually does; otherwise resolver.unstable_enablePackageExports = true in metro.config.js).
2. Wire the bridge (__DEV__)
import { setupBridge } from "@vitrion/expo-state-mcp";
import * as SQLite from "expo-sqlite";
const db = SQLite.openDatabaseSync("my.db");
import { useAuthStore } from "./stores/auth";
void setupBridge({
port: 9778,
appName: "my-app",
db,
stores: { "zustand.auth": useAuthStore },
});setupBridge returns a Promise (it collects device metadata before listening). You can void setupBridge(...) at module scope or await setupBridge(...) during app init. Call once early (e.g. next to Reactotron). No-ops when __DEV__ is false.
Android Emulator: from the host machine, run adb reverse tcp:9778 tcp:9778 so http://127.0.0.1:9778 reaches the emulated app.
Optional Bearer token: app token + env EXPO_STATE_MCP_TOKEN on the machine running the MCP CLI.
bindAllInterfaces (optional)
By default the bridge listens on loopback (127.0.0.1). That is enough in many setups, including a lot of iOS Simulator + Mac flows, so you do not need to set anything extra to start.
Turn it on when the MCP CLI (or curl on your Mac) cannot reach the app — errors like “bridge unreachable” or 127.0.0.1:9778 connection refused while the app is running:
iOS Simulator: the simulator and the Mac each have their own loopback; on some OS / simulator versions, only the simulator can see a
127.0.0.1listener. If the host cannot connect, passbindAllInterfaces: true(listens on0.0.0.0) so traffic from your Mac reaches the bridge. Example:void setupBridge({ // … bindAllInterfaces: true, });To limit binding to simulator only (not physical devices), you can use
Platform.OS === "ios" && !Device.isDevicefromreact-native/expo-device.Physical device: use
bindAllInterfaces: true(or bind to your LAN as needed), read the LAN IP from console logs if provided, and setEXPO_STATE_MCP_BRIDGE_URLon your machine tohttp://<device-ip>:9778.
If everything already works without it, leave it unset.
3. Wire your MCP client
Same stdio server everywhere: npx -y @vitrion/expo-state-mcp. Optional env EXPO_STATE_MCP_BRIDGE_URL (default http://127.0.0.1:9778).
Pick your client — then open the matching full details block for copy-paste config.
| Client | Where you wire it |
|--------|-------------------|
| Cursor | Project .cursor/mcp.json → mcpServers |
| Claude Desktop | claude_desktop_config.json (paths below) → same mcpServers JSON shape |
| Claude Code | claude mcp add … (MCP docs) |
| OpenAI Codex (CLI + IDE) | Shared ~/.codex/config.toml or codex mcp add … (Codex MCP); IDE: gear → MCP settings → Open config.toml |
Create .cursor/mcp.json:
{
"mcpServers": {
"expo-state-mcp": {
"command": "npx",
"args": ["-y", "@vitrion/expo-state-mcp"],
"env": {
"EXPO_STATE_MCP_BRIDGE_URL": "http://127.0.0.1:9778"
}
}
}
}Edit claude_desktop_config.json:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
Same mcpServers shape as Cursor:
{
"mcpServers": {
"expo-state-mcp": {
"command": "npx",
"args": ["-y", "@vitrion/expo-state-mcp"],
"env": {
"EXPO_STATE_MCP_BRIDGE_URL": "http://127.0.0.1:9778"
}
}
}
}From a terminal. Flags go before the server name; -- separates Claude’s options from the command that starts the MCP server.
claude mcp add --scope user --env EXPO_STATE_MCP_BRIDGE_URL=http://127.0.0.1:9778 expo-state-mcp -- npx -y @vitrion/expo-state-mcpSee Claude Code MCP docs.
OpenAI Codex reads MCP from ~/.codex/config.toml or project .codex/config.toml (trusted projects). CLI and IDE extension share one file — configure once, use in both.
Terminal:
codex mcp add expo-state-mcp --env EXPO_STATE_MCP_BRIDGE_URL=http://127.0.0.1:9778 -- npx -y @vitrion/expo-state-mcpOr edit config.toml (in the IDE: MCP settings → Open config.toml from the gear menu). Env block matches Codex docs ([mcp_servers.<name>.env]):
[mcp_servers.expo-state-mcp]
command = "npx"
args = ["-y", "@vitrion/expo-state-mcp"]
[mcp_servers.expo-state-mcp.env]
EXPO_STATE_MCP_BRIDGE_URL = "http://127.0.0.1:9778"More options: codex mcp help, timeouts, streamable HTTP servers — Model Context Protocol – Codex.
Use command: node and point args at ../expo-state-mcp/dist/cli/cli.js (after yarn build in the clone). Copy-paste JSON and TOML per client in DEVELOPMENT.md.
Connectivity
| Target | MCP URL on your machine | Notes |
|--------|-------------------------|--------|
| iOS Simulator | Usually http://127.0.0.1:9778 | Add bindAllInterfaces only if the Mac cannot reach the bridge (see above). |
| Android Emulator | http://127.0.0.1:9778 after adb reverse tcp:9778 tcp:9778 | Reverse forwards host port to the emulator. |
| Physical device | http://<lan-ip>:9778 | Use bindAllInterfaces / LAN binding; align EXPO_STATE_MCP_BRIDGE_URL with logs. |
Devices (MCP)
Each successful bridge response includes a device object (platform, model, id, optional lanIp, …). The MCP tools return JSON shaped as { "device": { … }, "data": … } so you always know which bridge answered.
list_devices— probesGET /deviceon every configured bridge and returns{ default, devices }. Use optionalrefresh: trueto drop the cache and re-probe.- Per-tool
device— every SQLite/Zustand tool accepts an optionaldevicestring: a deviceidoraliasfromlist_devices. If you configure more than one bridge URL, setEXPO_STATE_MCP_DEFAULT_DEVICEto an id or alias, or passdeviceon each call (otherwise the CLI errors with a hint).
One bridge (default): only EXPO_STATE_MCP_BRIDGE_URL (default http://127.0.0.1:9778) — same as before.
Several bridges (e.g. iOS simulator + Android emulator on different ports):
EXPO_STATE_MCP_BRIDGES— JSON array or comma-separated list of base URLs:- JSON:
[{"url":"http://127.0.0.1:9778"},{"url":"http://127.0.0.1:9779","alias":"android"}] - Shorthand:
http://127.0.0.1:9778,http://127.0.0.1:9779
- JSON:
EXPO_STATE_MCP_DEFAULT_DEVICE— id or alias used when a tool omitsdevice.
If the app bridge predates GET /device, the CLI still probes /health and assigns a stable legacy-… device id so list_devices and routing keep working after a CLI-only upgrade.
MCP tools
list_devices, sqlite_list_tables, sqlite_describe_table, sqlite_query, sqlite_explain, zustand_list_stores, zustand_get, zustand_set, zustand_call
Repo layout
src/app/— bridge (bundled by Metro)src/cli/— MCP stdio server
Contributor / agent process: AGENTS.md. Maintainer workflow: DEVELOPMENT.md.
CLI environment
| Variable | Default |
|----------|---------|
| EXPO_STATE_MCP_BRIDGE_URL | http://127.0.0.1:9778 |
| EXPO_STATE_MCP_BRIDGES | (empty — use EXPO_STATE_MCP_BRIDGE_URL only) |
| EXPO_STATE_MCP_DEFAULT_DEVICE | (empty — required when multiple bridges and tool omits device) |
| EXPO_STATE_MCP_TOKEN | (empty) |
Security
Dev-only: loopback by default, optional Bearer token (compared in constant time), request bodies capped at 5 MiB, setupBridge does nothing in production builds. Use a shared secret when binding beyond loopback.
License
MIT — see LICENSE.
