@synnode/expo-metro-mcp 1.0.6 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -3
- package/dist/index.js +24 -24
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,6 +32,22 @@ Restart Claude Code after adding the server.
|
|
|
32
32
|
METRO_PORT=8081
|
|
33
33
|
METRO_HOST=localhost
|
|
34
34
|
LOG_BUFFER_SIZE=1000
|
|
35
|
+
EXPO_METRO_MCP_ENABLE_EVAL=0
|
|
36
|
+
# optional, comma-separated key prefixes
|
|
37
|
+
EXPO_METRO_MCP_MMKV_PREFIX_ALLOWLIST=
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Additional safety toggles:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# do not register the raw Runtime.evaluate tool unless you opt in
|
|
44
|
+
EXPO_METRO_MCP_ENABLE_EVAL=1
|
|
45
|
+
|
|
46
|
+
# disable MMKV / Zustand write helpers
|
|
47
|
+
EXPO_METRO_MCP_READ_ONLY=1
|
|
48
|
+
|
|
49
|
+
# only allow specific MMKV key prefixes (applies to reads and writes)
|
|
50
|
+
EXPO_METRO_MCP_MMKV_PREFIX_ALLOWLIST=debug.,persist:dev:
|
|
35
51
|
```
|
|
36
52
|
|
|
37
53
|
If Metro runs on a different port:
|
|
@@ -59,7 +75,7 @@ claude mcp add expo-metro --env METRO_PORT=8082 npx @synnode/expo-metro-mcp
|
|
|
59
75
|
| `swipe` | Swipe from one coordinate to another. Optional: `duration_ms`, `platform`, `device_id` |
|
|
60
76
|
| `input_text` | Type text into the focused input field — works without the on-screen keyboard. Optional: `platform`, `device_id` |
|
|
61
77
|
| `input_key` | Send a special key press: `enter`, `backspace`, `delete`, `tab`, `escape`, `back`, `space`, arrow keys. Optional: `platform`, `device_id` |
|
|
62
|
-
| `evaluate` | Run JavaScript inside the connected app runtime via Metro CDP. Supports async expressions. Useful for reading state, calling app helpers, poking navigation, or mutating debug state. Optional: `timeout_ms` |
|
|
78
|
+
| `evaluate` | Run JavaScript inside the connected app runtime via Metro CDP. **Only registered when `EXPO_METRO_MCP_ENABLE_EVAL=1`.** Supports async expressions. Useful for reading state, calling app helpers, poking navigation, or mutating debug state. Optional: `timeout_ms` |
|
|
63
79
|
| `mmkv_get` | Read a raw string value from a dev-only MMKV debug hook exposed at `globalThis.__EXPO_METRO_MCP__.mmkv` |
|
|
64
80
|
| `mmkv_set` | Write a raw string value through that MMKV debug hook |
|
|
65
81
|
| `mmkv_remove` | Remove a key through that MMKV debug hook |
|
|
@@ -86,6 +102,8 @@ claude mcp add expo-metro --env METRO_PORT=8082 npx @synnode/expo-metro-mcp
|
|
|
86
102
|
- Expressions are awaited automatically, so `Promise` results work out of the box
|
|
87
103
|
- Returned values are serialized when possible; non-serializable objects fall back to their runtime description
|
|
88
104
|
- This is a sharp tool. Great for development, mildly cursed in the wrong hands
|
|
105
|
+
- The MCP does **not** sandbox eval internally; treat this as arbitrary code execution in the app runtime
|
|
106
|
+
- To hide the tool entirely unless you explicitly want it, leave `EXPO_METRO_MCP_ENABLE_EVAL` unset
|
|
89
107
|
|
|
90
108
|
## MMKV debug hook
|
|
91
109
|
|
|
@@ -97,14 +115,14 @@ Example app-side hook:
|
|
|
97
115
|
import {createMMKV} from "react-native-mmkv";
|
|
98
116
|
|
|
99
117
|
export const mmkv = createMMKV({
|
|
100
|
-
id: "
|
|
118
|
+
id: "demo-app",
|
|
101
119
|
});
|
|
102
120
|
|
|
103
121
|
if (__DEV__) {
|
|
104
122
|
globalThis.__EXPO_METRO_MCP__ = {
|
|
105
123
|
...globalThis.__EXPO_METRO_MCP__,
|
|
106
124
|
mmkv: {
|
|
107
|
-
id: "
|
|
125
|
+
id: "demo-app",
|
|
108
126
|
getItem: (key: string) => mmkv.getString(key) ?? null,
|
|
109
127
|
setItem: (key: string, value: string) => mmkv.set(key, value),
|
|
110
128
|
removeItem: (key: string) => mmkv.remove(key),
|
|
@@ -119,6 +137,11 @@ Once exposed, the MCP can use:
|
|
|
119
137
|
- JSON helpers: `mmkv_get_json`, `mmkv_set_json`, `mmkv_merge_json`
|
|
120
138
|
- Zustand helpers: `zustand_persist_get`, `zustand_persist_set`, `zustand_persist_merge`
|
|
121
139
|
|
|
140
|
+
Safety notes:
|
|
141
|
+
- `EXPO_METRO_MCP_READ_ONLY=1` disables all MMKV/Zustand write helpers
|
|
142
|
+
- `EXPO_METRO_MCP_MMKV_PREFIX_ALLOWLIST` limits MMKV/Zustand access to specific key prefixes
|
|
143
|
+
- `mmkv_keys` returns only allowed keys when a prefix allowlist is configured
|
|
144
|
+
|
|
122
145
|
This stays intentionally generic, so it works for persisted Zustand state and plain MMKV usage without coupling the MCP to your store internals.
|
|
123
146
|
|
|
124
147
|
For most AI-driven state seeding, the Zustand helpers are the sweet spot. They avoid hand-building the persisted wrapper shape every time.
|
|
@@ -147,8 +170,11 @@ For most AI-driven state seeding, the Zustand helpers are the sweet spot. They a
|
|
|
147
170
|
|
|
148
171
|
CDP only allows one client at a time. Switching between the MCP and DevTools is seamless — whichever connects last takes over, and the other is kicked out automatically.
|
|
149
172
|
|
|
173
|
+
Important practical note for agents: if a CDP-backed tool needs the runtime and `get_status` shows disconnected, the agent should try `connect` itself before asking the user to do anything. In many sessions, the MCP is only disconnected because DevTools had the socket last.
|
|
174
|
+
|
|
150
175
|
- **To use DevTools**: just open or reconnect it. The MCP will be disconnected automatically.
|
|
151
176
|
- **To return to MCP**: call `connect`. DevTools will lose its connection.
|
|
177
|
+
- **If a runtime tool fails due to no CDP connection**: retry after `connect`.
|
|
152
178
|
|
|
153
179
|
`disconnect` is available if you want to explicitly release the connection first, but it's not required.
|
|
154
180
|
|
package/dist/index.js
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{McpServer as
|
|
3
|
-
`)}import{z as
|
|
4
|
-
${
|
|
2
|
+
import{McpServer as Lt}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as zt}from"@modelcontextprotocol/sdk/server/stdio.js";import T from"ws";import He from"http";var qe=parseInt(process.env.METRO_PORT??"8081",10),Ye=process.env.METRO_HOST??"localhost",Qe=parseInt(process.env.LOG_BUFFER_SIZE??"1000",10),et=3e4,G=1e3,tt={log:"log",info:"info",warning:"warn",warn:"warn",error:"error",debug:"debug",dir:"log",dirxml:"log",table:"log",assert:"error"};function nt(e){return e.map(t=>t.value!==void 0&&t.value!==null?String(t.value):t.description?t.description:"").filter(Boolean).join(" ")}async function rt(e,t){return new Promise(n=>{let r=He.get(`http://${e}:${t}/json/list`,o=>{let i="";o.on("data",a=>i+=a),o.on("end",()=>{try{let a=JSON.parse(i);Array.isArray(a)?n(a):n([])}catch{n([])}})});r.on("error",()=>n([])),r.setTimeout(2e3,()=>{r.destroy(),n([])})})}var z=class{buffer=[];cdpWs=null;eventsWs=null;_connected=!1;_currentTargetId=null;_lastConnectedAt=null;_totalReceived=0;_stopped=!1;_eventsBackoff=G;_deviceTitle=null;_nextMessageId=1;pendingResponses=new Map;host=Ye;port=qe;get connected(){return this._connected}get lastConnectedAt(){return this._lastConnectedAt}get totalReceived(){return this._totalReceived}get bufferedEntries(){return this.buffer.length}get deviceTitle(){return this._deviceTitle}start(){this._stopped=!1,this.connectEvents()}stop(){this._stopped=!0,this.cdpWs&&(this.cdpWs.terminate(),this.cdpWs=null),this.eventsWs&&(this.eventsWs.terminate(),this.eventsWs=null)}disconnect(){this.rejectPendingResponses(new Error("Disconnected from Metro CDP.")),this.cdpWs&&(this.cdpWs.terminate(),this.cdpWs=null),this._connected=!1,this._currentTargetId=null,this._deviceTitle=null}getEntries(t={}){let n=this.buffer;t.since!==void 0&&(n=n.filter(o=>o.timestamp>=t.since)),t.level&&(n=n.filter(o=>o.level===t.level));let r=t.lines??50;return n.slice(-r)}clearBuffer(){let t=this.buffer.length;return this.buffer=[],t}async grabConnection(){return await this.checkForNewTarget(),await new Promise(t=>setTimeout(t,500)),this._connected?`Connected to ${this._deviceTitle??"device"}.`:"No device found. Is Metro running with a connected device?"}async evaluate(t,n=5e3){if((!this._connected||!this.cdpWs||this.cdpWs.readyState!==T.OPEN)&&(await this.checkForNewTarget(),await new Promise(r=>setTimeout(r,250))),!this._connected||!this.cdpWs||this.cdpWs.readyState!==T.OPEN)throw new Error("Not connected to Metro CDP. Start Expo, make sure a device is attached, then call connect.");return this.sendCdpCommand("Runtime.evaluate",{expression:t,awaitPromise:!0,returnByValue:!0,generatePreview:!1,replMode:!0},n)}addEntry(t){this._totalReceived++,this.buffer.push(t),this.buffer.length>Qe&&this.buffer.shift()}async checkForNewTarget(){let t=await rt(this.host,this.port);if(!t.length){this._connected&&(this._connected=!1,this._currentTargetId=null,this._deviceTitle=null);return}let n=t[0];n.id===this._currentTargetId&&this.cdpWs?.readyState===T.OPEN||this.connectCdp(n)}connectCdp(t){this.cdpWs&&(this.cdpWs.terminate(),this.cdpWs=null),this._currentTargetId=t.id,this._deviceTitle=t.title??null;let n=new T(t.webSocketDebuggerUrl);this.cdpWs=n,n.on("open",()=>{this._connected=!0,this._lastConnectedAt=new Date,n.send(JSON.stringify({id:1,method:"Runtime.enable",params:{}}))}),n.on("message",r=>{let o;try{o=JSON.parse(r.toString())}catch{return}if(typeof o.id=="number"){let i=this.pendingResponses.get(o.id);if(i){clearTimeout(i.timer),this.pendingResponses.delete(o.id),i.resolve(o);return}}o.method==="Runtime.consoleAPICalled"&&o.params&&this.handleConsoleEvent(o.params)}),n.on("close",()=>{this.cdpWs===n&&(this.rejectPendingResponses(new Error("Metro CDP connection closed.")),this._connected=!1,this.cdpWs=null,this._currentTargetId=null,this._deviceTitle=null)}),n.on("error",()=>{})}sendCdpCommand(t,n,r){let o=this.cdpWs;if(!o||o.readyState!==T.OPEN)return Promise.reject(new Error("Metro CDP is not connected."));let i=this._nextMessageId++;return new Promise((a,u)=>{let l=setTimeout(()=>{this.pendingResponses.delete(i),u(new Error(`CDP command timed out after ${r}ms.`))},r);this.pendingResponses.set(i,{resolve:a,reject:u,timer:l}),o.send(JSON.stringify({id:i,method:t,params:n}),p=>{p&&(clearTimeout(l),this.pendingResponses.delete(i),u(p instanceof Error?p:new Error(String(p))))})})}rejectPendingResponses(t){for(let[n,r]of this.pendingResponses.entries())clearTimeout(r.timer),r.reject(t),this.pendingResponses.delete(n)}handleConsoleEvent(t){let n=typeof t.type=="string"?t.type:"log",r=tt[n]??"log",o=Array.isArray(t.args)?t.args:[],i=nt(o),a=typeof t.timestamp=="number"?Math.round(t.timestamp):Date.now();if(!i)return;let l=t.stackTrace?.callFrames?.filter(g=>g.url&&!g.url.startsWith("native")).map(g=>({functionName:g.functionName??"(anonymous)",url:g.url,line:g.lineNumber??0,col:g.columnNumber??0})),p=i.includes("http://")?i:void 0;this.addEntry({timestamp:a,level:r,message:i,rawMessage:p,rawFrames:l?.length?l:void 0})}connectEvents(){if(this._stopped)return;let t=`ws://${this.host}:${this.port}/events`,n;try{n=new T(t)}catch{this.scheduleEventsReconnect();return}this.eventsWs=n,n.on("open",()=>{this._eventsBackoff=G}),n.on("message",r=>{let o=null;try{o=JSON.parse(r.toString())}catch{return}(o.type==="build_failed"||o.type==="bundling_error")&&this.addEntry({timestamp:Date.now(),level:"error",message:o.message??o.type})}),n.on("close",()=>{this.eventsWs===n&&(this.eventsWs=null,this.scheduleEventsReconnect())}),n.on("error",()=>{})}scheduleEventsReconnect(){this._stopped||setTimeout(()=>{this._eventsBackoff=Math.min(this._eventsBackoff*2,et),this.connectEvents()},this._eventsBackoff)}},c=new z;import{z as I}from"zod";var ot=/\(https?:\/\/[^)]+\.bundle[^)]*:(\d+:\d+)\)/g;function E(e){return e.replace(ot,"(:$1)")}function P(e){let t=new Date(e),n=String(t.getHours()).padStart(2,"0"),r=String(t.getMinutes()).padStart(2,"0"),o=String(t.getSeconds()).padStart(2,"0");return`${n}:${r}:${o}`}var U=I.object({lines:I.coerce.number().int().min(1).max(500).optional().default(50),level:I.enum(["error","warn","info","log","debug"]).optional(),since:I.string().optional()});function it(e){let t=Number(e);if(!isNaN(t)&&t>1e9)return t<1e12?t*1e3:t;let n=e.match(/^(\d+(?:\.\d+)?)(s|m|h)$/);if(n){let r=parseFloat(n[1]),o=n[2],i={s:1e3,m:6e4,h:36e5};return Date.now()-r*i[o]}return t}function V(e){let t=e.since?it(e.since):void 0,n=c.getEntries({lines:e.lines,level:e.level,since:t});return n.length===0?"No log entries found.":n.map(r=>`[${P(r.timestamp)}] [${r.level.toUpperCase()}] ${E(r.message)}`).join(`
|
|
3
|
+
`)}import{z as X}from"zod";var Z=X.object({lines:X.coerce.number().int().min(1).max(200).optional().default(20)});function H(e){let t=c.getEntries({level:"error",lines:e.lines});return t.length===0?"No errors in buffer.":t.map(n=>`[${P(n.timestamp)}] [ERROR]
|
|
4
|
+
${E(n.message)}`).join(`
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
`)}function
|
|
9
|
-
`)}import
|
|
8
|
+
`)}function q(){let e={connected:c.connected,host:c.host,port:c.port,device:c.deviceTitle,buffered_entries:c.bufferedEntries,last_connected_at:c.lastConnectedAt?.toISOString()??null,total_received:c.totalReceived,expo_sdk_version:null};return JSON.stringify(e,null,2)}function Y(){let e=c.clearBuffer();return`Cleared ${e} log ${e===1?"entry":"entries"} from the buffer.`}import{z as W}from"zod";var Q=W.object({duration:W.string().optional().default("10s"),level:W.enum(["error","warn","info","log","debug"]).optional()});function st(e){let t=e.match(/^(\d+(?:\.\d+)?)(s|m)$/);if(!t)return 1e4;let n=parseFloat(t[1]),o=t[2]==="m"?n*6e4:n*1e3;return Math.min(o,3e4)}var at=500;async function ee(e){if(!c.connected)return"Metro is not connected. Start Expo dev server and try again.";let t=st(e.duration),n=c.bufferedEntries,r=Date.now(),o=e.level;await new Promise(l=>{let p=setInterval(()=>{Date.now()-r>=t&&(clearInterval(p),l())},at)});let a=c.getEntries({lines:c.bufferedEntries}).slice(n),u=o?a.filter(l=>l.level===o):a;return u.length===0?`No logs received during ${e.duration} window.`:u.map(l=>`[${P(l.timestamp)}] [${l.level.toUpperCase()}] ${E(l.message)}`).join(`
|
|
9
|
+
`)}import ct from"http";var ut=parseInt(process.env.METRO_PORT??"8081",10),lt=process.env.METRO_HOST??"localhost";async function te(){return new Promise(e=>{let t=ct.request({hostname:lt,port:ut,path:"/reload",method:"POST"},n=>{n.resume(),n.statusCode===200?e("App reloaded."):e(`Reload failed: HTTP ${n.statusCode}`)});t.on("error",n=>e(`Reload failed: ${n.message}`)),t.setTimeout(3e3,()=>{t.destroy(),e("Reload failed: timeout")}),t.end()})}import{z as ne}from"zod";import mt from"http";import{SourceMapConsumer as pt}from"source-map";var ie=ne.object({message:ne.string().optional()}),j=new Map,dt=6e4;function re(e,t,n){return e.replace(/^https?:\/\/[^/]+/,`http://${t}:${n}`)}function oe(e){let t=e.match(/(?:\/\/&|\?)(.+)$/),n=t?t[1]:"dev=true&minify=false";return`${e.replace(/(\/[^/?]+)\.bundle.*$/,"$1.map")}?${n}`}async function ft(e){let t=j.get(e);return t&&Date.now()-t.fetchedAt<dt?t.consumer:new Promise(n=>{let r=new URL(e),o=mt.get({hostname:r.hostname,port:Number(r.port)||8081,path:r.pathname+r.search},i=>{let a=[];i.on("data",u=>a.push(u)),i.on("end",async()=>{if(i.statusCode!==200){n(null);return}try{let u=JSON.parse(Buffer.concat(a).toString()),l=await pt.with(u,null,p=>p);j.set(e,{consumer:l,fetchedAt:Date.now()}),n(l)}catch{n(null)}})});o.on("error",()=>n(null)),o.setTimeout(1e4,()=>{o.destroy(),n(null)})})}function se(){j.forEach(e=>e.consumer.destroy()),j.clear()}function gt(e){let t=/at\s+([\w$.<>[\] ]+?)\s+\((https?:\/\/[^)]+\.bundle[^)]*):(\d+):(\d+)\)/g,n=[],r;for(;(r=t.exec(e))!==null;)n.push({functionName:r[1].trim(),url:r[2],line:parseInt(r[3])-1,col:parseInt(r[4])});return n}async function ht(e,t){let n=[],r=new Map;for(let i of e){let a=re(i.url,c.host,c.port),u=oe(a);r.has(u)||r.set(u,[]),r.get(u).push(i)}let o=new Map;await Promise.all([...r.keys()].map(async i=>{o.set(i,await ft(i))}));for(let i of e){let a=re(i.url,c.host,c.port),u=oe(a),l=o.get(u)??null;if(l){let p=l.originalPositionFor({line:i.line+1,column:i.col});if(p.source){let g=p.source.replace(/^.*\/\/\//,"").replace(/\?.*$/,"");n.push(` at ${i.functionName} (${g}:${p.line}:${p.column})`);continue}}n.push(` at ${i.functionName} (:${i.line+1}:${i.col})`)}return n}async function ae(e){let t=c.getEntries({level:"error",lines:50}),n=e.message?[...t].reverse().find(l=>l.message.includes(e.message)):t.at(-1);if(!n)return"No error entries in buffer.";let r=`[ERROR] ${n.message.split(`
|
|
10
10
|
`)[0]}`;if(!n.rawMessage)return`${r}
|
|
11
11
|
|
|
12
|
-
No stack frames available.`;let o=
|
|
12
|
+
No stack frames available.`;let o=gt(n.rawMessage);if(!o.length)return`${r}
|
|
13
13
|
|
|
14
|
-
No stack frames available.`;let i=await
|
|
15
|
-
`)}import{z as
|
|
16
|
-
`).slice(1);for(let r of n){let o=r.trim().split(/\s+/);if(o.length<2||o[1]!=="device")continue;let i=o[0],a=r.match(/model:(\S+)/),u=a?a[1].replace(/_/g," "):i;t.push({id:i,name:u,platform:"android"})}return t}function h(){return[...
|
|
14
|
+
No stack frames available.`;let i=await ht(o),a=i.filter(l=>!l.includes("node_modules")&&!l.includes("(:")),u=[r,""];return a.length?u.push(...a):u.push(...i),u.join(`
|
|
15
|
+
`)}import{z as J}from"zod";import{execSync as x}from"child_process";import*as S from"fs";import*as me from"os";import*as pe from"path";import{execSync as ce}from"child_process";function ue(e){try{return ce(e,{timeout:5e3,stdio:["ignore","pipe","ignore"]}).toString().trim()}catch{return""}}function le(e){try{return ce(`which ${e}`,{timeout:2e3,stdio:"ignore"}),!0}catch{return!1}}function vt(){if(!le("xcrun"))return[];let e=ue("xcrun simctl list devices booted --json");if(!e)return[];try{let t=JSON.parse(e),n=[];for(let[,r]of Object.entries(t.devices))for(let o of r)o.state==="Booted"&&n.push({id:o.udid,name:o.name,platform:"ios"});return n}catch{return[]}}function yt(){if(!le("adb"))return[];let e=ue("adb devices -l");if(!e)return[];let t=[],n=e.split(`
|
|
16
|
+
`).slice(1);for(let r of n){let o=r.trim().split(/\s+/);if(o.length<2||o[1]!=="device")continue;let i=o[0],a=r.match(/model:(\S+)/),u=a?a[1].replace(/_/g," "):i;t.push({id:i,name:u,platform:"android"})}return t}function h(){return[...vt(),...yt()]}function k(e,t){let n=h();return n.length?t?n.find(r=>r.id===t||r.name.toLowerCase().includes(t.toLowerCase()))??null:e==="ios"?n.find(r=>r.platform==="ios")??null:e==="android"?n.find(r=>r.platform==="android")??null:n.find(r=>r.platform==="ios")??n[0]:null}var de=J.object({device_id:J.string().optional(),platform:J.enum(["ios","android"]).optional()});function bt(e){try{let t=x(`sips -g pixelWidth -g pixelHeight "${e}"`,{timeout:5e3,stdio:["ignore","pipe","ignore"]}).toString(),n=t.match(/pixelWidth:\s*(\d+)/),r=t.match(/pixelHeight:\s*(\d+)/);if(!n||!r)return 1;let o=parseInt(n[1]),i=parseInt(r[1]),a=Math.max(o,i);return a>=2500&&o%3===0||a>=2500?3:a>=1334?2:1}catch{return 1}}function _t(e,t){x(`xcrun simctl io "${e}" screenshot "${t}"`,{timeout:1e4,stdio:["ignore","ignore","pipe"]});let n=bt(t);if(n>1)try{let r=x(`sips -g pixelWidth -g pixelHeight "${t}"`,{timeout:5e3,stdio:["ignore","pipe","ignore"]}).toString(),o=r.match(/pixelWidth:\s*(\d+)/),i=r.match(/pixelHeight:\s*(\d+)/);if(o&&i){let a=Math.round(parseInt(o[1])/n),u=Math.round(parseInt(i[1])/n),l=t.replace(".png","-points.png");x(`sips -z ${u} ${a} "${t}" --out "${l}"`,{timeout:1e4,stdio:"ignore"}),S.renameSync(l,t)}}catch{}}function kt(e,t){let n=`/sdcard/mcp_screenshot_${Date.now()}.png`;x(`adb -s "${e}" shell screencap -p "${n}"`,{timeout:1e4,stdio:["ignore","ignore","pipe"]}),x(`adb -s "${e}" pull "${n}" "${t}"`,{timeout:1e4,stdio:["ignore","ignore","pipe"]}),x(`adb -s "${e}" shell rm "${n}"`,{timeout:5e3,stdio:"ignore"})}function wt(e){if(e.length<24||e.readUInt32BE(0)!==2303741511)return null;let t=e.readUInt32BE(16),n=e.readUInt32BE(20);return{width:t,height:n}}function fe(e){let t=h();if(!t.length)return{type:"text",text:"No active simulators or emulators found. Start a simulator (iOS) or emulator (Android) first."};let n=k(e.platform,e.device_id);if(!n)return{type:"text",text:`No matching device found. Available: ${t.map(o=>`${o.name} (${o.platform})`).join(", ")}`};let r=pe.join(me.tmpdir(),`expo-mcp-screenshot-${Date.now()}.png`);try{n.platform==="ios"?_t(n.id,r):kt(n.id,r);let o=S.readFileSync(r),i=o.toString("base64"),a=wt(o);return S.unlinkSync(r),{type:"image",data:i,mimeType:"image/png",width:a?.width??0,height:a?.height??0}}catch(o){try{S.unlinkSync(r)}catch{}return{type:"text",text:`Screenshot failed: ${o instanceof Error?o.message:String(o)}`}}}import{z as f}from"zod";import{execSync as O}from"child_process";import{spawn as xt}from"child_process";var v=null,K=null;function R(e){v&&!v.killed&&K===e||(v&&!v.killed&&v.kill(),v=xt("idb_companion",["--udid",e],{detached:!1,stdio:"ignore"}),K=e,v.on("exit",()=>{v=null,K=null}),Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,800))}process.on("exit",()=>v?.kill());process.on("SIGTERM",()=>{v?.kill(),process.exit(0)});process.on("SIGINT",()=>{v?.kill(),process.exit(0)});var ge=f.object({x:f.number().int().describe("X coordinate in points/pixels"),y:f.number().int().describe("Y coordinate in points/pixels"),device_id:f.string().optional(),platform:f.enum(["ios","android"]).optional()}),he=f.object({x1:f.number().int(),y1:f.number().int(),x2:f.number().int(),y2:f.number().int(),duration_ms:f.number().int().min(50).max(5e3).optional().default(300),device_id:f.string().optional(),platform:f.enum(["ios","android"]).optional()});function ve(){try{return O("which idb",{timeout:2e3,stdio:"ignore"}),!0}catch{return!1}}function St(e,t,n){if(ve()){R(e),O(`idb ui tap ${t} ${n} --udid "${e}"`,{timeout:5e3,stdio:["ignore","ignore","pipe"]});return}let r=JSON.stringify({touches:[{x:t,y:n,action:"began"}]});try{O(`echo '${r}' | xcrun simctl io "${e}" sendtouchJSON -`,{timeout:5e3,stdio:["pipe","ignore","pipe"]});return}catch{}throw new Error(`iOS tap requires idb (Facebook iOS Development Bridge).
|
|
17
17
|
Install via: brew install idb-companion && pip3 install fb-idb
|
|
18
|
-
Or: brew install facebook/fb/idb-companion`)}function
|
|
19
|
-
Install via: brew install idb-companion && pip3 install fb-idb`)}function
|
|
20
|
-
Install via: brew install idb-companion && pip3 install fb-idb`)}function
|
|
21
|
-
Install via: brew install idb-companion && pip3 install fb-idb`)}function
|
|
18
|
+
Or: brew install facebook/fb/idb-companion`)}function Mt(e,t,n){O(`adb -s "${e}" shell input tap ${t} ${n}`,{timeout:5e3,stdio:["ignore","ignore","pipe"]})}function $t(e,t,n,r,o,i){if(ve()){R(e);let a=(i/1e3).toFixed(2);O(`idb ui swipe ${t} ${n} ${r} ${o} ${a} --udid "${e}"`,{timeout:i+5e3,stdio:["ignore","ignore","pipe"]});return}throw new Error(`iOS swipe requires idb (Facebook iOS Development Bridge).
|
|
19
|
+
Install via: brew install idb-companion && pip3 install fb-idb`)}function Tt(e,t,n,r,o,i){O(`adb -s "${e}" shell input swipe ${t} ${n} ${r} ${o} ${i}`,{timeout:1e4,stdio:["ignore","ignore","pipe"]})}function ye(e){let t=h();if(!t.length)return"No active simulators or emulators found.";let n=k(e.platform,e.device_id);if(!n)return`No matching device found. Available: ${t.map(r=>`${r.name} (${r.platform})`).join(", ")}`;try{return n.platform==="ios"?St(n.id,e.x,e.y):Mt(n.id,e.x,e.y),`Tapped (${e.x}, ${e.y}) on ${n.name} [${n.platform}].`}catch(r){return`Tap failed: ${r instanceof Error?r.message:String(r)}`}}function be(e){let t=h();if(!t.length)return"No active simulators or emulators found.";let n=k(e.platform,e.device_id);if(!n)return`No matching device found. Available: ${t.map(r=>`${r.name} (${r.platform})`).join(", ")}`;try{return n.platform==="ios"?($t(n.id,e.x1,e.y1,e.x2,e.y2,e.duration_ms),`Swiped from (${e.x1}, ${e.y1}) to (${e.x2}, ${e.y2}) on ${n.name} [ios]. Note: iOS swipe simulation is limited \u2014 use Android for reliable swipe gestures.`):(Tt(n.id,e.x1,e.y1,e.x2,e.y2,e.duration_ms),`Swiped from (${e.x1}, ${e.y1}) to (${e.x2}, ${e.y2}) on ${n.name} [android] over ${e.duration_ms}ms.`)}catch(r){return`Swipe failed: ${r instanceof Error?r.message:String(r)}`}}import{z as w}from"zod";import{execSync as C}from"child_process";var _e=w.object({text:w.string().describe("Text to type into the focused input field"),device_id:w.string().optional(),platform:w.enum(["ios","android"]).optional()}),ke=w.object({key:w.enum(["enter","backspace","delete","tab","escape","home","end","back","space","up","down","left","right"]).describe("Key to press"),device_id:w.string().optional(),platform:w.enum(["ios","android"]).optional()}),Et={enter:66,backspace:67,delete:67,tab:61,escape:111,home:3,end:123,back:4,space:62,up:19,down:20,left:21,right:22},Pt={enter:40,backspace:42,delete:76,tab:43,escape:41,home:74,end:77,back:41,space:44,up:82,down:81,left:80,right:79};function we(){try{return C("which idb",{timeout:2e3,stdio:"ignore"}),!0}catch{return!1}}function Rt(e,t){if(we()){R(e);let n=t.replace(/'/g,`'"'"'`);C(`idb ui text '${n}' --udid "${e}"`,{timeout:1e4,stdio:["ignore","ignore","pipe"]});return}throw new Error(`iOS text input requires idb.
|
|
20
|
+
Install via: brew install idb-companion && pip3 install fb-idb`)}function Ot(e,t){let n=t.replace(/\\/g,"\\\\").replace(/ /g,"%s").replace(/'/g,"\\'").replace(/"/g,'\\"');C(`adb -s "${e}" shell input text "${n}"`,{timeout:1e4,stdio:["ignore","ignore","pipe"]})}function Ct(e,t){if(we()){R(e);let n=Pt[t];if(n===void 0)throw new Error(`Unknown key: ${t}`);C(`idb ui key ${n} --udid "${e}"`,{timeout:5e3,stdio:["ignore","ignore","pipe"]});return}throw new Error(`iOS key input requires idb.
|
|
21
|
+
Install via: brew install idb-companion && pip3 install fb-idb`)}function At(e,t){let n=Et[t];if(n===void 0)throw new Error(`Unknown key: ${t}`);C(`adb -s "${e}" shell input keyevent ${n}`,{timeout:5e3,stdio:["ignore","ignore","pipe"]})}function xe(e){let t=h();if(!t.length)return"No active simulators or emulators found.";let n=k(e.platform,e.device_id);if(!n)return`No matching device found. Available: ${t.map(r=>`${r.name} (${r.platform})`).join(", ")}`;try{return n.platform==="ios"?Rt(n.id,e.text):Ot(n.id,e.text),`Typed "${e.text}" on ${n.name} [${n.platform}].`}catch(r){return`input_text failed: ${r instanceof Error?r.message:String(r)}`}}function Se(e){let t=h();if(!t.length)return"No active simulators or emulators found.";let n=k(e.platform,e.device_id);if(!n)return`No matching device found. Available: ${t.map(r=>`${r.name} (${r.platform})`).join(", ")}`;try{return n.platform==="ios"?Ct(n.id,e.key):At(n.id,e.key),`Key "${e.key}" sent to ${n.name} [${n.platform}].`}catch(r){return`input_key failed: ${r instanceof Error?r.message:String(r)}`}}function Me(){let e=h();if(!e.length)return"No active simulators or emulators found.\n\n- iOS: start a simulator via Xcode or `xcrun simctl boot <device>`\n- Android: start an emulator via Android Studio or `emulator -avd <name>`";let t=e.map(n=>`- ${n.name} [${n.platform}] id: ${n.id}`);return`Active devices (${e.length}):
|
|
22
22
|
${t.join(`
|
|
23
|
-
`)}`}import{z as
|
|
24
|
-
${r}`:t}async function
|
|
25
|
-
${
|
|
23
|
+
`)}`}import{z as F}from"zod";var Te=F.object({code:F.string().min(1).max(2e4),timeout_ms:F.coerce.number().int().min(100).max(3e4).optional().default(5e3)});function $e(e){if(e===void 0)return"undefined";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean"||e===null)return String(e);try{return JSON.stringify(e,null,2)}catch{return String(e)}}function Nt(e){if(!e)return"Unknown runtime error.";let t=typeof e.text=="string"?e.text:"Runtime evaluation failed.",n=e.exception&&typeof e.exception=="object"?e.exception:void 0,r=typeof n?.description=="string"?n.description:typeof n?.value=="string"?n.value:void 0;return r?`${t}
|
|
24
|
+
${r}`:t}async function Ee(e){let t=await c.evaluate(e.code,e.timeout_ms);if(t.error){let B=[t.error.message??"CDP error"];return t.error.data!==void 0&&B.push($e(t.error.data)),`Evaluation failed.
|
|
25
|
+
${B.join(`
|
|
26
26
|
`)}`}let n=t.result&&typeof t.result=="object"?t.result:void 0,r=n?.exceptionDetails&&typeof n.exceptionDetails=="object"?n.exceptionDetails:void 0;if(r)return`Evaluation threw.
|
|
27
|
-
${
|
|
28
|
-
${g}`}import{z as s}from"zod";var
|
|
29
|
-
${l}`:a)}return(r?.result&&typeof
|
|
30
|
-
const mmkv = ${
|
|
27
|
+
${Nt(r)}`;let o=n?.result&&typeof n.result=="object"?n.result:void 0;if(!o)return"Evaluation completed, but no result was returned.";let i=typeof o.type=="string"?o.type:"unknown",a=typeof o.subtype=="string"?o.subtype:void 0,u=o.value,l=typeof o.description=="string"?o.description:void 0,p=[`type: ${i}`];a&&p.push(`subtype: ${a}`);let g=u!==void 0?$e(u):l??"(no serializable value)";return`${p.join(", ")}
|
|
28
|
+
${g}`}var Dt=new Set(["1","true","yes","on"]);function Pe(e,t=!1){let n=process.env[e];return n==null?t:Dt.has(n.trim().toLowerCase())}function It(e){let t=process.env[e];return t?t.split(",").map(n=>n.trim()).filter(Boolean):[]}var A={readOnly:Pe("EXPO_METRO_MCP_READ_ONLY"),enableEval:Pe("EXPO_METRO_MCP_ENABLE_EVAL"),mmkvPrefixAllowlist:It("EXPO_METRO_MCP_MMKV_PREFIX_ALLOWLIST")};function M(e){if(A.readOnly)throw new Error(`${e} is disabled because EXPO_METRO_MCP_READ_ONLY=1.`)}function y(e){let{mmkvPrefixAllowlist:t}=A;if(t.length&&!t.some(n=>e.startsWith(n)))throw new Error(`MMKV key "${e}" is not allowed by EXPO_METRO_MCP_MMKV_PREFIX_ALLOWLIST.`)}function Re(e){let{mmkvPrefixAllowlist:t}=A;return t.length?e.filter(n=>t.some(r=>n.startsWith(r))):e}import{z as s}from"zod";var jt="globalThis.__EXPO_METRO_MCP__?.mmkv",Oe=s.object({key:s.string().min(1).max(500),timeout_ms:s.coerce.number().int().min(100).max(3e4).optional().default(5e3)}),Ce=s.object({key:s.string().min(1).max(500),value:s.string().max(2e5),timeout_ms:s.coerce.number().int().min(100).max(3e4).optional().default(5e3)}),Ae=s.object({key:s.string().min(1).max(500),timeout_ms:s.coerce.number().int().min(100).max(3e4).optional().default(5e3)}),Ne=s.object({timeout_ms:s.coerce.number().int().min(100).max(3e4).optional().default(5e3)}),De=s.object({key:s.string().min(1).max(500),timeout_ms:s.coerce.number().int().min(100).max(3e4).optional().default(5e3)}),Ie=s.object({key:s.string().min(1).max(500),value:s.unknown(),timeout_ms:s.coerce.number().int().min(100).max(3e4).optional().default(5e3)}),je=s.object({key:s.string().min(1).max(500),value:s.record(s.string(),s.unknown()),timeout_ms:s.coerce.number().int().min(100).max(3e4).optional().default(5e3)}),Le=s.object({key:s.string().min(1).max(500),timeout_ms:s.coerce.number().int().min(100).max(3e4).optional().default(5e3)}),ze=s.object({key:s.string().min(1).max(500),state:s.record(s.string(),s.unknown()),version:s.number().int().optional(),timeout_ms:s.coerce.number().int().min(100).max(3e4).optional().default(5e3)}),We=s.object({key:s.string().min(1).max(500),state:s.record(s.string(),s.unknown()),version:s.number().int().optional(),timeout_ms:s.coerce.number().int().min(100).max(3e4).optional().default(5e3)});function d(e){return JSON.stringify(e)}function $(e){return JSON.stringify(e,null,2)}async function b(e,t){let n=await c.evaluate(e,t);if(n.error)throw new Error(n.error.message??"CDP evaluation failed.");let r=n.result&&typeof N(n.result)=="object"?N(n.result):void 0,o=r?.exceptionDetails&&typeof N(r.exceptionDetails)=="object"?r.exceptionDetails:void 0;if(o){let a=typeof o.text=="string"?o.text:"Runtime evaluation failed.",u=o.exception&&typeof N(o.exception)=="object"?o.exception:void 0,l=typeof u?.description=="string"?u.description:typeof u?.value=="string"?u.value:void 0;throw new Error(l?`${a}
|
|
29
|
+
${l}`:a)}return(r?.result&&typeof N(r.result)=="object"?r.result:void 0)?.value}function N(e){return e&&typeof e=="object"?e:null}function _(e){return`(() => {
|
|
30
|
+
const mmkv = ${jt};
|
|
31
31
|
if (!mmkv) {
|
|
32
32
|
throw new Error("MMKV debug hook not found at globalThis.__EXPO_METRO_MCP__.mmkv");
|
|
33
33
|
}
|
|
@@ -35,14 +35,14 @@ ${l}`:a)}return(r?.result&&typeof O(r.result)=="object"?r.result:void 0)?.value}
|
|
|
35
35
|
throw new Error("MMKV debug hook is present but missing one or more required methods: getItem, setItem, removeItem, getAllKeys");
|
|
36
36
|
}
|
|
37
37
|
return (${e})();
|
|
38
|
-
})()`}function
|
|
38
|
+
})()`}function L(e){return e==null?null:JSON.parse(e)}async function Je(e){y(e.key);let t=await b(_(`() => ({ key: ${d(e.key)}, value: mmkv.getItem(${d(e.key)}) ?? null })`),e.timeout_ms);return $(t)}async function Ke(e){M("mmkv_set"),y(e.key);let t=await b(_(`() => {
|
|
39
39
|
mmkv.setItem(${d(e.key)}, ${d(e.value)});
|
|
40
40
|
return { ok: true, key: ${d(e.key)}, value: mmkv.getItem(${d(e.key)}) ?? null };
|
|
41
|
-
}`),e.timeout_ms);return
|
|
41
|
+
}`),e.timeout_ms);return $(t)}async function Fe(e){M("mmkv_remove"),y(e.key);let t=await b(_(`() => {
|
|
42
42
|
mmkv.removeItem(${d(e.key)});
|
|
43
43
|
return { ok: true, key: ${d(e.key)} };
|
|
44
|
-
}`),e.timeout_ms);return
|
|
44
|
+
}`),e.timeout_ms);return $(t)}async function Be(e){let t=await b(_("() => ({ keys: mmkv.getAllKeys() })"),e.timeout_ms),n=t&&typeof t=="object"&&!Array.isArray(t)?t:{};return $({keys:Re(Array.isArray(n.keys)?n.keys:[])})}async function Ge(e){y(e.key);let t=await b(_(`() => mmkv.getItem(${d(e.key)}) ?? null`),e.timeout_ms),n=L(t);return $({key:e.key,value:n})}async function D(e){M("mmkv_set_json"),y(e.key);let t=JSON.stringify(e.value),n=await b(_(`() => {
|
|
45
45
|
mmkv.setItem(${d(e.key)}, ${d(t)});
|
|
46
46
|
return { ok: true, key: ${d(e.key)}, value: JSON.parse(mmkv.getItem(${d(e.key)}) ?? "null") };
|
|
47
|
-
}`),e.timeout_ms);return
|
|
47
|
+
}`),e.timeout_ms);return $(n)}async function Ue(e){M("mmkv_merge_json"),y(e.key);let t=await b(_(`() => mmkv.getItem(${d(e.key)}) ?? null`),e.timeout_ms),n=t==null?{}:L(t);if(!n||typeof n!="object"||Array.isArray(n))throw new Error("Existing MMKV value is not a JSON object, so merge would be sketchy. Use mmkv_set_json instead.");let r={...n,...e.value};return D({key:e.key,value:r,timeout_ms:e.timeout_ms})}async function Ve(e){y(e.key);let t=await b(_(`() => mmkv.getItem(${d(e.key)}) ?? null`),e.timeout_ms),n=L(t);return $({key:e.key,state:n?.state??null,version:typeof n?.version=="number"?n.version:null,raw:n})}async function Xe(e){M("zustand_persist_set"),y(e.key);let t={state:e.state,...e.version!==void 0?{version:e.version}:{}};return D({key:e.key,value:t,timeout_ms:e.timeout_ms})}async function Ze(e){M("zustand_persist_merge"),y(e.key);let t=await b(_(`() => mmkv.getItem(${d(e.key)}) ?? null`),e.timeout_ms),n=t==null?null:L(t),r=n?.state;if(r!=null&&(typeof r!="object"||Array.isArray(r)))throw new Error("Existing persisted Zustand state is not an object, so merge would be sketchy. Use zustand_persist_set instead.");let o={state:{...r??{},...e.state},version:e.version??(typeof n?.version=="number"?n.version:void 0)};return D({key:e.key,value:o,timeout_ms:e.timeout_ms})}var m=new Lt({name:"expo-metro-mcp",version:"1.0.7"});m.registerTool("get_logs",{description:"Fetch recent logs from the Metro dev server buffer. Supports filtering by level and time.",inputSchema:U.shape},async e=>({content:[{type:"text",text:V(e)}]}));m.registerTool("get_errors",{description:"Fetch recent errors from the Metro dev server buffer, with stack traces.",inputSchema:Z.shape},async e=>({content:[{type:"text",text:H(e)}]}));m.registerTool("get_status",{description:"Check the connection status of the Metro dev server and buffer statistics."},async()=>({content:[{type:"text",text:q()}]}));m.registerTool("connect",{description:"Grab the CDP connection to the Metro dev server. Use this if get_status shows disconnected, or to take over the connection from React Native DevTools."},async()=>({content:[{type:"text",text:await c.grabConnection()}]}));m.registerTool("disconnect",{description:"Release the CDP connection so React Native DevTools can connect. Use this before switching to DevTools. Call connect when you want to reattach."},async()=>(c.disconnect(),{content:[{type:"text",text:"Disconnected. DevTools can now connect freely."}]}));m.registerTool("clear_logs",{description:"Clear the internal log buffer. Useful after resolving an issue."},async()=>({content:[{type:"text",text:Y()}]}));m.registerTool("watch_logs",{description:"Listen for incoming logs for a short time window and return all collected entries.",inputSchema:Q.shape},async e=>({content:[{type:"text",text:await ee(e)}]}));m.registerTool("reload",{description:"Reload the React Native app via Metro."},async()=>(se(),{content:[{type:"text",text:await te()}]}));m.registerTool("resolve_stack",{description:"Resolve a stack trace from the buffer against the Metro source map, showing original file:line instead of bundle offsets. Optionally filter by error message substring.",inputSchema:ie.shape},async e=>({content:[{type:"text",text:await ae(e)}]}));m.registerTool("list_devices",{description:"List active iOS simulators and Android emulators. Use this to find available devices before taking screenshots or sending taps."},async()=>({content:[{type:"text",text:Me()}]}));m.registerTool("screenshot",{description:"Take a screenshot of the active iOS simulator or Android emulator. Returns the image directly. Optionally specify platform ('ios' or 'android') or device_id if multiple devices are running.",inputSchema:de.shape},async e=>{let t=fe(e);if(t.type==="image"){let n=t.width&&t.height?`Screenshot dimensions: ${t.width}x${t.height}px. Use these exact coordinates for tap and swipe \u2014 no scaling needed.`:"Screenshot dimensions unknown.";return{content:[{type:"image",data:t.data,mimeType:t.mimeType},{type:"text",text:n}]}}return{content:[{type:"text",text:t.text}]}});m.registerTool("tap",{description:"Tap at x,y coordinates on the active simulator/emulator. Use screenshot first to determine coordinates. iOS requires idb (brew install idb-companion && pip3 install fb-idb). Android works via adb out of the box. Optionally specify platform or device_id.",inputSchema:ge.shape},async e=>({content:[{type:"text",text:ye(e)}]}));m.registerTool("swipe",{description:"Swipe from one coordinate to another. Requires idb on iOS (brew install idb-companion && pip3 install fb-idb). Android works via adb out of the box. Useful for scrolling lists or dismissing sheets.",inputSchema:he.shape},async e=>({content:[{type:"text",text:be(e)}]}));m.registerTool("input_text",{description:"Type text into the currently focused input field on the active simulator/emulator. Use tap to focus an input first, then call this tool. Works without requiring the on-screen keyboard to be visible. iOS requires idb (brew install idb-companion && pip3 install fb-idb). Android works via adb out of the box.",inputSchema:_e.shape},async e=>({content:[{type:"text",text:xe(e)}]}));m.registerTool("input_key",{description:"Send a special key press to the active simulator/emulator. Supported keys: enter, backspace, delete, tab, escape, home, end, back, space, up, down, left, right. Use after input_text to submit forms (enter) or correct mistakes (backspace).",inputSchema:ke.shape},async e=>({content:[{type:"text",text:Se(e)}]}));A.enableEval&&m.registerTool("evaluate",{description:"Run JavaScript inside the connected React Native app runtime via Metro CDP. Supports async expressions, so you can inspect globals, read or mutate state, trigger navigation, or call app helpers directly.",inputSchema:Te.shape},async e=>({content:[{type:"text",text:await Ee(e)}]}));m.registerTool("mmkv_get",{description:"Read a value from the app's MMKV debug hook at globalThis.__EXPO_METRO_MCP__.mmkv. Returns JSON with key and value.",inputSchema:Oe.shape},async e=>({content:[{type:"text",text:await Je(e)}]}));m.registerTool("mmkv_set",{description:"Write a string value through the app's MMKV debug hook at globalThis.__EXPO_METRO_MCP__.mmkv. Useful for seeding persisted Zustand state before a screen renders.",inputSchema:Ce.shape},async e=>({content:[{type:"text",text:await Ke(e)}]}));m.registerTool("mmkv_remove",{description:"Remove a key through the app's MMKV debug hook at globalThis.__EXPO_METRO_MCP__.mmkv.",inputSchema:Ae.shape},async e=>({content:[{type:"text",text:await Fe(e)}]}));m.registerTool("mmkv_keys",{description:"List all keys exposed by the app's MMKV debug hook at globalThis.__EXPO_METRO_MCP__.mmkv.",inputSchema:Ne.shape},async e=>({content:[{type:"text",text:await Be(e)}]}));m.registerTool("mmkv_get_json",{description:"Read a JSON value from the app's MMKV debug hook and parse it before returning it.",inputSchema:De.shape},async e=>({content:[{type:"text",text:await Ge(e)}]}));m.registerTool("mmkv_set_json",{description:"Store any JSON-serializable value in MMKV through the app's debug hook. Safer than manually stringifying payloads in AI prompts.",inputSchema:Ie.shape},async e=>({content:[{type:"text",text:await D(e)}]}));m.registerTool("mmkv_merge_json",{description:"Merge a shallow JSON object into an existing MMKV JSON object. Good for patching persisted debug/config state without replacing the whole blob.",inputSchema:je.shape},async e=>({content:[{type:"text",text:await Ue(e)}]}));m.registerTool("zustand_persist_get",{description:"Read a persisted Zustand entry from MMKV and return its parsed state and version fields separately.",inputSchema:Le.shape},async e=>({content:[{type:"text",text:await Ve(e)}]}));m.registerTool("zustand_persist_set",{description:"Write a persisted Zustand payload to MMKV in the canonical { state, version? } shape.",inputSchema:ze.shape},async e=>({content:[{type:"text",text:await Xe(e)}]}));m.registerTool("zustand_persist_merge",{description:"Merge fields into the state object of an existing persisted Zustand MMKV entry, preserving version unless you override it.",inputSchema:We.shape},async e=>({content:[{type:"text",text:await Ze(e)}]}));async function Wt(){c.start();let e=new zt;await m.connect(e),process.on("SIGINT",()=>{c.stop(),process.exit(0)}),process.on("SIGTERM",()=>{c.stop(),process.exit(0)})}Wt().catch(e=>{process.stderr.write(`Fatal error: ${e}
|
|
48
48
|
`),process.exit(1)});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synnode/expo-metro-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "MCP server for Expo/React Native development — live logs, stack trace resolution, runtime evaluation, and simulator/emulator automation via CDP and platform CLIs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|