mobridge-mcp 0.0.1

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 (47) hide show
  1. package/README.md +155 -0
  2. package/dist/adb-baseline/act.mjs +1 -0
  3. package/dist/adb-baseline/capture-lib.mjs +2 -0
  4. package/dist/main.obf.cjs +2 -0
  5. package/dist/protocol/fixtures/01-login-views/expected.json +32 -0
  6. package/dist/protocol/fixtures/01-login-views/snapshot.json +20 -0
  7. package/dist/protocol/fixtures/02-alert-dialog/expected.json +22 -0
  8. package/dist/protocol/fixtures/02-alert-dialog/snapshot.json +20 -0
  9. package/dist/protocol/fixtures/03-recyclerview-list/expected.json +22 -0
  10. package/dist/protocol/fixtures/03-recyclerview-list/snapshot.json +19 -0
  11. package/dist/protocol/fixtures/04-ambiguous-ctas/expected.json +17 -0
  12. package/dist/protocol/fixtures/04-ambiguous-ctas/snapshot.json +21 -0
  13. package/dist/protocol/fixtures/05-compose-settings/expected.json +17 -0
  14. package/dist/protocol/fixtures/05-compose-settings/snapshot.json +19 -0
  15. package/dist/protocol/fixtures/06-canvas-game/expected.json +12 -0
  16. package/dist/protocol/fixtures/06-canvas-game/snapshot.json +16 -0
  17. package/dist/protocol/fixtures/07-search-results/expected.json +22 -0
  18. package/dist/protocol/fixtures/07-search-results/snapshot.json +19 -0
  19. package/dist/protocol/fixtures/README.md +34 -0
  20. package/dist/protocol/platform-capabilities.json +95 -0
  21. package/dist/protocol/schemas/action.schema.json +170 -0
  22. package/dist/protocol/schemas/common.schema.json +59 -0
  23. package/dist/protocol/schemas/diff.schema.json +74 -0
  24. package/dist/protocol/schemas/error.schema.json +34 -0
  25. package/dist/protocol/schemas/node-ref.schema.json +100 -0
  26. package/dist/protocol/schemas/query.schema.json +53 -0
  27. package/dist/protocol/schemas/screen-snapshot.schema.json +50 -0
  28. package/dist/protocol/schemas/semantic-node.schema.json +95 -0
  29. package/dist/protocol/schemas/tools.schema.json +215 -0
  30. package/dist/protocol/schemas/wait.schema.json +57 -0
  31. package/dist/protocol/synonyms.json +74 -0
  32. package/dist/protocol/validate/package-lock.json +71 -0
  33. package/dist/protocol/validate/package.json +13 -0
  34. package/dist/protocol/validate/query-engine.mjs +1 -0
  35. package/dist/protocol/validate/run-fixtures.mjs +6 -0
  36. package/dist/protocol/validate/schema-lint.mjs +2 -0
  37. package/dist/protocol/validate/validate-file.mjs +1 -0
  38. package/integrity.json +4 -0
  39. package/loader.cjs +85 -0
  40. package/package.json +40 -0
  41. package/resources/android/app-developer-debug.apk +0 -0
  42. package/resources/ios/MobileSemantic/Fixture/MobileSemanticFixtureApp.swift +50 -0
  43. package/resources/ios/MobileSemantic/MobileSemantic.xcodeproj/project.pbxproj +463 -0
  44. package/resources/ios/MobileSemantic/MobileSemantic.xcodeproj/xcshareddata/xcschemes/MobileSemanticUITests.xcscheme +97 -0
  45. package/resources/ios/MobileSemantic/UITests/BridgeServer.swift +216 -0
  46. package/resources/ios/MobileSemantic/UITests/XcuiSnapshot.swift +73 -0
  47. package/resources/ios/MobileSemantic/gen_project.rb +67 -0
@@ -0,0 +1,215 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "msm:tools.schema.json",
4
+ "title": "MCP tool I/O",
5
+ "description": "Input/output contracts for the 9 core MCP tools. mobile.workflow.run (10th) is added in P8 and intentionally absent here.",
6
+ "$defs": {
7
+ "annotation": {
8
+ "enum": ["READ_ONLY", "REVERSIBLE", "SIDE_EFFECT", "DESTRUCTIVE"],
9
+ "description": "Per-tool safety annotation surfaced to the host."
10
+ },
11
+
12
+ "device.connect": {
13
+ "input": {
14
+ "type": "object",
15
+ "additionalProperties": false,
16
+ "properties": {
17
+ "deviceId": { "$ref": "msm:common.schema.json#/$defs/deviceId" },
18
+ "pairingCode": { "type": "string" },
19
+ "transport": { "enum": ["ADB_FORWARD", "LAN"], "default": "ADB_FORWARD" },
20
+ "setup": { "enum": ["ask", "auto", "manual"], "description": "iOS only. How to bring up the XCTest Runner when it isn't running. 'ask' (default): return a SETUP_REQUIRED envelope with estimate + manual steps; 'auto': build + launch it now (~3-5 min first time). Ignored on Android." }
21
+ }
22
+ },
23
+ "output": {
24
+ "oneOf": [
25
+ {
26
+ "type": "object",
27
+ "additionalProperties": false,
28
+ "required": ["deviceId", "sessionId", "capabilities"],
29
+ "properties": {
30
+ "deviceId": { "$ref": "msm:common.schema.json#/$defs/deviceId" },
31
+ "sessionId": { "type": "string" },
32
+ "capabilities": { "type": "array", "items": { "type": "string" } },
33
+ "expiresInMinutes": { "type": "integer" },
34
+ "platform": { "enum": ["ANDROID", "IOS"], "description": "Session platform. Optional for back-compat." },
35
+ "mode": { "type": ["string", "null"], "description": "iOS driver mode (PHYSICAL|SIMULATOR); null on Android." },
36
+ "supportedTools": { "type": "array", "items": { "type": "string" }, "description": "Tool names actually advertised this session (platform ∩ profile). Informational; gating is at registration." },
37
+ "unsupportedCapabilities": { "type": "array", "items": { "type": "string" }, "description": "Capabilities the connected platform/mode cannot honor, e.g. BATTERYSTATS on iOS." }
38
+ }
39
+ },
40
+ { "$ref": "msm:error.schema.json" }
41
+ ]
42
+ }
43
+ },
44
+
45
+ "app.open": {
46
+ "input": {
47
+ "type": "object",
48
+ "additionalProperties": false,
49
+ "required": ["appAction"],
50
+ "properties": {
51
+ "appAction": { "$ref": "msm:action.schema.json#/$defs/appAction" },
52
+ "packageName": { "$ref": "msm:common.schema.json#/$defs/packageName" },
53
+ "bundleId": { "$ref": "msm:common.schema.json#/$defs/bundleId" },
54
+ "deepLink": { "type": "string" },
55
+ "verify": { "$ref": "msm:wait.schema.json" }
56
+ }
57
+ },
58
+ "output": {
59
+ "oneOf": [
60
+ { "$ref": "msm:action.schema.json#/$defs/actionResult" },
61
+ { "$ref": "msm:error.schema.json" }
62
+ ]
63
+ }
64
+ },
65
+
66
+ "screen.get": {
67
+ "input": {
68
+ "type": "object",
69
+ "additionalProperties": false,
70
+ "properties": {
71
+ "compact": { "type": "boolean", "default": true },
72
+ "tokenBudget": { "type": "integer", "minimum": 0 },
73
+ "metrics": { "type": "boolean", "default": false, "description": "Attach an opt-in `_metrics` capture-counter block (benchmarks only). Default off keeps the wire byte-identical." }
74
+ }
75
+ },
76
+ "output": {
77
+ "oneOf": [
78
+ { "$ref": "msm:screen-snapshot.schema.json" },
79
+ { "$ref": "msm:error.schema.json" }
80
+ ]
81
+ }
82
+ },
83
+
84
+ "screen.find": {
85
+ "input": {
86
+ "type": "object",
87
+ "additionalProperties": false,
88
+ "required": ["query"],
89
+ "properties": {
90
+ "query": { "$ref": "msm:query.schema.json" },
91
+ "queryMode": {
92
+ "enum": ["oracle", "targeted", "auto"],
93
+ "default": "oracle",
94
+ "description": "Resolution strategy. `oracle` (default) = full-walk capture then QueryEngine (ground truth). `targeted` = platform seed only, NO silent fallback (fails if the seed cannot prove completeness for the query class). `auto` = targeted iff eligibility + completeness are proven, else full-walk oracle (fail-closed)."
95
+ },
96
+ "metrics": { "type": "boolean", "default": false, "description": "Attach an opt-in `_find` block (seed/ancestor/walk/getChild counters + fallbackReason). Default off keeps the wire byte-identical." }
97
+ }
98
+ },
99
+ "output": {
100
+ "oneOf": [
101
+ {
102
+ "type": "object",
103
+ "additionalProperties": false,
104
+ "required": ["matchCount", "matches"],
105
+ "properties": {
106
+ "matchCount": { "type": "integer", "minimum": 0 },
107
+ "matches": {
108
+ "type": "array",
109
+ "items": {
110
+ "type": "object",
111
+ "additionalProperties": false,
112
+ "required": ["ref", "reason", "confidence"],
113
+ "properties": {
114
+ "ref": { "$ref": "msm:node-ref.schema.json" },
115
+ "reason": { "type": "string", "description": "Which locator/criteria matched." },
116
+ "confidence": { "$ref": "msm:common.schema.json#/$defs/confidence" }
117
+ }
118
+ }
119
+ },
120
+ "generation": { "type": "integer", "description": "Capture generation these refs were built against; pair with screen.node/children and screen.act's generation gate." },
121
+ "mode": { "enum": ["oracle", "targeted", "auto"], "description": "The strategy actually executed (auto reports the resolved path)." },
122
+ "fallbackReason": { "type": ["string", "null"], "description": "When a targeted/auto request ran the oracle instead, why (e.g. UNSUPPORTED_QUERY_CLASS, ANCESTOR_INCOMPLETE, SEED_UNPROVEN, OVER_BUDGET)." },
123
+ "_find": { "type": "object", "additionalProperties": true, "description": "Opt-in instrumentation (metrics:true only)." }
124
+ }
125
+ },
126
+ { "$ref": "msm:error.schema.json" }
127
+ ]
128
+ }
129
+ },
130
+
131
+ "screen.act": {
132
+ "input": { "$ref": "msm:action.schema.json#/$defs/actionRequest" },
133
+ "output": {
134
+ "oneOf": [
135
+ { "$ref": "msm:action.schema.json#/$defs/actionResult" },
136
+ { "$ref": "msm:error.schema.json" }
137
+ ]
138
+ }
139
+ },
140
+
141
+ "screen.wait": {
142
+ "input": { "$ref": "msm:wait.schema.json" },
143
+ "output": {
144
+ "oneOf": [
145
+ {
146
+ "type": "object",
147
+ "additionalProperties": false,
148
+ "required": ["satisfied"],
149
+ "properties": {
150
+ "satisfied": { "type": "boolean" },
151
+ "snapshotId": { "$ref": "msm:common.schema.json#/$defs/snapshotId" },
152
+ "elapsedMs": { "type": "integer" }
153
+ }
154
+ },
155
+ { "$ref": "msm:error.schema.json" }
156
+ ]
157
+ }
158
+ },
159
+
160
+ "screen.diff": {
161
+ "input": {
162
+ "type": "object",
163
+ "additionalProperties": false,
164
+ "required": ["fromSnapshot"],
165
+ "properties": {
166
+ "fromSnapshot": { "$ref": "msm:common.schema.json#/$defs/snapshotId" },
167
+ "toSnapshot": { "$ref": "msm:common.schema.json#/$defs/snapshotId" }
168
+ }
169
+ },
170
+ "output": {
171
+ "oneOf": [
172
+ { "$ref": "msm:diff.schema.json" },
173
+ { "$ref": "msm:error.schema.json" }
174
+ ]
175
+ }
176
+ },
177
+
178
+ "system.action": {
179
+ "input": {
180
+ "type": "object",
181
+ "additionalProperties": false,
182
+ "required": ["action"],
183
+ "properties": {
184
+ "action": { "$ref": "msm:action.schema.json#/$defs/globalAction" },
185
+ "verify": { "$ref": "msm:wait.schema.json" }
186
+ }
187
+ },
188
+ "output": {
189
+ "oneOf": [
190
+ { "$ref": "msm:action.schema.json#/$defs/actionResult" },
191
+ { "$ref": "msm:error.schema.json" }
192
+ ]
193
+ }
194
+ },
195
+
196
+ "capability.invoke": {
197
+ "input": {
198
+ "type": "object",
199
+ "additionalProperties": false,
200
+ "required": ["capability", "operation"],
201
+ "properties": {
202
+ "capability": { "enum": ["CLIPBOARD", "NOTIFICATION", "APP"] },
203
+ "operation": { "type": "string" },
204
+ "args": { "type": "object" }
205
+ }
206
+ },
207
+ "output": {
208
+ "oneOf": [
209
+ { "type": "object" },
210
+ { "$ref": "msm:error.schema.json" }
211
+ ]
212
+ }
213
+ }
214
+ }
215
+ }
@@ -0,0 +1,57 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "msm:wait.schema.json",
4
+ "title": "WaitSpec",
5
+ "description": "Event-driven wait for a UI condition, with a stability window.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["condition"],
9
+ "properties": {
10
+ "condition": { "$ref": "#/$defs/condition" },
11
+ "timeoutMs": { "type": "integer", "minimum": 0, "default": 10000 },
12
+ "stableForMs": {
13
+ "type": "integer",
14
+ "minimum": 0,
15
+ "default": 400,
16
+ "description": "Tree must be unchanged for this long before condition is evaluated satisfied."
17
+ }
18
+ },
19
+ "$defs": {
20
+ "condition": {
21
+ "type": "object",
22
+ "minProperties": 1,
23
+ "maxProperties": 1,
24
+ "description": "Exactly one of the condition forms.",
25
+ "additionalProperties": false,
26
+ "properties": {
27
+ "anyOf": {
28
+ "type": "array",
29
+ "minItems": 1,
30
+ "items": { "$ref": "#/$defs/condition" }
31
+ },
32
+ "allOf": {
33
+ "type": "array",
34
+ "minItems": 1,
35
+ "items": { "$ref": "#/$defs/condition" }
36
+ },
37
+ "not": { "$ref": "#/$defs/condition" },
38
+ "nodeExists": { "$ref": "msm:query.schema.json" },
39
+ "nodeAbsent": { "$ref": "msm:query.schema.json" },
40
+ "nodeValueEquals": {
41
+ "type": "object",
42
+ "additionalProperties": false,
43
+ "required": ["query", "value"],
44
+ "properties": {
45
+ "query": { "$ref": "msm:query.schema.json" },
46
+ "value": { "type": "string" }
47
+ }
48
+ },
49
+ "packageChanged": { "$ref": "msm:common.schema.json#/$defs/packageName" },
50
+ "windowChanged": { "type": "boolean" },
51
+ "screenStable": { "type": "boolean" },
52
+ "collectionPositionChanged": { "$ref": "msm:query.schema.json" },
53
+ "clipboardEquals": { "type": "string" }
54
+ }
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,74 @@
1
+ {
2
+ "$comment": "Source of truth for TextMatch.SEMANTIC_SYNONYM (schemaVersion 1). Deterministic table, NOT a model: principle 8 forbids a second LLM inside the runtime, so equivalence here is a lookup and nothing else. Each group is a set of labels that name the SAME UI affordance across languages/wordings. Matching is symmetric (any member matches any other) and applied after normalization (lowercase, whitespace collapsed) and diacritic folding, so 'Cài đặt' matches 'cai dat'. Mirrored byte-for-behavior in android-runtime SynonymTable.kt and protocol/validate/query-engine.mjs; SynonymTableParityTest fails the build if they drift.",
3
+ "version": 1,
4
+ "locales": ["en", "vi"],
5
+ "groups": [
6
+ ["settings", "setting", "cài đặt", "thiết lập", "tuỳ chọn", "tùy chọn"],
7
+ ["back", "go back", "quay lại", "trở lại", "trở về"],
8
+ ["next", "continue", "tiếp", "tiếp tục", "tiếp theo"],
9
+ ["previous", "prev", "trước", "trước đó"],
10
+ ["done", "finish", "complete", "xong", "hoàn tất", "hoàn thành"],
11
+ ["ok", "okay", "đồng ý", "được"],
12
+ ["cancel", "huỷ", "hủy", "huỷ bỏ", "hủy bỏ"],
13
+ ["close", "dismiss", "đóng"],
14
+ ["save", "lưu", "lưu lại"],
15
+ ["delete", "remove", "xoá", "xóa", "gỡ", "loại bỏ"],
16
+ ["edit", "sửa", "chỉnh sửa"],
17
+ ["add", "new", "create", "thêm", "tạo", "tạo mới"],
18
+ ["search", "find", "tìm", "tìm kiếm"],
19
+ ["send", "gửi"],
20
+ ["share", "chia sẻ"],
21
+ ["sign in", "log in", "login", "đăng nhập"],
22
+ ["sign out", "log out", "logout", "đăng xuất"],
23
+ ["sign up", "register", "đăng ký", "đăng kí"],
24
+ ["password", "mật khẩu"],
25
+ ["email", "e-mail", "thư điện tử"],
26
+ ["phone", "phone number", "số điện thoại", "điện thoại"],
27
+ ["username", "user name", "account", "tên đăng nhập", "tài khoản"],
28
+ ["confirm", "xác nhận"],
29
+ ["retry", "try again", "thử lại"],
30
+ ["refresh", "reload", "làm mới", "tải lại"],
31
+ ["yes", "có"],
32
+ ["no", "không"],
33
+ ["allow", "grant", "cho phép"],
34
+ ["deny", "block", "từ chối", "chặn"],
35
+ ["network", "internet", "mạng", "mạng và internet", "network & internet"],
36
+ ["wifi", "wi-fi", "wlan"],
37
+ ["bluetooth"],
38
+ ["notifications", "notification", "thông báo"],
39
+ ["battery", "pin"],
40
+ ["storage", "bộ nhớ", "lưu trữ"],
41
+ ["display", "screen", "màn hình", "hiển thị"],
42
+ ["sound", "volume", "âm thanh", "âm lượng"],
43
+ ["language", "ngôn ngữ"],
44
+ ["security", "bảo mật"],
45
+ ["privacy", "quyền riêng tư", "riêng tư"],
46
+ ["about", "about phone", "giới thiệu", "thông tin", "thông tin điện thoại"],
47
+ ["apps", "applications", "ứng dụng"],
48
+ ["home", "trang chủ", "màn hình chính"],
49
+ ["menu", "more", "thêm nữa", "tuỳ chọn khác", "tùy chọn khác"],
50
+ ["profile", "hồ sơ", "trang cá nhân"],
51
+ ["camera", "máy ảnh"],
52
+ ["gallery", "photos", "thư viện", "ảnh"],
53
+ ["download", "tải xuống", "tải về"],
54
+ ["upload", "tải lên"],
55
+ ["copy", "sao chép"],
56
+ ["paste", "dán"],
57
+ ["select", "choose", "chọn"],
58
+ ["start", "begin", "bắt đầu"],
59
+ ["stop", "dừng", "dừng lại"],
60
+ ["pause", "tạm dừng"],
61
+ ["play", "phát"],
62
+ ["update", "cập nhật"],
63
+ ["install", "cài đặt ứng dụng", "cài"],
64
+ ["uninstall", "gỡ cài đặt"],
65
+ ["pay", "payment", "thanh toán"],
66
+ ["order", "đặt hàng", "đơn hàng"],
67
+ ["cart", "giỏ hàng"],
68
+ ["skip", "bỏ qua", "bỏ qua bước này"],
69
+ ["help", "support", "trợ giúp", "hỗ trợ"],
70
+ ["error", "lỗi"],
71
+ ["warning", "cảnh báo"],
72
+ ["loading", "please wait", "đang tải", "vui lòng đợi", "vui lòng chờ"]
73
+ ]
74
+ }
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "msm-protocol-validate",
3
+ "lockfileVersion": 3,
4
+ "requires": true,
5
+ "packages": {
6
+ "": {
7
+ "name": "msm-protocol-validate",
8
+ "devDependencies": {
9
+ "ajv": "^8.17.1"
10
+ }
11
+ },
12
+ "node_modules/ajv": {
13
+ "version": "8.20.0",
14
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
15
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
16
+ "dev": true,
17
+ "license": "MIT",
18
+ "dependencies": {
19
+ "fast-deep-equal": "^3.1.3",
20
+ "fast-uri": "^3.0.1",
21
+ "json-schema-traverse": "^1.0.0",
22
+ "require-from-string": "^2.0.2"
23
+ },
24
+ "funding": {
25
+ "type": "github",
26
+ "url": "https://github.com/sponsors/epoberezkin"
27
+ }
28
+ },
29
+ "node_modules/fast-deep-equal": {
30
+ "version": "3.1.3",
31
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
32
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
33
+ "dev": true,
34
+ "license": "MIT"
35
+ },
36
+ "node_modules/fast-uri": {
37
+ "version": "3.1.4",
38
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz",
39
+ "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==",
40
+ "dev": true,
41
+ "funding": [
42
+ {
43
+ "type": "github",
44
+ "url": "https://github.com/sponsors/fastify"
45
+ },
46
+ {
47
+ "type": "opencollective",
48
+ "url": "https://opencollective.com/fastify"
49
+ }
50
+ ],
51
+ "license": "BSD-3-Clause"
52
+ },
53
+ "node_modules/json-schema-traverse": {
54
+ "version": "1.0.0",
55
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
56
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
57
+ "dev": true,
58
+ "license": "MIT"
59
+ },
60
+ "node_modules/require-from-string": {
61
+ "version": "2.0.2",
62
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
63
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
64
+ "dev": true,
65
+ "license": "MIT",
66
+ "engines": {
67
+ "node": ">=0.10.0"
68
+ }
69
+ }
70
+ }
71
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "msm-protocol-validate",
3
+ "private": true,
4
+ "type": "module",
5
+ "description": "P0 gate: fixture query runner (no deps) + optional JSON Schema lint (ajv).",
6
+ "scripts": {
7
+ "test": "node run-fixtures.mjs",
8
+ "lint:schemas": "node schema-lint.mjs"
9
+ },
10
+ "devDependencies": {
11
+ "ajv": "^8.17.1"
12
+ }
13
+ }
@@ -0,0 +1 @@
1
+ const c=new Set(["BUTTON","INPUT","LINK","LIST_ITEM","CHECKBOX","RADIO","SWITCH","TAB","SLIDER","MENU"]);function a(t){return(t??"").toLowerCase().replace(/\s+/g," ").trim()}import{readFileSync as d}from"node:fs";import{fileURLToPath as I}from"node:url";import{dirname as m,join as N}from"node:path";const h=JSON.parse(d(N(m(I(import.meta.url)),"..","synonyms.json"),"utf8")).groups;function f(t){return(t??"").toLowerCase().replace(/\s+/g," ").trim().normalize("NFD").replace(new RegExp("\\p{Mn}+","gu"),"").replace(/đ/g,"d")}const s=new Map;h.forEach((t,e)=>{for(const n of t){const r=f(n);s.has(r)||s.set(r,new Set),s.get(r).add(e)}});function E(t,e){const n=f(t),r=f(e);if(!n||!r)return!1;if(n===r)return!0;const o=s.get(n),u=s.get(r);if(!o||!u)return!1;for(const i of o)if(u.has(i))return!0;return!1}function l(t,e){if(e==null)return!1;switch(t.match??"NORMALIZED_EXACT"){case"EXACT":return e===t.value;case"NORMALIZED_EXACT":return a(e)===a(t.value);case"CONTAINS":return a(e).includes(a(t.value));case"REGEX":{if(t.value.length>200)return!1;try{return new RegExp(t.value).test(e)}catch{return!1}}case"SEMANTIC_SYNONYM":return E(e,t.value);default:return!1}}function g(t,e,n){let r=t.parentId?n.get(t.parentId):null;for(;r;){let o=!0;if(e.role&&r.role!==e.role&&(o=!1),o&&e.resourceId&&r.resourceId!==e.resourceId&&(o=!1),o&&e.text&&!l(e.text,r.text)&&(o=!1),o)return!0;r=r.parentId?n.get(r.parentId):null}return!1}function O(t,e,n){if(e.role&&t.role!==e.role||e.text&&!l(e.text,t.text)||e.label&&!l(e.label,t.label)||e.resourceId&&t.resourceId!==e.resourceId)return!1;if(e.state){for(const[r,o]of Object.entries(e.state))if(t.state?.[r]!==o)return!1}return!(e.within&&!g(t,e.within,n))}function p(t){const e=t.nodes.some(r=>r.role==="CANVAS"),n=t.nodes.filter(r=>c.has(r.role)&&r.state?.clickable).length;return e&&n===0}function S(t,e){const n=new Map(t.nodes.map(o=>[o.nodeId,o]));let r=t.nodes.filter(o=>O(o,e,n));return e.firstVisible&&(r=r.slice(0,1)),typeof e.nth=="number"&&(r=r[e.nth]?[r[e.nth]]:[]),r}function x(t,e){const n=S(t,e);return n.length===0?{matchCount:0,nodeIds:[],error:p(t)?"VISION_REQUIRED":"NODE_NOT_FOUND"}:{matchCount:n.length,nodeIds:n.map(r=>r.nodeId),ambiguous:n.length>1}}export{x as find,f as fold,S as matchNodes,a as normalize,E as synonymEquivalent};
@@ -0,0 +1,6 @@
1
+ import{readdirSync as S,readFileSync as l,statSync as $,existsSync as m}from"node:fs";import{fileURLToPath as x}from"node:url";import{dirname as y,join as a}from"node:path";import{find as N}from"./query-engine.mjs";const b=y(x(import.meta.url)),I=a(b,"..","fixtures"),k=["schemaVersion","deviceId","snapshotId","timestamp","packageName","windowId","orientation","screenStateId","compact","nodes","treeHash"];function w(o,s){const n=new Set(o),e=new Set(s);if(n.size!==e.size)return!1;for(const i of n)if(!e.has(i))return!1;return!0}function E(o){const s=[];for(const e of k)e in o||s.push(`missing field: ${e}`);o.schemaVersion!==1&&s.push(`schemaVersion must be 1, got ${o.schemaVersion}`);const n=new Set;for(const e of o.nodes??[])n.has(e.nodeId)&&s.push(`duplicate nodeId: ${e.nodeId}`),n.add(e.nodeId),e.parentId&&!o.nodes.some(i=>i.nodeId===e.parentId)&&s.push(`node ${e.nodeId} has dangling parentId ${e.parentId}`);return s}let p=0,u=0;const d=[];for(const o of S(I)){const s=a(I,o);if(!$(s).isDirectory())continue;const n=a(s,"snapshot.json"),e=a(s,"expected.json");if(!m(n)||!m(e))continue;const i=JSON.parse(l(n,"utf8")),g=JSON.parse(l(e,"utf8")),h=E(i);p++,h.length?d.push(`[${o}] snapshot invalid: ${h.join("; ")}`):u++;for(const f of g){p++;const c=N(i,f.query),t=f.expect;let r=c.matchCount===t.matchCount;r&&t.nodeIds&&(r=w(c.nodeIds,t.nodeIds)),r&&t.error&&(r=c.error===t.error),r&&typeof t.ambiguous=="boolean"&&(r=(c.ambiguous??!1)===t.ambiguous),r?u++:d.push(`[${o}] ${f.name}
2
+ expected ${JSON.stringify(t)}
3
+ got ${JSON.stringify(c)}`)}}if(console.log(`
4
+ Fixture gate: ${u}/${p} checks passed`),d.length){console.log(`
5
+ FAILURES:`);for(const o of d)console.log(" - "+o);process.exit(1)}console.log(`All fixture checks passed.
6
+ `);
@@ -0,0 +1,2 @@
1
+ import{readdirSync as r,readFileSync as c,statSync as S,existsSync as j}from"node:fs";import{fileURLToPath as v}from"node:url";import{dirname as y,join as t}from"node:path";const i=y(v(import.meta.url)),l=t(i,"..","schemas"),f=t(i,"..","fixtures");let m;try{({default:m}=await import("ajv/dist/2020.js"))}catch{console.error("ajv not installed. Run: cd protocol/validate && npm install"),process.exit(2)}const h=new m({allErrors:!0,strict:!1});for(const o of r(l)){if(!o.endsWith(".schema.json"))continue;const s=JSON.parse(c(t(l,o),"utf8"));h.addSchema(s,s.$id)}const d=h.getSchema("msm:screen-snapshot.schema.json");let p=0,e=0;for(const o of r(f)){const s=t(f,o);if(!S(s).isDirectory())continue;const n=t(s,"snapshot.json");if(!j(n))continue;const u=JSON.parse(c(n,"utf8"));if(d(u))p++;else{e++,console.log(`[${o}] INVALID`);for(const a of d.errors)console.log(` ${a.instancePath} ${a.message}`)}}console.log(`
2
+ Schema lint: ${p} valid, ${e} invalid`),process.exit(e?1:0);
@@ -0,0 +1 @@
1
+ import{readdirSync as m,readFileSync as r}from"node:fs";import{fileURLToPath as l}from"node:url";import{dirname as f,join as a,resolve as d}from"node:path";const p=f(l(import.meta.url)),c=a(p,"..","schemas"),e=process.argv[2];e||(console.error("usage: node validate-file.mjs <snapshot.json>"),process.exit(2));const{default:h}=await import("ajv/dist/2020.js"),n=new h({allErrors:!0,strict:!1});for(const o of m(c)){if(!o.endsWith(".schema.json"))continue;const t=JSON.parse(r(a(c,o),"utf8"));n.addSchema(t,t.$id)}const i=n.getSchema("msm:screen-snapshot.schema.json"),s=JSON.parse(r(d(e),"utf8"));i(s)&&(console.log(`VALID: ${e} (${s.nodes.length} nodes, ${s.compact?"compact":"full"})`),process.exit(0)),console.log(`INVALID: ${e}`);for(const o of i.errors)console.log(` ${o.instancePath} ${o.message}`);process.exit(1);
package/integrity.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "buildId": "3d89790-20260802091136",
3
+ "bundleSha256": "3dca6b6e68a7d79ea10d26c07bff1d01a3dd04491905dcc8ced35806bad4fd93"
4
+ }
package/loader.cjs ADDED
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env node
2
+ // npx entrypoint for mobridge-mcp. Tiny, NOT obfuscated (a few KB).
3
+ //
4
+ // Why compile-on-first-run instead of shipping a prebuilt .jsc: V8 cachedData is keyed
5
+ // to the EXACT V8 build, not the Node major — a prebuilt .jsc only runs on that one V8,
6
+ // and the user's V8 is unknowable. So we ship an OBFUSCATED CJS bundle; on first run we
7
+ // compile it to .jsc against the user's own Node and cache it; later runs load the cache
8
+ // instantly. If compiling/loading fails (unknown V8, no bytenode, read-only HOME) we fall
9
+ // back to requiring the obfuscated JS directly — robust beats bytecode-only. Either way
10
+ // what sits on disk is obfuscated (not source); bytecode is a runtime hardening layer.
11
+ "use strict";
12
+ const fs = require("node:fs");
13
+ const os = require("node:os");
14
+ const path = require("node:path");
15
+ const crypto = require("node:crypto");
16
+
17
+ const BUILD_ID = "3d89790-20260802091136"; // pack-npx stamps the real build id
18
+ const ROOT = __dirname; // package root (loader.cjs sits here)
19
+ const DIST = path.join(ROOT, "dist");
20
+ const BUNDLE = path.join(DIST, "main.obf.cjs");
21
+
22
+ // Asset roots the bundled server/installer read via env (import.meta.url is unreliable
23
+ // once bundled/compiled — see src/assets.ts, src/install.ts, src/index.ts).
24
+ process.env.MOBRIDGE_PKG_ROOT = process.env.MOBRIDGE_PKG_ROOT || ROOT;
25
+ process.env.MOBRIDGE_DIST = process.env.MOBRIDGE_DIST || DIST;
26
+
27
+ const die = (msg) => {
28
+ process.stderr.write(`[mobridge-mcp] ${msg}\n`);
29
+ process.exit(1);
30
+ };
31
+ const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
32
+
33
+ function readManifest() {
34
+ const p = path.join(ROOT, "integrity.json");
35
+ if (!fs.existsSync(p)) die("integrity.json missing — package broken, reinstall.");
36
+ let m;
37
+ try { m = JSON.parse(fs.readFileSync(p, "utf8")); } catch { die("integrity.json unreadable — package broken."); }
38
+ if (m.buildId !== BUILD_ID) die(`build mismatch: loader=${BUILD_ID} bundle=${m.buildId}. Refusing to run.`);
39
+ if (!fs.existsSync(BUNDLE)) die("dist/main.obf.cjs missing — package broken.");
40
+ const got = sha256(BUNDLE);
41
+ if (m.bundleSha256 && got !== m.bundleSha256) {
42
+ die(`integrity fail: main.obf.cjs was modified (${got.slice(0, 12)} != ${String(m.bundleSha256).slice(0, 12)}).`);
43
+ }
44
+ }
45
+
46
+ // Ensure the package's own node_modules is resolvable from a .jsc that lives outside the
47
+ // package (in ~/.mobridge/bytecode). Externals: @modelcontextprotocol/sdk, ws, ajv, @clack/prompts.
48
+ function injectNodePath() {
49
+ try {
50
+ const nm = path.dirname(path.dirname(require.resolve("ws/package.json")));
51
+ process.env.NODE_PATH = process.env.NODE_PATH ? `${nm}${path.delimiter}${process.env.NODE_PATH}` : nm;
52
+ require("node:module").Module._initPaths();
53
+ } catch { /* deps resolve normally when running the in-package fallback */ }
54
+ }
55
+
56
+ // Cache .jsc by (V8 build + buildId): a V8 patch bump changes the key -> recompile.
57
+ function cachedJscPath() {
58
+ const key = `${process.versions.v8}-${BUILD_ID}`.replace(/[^\w.-]/g, "_");
59
+ const dir = path.join(os.homedir(), ".mobridge", "bytecode", key);
60
+ return { dir, file: path.join(dir, "main.jsc") };
61
+ }
62
+
63
+ function loadViaBytecode() {
64
+ let bytenode;
65
+ try { bytenode = require("bytenode"); } catch { return false; } // no bytenode -> fallback JS
66
+ const { dir, file } = cachedJscPath();
67
+ try {
68
+ if (!fs.existsSync(file)) {
69
+ fs.mkdirSync(dir, { recursive: true });
70
+ bytenode.compileFile({ filename: BUNDLE, output: file }); // compiled against the user's own Node
71
+ }
72
+ injectNodePath();
73
+ require(file);
74
+ return true;
75
+ } catch (err) {
76
+ process.stderr.write(`[mobridge-mcp] bytecode unavailable (${err && err.message ? err.message : err}); running obfuscated JS directly.\n`);
77
+ return false;
78
+ }
79
+ }
80
+
81
+ readManifest();
82
+ if (!loadViaBytecode()) {
83
+ injectNodePath();
84
+ require(BUNDLE); // fallback: run the obfuscated JS bundle directly
85
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "mobridge-mcp",
3
+ "version": "0.0.1",
4
+ "description": "MoBridge MCP — a stdio MCP server exposing on-device Android/iOS UIs as fast semantic read / observe / find / act / verify tools plus screenshots. Ships an installer that wires it into Claude Code, Claude Desktop, Cursor and Codex.",
5
+ "keywords": [
6
+ "mcp",
7
+ "model-context-protocol",
8
+ "android",
9
+ "ios",
10
+ "accessibility",
11
+ "ui-automation",
12
+ "claude",
13
+ "cursor",
14
+ "mobridge"
15
+ ],
16
+ "license": "UNLICENSED",
17
+ "bin": {
18
+ "mobridge-mcp": "loader.cjs"
19
+ },
20
+ "engines": {
21
+ "node": ">=18"
22
+ },
23
+ "dependencies": {
24
+ "@modelcontextprotocol/sdk": "^1.0.0",
25
+ "ws": "^8.18.0",
26
+ "ajv": "^8.17.1",
27
+ "@clack/prompts": "^0.10.0",
28
+ "bytenode": "^1.6.0"
29
+ },
30
+ "files": [
31
+ "loader.cjs",
32
+ "dist",
33
+ "resources",
34
+ "integrity.json",
35
+ "README.md"
36
+ ],
37
+ "publishConfig": {
38
+ "access": "public"
39
+ }
40
+ }
@@ -0,0 +1,50 @@
1
+ import SwiftUI
2
+
3
+ // Deterministic fixture app under test. Deployment target 14.0 so the same binary runs on the
4
+ // physical iPhone 7 Plus (iOS 14.2) later. Elements carry stable accessibilityIdentifiers so the
5
+ // TS resolver can locate by automationId; two same-labelled "Continue" buttons give a controlled
6
+ // AMBIGUOUS_MATCH case; signing in flips to a "welcome" screen for wait/diff.
7
+ @main
8
+ struct MobileSemanticFixtureApp: App {
9
+ var body: some Scene {
10
+ WindowGroup { ContentView() }
11
+ }
12
+ }
13
+
14
+ struct ContentView: View {
15
+ @State private var username = ""
16
+ @State private var signedIn = false
17
+ @State private var playing = false
18
+
19
+ var body: some View {
20
+ if signedIn {
21
+ VStack(spacing: 16) {
22
+ Text("Welcome")
23
+ .accessibilityIdentifier("welcome")
24
+ // A plain Button whose OWN title flips on tap — a reliable XCUI hit target (unlike a
25
+ // SwiftUI switch), giving a deterministic NODE_STATE_CHANGED postcondition.
26
+ Button(playing ? "Pause" : "Play") { playing.toggle() }
27
+ .accessibilityIdentifier("playPause")
28
+ Button("Sign Out") { signedIn = false }
29
+ .accessibilityIdentifier("signOut")
30
+ }
31
+ .padding()
32
+ } else {
33
+ VStack(spacing: 16) {
34
+ Text("Sign In").font(.title)
35
+ TextField("Username", text: $username)
36
+ .accessibilityIdentifier("username")
37
+ .textFieldStyle(.roundedBorder)
38
+ Button("Sign In") { signedIn = true }
39
+ .accessibilityIdentifier("signIn")
40
+ // Two buttons with the SAME visible label "Continue" -> ambiguity when a NodeRef
41
+ // relies on ROLE_LABEL alone; distinct identifiers let a stronger locator disambiguate.
42
+ HStack(spacing: 24) {
43
+ Button("Continue") {}.accessibilityIdentifier("continuePrimary")
44
+ Button("Continue") {}.accessibilityIdentifier("continueSecondary")
45
+ }
46
+ }
47
+ .padding()
48
+ }
49
+ }
50
+ }