rnxsim 0.1.366 → 0.1.368
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 +14 -2
- package/cli/bin.ts +18 -0
- package/cli/bridge-flow-runner.ts +14 -151
- package/cli/commands/assert.ts +1 -1
- package/cli/commands/box/checkout-plane.ts +126 -0
- package/cli/commands/box/rnx-command.ts +68 -0
- package/cli/commands/box.ts +200 -0
- package/cli/commands/control.ts +12 -32
- package/cli/commands/do-chain.ts +412 -0
- package/cli/commands/flow.ts +0 -10
- package/cli/commands/inspect/core.ts +302 -725
- package/cli/commands/inspect/describe.ts +3 -76
- package/cli/commands/inspect/find.ts +3 -6
- package/cli/commands/inspect/get-layout.ts +14 -1
- package/cli/commands/inspect/resolve-target.ts +41 -47
- package/cli/commands/inspect/shared.ts +28 -123
- package/cli/commands/inspect/tree.ts +6 -15
- package/cli/commands/inspect/wait-ready.ts +2 -2
- package/cli/commands/inspect/wait-selector.ts +4 -8
- package/cli/commands/inspect.ts +23 -17
- package/cli/commands/install-desktop.ts +44 -7
- package/cli/commands/maestro-generate.ts +0 -2
- package/cli/commands/platform.ts +68 -0
- package/cli/commands/state.ts +5 -2
- package/cli/outbound-endpoints.ts +6 -1
- package/cli/parse-args.ts +13 -2
- package/cli/ws-bridge.ts +30 -2
- package/detox/index.ts +34 -17
- package/dist-cli/bin.js +5 -5
- package/dist-cli/chunks/{agent-SO6IOPRK.js → agent-GXCQMYV4.js} +2 -2
- package/dist-cli/chunks/{agent-wrapper-PMYCB2KO.js → agent-wrapper-Y3PQAL6I.js} +2 -2
- package/dist-cli/chunks/{app-fonts-LULQGFY7.js → app-fonts-MDD3W4VN.js} +2 -2
- package/dist-cli/chunks/{assert-WGNCD4ZU.js → assert-BSRQBRH7.js} +3 -3
- package/dist-cli/chunks/{auth-G7CCHH3M.js → auth-SLPHDPCF.js} +2 -2
- package/dist-cli/chunks/{beta-N4LXL5TO.js → beta-R4IZ5SWO.js} +2 -2
- package/dist-cli/chunks/box-2NQYYY3B.js +1709 -0
- package/dist-cli/chunks/{camera-4JHCYBQS.js → camera-ER66WWAX.js} +2 -2
- package/dist-cli/chunks/chunk-2YK5J2D2.js +24 -0
- package/dist-cli/chunks/{chunk-I6KIM6PN.js → chunk-3MPFC3DS.js} +1 -1
- package/dist-cli/chunks/{chunk-77ZHQJZR.js → chunk-3S2J6IVC.js} +2 -2
- package/dist-cli/chunks/chunk-3XMEIZEV.js +4 -0
- package/dist-cli/chunks/chunk-42TLK75A.js +497 -0
- package/dist-cli/chunks/{chunk-OW5XHAA4.js → chunk-4FLQYRGN.js} +1 -1
- package/dist-cli/chunks/{chunk-EFR5DVNP.js → chunk-4VA2ISWX.js} +1 -1
- package/dist-cli/chunks/chunk-57LITN73.js +7 -0
- package/dist-cli/chunks/{chunk-EQK5MJCU.js → chunk-7B22IPSC.js} +2 -2
- package/dist-cli/chunks/{chunk-EGAGZHO6.js → chunk-7V3S2DY2.js} +1 -1
- package/dist-cli/chunks/{chunk-IZKHOM37.js → chunk-AUPJX4KI.js} +1 -1
- package/dist-cli/chunks/{chunk-2S7BKUP2.js → chunk-BWJOLJOK.js} +2 -2
- package/dist-cli/chunks/{chunk-CN6XYYEA.js → chunk-CKNNO6ZG.js} +27 -27
- package/dist-cli/chunks/{chunk-MGDSJZ37.js → chunk-DJSPNOF6.js} +2 -2
- package/dist-cli/chunks/{chunk-YXXKSL6R.js → chunk-E6XYIIJK.js} +1 -1
- package/dist-cli/chunks/{chunk-ONORMKWX.js → chunk-ECXDCED5.js} +2 -2
- package/dist-cli/chunks/chunk-EHA65UVJ.js +5 -0
- package/dist-cli/chunks/chunk-ESVUVW3R.js +32 -0
- package/dist-cli/chunks/{chunk-XJU2QIRY.js → chunk-FDAFSRQE.js} +2 -2
- package/dist-cli/chunks/{chunk-NM2L2QV4.js → chunk-FSPZY4AI.js} +2 -2
- package/dist-cli/chunks/chunk-FVODUDPS.js +4 -0
- package/dist-cli/chunks/{chunk-H23KWIKA.js → chunk-GFWUIGGY.js} +2 -2
- package/dist-cli/chunks/{chunk-CTG7HBCA.js → chunk-GS5VUDCF.js} +1 -1
- package/dist-cli/chunks/{chunk-ORWP2E7R.js → chunk-GXHCJRKE.js} +2 -2
- package/dist-cli/chunks/{chunk-TTASWLKM.js → chunk-HKQXGCQT.js} +1 -1
- package/dist-cli/chunks/{chunk-5OK3LCEJ.js → chunk-IXRVFQMT.js} +2 -2
- package/dist-cli/chunks/{chunk-TIAEYO6I.js → chunk-JFGW75UL.js} +2 -2
- package/dist-cli/chunks/{chunk-RYIMSKDO.js → chunk-JHYXRORU.js} +2 -2
- package/dist-cli/chunks/chunk-LAKWOJ5Y.js +5 -0
- package/dist-cli/chunks/{chunk-GAJQSITE.js → chunk-LPAOOJIW.js} +1 -1
- package/dist-cli/chunks/chunk-M7COA4BE.js +312 -0
- package/dist-cli/chunks/{chunk-6PL2CCGP.js → chunk-MCO7V4VN.js} +2 -2
- package/dist-cli/chunks/{chunk-YGMEDJKJ.js → chunk-MGOAQHMO.js} +2 -2
- package/dist-cli/chunks/chunk-NERY6HGZ.js +4 -0
- package/dist-cli/chunks/chunk-NGWUGNPV.js +4 -0
- package/dist-cli/chunks/{chunk-ZMJ75EDG.js → chunk-NPSPWMBO.js} +2 -2
- package/dist-cli/chunks/{chunk-2UXU53MG.js → chunk-OXCDE3DD.js} +1 -1
- package/dist-cli/chunks/{chunk-ZLVRYXZ7.js → chunk-OZJMWTGA.js} +1 -1
- package/dist-cli/chunks/{chunk-HT6GFZOZ.js → chunk-PNBQBBG5.js} +1 -1
- package/dist-cli/chunks/{chunk-JHXETDVC.js → chunk-Q65KH25G.js} +1 -1
- package/dist-cli/chunks/{chunk-NQFJOCSF.js → chunk-QKOY7UW5.js} +2 -2
- package/dist-cli/chunks/{chunk-5DAIA77C.js → chunk-QMVBWZH2.js} +2 -2
- package/dist-cli/chunks/chunk-RRALQ4WM.js +38 -0
- package/dist-cli/chunks/{chunk-GXAB52K7.js → chunk-SHMLYTNH.js} +2 -2
- package/dist-cli/chunks/chunk-SSUPPM6K.js +130 -0
- package/dist-cli/chunks/chunk-T2MGLXY5.js +27 -0
- package/dist-cli/chunks/{chunk-ZJIS3CTW.js → chunk-T4ZZ26A2.js} +2 -2
- package/dist-cli/chunks/{chunk-YCCCPFWT.js → chunk-TC6DNQL6.js} +2 -2
- package/dist-cli/chunks/chunk-TE645QCO.js +423 -0
- package/dist-cli/chunks/{chunk-UCHW7ZHR.js → chunk-TOE5O6JE.js} +2 -2
- package/dist-cli/chunks/{chunk-N3UC7HIX.js → chunk-TPIPKXJL.js} +1 -1
- package/dist-cli/chunks/chunk-UBH2BWCY.js +4 -0
- package/dist-cli/chunks/{chunk-AFV32WZT.js → chunk-UXROQPIN.js} +3 -3
- package/dist-cli/chunks/{chunk-SA6XVI55.js → chunk-WF2CRNYF.js} +1 -1
- package/dist-cli/chunks/chunk-WFAFAGMG.js +4 -0
- package/dist-cli/chunks/{chunk-A5FXX5PJ.js → chunk-WJQB6AXZ.js} +1 -1
- package/dist-cli/chunks/{chunk-5NMCF5VF.js → chunk-WVVAYLWZ.js} +1 -1
- package/dist-cli/chunks/chunk-WXRDFUR7.js +4 -0
- package/dist-cli/chunks/{chunk-M3BNCF7E.js → chunk-X5MXQB3L.js} +2 -2
- package/dist-cli/chunks/{chunk-LN3RNW6X.js → chunk-YSHJ3LMQ.js} +2 -2
- package/dist-cli/chunks/{chunk-34IQM22N.js → chunk-YYPTSF6W.js} +15 -15
- package/dist-cli/chunks/{cleanup-KRIATKFH.js → cleanup-WHGLBAJ2.js} +2 -2
- package/dist-cli/chunks/cli-version-DMXIXBIM.js +4 -0
- package/dist-cli/chunks/{compat-6PTQ3QEA.js → compat-MGBKGYGQ.js} +2 -2
- package/dist-cli/chunks/{config-ETGDUANO.js → config-TRDOMP4T.js} +2 -2
- package/dist-cli/chunks/connect-N4FV3DDV.js +4 -0
- package/dist-cli/chunks/control-5GRBZVYK.js +4 -0
- package/dist-cli/chunks/daemon-MBT5E3ZC.js +4 -0
- package/dist-cli/chunks/{debug-WIIB6ZKC.js → debug-AHEK43AJ.js} +3 -3
- package/dist-cli/chunks/{desktop-TD7Q5LNB.js → desktop-LDRIXOVV.js} +3 -3
- package/dist-cli/chunks/{detox-7GNR33VM.js → detox-6HAKYISO.js} +2 -2
- package/dist-cli/chunks/{device-QOR3PGUF.js → device-QM3S7OVU.js} +2 -2
- package/dist-cli/chunks/{diagnose-RRCBLP6E.js → diagnose-WDP3CVIX.js} +2 -2
- package/dist-cli/chunks/{disk-cleanup-MJUGJPXT.js → disk-cleanup-JRT5MKFW.js} +2 -2
- package/dist-cli/chunks/do-chain-7FDMO56C.js +5 -0
- package/dist-cli/chunks/drivers-AJL2EVXH.js +4 -0
- package/dist-cli/chunks/env-YIBOMB24.js +4 -0
- package/dist-cli/chunks/{film-D7Y64LID.js → film-R3EQADMG.js} +3 -3
- package/dist-cli/chunks/flow-BDAA6BWL.js +4 -0
- package/dist-cli/chunks/help-5EJD6LJF.js +4 -0
- package/dist-cli/chunks/{hidden-runtime-alias-JGURVR2Z.js → hidden-runtime-alias-6M4UERRQ.js} +2 -2
- package/dist-cli/chunks/{home-paths-4ICFIGXO.js → home-paths-QJNOHZJO.js} +2 -2
- package/dist-cli/chunks/inspect-S3LR3R2A.js +4 -0
- package/dist-cli/chunks/install-desktop-GXE6K7BJ.js +4 -0
- package/dist-cli/chunks/{login-RU33QR2G.js → login-ISWDW4WU.js} +2 -2
- package/dist-cli/chunks/{logout-2IUG3Y5J.js → logout-VNJX2IEW.js} +2 -2
- package/dist-cli/chunks/{maestro-2MXTGG7I.js → maestro-SQS2OS2D.js} +3 -3
- package/dist-cli/chunks/{maestro-generate-2PYIFGLR.js → maestro-generate-GWVPISG5.js} +5 -5
- package/dist-cli/chunks/{mode-3DAPG3HF.js → mode-3URDGBZB.js} +2 -2
- package/dist-cli/chunks/{optional-demo-registry-BWSDJF2Z.js → optional-demo-registry-L3LRBQHG.js} +2 -2
- package/dist-cli/chunks/{perf-2TKPWB4N.js → perf-XLPC4UKR.js} +2 -2
- package/dist-cli/chunks/{permissions-5CSMHYNA.js → permissions-QRHWVMDP.js} +2 -2
- package/dist-cli/chunks/platform-F47URZNB.js +6 -0
- package/dist-cli/chunks/privacy-BF23T67T.js +4 -0
- package/dist-cli/chunks/{record-I2HJCFP3.js → record-2DN3OUE5.js} +3 -3
- package/dist-cli/chunks/{report-issue-A4FZWBOC.js → report-issue-D3BQPSLU.js} +2 -2
- package/dist-cli/chunks/reset-T5CNZ7D3.js +4 -0
- package/dist-cli/chunks/resolve-target-KAAS274S.js +4 -0
- package/dist-cli/chunks/runtime-AQJDBR3X.js +4 -0
- package/dist-cli/chunks/{screenshot-command-UFW5BFHP.js → screenshot-command-RK7IJHVK.js} +3 -3
- package/dist-cli/chunks/{screenshot-layers-5BIAL4YN.js → screenshot-layers-DP2QKSMN.js} +2 -2
- package/dist-cli/chunks/{screenshots-capture-EHYS3BV2.js → screenshots-capture-MA7P3FUT.js} +2 -2
- package/dist-cli/chunks/{serve-O4FDM3AC.js → serve-UBXNGLNP.js} +3 -3
- package/dist-cli/chunks/setup-GH7OW4GC.js +4 -0
- package/dist-cli/chunks/sim-client-VTKFEDJX.js +4 -0
- package/dist-cli/chunks/{skills-ZK55SY32.js → skills-HPK25R4B.js} +2 -2
- package/dist-cli/chunks/state-273XJPUE.js +14 -0
- package/dist-cli/chunks/{storage-WP33ISEM.js → storage-RTDFWIY7.js} +2 -2
- package/dist-cli/chunks/store-DLKNNM4L.js +4 -0
- package/dist-cli/chunks/telemetry-7CWDXEOO.js +4 -0
- package/dist-cli/chunks/{timeline-DKGW5S4I.js → timeline-IF2PGK7C.js} +2 -2
- package/dist-cli/chunks/{upgrade-LL7G4B7G.js → upgrade-R74NVHKG.js} +2 -2
- package/dist-cli/chunks/upload-3JFFVCZ4.js +4 -0
- package/dist-cli/chunks/{version-YLWR5G4F.js → version-RTFCYQZ7.js} +2 -2
- package/dist-cli/chunks/{web-QELZ4CP6.js → web-35SSKBRU.js} +2 -2
- package/dist-cli/chunks/welcome-N5V352SE.js +5 -0
- package/dist-cli/chunks/{what-happened-MIOMZWM5.js → what-happened-WXTP6EPR.js} +2 -2
- package/dist-cli/chunks/ws-bridge-JXQUAWFA.js +4 -0
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +336 -0
- package/dist-lib/bridge-contract-input.mjs +303 -0
- package/dist-lib/bridge-contract.cjs +29 -1
- package/dist-lib/bridge-contract.mjs +19 -0
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +32 -18
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +5 -1
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/replacement-module-handler.cjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +214 -594
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +1 -1
- package/dist-lib/metro-production-bundle.mjs +1 -1
- package/dist-lib/metro.cjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +273 -657
- package/dist-lib/sdk.mjs +271 -652
- package/dist-lib/skills.cjs +190 -287
- package/dist-lib/vite.cjs +1 -1
- package/package.json +9 -6
- package/skills/contrast/SKILL.md +3 -0
- package/skills/rnx-debug/SKILL.md +8 -0
- package/skills/rnx-test/SKILL.md +21 -0
- package/src/bridge-contract-input.ts +359 -0
- package/src/bridge-contract.ts +121 -1
- package/src/host/bridge-host.ts +7 -12
- package/src/index.ts +7 -0
- package/src/sim-client.ts +12 -3
- package/dist-cli/chunks/chunk-3X6IBUBX.js +0 -38
- package/dist-cli/chunks/chunk-7B37IZBN.js +0 -15
- package/dist-cli/chunks/chunk-DMPF2D24.js +0 -4
- package/dist-cli/chunks/chunk-DW37EVX5.js +0 -767
- package/dist-cli/chunks/chunk-DWFIH7CB.js +0 -32
- package/dist-cli/chunks/chunk-GUI3PDH2.js +0 -27
- package/dist-cli/chunks/chunk-KLF3K6ZZ.js +0 -4
- package/dist-cli/chunks/chunk-M5QCGLXR.js +0 -944
- package/dist-cli/chunks/chunk-M6SKQGAN.js +0 -5
- package/dist-cli/chunks/chunk-NAYLPOC5.js +0 -24
- package/dist-cli/chunks/chunk-VIND4SP3.js +0 -124
- package/dist-cli/chunks/chunk-XXNLYR64.js +0 -5
- package/dist-cli/chunks/chunk-ZFM24DOX.js +0 -4
- package/dist-cli/chunks/cli-version-SYGNHGFR.js +0 -4
- package/dist-cli/chunks/control-RBTNOLQM.js +0 -4
- package/dist-cli/chunks/daemon-N3SC5DK6.js +0 -4
- package/dist-cli/chunks/drivers-W6E3BZH4.js +0 -4
- package/dist-cli/chunks/flow-7QAE2V5Y.js +0 -4
- package/dist-cli/chunks/help-NLD6KWVG.js +0 -4
- package/dist-cli/chunks/inspect-C347BG52.js +0 -4
- package/dist-cli/chunks/install-desktop-YR6TWZYK.js +0 -4
- package/dist-cli/chunks/privacy-PWWC2BXO.js +0 -4
- package/dist-cli/chunks/reset-Y3PBPFYA.js +0 -4
- package/dist-cli/chunks/runtime-IC6NVVGA.js +0 -4
- package/dist-cli/chunks/setup-Y4QGSJBM.js +0 -4
- package/dist-cli/chunks/state-KFLAZNEY.js +0 -14
- package/dist-cli/chunks/store-VEE22CAY.js +0 -4
- package/dist-cli/chunks/telemetry-PZV4BISJ.js +0 -4
- package/dist-cli/chunks/upload-Y4C3YZDK.js +0 -4
- package/dist-cli/chunks/welcome-ZPKA5PZJ.js +0 -5
- package/dist-lib/mcp.cjs +0 -673
- package/dist-lib/mcp.mjs +0 -641
- package/src/mcp.ts +0 -791
package/src/mcp.ts
DELETED
|
@@ -1,791 +0,0 @@
|
|
|
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
|
-
}
|