rnxsim 0.1.365 → 0.1.366

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.
Files changed (184) hide show
  1. package/README.md +2 -0
  2. package/cli/bridge-flow-runner.ts +2 -2
  3. package/cli/commands/control.ts +3 -44
  4. package/cli/commands/detox.ts +18 -1
  5. package/cli/commands/flow.ts +2 -1
  6. package/detox/index.ts +3 -1
  7. package/dist-cli/bin.js +5 -5
  8. package/dist-cli/chunks/{agent-BVMNQFKF.js → agent-SO6IOPRK.js} +2 -2
  9. package/dist-cli/chunks/{agent-wrapper-B6YFXGC3.js → agent-wrapper-PMYCB2KO.js} +2 -2
  10. package/dist-cli/chunks/{app-fonts-MEI6R6XR.js → app-fonts-LULQGFY7.js} +2 -2
  11. package/dist-cli/chunks/{assert-JGHI53HV.js → assert-WGNCD4ZU.js} +2 -2
  12. package/dist-cli/chunks/{auth-STEOXUS7.js → auth-G7CCHH3M.js} +2 -2
  13. package/dist-cli/chunks/{beta-HT3FLS5T.js → beta-N4LXL5TO.js} +2 -2
  14. package/dist-cli/chunks/{camera-ZCLFSLON.js → camera-4JHCYBQS.js} +2 -2
  15. package/dist-cli/chunks/{chunk-5AS7S6HY.js → chunk-2S7BKUP2.js} +2 -2
  16. package/dist-cli/chunks/{chunk-RI3NS77J.js → chunk-2UXU53MG.js} +1 -1
  17. package/dist-cli/chunks/{chunk-XPOTXF5Q.js → chunk-34IQM22N.js} +2 -2
  18. package/dist-cli/chunks/{chunk-52ES4API.js → chunk-3X6IBUBX.js} +2 -2
  19. package/dist-cli/chunks/{chunk-TULWPOAY.js → chunk-5DAIA77C.js} +2 -2
  20. package/dist-cli/chunks/{chunk-FIXRGPUT.js → chunk-5NMCF5VF.js} +1 -1
  21. package/dist-cli/chunks/{chunk-QIQL7Y5V.js → chunk-5OK3LCEJ.js} +2 -2
  22. package/dist-cli/chunks/{chunk-NEH4T67X.js → chunk-6PL2CCGP.js} +2 -2
  23. package/dist-cli/chunks/{chunk-MRNW4GVL.js → chunk-77ZHQJZR.js} +2 -2
  24. package/dist-cli/chunks/chunk-7B37IZBN.js +15 -0
  25. package/dist-cli/chunks/{chunk-CYIE65QN.js → chunk-A5FXX5PJ.js} +1 -1
  26. package/dist-cli/chunks/{chunk-JRPU5TFI.js → chunk-AFV32WZT.js} +3 -3
  27. package/dist-cli/chunks/{chunk-M5KJCLUW.js → chunk-CN6XYYEA.js} +12 -12
  28. package/dist-cli/chunks/{chunk-J66US2OZ.js → chunk-CTG7HBCA.js} +1 -1
  29. package/dist-cli/chunks/{chunk-ZGD7GZRI.js → chunk-DMPF2D24.js} +2 -2
  30. package/dist-cli/chunks/{chunk-622E4JET.js → chunk-DW37EVX5.js} +1 -1
  31. package/dist-cli/chunks/{chunk-XKRUNS7A.js → chunk-DWFIH7CB.js} +2 -2
  32. package/dist-cli/chunks/{chunk-WGNDJARS.js → chunk-EFR5DVNP.js} +1 -1
  33. package/dist-cli/chunks/{chunk-6BAF6GFS.js → chunk-EGAGZHO6.js} +1 -1
  34. package/dist-cli/chunks/{chunk-MACI34ST.js → chunk-EQK5MJCU.js} +2 -2
  35. package/dist-cli/chunks/{chunk-JAFF4RAU.js → chunk-GAJQSITE.js} +1 -1
  36. package/dist-cli/chunks/{chunk-ZGH5U4AJ.js → chunk-GUI3PDH2.js} +2 -2
  37. package/dist-cli/chunks/{chunk-DBJZUCFP.js → chunk-GXAB52K7.js} +2 -2
  38. package/dist-cli/chunks/{chunk-O6P4N2JD.js → chunk-H23KWIKA.js} +2 -2
  39. package/dist-cli/chunks/{chunk-2CXHDTQ5.js → chunk-HT6GFZOZ.js} +1 -1
  40. package/dist-cli/chunks/{chunk-BGRGP3QR.js → chunk-I6KIM6PN.js} +1 -1
  41. package/dist-cli/chunks/{chunk-XRO5WQXP.js → chunk-IZKHOM37.js} +1 -1
  42. package/dist-cli/chunks/{chunk-EVBQDQVY.js → chunk-JHXETDVC.js} +1 -1
  43. package/dist-cli/chunks/{chunk-2JFZ5Z7Q.js → chunk-KLF3K6ZZ.js} +1 -1
  44. package/dist-cli/chunks/{chunk-EFXNSJ55.js → chunk-LN3RNW6X.js} +2 -2
  45. package/dist-cli/chunks/{chunk-TKCGP3DD.js → chunk-M3BNCF7E.js} +2 -2
  46. package/dist-cli/chunks/{chunk-YE3M3YD4.js → chunk-M5QCGLXR.js} +3 -3
  47. package/dist-cli/chunks/{chunk-4WFN4DFR.js → chunk-M6SKQGAN.js} +2 -2
  48. package/dist-cli/chunks/{chunk-R4FQZXJC.js → chunk-MGDSJZ37.js} +2 -2
  49. package/dist-cli/chunks/{chunk-2SFMW5K6.js → chunk-N3UC7HIX.js} +1 -1
  50. package/dist-cli/chunks/{chunk-OSZ5HZSI.js → chunk-NAYLPOC5.js} +2 -2
  51. package/dist-cli/chunks/{chunk-XR4KID25.js → chunk-NM2L2QV4.js} +2 -2
  52. package/dist-cli/chunks/{chunk-67ATAITF.js → chunk-NQFJOCSF.js} +2 -2
  53. package/dist-cli/chunks/{chunk-HIDS3PYC.js → chunk-ONORMKWX.js} +2 -2
  54. package/dist-cli/chunks/{chunk-XXNPISER.js → chunk-ORWP2E7R.js} +2 -2
  55. package/dist-cli/chunks/{chunk-GK7PGYFH.js → chunk-OW5XHAA4.js} +1 -1
  56. package/dist-cli/chunks/{chunk-GZEUDDAW.js → chunk-RYIMSKDO.js} +2 -2
  57. package/dist-cli/chunks/{chunk-X5ZOZVHC.js → chunk-SA6XVI55.js} +1 -1
  58. package/dist-cli/chunks/{chunk-ZTWUNK45.js → chunk-TIAEYO6I.js} +2 -2
  59. package/dist-cli/chunks/{chunk-5VHWQGGF.js → chunk-TTASWLKM.js} +1 -1
  60. package/dist-cli/chunks/{chunk-O2ORLK6W.js → chunk-UCHW7ZHR.js} +2 -2
  61. package/dist-cli/chunks/{chunk-EFL2ASZO.js → chunk-VIND4SP3.js} +1 -1
  62. package/dist-cli/chunks/{chunk-UR345KM3.js → chunk-XJU2QIRY.js} +2 -2
  63. package/dist-cli/chunks/{chunk-W3UIDI3W.js → chunk-XXNLYR64.js} +3 -3
  64. package/dist-cli/chunks/{chunk-25BTVHK2.js → chunk-YCCCPFWT.js} +2 -2
  65. package/dist-cli/chunks/chunk-YGMEDJKJ.js +4 -0
  66. package/dist-cli/chunks/{chunk-FHWZPUGC.js → chunk-YXXKSL6R.js} +1 -1
  67. package/dist-cli/chunks/{chunk-ZCP4CRP6.js → chunk-ZFM24DOX.js} +2 -2
  68. package/dist-cli/chunks/{chunk-EAFKG5R3.js → chunk-ZJIS3CTW.js} +2 -2
  69. package/dist-cli/chunks/{chunk-DQFOHIRF.js → chunk-ZLVRYXZ7.js} +1 -1
  70. package/dist-cli/chunks/{chunk-MR3YPNUI.js → chunk-ZMJ75EDG.js} +2 -2
  71. package/dist-cli/chunks/{cleanup-G4TEYVKY.js → cleanup-KRIATKFH.js} +2 -2
  72. package/dist-cli/chunks/cli-version-SYGNHGFR.js +4 -0
  73. package/dist-cli/chunks/{compat-EZ5NP32J.js → compat-6PTQ3QEA.js} +2 -2
  74. package/dist-cli/chunks/{config-27M2DGCS.js → config-ETGDUANO.js} +2 -2
  75. package/dist-cli/chunks/control-RBTNOLQM.js +4 -0
  76. package/dist-cli/chunks/daemon-N3SC5DK6.js +4 -0
  77. package/dist-cli/chunks/{debug-EXU3D7PC.js → debug-WIIB6ZKC.js} +3 -3
  78. package/dist-cli/chunks/{desktop-SHHPZAWQ.js → desktop-TD7Q5LNB.js} +3 -3
  79. package/dist-cli/chunks/detox-7GNR33VM.js +52 -0
  80. package/dist-cli/chunks/{device-4NI26HG6.js → device-QOR3PGUF.js} +2 -2
  81. package/dist-cli/chunks/{diagnose-P4SFXH6J.js → diagnose-RRCBLP6E.js} +2 -2
  82. package/dist-cli/chunks/{disk-cleanup-BUAKWYMY.js → disk-cleanup-MJUGJPXT.js} +2 -2
  83. package/dist-cli/chunks/drivers-W6E3BZH4.js +4 -0
  84. package/dist-cli/chunks/{film-Y2BAQINB.js → film-D7Y64LID.js} +3 -3
  85. package/dist-cli/chunks/flow-7QAE2V5Y.js +4 -0
  86. package/dist-cli/chunks/help-NLD6KWVG.js +4 -0
  87. package/dist-cli/chunks/{hidden-runtime-alias-U2MAY2XV.js → hidden-runtime-alias-JGURVR2Z.js} +2 -2
  88. package/dist-cli/chunks/{home-paths-PAJMZCOS.js → home-paths-4ICFIGXO.js} +2 -2
  89. package/dist-cli/chunks/inspect-C347BG52.js +4 -0
  90. package/dist-cli/chunks/install-desktop-YR6TWZYK.js +4 -0
  91. package/dist-cli/chunks/{login-D53XKBEO.js → login-RU33QR2G.js} +2 -2
  92. package/dist-cli/chunks/{logout-HZQKLNUJ.js → logout-2IUG3Y5J.js} +2 -2
  93. package/dist-cli/chunks/{maestro-XSS2KX6L.js → maestro-2MXTGG7I.js} +3 -3
  94. package/dist-cli/chunks/{maestro-generate-DNDQX3JQ.js → maestro-generate-2PYIFGLR.js} +3 -3
  95. package/dist-cli/chunks/{mode-LEVGOKI3.js → mode-3DAPG3HF.js} +2 -2
  96. package/dist-cli/chunks/{optional-demo-registry-2XZIJNDG.js → optional-demo-registry-BWSDJF2Z.js} +2 -2
  97. package/dist-cli/chunks/{perf-OKMAXDLB.js → perf-2TKPWB4N.js} +2 -2
  98. package/dist-cli/chunks/{permissions-VG5TBUTN.js → permissions-5CSMHYNA.js} +2 -2
  99. package/dist-cli/chunks/privacy-PWWC2BXO.js +4 -0
  100. package/dist-cli/chunks/{record-BQYGZOMV.js → record-I2HJCFP3.js} +3 -3
  101. package/dist-cli/chunks/{report-issue-HCPZESWE.js → report-issue-A4FZWBOC.js} +2 -2
  102. package/dist-cli/chunks/reset-Y3PBPFYA.js +4 -0
  103. package/dist-cli/chunks/runtime-IC6NVVGA.js +4 -0
  104. package/dist-cli/chunks/{screenshot-command-DWEYPJE7.js → screenshot-command-UFW5BFHP.js} +3 -3
  105. package/dist-cli/chunks/{screenshot-layers-5GMBXLO7.js → screenshot-layers-5BIAL4YN.js} +2 -2
  106. package/dist-cli/chunks/{screenshots-capture-5KCI7IZD.js → screenshots-capture-EHYS3BV2.js} +2 -2
  107. package/dist-cli/chunks/{serve-QNKTA2HR.js → serve-O4FDM3AC.js} +2 -2
  108. package/dist-cli/chunks/setup-Y4QGSJBM.js +4 -0
  109. package/dist-cli/chunks/{skills-DOEALD3X.js → skills-ZK55SY32.js} +2 -2
  110. package/dist-cli/chunks/{state-3HH2IHIF.js → state-KFLAZNEY.js} +2 -2
  111. package/dist-cli/chunks/{storage-E7WUWN3E.js → storage-WP33ISEM.js} +2 -2
  112. package/dist-cli/chunks/store-VEE22CAY.js +4 -0
  113. package/dist-cli/chunks/telemetry-PZV4BISJ.js +4 -0
  114. package/dist-cli/chunks/{timeline-IKNUGQ2A.js → timeline-DKGW5S4I.js} +2 -2
  115. package/dist-cli/chunks/{upgrade-VS3OKXE2.js → upgrade-LL7G4B7G.js} +2 -2
  116. package/dist-cli/chunks/upload-Y4C3YZDK.js +4 -0
  117. package/dist-cli/chunks/{version-F4XHNTBZ.js → version-YLWR5G4F.js} +2 -2
  118. package/dist-cli/chunks/{web-F72724QL.js → web-QELZ4CP6.js} +2 -2
  119. package/dist-cli/chunks/welcome-ZPKA5PZJ.js +5 -0
  120. package/dist-cli/chunks/{what-happened-APK7YKOV.js → what-happened-MIOMZWM5.js} +2 -2
  121. package/dist-lib/agent-daemon-client.cjs +1 -1
  122. package/dist-lib/agent-events.cjs +1 -1
  123. package/dist-lib/agent-identity.cjs +1 -1
  124. package/dist-lib/agent-sessions.cjs +1 -1
  125. package/dist-lib/attached-projects.cjs +1 -1
  126. package/dist-lib/auth/shared-session.cjs +1 -1
  127. package/dist-lib/backend-origin.cjs +1 -1
  128. package/dist-lib/beta.cjs +1 -1
  129. package/dist-lib/beta.mjs +1 -1
  130. package/dist-lib/bridge-constants.cjs +1 -1
  131. package/dist-lib/bridge-contract.cjs +1 -1
  132. package/dist-lib/cli-constants.cjs +1 -1
  133. package/dist-lib/config.cjs +1 -1
  134. package/dist-lib/detox/index.cjs +4 -2
  135. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  136. package/dist-lib/home-paths.cjs +1 -1
  137. package/dist-lib/host/bridge-host.cjs +1 -1
  138. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  139. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  140. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  141. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  142. package/dist-lib/host/websocket-proxy.cjs +1 -1
  143. package/dist-lib/index.cjs +31 -1
  144. package/dist-lib/jump-to-source-babel.cjs +1 -1
  145. package/dist-lib/mcp.cjs +673 -0
  146. package/dist-lib/mcp.mjs +641 -0
  147. package/dist-lib/menu.cjs +1 -1
  148. package/dist-lib/menu.mjs +1 -1
  149. package/dist-lib/metro-production-bundle.cjs +1 -1
  150. package/dist-lib/metro-production-bundle.mjs +1 -1
  151. package/dist-lib/metro.cjs +1 -1
  152. package/dist-lib/profiles.cjs +1 -1
  153. package/dist-lib/public-brand.cjs +1 -1
  154. package/dist-lib/react-native-host-modules.cjs +1 -1
  155. package/dist-lib/react-native-host-modules.mjs +1 -1
  156. package/dist-lib/render-mode.cjs +1 -1
  157. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  158. package/dist-lib/sdk.cjs +31 -1
  159. package/dist-lib/sdk.mjs +31 -1
  160. package/dist-lib/skills.cjs +67 -53
  161. package/dist-lib/vite.cjs +1 -1
  162. package/package.json +7 -1
  163. package/src/app-url.ts +40 -0
  164. package/src/mcp.ts +791 -0
  165. package/src/sim-client.ts +12 -6
  166. package/dist-cli/chunks/chunk-IWPS3EKJ.js +0 -15
  167. package/dist-cli/chunks/chunk-MCHKBFUL.js +0 -4
  168. package/dist-cli/chunks/cli-version-ZM3PVKQ5.js +0 -4
  169. package/dist-cli/chunks/control-4QR43FXV.js +0 -4
  170. package/dist-cli/chunks/daemon-2U4CJ6AT.js +0 -4
  171. package/dist-cli/chunks/detox-RQ6FFSBS.js +0 -52
  172. package/dist-cli/chunks/drivers-OVCUPQTM.js +0 -4
  173. package/dist-cli/chunks/flow-ELTLKKZ3.js +0 -4
  174. package/dist-cli/chunks/help-CDEZAG2L.js +0 -4
  175. package/dist-cli/chunks/inspect-VVQRPJLS.js +0 -4
  176. package/dist-cli/chunks/install-desktop-YXSRKR35.js +0 -4
  177. package/dist-cli/chunks/privacy-MNRHREME.js +0 -4
  178. package/dist-cli/chunks/reset-R4E4W4YI.js +0 -4
  179. package/dist-cli/chunks/runtime-XRQZ7W34.js +0 -4
  180. package/dist-cli/chunks/setup-RNMM5GO5.js +0 -4
  181. package/dist-cli/chunks/store-HUPZUCFG.js +0 -4
  182. package/dist-cli/chunks/telemetry-IJ53XR2Y.js +0 -4
  183. package/dist-cli/chunks/upload-A57NAAHF.js +0 -4
  184. package/dist-cli/chunks/welcome-B72RRMBW.js +0 -5
package/src/mcp.ts ADDED
@@ -0,0 +1,791 @@
1
+ import type { LogFilterOptions, LogLevel } from '../cli/commands/inspect/core'
2
+ import type { PerformOptions, ResetOptions, SimStateOptions } from './bridge-contract'
3
+ import type { SimClient, SimPerformStep, SimScreenshotOptions } from './sim-client'
4
+
5
+ export type PerSimMcpToolName = 'state' | 'perform' | 'reset' | 'screenshot' | 'logs'
6
+
7
+ export interface McpJsonSchema {
8
+ type?: 'object' | 'array' | 'string' | 'number' | 'integer' | 'boolean'
9
+ description?: string
10
+ properties?: Readonly<Record<string, McpJsonSchema>>
11
+ required?: readonly string[]
12
+ additionalProperties?: boolean
13
+ items?: McpJsonSchema
14
+ oneOf?: readonly McpJsonSchema[]
15
+ enum?: readonly (string | number | boolean)[]
16
+ const?: string | number | boolean
17
+ minimum?: number
18
+ maximum?: number
19
+ minLength?: number
20
+ minItems?: number
21
+ maxItems?: number
22
+ uniqueItems?: boolean
23
+ default?: string | number | boolean
24
+ }
25
+
26
+ export interface PerSimMcpToolDefinition {
27
+ name: PerSimMcpToolName
28
+ description: string
29
+ inputSchema: McpJsonSchema
30
+ }
31
+
32
+ export interface PerSimMcpTextContent {
33
+ type: 'text'
34
+ text: string
35
+ }
36
+
37
+ export interface PerSimMcpToolResult {
38
+ content: PerSimMcpTextContent[]
39
+ }
40
+
41
+ export interface PerSimMcpTool extends PerSimMcpToolDefinition {
42
+ execute(input: unknown): Promise<PerSimMcpToolResult>
43
+ }
44
+
45
+ export interface PerSimMcpServerAdapter {
46
+ registerTool(tool: PerSimMcpTool): void
47
+ }
48
+
49
+ export class McpToolInputError extends Error {
50
+ constructor(message: string) {
51
+ super(message)
52
+ this.name = 'McpToolInputError'
53
+ }
54
+ }
55
+
56
+ function strictObject(
57
+ properties: Readonly<Record<string, McpJsonSchema>>,
58
+ required: readonly string[] = [],
59
+ ): McpJsonSchema {
60
+ return {
61
+ type: 'object',
62
+ properties,
63
+ required,
64
+ additionalProperties: false,
65
+ }
66
+ }
67
+
68
+ function stepSchema(
69
+ type: SimPerformStep['type'],
70
+ properties: Readonly<Record<string, McpJsonSchema>> = {},
71
+ required: readonly string[] = [],
72
+ ): McpJsonSchema {
73
+ return strictObject(
74
+ {
75
+ type: { type: 'string', const: type },
76
+ ...properties,
77
+ },
78
+ ['type', ...required],
79
+ )
80
+ }
81
+
82
+ const coordinateSchema: McpJsonSchema = { type: 'number' }
83
+ const pointerIdSchema: McpJsonSchema = { type: 'integer', minimum: 0 }
84
+ const millisecondsSchema: McpJsonSchema = { type: 'integer', minimum: 0 }
85
+ const nonEmptyStringSchema: McpJsonSchema = { type: 'string', minLength: 1 }
86
+
87
+ const performStepSchema: McpJsonSchema = {
88
+ oneOf: [
89
+ stepSchema(
90
+ 'touchDown',
91
+ { x: coordinateSchema, y: coordinateSchema, pointerId: pointerIdSchema },
92
+ ['x', 'y'],
93
+ ),
94
+ stepSchema(
95
+ 'touchMove',
96
+ { x: coordinateSchema, y: coordinateSchema, pointerId: pointerIdSchema },
97
+ ['x', 'y'],
98
+ ),
99
+ stepSchema(
100
+ 'touchUp',
101
+ { x: coordinateSchema, y: coordinateSchema, pointerId: pointerIdSchema },
102
+ ['x', 'y'],
103
+ ),
104
+ stepSchema('touchCancel', { pointerId: pointerIdSchema }),
105
+ stepSchema('tap', { x: coordinateSchema, y: coordinateSchema }, ['x', 'y']),
106
+ stepSchema('tapId', { id: nonEmptyStringSchema }, ['id']),
107
+ stepSchema('tapText', { text: nonEmptyStringSchema }, ['text']),
108
+ stepSchema(
109
+ 'doubleTap',
110
+ { x: coordinateSchema, y: coordinateSchema, gapMs: millisecondsSchema },
111
+ ['x', 'y'],
112
+ ),
113
+ stepSchema(
114
+ 'longPress',
115
+ {
116
+ x: coordinateSchema,
117
+ y: coordinateSchema,
118
+ durationMs: millisecondsSchema,
119
+ },
120
+ ['x', 'y'],
121
+ ),
122
+ stepSchema(
123
+ 'drag',
124
+ {
125
+ fromX: coordinateSchema,
126
+ fromY: coordinateSchema,
127
+ toX: coordinateSchema,
128
+ toY: coordinateSchema,
129
+ steps: { type: 'integer', minimum: 1 },
130
+ stepMs: millisecondsSchema,
131
+ },
132
+ ['fromX', 'fromY', 'toX', 'toY'],
133
+ ),
134
+ stepSchema(
135
+ 'pinch',
136
+ {
137
+ fromX1: coordinateSchema,
138
+ fromY1: coordinateSchema,
139
+ fromX2: coordinateSchema,
140
+ fromY2: coordinateSchema,
141
+ toX1: coordinateSchema,
142
+ toY1: coordinateSchema,
143
+ toX2: coordinateSchema,
144
+ toY2: coordinateSchema,
145
+ steps: { type: 'integer', minimum: 1 },
146
+ stepMs: millisecondsSchema,
147
+ },
148
+ ['fromX1', 'fromY1', 'fromX2', 'fromY2', 'toX1', 'toY1', 'toX2', 'toY2'],
149
+ ),
150
+ stepSchema(
151
+ 'scroll',
152
+ {
153
+ id: nonEmptyStringSchema,
154
+ x: coordinateSchema,
155
+ y: coordinateSchema,
156
+ animated: { type: 'boolean' },
157
+ },
158
+ ['id'],
159
+ ),
160
+ stepSchema('type', { text: { type: 'string' } }, ['text']),
161
+ stepSchema('key', { key: nonEmptyStringSchema }, ['key']),
162
+ stepSchema('dismissKeyboard'),
163
+ stepSchema('wait', { ms: millisecondsSchema }, ['ms']),
164
+ stepSchema(
165
+ 'waitFor',
166
+ { testID: nonEmptyStringSchema, timeoutMs: millisecondsSchema },
167
+ ['testID'],
168
+ ),
169
+ stepSchema('waitFor', { id: nonEmptyStringSchema, timeoutMs: millisecondsSchema }, [
170
+ 'id',
171
+ ]),
172
+ ],
173
+ }
174
+
175
+ export const PER_SIM_MCP_TOOL_DEFINITIONS: Readonly<
176
+ Record<PerSimMcpToolName, PerSimMcpToolDefinition>
177
+ > = {
178
+ state: {
179
+ name: 'state',
180
+ description:
181
+ 'Capture the current app screenshot and UI tree in one call, with optional route, error, depth, and layer controls.',
182
+ inputSchema: strictObject({
183
+ screenshot: {
184
+ type: 'boolean',
185
+ default: true,
186
+ description: 'include the composed PNG screenshot',
187
+ },
188
+ tree: {
189
+ type: 'boolean',
190
+ default: true,
191
+ description: 'include the current UI tree',
192
+ },
193
+ route: { type: 'boolean', description: 'include route and navigation state' },
194
+ errors: { type: 'boolean', description: 'include recent runtime errors' },
195
+ depth: { type: 'integer', minimum: 0, description: 'maximum tree depth' },
196
+ layers: {
197
+ type: 'string',
198
+ enum: ['full', 'tenant', 'shell'],
199
+ description: 'screenshot layers to compose',
200
+ },
201
+ errorLimit: {
202
+ type: 'integer',
203
+ minimum: 0,
204
+ description: 'maximum recent error entries',
205
+ },
206
+ }),
207
+ },
208
+ perform: {
209
+ name: 'perform',
210
+ description:
211
+ 'Execute an ordered interaction batch inside the current app so gesture timing does not depend on MCP round trips.',
212
+ inputSchema: strictObject(
213
+ {
214
+ steps: { type: 'array', items: performStepSchema, minItems: 1 },
215
+ stopOnError: {
216
+ type: 'boolean',
217
+ description: 'stop before starting the next step after a failure',
218
+ },
219
+ timeoutMs: {
220
+ type: 'integer',
221
+ minimum: 1,
222
+ description: 'batch progress deadline checked between steps',
223
+ },
224
+ },
225
+ ['steps'],
226
+ ),
227
+ },
228
+ reset: {
229
+ name: 'reset',
230
+ description:
231
+ 'Clear data or full simulated install state for the current app and optionally relaunch it.',
232
+ inputSchema: strictObject({
233
+ strategy: {
234
+ type: 'string',
235
+ enum: ['data', 'full'],
236
+ default: 'data',
237
+ description: 'data clears app storage; full also clears install-scoped state',
238
+ },
239
+ relaunch: {
240
+ type: 'boolean',
241
+ default: true,
242
+ description: 'relaunch the app after clearing state',
243
+ },
244
+ }),
245
+ },
246
+ screenshot: {
247
+ name: 'screenshot',
248
+ description: 'Capture a PNG screenshot of the current app.',
249
+ inputSchema: strictObject({
250
+ layers: {
251
+ type: 'string',
252
+ enum: ['full', 'tenant', 'shell'],
253
+ description: 'screenshot layers to compose',
254
+ },
255
+ crop: strictObject(
256
+ {
257
+ x: coordinateSchema,
258
+ y: coordinateSchema,
259
+ w: { type: 'number', minimum: 0 },
260
+ h: { type: 'number', minimum: 0 },
261
+ },
262
+ ['x', 'y', 'w', 'h'],
263
+ ),
264
+ }),
265
+ },
266
+ logs: {
267
+ name: 'logs',
268
+ description:
269
+ 'Read filtered app console logs and the recent merged runtime error buffer from the current app.',
270
+ inputSchema: strictObject({
271
+ limit: {
272
+ type: 'integer',
273
+ minimum: 1,
274
+ default: 50,
275
+ description: 'maximum filtered log entries',
276
+ },
277
+ levels: {
278
+ type: 'array',
279
+ items: { type: 'string', enum: ['log', 'info', 'warn', 'error', 'debug'] },
280
+ minItems: 1,
281
+ uniqueItems: true,
282
+ description: 'console levels to include',
283
+ },
284
+ filter: {
285
+ type: 'string',
286
+ minLength: 1,
287
+ description: 'case-insensitive message substring',
288
+ },
289
+ includeInternal: {
290
+ type: 'boolean',
291
+ default: false,
292
+ description: 'include RNX engine-internal log entries',
293
+ },
294
+ errorLimit: {
295
+ type: 'integer',
296
+ minimum: 1,
297
+ default: 20,
298
+ description: 'maximum merged runtime error entries',
299
+ },
300
+ }),
301
+ },
302
+ }
303
+
304
+ const PER_SIM_MCP_TOOL_ORDER: readonly PerSimMcpToolName[] = [
305
+ 'state',
306
+ 'perform',
307
+ 'reset',
308
+ 'screenshot',
309
+ 'logs',
310
+ ]
311
+
312
+ function validationFailure(path: string, expectation: string): never {
313
+ throw new McpToolInputError(`${path} ${expectation}`)
314
+ }
315
+
316
+ function validateMcpInput(value: unknown, schema: McpJsonSchema, path: string): void {
317
+ if (schema.oneOf) {
318
+ let matches = 0
319
+ for (const candidate of schema.oneOf) {
320
+ try {
321
+ validateMcpInput(value, candidate, path)
322
+ matches++
323
+ } catch (error) {
324
+ if (!(error instanceof McpToolInputError)) throw error
325
+ }
326
+ }
327
+ if (matches !== 1) validationFailure(path, 'must match exactly one allowed shape')
328
+ return
329
+ }
330
+
331
+ if (schema.const !== undefined && value !== schema.const) {
332
+ validationFailure(path, `must equal ${JSON.stringify(schema.const)}`)
333
+ }
334
+ if (schema.enum && !schema.enum.some((candidate) => candidate === value)) {
335
+ validationFailure(path, `must be one of ${schema.enum.join(', ')}`)
336
+ }
337
+
338
+ if (schema.type === 'object') {
339
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
340
+ validationFailure(path, 'must be an object')
341
+ }
342
+ const properties = schema.properties ?? {}
343
+ for (const required of schema.required ?? []) {
344
+ if (!Object.prototype.hasOwnProperty.call(value, required)) {
345
+ validationFailure(`${path}.${required}`, 'is required')
346
+ }
347
+ }
348
+ for (const [key, child] of Object.entries(value)) {
349
+ const propertySchema = properties[key]
350
+ if (!propertySchema) {
351
+ if (schema.additionalProperties === false) {
352
+ validationFailure(`${path}.${key}`, 'is not allowed')
353
+ }
354
+ continue
355
+ }
356
+ validateMcpInput(child, propertySchema, `${path}.${key}`)
357
+ }
358
+ return
359
+ }
360
+
361
+ if (schema.type === 'array') {
362
+ if (!Array.isArray(value)) validationFailure(path, 'must be an array')
363
+ if (schema.minItems !== undefined && value.length < schema.minItems) {
364
+ validationFailure(path, `must contain at least ${schema.minItems} item(s)`)
365
+ }
366
+ if (schema.maxItems !== undefined && value.length > schema.maxItems) {
367
+ validationFailure(path, `must contain at most ${schema.maxItems} item(s)`)
368
+ }
369
+ if (
370
+ schema.uniqueItems &&
371
+ new Set(value.map((entry) => JSON.stringify(entry))).size !== value.length
372
+ ) {
373
+ validationFailure(path, 'must contain unique items')
374
+ }
375
+ const itemSchema = schema.items
376
+ if (itemSchema) {
377
+ value.forEach((entry, index) =>
378
+ validateMcpInput(entry, itemSchema, `${path}[${index}]`),
379
+ )
380
+ }
381
+ return
382
+ }
383
+
384
+ if (schema.type === 'string') {
385
+ if (typeof value !== 'string') validationFailure(path, 'must be a string')
386
+ if (schema.minLength !== undefined && value.length < schema.minLength) {
387
+ validationFailure(path, `must contain at least ${schema.minLength} character(s)`)
388
+ }
389
+ return
390
+ }
391
+
392
+ if (schema.type === 'number' || schema.type === 'integer') {
393
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
394
+ validationFailure(path, 'must be a finite number')
395
+ }
396
+ if (schema.type === 'integer' && !Number.isInteger(value)) {
397
+ validationFailure(path, 'must be an integer')
398
+ }
399
+ if (schema.minimum !== undefined && value < schema.minimum) {
400
+ validationFailure(path, `must be at least ${schema.minimum}`)
401
+ }
402
+ if (schema.maximum !== undefined && value > schema.maximum) {
403
+ validationFailure(path, `must be at most ${schema.maximum}`)
404
+ }
405
+ return
406
+ }
407
+
408
+ if (schema.type === 'boolean' && typeof value !== 'boolean') {
409
+ validationFailure(path, 'must be a boolean')
410
+ }
411
+ }
412
+
413
+ function isRecordValue(value: unknown): value is Record<string, unknown> {
414
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
415
+ }
416
+
417
+ function recordValue(value: unknown, path: string): Record<string, unknown> {
418
+ if (!isRecordValue(value)) {
419
+ validationFailure(path, 'must be an object')
420
+ }
421
+ return value
422
+ }
423
+
424
+ function stringValue(value: unknown, path: string): string {
425
+ if (typeof value !== 'string') validationFailure(path, 'must be a string')
426
+ return value
427
+ }
428
+
429
+ function numberValue(value: unknown, path: string): number {
430
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
431
+ validationFailure(path, 'must be a finite number')
432
+ }
433
+ return value
434
+ }
435
+
436
+ function booleanValue(value: unknown, path: string): boolean {
437
+ if (typeof value !== 'boolean') validationFailure(path, 'must be a boolean')
438
+ return value
439
+ }
440
+
441
+ function optionalNumber(
442
+ record: Record<string, unknown>,
443
+ key: string,
444
+ path: string,
445
+ ): number | undefined {
446
+ return record[key] === undefined
447
+ ? undefined
448
+ : numberValue(record[key], `${path}.${key}`)
449
+ }
450
+
451
+ function optionalBoolean(
452
+ record: Record<string, unknown>,
453
+ key: string,
454
+ path: string,
455
+ ): boolean | undefined {
456
+ return record[key] === undefined
457
+ ? undefined
458
+ : booleanValue(record[key], `${path}.${key}`)
459
+ }
460
+
461
+ // the parsers below are the single owner of "untrusted JSON -> bridge-contract
462
+ // argument". a transport in front of these tools (the RNX Cloud sim service's
463
+ // HTTP and WebSocket routes) validates its own request bodies with them rather
464
+ // than growing a second reader for the same shapes.
465
+ export function parsePerformStep(value: unknown, index: number): SimPerformStep {
466
+ const path = `perform.steps[${index}]`
467
+ const step = recordValue(value, path)
468
+ const type = stringValue(step.type, `${path}.type`)
469
+ switch (type) {
470
+ case 'touchDown':
471
+ case 'touchMove':
472
+ case 'touchUp': {
473
+ const pointerId = optionalNumber(step, 'pointerId', path)
474
+ return {
475
+ type,
476
+ x: numberValue(step.x, `${path}.x`),
477
+ y: numberValue(step.y, `${path}.y`),
478
+ ...(pointerId === undefined ? {} : { pointerId }),
479
+ }
480
+ }
481
+ case 'touchCancel': {
482
+ const pointerId = optionalNumber(step, 'pointerId', path)
483
+ return { type, ...(pointerId === undefined ? {} : { pointerId }) }
484
+ }
485
+ case 'tap':
486
+ return {
487
+ type,
488
+ x: numberValue(step.x, `${path}.x`),
489
+ y: numberValue(step.y, `${path}.y`),
490
+ }
491
+ case 'tapId':
492
+ return { type, id: stringValue(step.id, `${path}.id`) }
493
+ case 'tapText':
494
+ return { type, text: stringValue(step.text, `${path}.text`) }
495
+ case 'doubleTap': {
496
+ const gapMs = optionalNumber(step, 'gapMs', path)
497
+ return {
498
+ type,
499
+ x: numberValue(step.x, `${path}.x`),
500
+ y: numberValue(step.y, `${path}.y`),
501
+ ...(gapMs === undefined ? {} : { gapMs }),
502
+ }
503
+ }
504
+ case 'longPress': {
505
+ const durationMs = optionalNumber(step, 'durationMs', path)
506
+ return {
507
+ type,
508
+ x: numberValue(step.x, `${path}.x`),
509
+ y: numberValue(step.y, `${path}.y`),
510
+ ...(durationMs === undefined ? {} : { durationMs }),
511
+ }
512
+ }
513
+ case 'drag': {
514
+ const steps = optionalNumber(step, 'steps', path)
515
+ const stepMs = optionalNumber(step, 'stepMs', path)
516
+ return {
517
+ type,
518
+ fromX: numberValue(step.fromX, `${path}.fromX`),
519
+ fromY: numberValue(step.fromY, `${path}.fromY`),
520
+ toX: numberValue(step.toX, `${path}.toX`),
521
+ toY: numberValue(step.toY, `${path}.toY`),
522
+ ...(steps === undefined ? {} : { steps }),
523
+ ...(stepMs === undefined ? {} : { stepMs }),
524
+ }
525
+ }
526
+ case 'pinch': {
527
+ const steps = optionalNumber(step, 'steps', path)
528
+ const stepMs = optionalNumber(step, 'stepMs', path)
529
+ return {
530
+ type,
531
+ fromX1: numberValue(step.fromX1, `${path}.fromX1`),
532
+ fromY1: numberValue(step.fromY1, `${path}.fromY1`),
533
+ fromX2: numberValue(step.fromX2, `${path}.fromX2`),
534
+ fromY2: numberValue(step.fromY2, `${path}.fromY2`),
535
+ toX1: numberValue(step.toX1, `${path}.toX1`),
536
+ toY1: numberValue(step.toY1, `${path}.toY1`),
537
+ toX2: numberValue(step.toX2, `${path}.toX2`),
538
+ toY2: numberValue(step.toY2, `${path}.toY2`),
539
+ ...(steps === undefined ? {} : { steps }),
540
+ ...(stepMs === undefined ? {} : { stepMs }),
541
+ }
542
+ }
543
+ case 'scroll': {
544
+ const x = optionalNumber(step, 'x', path)
545
+ const y = optionalNumber(step, 'y', path)
546
+ const animated = optionalBoolean(step, 'animated', path)
547
+ return {
548
+ type,
549
+ id: stringValue(step.id, `${path}.id`),
550
+ ...(x === undefined ? {} : { x }),
551
+ ...(y === undefined ? {} : { y }),
552
+ ...(animated === undefined ? {} : { animated }),
553
+ }
554
+ }
555
+ case 'type':
556
+ return { type, text: stringValue(step.text, `${path}.text`) }
557
+ case 'key':
558
+ return { type, key: stringValue(step.key, `${path}.key`) }
559
+ case 'dismissKeyboard':
560
+ return { type }
561
+ case 'wait':
562
+ return { type, ms: numberValue(step.ms, `${path}.ms`) }
563
+ case 'waitFor': {
564
+ const timeoutMs = optionalNumber(step, 'timeoutMs', path)
565
+ const selector =
566
+ step.testID === undefined
567
+ ? { id: stringValue(step.id, `${path}.id`) }
568
+ : { testID: stringValue(step.testID, `${path}.testID`) }
569
+ return {
570
+ type,
571
+ ...selector,
572
+ ...(timeoutMs === undefined ? {} : { timeoutMs }),
573
+ }
574
+ }
575
+ default:
576
+ return validationFailure(`${path}.type`, 'is not a supported perform step')
577
+ }
578
+ }
579
+
580
+ export function parseStateOptions(value: unknown): SimStateOptions {
581
+ const input = recordValue(value, 'state')
582
+ const route = optionalBoolean(input, 'route', 'state')
583
+ const errors = optionalBoolean(input, 'errors', 'state')
584
+ const depth = optionalNumber(input, 'depth', 'state')
585
+ const errorLimit = optionalNumber(input, 'errorLimit', 'state')
586
+ const layers =
587
+ input.layers === undefined ? undefined : stringValue(input.layers, 'state.layers')
588
+ if (
589
+ layers !== undefined &&
590
+ layers !== 'full' &&
591
+ layers !== 'tenant' &&
592
+ layers !== 'shell'
593
+ ) {
594
+ return validationFailure('state.layers', 'must be full, tenant, or shell')
595
+ }
596
+ return {
597
+ screenshot: optionalBoolean(input, 'screenshot', 'state') ?? true,
598
+ tree: optionalBoolean(input, 'tree', 'state') ?? true,
599
+ ...(route === undefined ? {} : { route }),
600
+ ...(errors === undefined ? {} : { errors }),
601
+ ...(depth === undefined ? {} : { depth }),
602
+ ...(layers === undefined ? {} : { layers }),
603
+ ...(errorLimit === undefined ? {} : { errorLimit }),
604
+ }
605
+ }
606
+
607
+ interface ParsedPerformInput {
608
+ steps: SimPerformStep[]
609
+ options?: PerformOptions
610
+ }
611
+
612
+ function parsePerformInput(value: unknown): ParsedPerformInput {
613
+ const input = recordValue(value, 'perform')
614
+ if (!Array.isArray(input.steps)) validationFailure('perform.steps', 'must be an array')
615
+ const stopOnError = optionalBoolean(input, 'stopOnError', 'perform')
616
+ const timeoutMs = optionalNumber(input, 'timeoutMs', 'perform')
617
+ const hasOptions = stopOnError !== undefined || timeoutMs !== undefined
618
+ return {
619
+ steps: input.steps.map(parsePerformStep),
620
+ ...(hasOptions
621
+ ? {
622
+ options: {
623
+ ...(stopOnError === undefined ? {} : { stopOnError }),
624
+ ...(timeoutMs === undefined ? {} : { timeoutMs }),
625
+ },
626
+ }
627
+ : {}),
628
+ }
629
+ }
630
+
631
+ function parseLogLevel(value: unknown, path: string): LogLevel {
632
+ const level = stringValue(value, path)
633
+ if (
634
+ level !== 'log' &&
635
+ level !== 'info' &&
636
+ level !== 'warn' &&
637
+ level !== 'error' &&
638
+ level !== 'debug'
639
+ ) {
640
+ return validationFailure(path, 'must be log, info, warn, error, or debug')
641
+ }
642
+ return level
643
+ }
644
+
645
+ interface ParsedLogsInput {
646
+ filterOptions: LogFilterOptions
647
+ limit: number
648
+ errorLimit: number
649
+ }
650
+
651
+ function parseLogsInput(value: unknown): ParsedLogsInput {
652
+ const input = recordValue(value, 'logs')
653
+ const levels = input.levels
654
+ if (levels !== undefined && !Array.isArray(levels)) {
655
+ validationFailure('logs.levels', 'must be an array')
656
+ }
657
+ const levelSet = levels
658
+ ? new Set(levels.map((level, index) => parseLogLevel(level, `logs.levels[${index}]`)))
659
+ : null
660
+ const filter =
661
+ input.filter === undefined ? null : stringValue(input.filter, 'logs.filter')
662
+ return {
663
+ filterOptions: {
664
+ level: levelSet,
665
+ filter,
666
+ showInternal: optionalBoolean(input, 'includeInternal', 'logs') ?? false,
667
+ },
668
+ limit: optionalNumber(input, 'limit', 'logs') ?? 50,
669
+ errorLimit: optionalNumber(input, 'errorLimit', 'logs') ?? 20,
670
+ }
671
+ }
672
+
673
+ export function parseResetOptions(value: unknown): ResetOptions {
674
+ const input = recordValue(value, 'reset')
675
+ const strategy =
676
+ input.strategy === undefined ? 'data' : stringValue(input.strategy, 'reset.strategy')
677
+ if (strategy !== 'data' && strategy !== 'full') {
678
+ return validationFailure('reset.strategy', 'must be data or full')
679
+ }
680
+ return {
681
+ strategy,
682
+ relaunch: optionalBoolean(input, 'relaunch', 'reset') ?? true,
683
+ }
684
+ }
685
+
686
+ export function parseScreenshotOptions(value: unknown): SimScreenshotOptions {
687
+ const input = recordValue(value, 'screenshot')
688
+ const layers =
689
+ input.layers === undefined
690
+ ? undefined
691
+ : stringValue(input.layers, 'screenshot.layers')
692
+ if (
693
+ layers !== undefined &&
694
+ layers !== 'full' &&
695
+ layers !== 'tenant' &&
696
+ layers !== 'shell'
697
+ ) {
698
+ return validationFailure('screenshot.layers', 'must be full, tenant, or shell')
699
+ }
700
+ const crop =
701
+ input.crop === undefined ? undefined : recordValue(input.crop, 'screenshot.crop')
702
+ return {
703
+ ...(layers === undefined ? {} : { layers }),
704
+ ...(crop === undefined
705
+ ? {}
706
+ : {
707
+ crop: {
708
+ x: numberValue(crop.x, 'screenshot.crop.x'),
709
+ y: numberValue(crop.y, 'screenshot.crop.y'),
710
+ w: numberValue(crop.w, 'screenshot.crop.w'),
711
+ h: numberValue(crop.h, 'screenshot.crop.h'),
712
+ },
713
+ }),
714
+ }
715
+ }
716
+
717
+ function stableJsonValue(value: unknown): unknown {
718
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') {
719
+ return value
720
+ }
721
+ if (typeof value === 'number') {
722
+ if (!Number.isFinite(value))
723
+ throw new Error('MCP result contains a non-finite number')
724
+ return value
725
+ }
726
+ if (Array.isArray(value)) return value.map(stableJsonValue)
727
+ if (typeof value === 'object') {
728
+ const sorted: Record<string, unknown> = {}
729
+ for (const key of Object.keys(value).sort()) {
730
+ sorted[key] = stableJsonValue(Reflect.get(value, key))
731
+ }
732
+ return sorted
733
+ }
734
+ throw new Error(`MCP result contains unsupported ${typeof value} value`)
735
+ }
736
+
737
+ function textResult(value: unknown): PerSimMcpToolResult {
738
+ const text = JSON.stringify(stableJsonValue(value))
739
+ if (text === undefined) throw new Error('MCP result did not serialize to JSON')
740
+ return { content: [{ type: 'text', text }] }
741
+ }
742
+
743
+ type PerSimMcpHandler = (input: unknown) => Promise<unknown>
744
+
745
+ function createPerSimMcpHandlers(
746
+ sim: SimClient,
747
+ ): Record<PerSimMcpToolName, PerSimMcpHandler> {
748
+ return {
749
+ state: async (input) => sim.state(parseStateOptions(input)),
750
+ perform: async (input) => {
751
+ const parsed = parsePerformInput(input)
752
+ return sim.perform(parsed.steps, parsed.options)
753
+ },
754
+ reset: async (input) => sim.reset(parseResetOptions(input)),
755
+ screenshot: async (input) => {
756
+ const result = await sim.screenshot(parseScreenshotOptions(input))
757
+ return { dataUrl: result.dataUrl }
758
+ },
759
+ logs: async (input) => {
760
+ const parsed = parseLogsInput(input)
761
+ const [logs, recentErrors] = await Promise.all([
762
+ sim.logs(parsed.filterOptions),
763
+ sim.errors(parsed.errorLimit),
764
+ ])
765
+ return { logs: logs.slice(-parsed.limit), recentErrors }
766
+ },
767
+ }
768
+ }
769
+
770
+ export function createPerSimMcpTools(sim: SimClient): PerSimMcpTool[] {
771
+ const handlers = createPerSimMcpHandlers(sim)
772
+ return PER_SIM_MCP_TOOL_ORDER.map((name) => {
773
+ const definition = PER_SIM_MCP_TOOL_DEFINITIONS[name]
774
+ return {
775
+ ...definition,
776
+ async execute(input: unknown): Promise<PerSimMcpToolResult> {
777
+ validateMcpInput(input, definition.inputSchema, name)
778
+ return textResult(await handlers[name](input))
779
+ },
780
+ }
781
+ })
782
+ }
783
+
784
+ export function registerPerSimMcpTools(
785
+ server: PerSimMcpServerAdapter,
786
+ sim: SimClient,
787
+ ): PerSimMcpTool[] {
788
+ const tools = createPerSimMcpTools(sim)
789
+ for (const tool of tools) server.registerTool(tool)
790
+ return tools
791
+ }