@sensaiorg/adapter-android 0.1.0

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 (89) hide show
  1. package/dist/android-adapter.d.ts.map +1 -0
  2. package/dist/android-adapter.js +89 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +12 -0
  5. package/dist/tools/accessibility.d.ts.map +1 -0
  6. package/dist/tools/accessibility.js +85 -0
  7. package/dist/tools/adb.d.ts.map +1 -0
  8. package/dist/tools/adb.js +66 -0
  9. package/dist/tools/app-state.d.ts.map +1 -0
  10. package/dist/tools/app-state.js +173 -0
  11. package/dist/tools/diagnose.d.ts.map +1 -0
  12. package/dist/tools/diagnose.js +128 -0
  13. package/dist/tools/hot-reload.d.ts.map +1 -0
  14. package/dist/tools/hot-reload.js +97 -0
  15. package/dist/tools/index.d.ts.map +1 -0
  16. package/dist/tools/index.js +66 -0
  17. package/dist/tools/interaction.d.ts.map +1 -0
  18. package/dist/tools/interaction.js +395 -0
  19. package/dist/tools/logcat.d.ts.map +1 -0
  20. package/dist/tools/logcat.js +216 -0
  21. package/dist/tools/network.d.ts.map +1 -0
  22. package/dist/tools/network.js +123 -0
  23. package/dist/tools/performance.d.ts.map +1 -0
  24. package/dist/tools/performance.js +143 -0
  25. package/dist/tools/recording.d.ts.map +1 -0
  26. package/dist/tools/recording.js +102 -0
  27. package/dist/tools/rn-tools.d.ts.map +1 -0
  28. package/dist/tools/rn-tools.js +120 -0
  29. package/dist/tools/smart-actions.d.ts.map +1 -0
  30. package/dist/tools/smart-actions.js +506 -0
  31. package/dist/tools/ui-tree.d.ts.map +1 -0
  32. package/dist/tools/ui-tree.js +226 -0
  33. package/dist/transport/adb-client.d.ts.map +1 -0
  34. package/dist/transport/adb-client.js +124 -0
  35. package/dist/transport/adb-client.test.d.ts.map +1 -0
  36. package/dist/transport/adb-client.test.js +153 -0
  37. package/dist/transport/agent-client.d.ts.map +1 -0
  38. package/dist/transport/agent-client.js +157 -0
  39. package/dist/transport/agent-client.test.d.ts.map +1 -0
  40. package/dist/transport/agent-client.test.js +199 -0
  41. package/dist/transport/connection-manager.d.ts.map +1 -0
  42. package/dist/transport/connection-manager.js +119 -0
  43. package/dist/util/logcat-parser.d.ts.map +1 -0
  44. package/dist/util/logcat-parser.js +79 -0
  45. package/dist/util/safety.d.ts.map +1 -0
  46. package/dist/util/safety.js +132 -0
  47. package/dist/util/safety.test.d.ts.map +1 -0
  48. package/dist/util/safety.test.js +205 -0
  49. package/dist/util/text-extractor.d.ts.map +1 -0
  50. package/dist/util/text-extractor.js +71 -0
  51. package/dist/util/ui-tree-cache.d.ts.map +1 -0
  52. package/dist/util/ui-tree-cache.js +46 -0
  53. package/dist/util/ui-tree-cache.test.d.ts.map +1 -0
  54. package/dist/util/ui-tree-cache.test.js +84 -0
  55. package/dist/util/ui-tree-parser.d.ts.map +1 -0
  56. package/dist/util/ui-tree-parser.js +123 -0
  57. package/dist/util/ui-tree-parser.test.d.ts.map +1 -0
  58. package/dist/util/ui-tree-parser.test.js +167 -0
  59. package/package.json +22 -0
  60. package/src/android-adapter.ts +124 -0
  61. package/src/index.ts +8 -0
  62. package/src/tools/accessibility.ts +94 -0
  63. package/src/tools/adb.ts +75 -0
  64. package/src/tools/app-state.ts +193 -0
  65. package/src/tools/diagnose.ts +146 -0
  66. package/src/tools/hot-reload.ts +103 -0
  67. package/src/tools/index.ts +66 -0
  68. package/src/tools/interaction.ts +448 -0
  69. package/src/tools/logcat.ts +252 -0
  70. package/src/tools/network.ts +145 -0
  71. package/src/tools/performance.ts +169 -0
  72. package/src/tools/recording.ts +123 -0
  73. package/src/tools/rn-tools.ts +143 -0
  74. package/src/tools/smart-actions.ts +593 -0
  75. package/src/tools/ui-tree.ts +258 -0
  76. package/src/transport/adb-client.test.ts +228 -0
  77. package/src/transport/adb-client.ts +139 -0
  78. package/src/transport/agent-client.test.ts +267 -0
  79. package/src/transport/agent-client.ts +188 -0
  80. package/src/transport/connection-manager.ts +140 -0
  81. package/src/util/logcat-parser.ts +94 -0
  82. package/src/util/safety.test.ts +251 -0
  83. package/src/util/safety.ts +143 -0
  84. package/src/util/text-extractor.ts +87 -0
  85. package/src/util/ui-tree-cache.test.ts +105 -0
  86. package/src/util/ui-tree-cache.ts +54 -0
  87. package/src/util/ui-tree-parser.test.ts +182 -0
  88. package/src/util/ui-tree-parser.ts +169 -0
  89. package/tsconfig.json +11 -0
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const ui_tree_parser_js_1 = require("./ui-tree-parser.js");
5
+ const MINIMAL_XML = `<?xml version="1.0" encoding="UTF-8"?>
6
+ <hierarchy rotation="0">
7
+ <node index="0" text="Hello" resource-id="com.example:id/greeting"
8
+ class="android.widget.TextView" package="com.example"
9
+ content-desc="Greeting text" checkable="false" checked="false"
10
+ clickable="true" enabled="true" focusable="true" focused="false"
11
+ scrollable="false" long-clickable="false" password="false"
12
+ selected="false" bounds="[0,0][500,100]">
13
+ </node>
14
+ </hierarchy>`;
15
+ const NESTED_XML = `<?xml version="1.0" encoding="UTF-8"?>
16
+ <hierarchy rotation="0">
17
+ <node index="0" text="" resource-id="" class="android.widget.FrameLayout"
18
+ package="com.example" content-desc="" checkable="false" checked="false"
19
+ clickable="false" enabled="true" focusable="false" focused="false"
20
+ scrollable="false" long-clickable="false" password="false"
21
+ selected="false" bounds="[0,0][1080,1920]">
22
+ <node index="0" text="Button 1" resource-id="com.example:id/btn1"
23
+ class="android.widget.Button" package="com.example"
24
+ content-desc="First button" checkable="false" checked="false"
25
+ clickable="true" enabled="true" focusable="true" focused="false"
26
+ scrollable="false" long-clickable="false" password="false"
27
+ selected="false" bounds="[50,100][300,200]">
28
+ </node>
29
+ <node index="1" text="Button 2" resource-id="com.example:id/btn2"
30
+ class="android.widget.Button" package="com.example"
31
+ content-desc="" checkable="false" checked="false"
32
+ clickable="true" enabled="true" focusable="true" focused="false"
33
+ scrollable="false" long-clickable="false" password="false"
34
+ selected="false" bounds="[50,250][300,350]">
35
+ </node>
36
+ </node>
37
+ </hierarchy>`;
38
+ const SYSTEM_UI_XML = `<?xml version="1.0" encoding="UTF-8"?>
39
+ <hierarchy rotation="0">
40
+ <node index="0" text="12:00" resource-id="" class="android.widget.TextView"
41
+ package="com.android.systemui" content-desc="" checkable="false" checked="false"
42
+ clickable="false" enabled="true" focusable="false" focused="false"
43
+ scrollable="false" long-clickable="false" password="false"
44
+ selected="false" bounds="[0,0][100,50]">
45
+ </node>
46
+ <node index="1" text="App Content" resource-id="" class="android.widget.TextView"
47
+ package="com.example" content-desc="" checkable="false" checked="false"
48
+ clickable="false" enabled="true" focusable="false" focused="false"
49
+ scrollable="false" long-clickable="false" password="false"
50
+ selected="false" bounds="[0,50][1080,1920]">
51
+ </node>
52
+ </hierarchy>`;
53
+ (0, vitest_1.describe)("parseUiTree", () => {
54
+ (0, vitest_1.it)("parses minimal XML with one node", () => {
55
+ const nodes = (0, ui_tree_parser_js_1.parseUiTree)(MINIMAL_XML);
56
+ (0, vitest_1.expect)(nodes).toHaveLength(1);
57
+ const node = nodes[0];
58
+ (0, vitest_1.expect)(node.text).toBe("Hello");
59
+ (0, vitest_1.expect)(node.resourceId).toBe("com.example:id/greeting");
60
+ (0, vitest_1.expect)(node.className).toBe("android.widget.TextView");
61
+ (0, vitest_1.expect)(node.contentDescription).toBe("Greeting text");
62
+ (0, vitest_1.expect)(node.clickable).toBe(true);
63
+ (0, vitest_1.expect)(node.enabled).toBe(true);
64
+ (0, vitest_1.expect)(node.focusable).toBe(true);
65
+ (0, vitest_1.expect)(node.scrollable).toBe(false);
66
+ (0, vitest_1.expect)(node.checked).toBe(false);
67
+ (0, vitest_1.expect)(node.selected).toBe(false);
68
+ (0, vitest_1.expect)(node.packageName).toBe("com.example");
69
+ (0, vitest_1.expect)(node.depth).toBe(0);
70
+ (0, vitest_1.expect)(node.bounds).toEqual({ left: 0, top: 0, right: 500, bottom: 100 });
71
+ (0, vitest_1.expect)(node.children).toHaveLength(0);
72
+ });
73
+ (0, vitest_1.it)("parses nested nodes", () => {
74
+ const nodes = (0, ui_tree_parser_js_1.parseUiTree)(NESTED_XML);
75
+ (0, vitest_1.expect)(nodes).toHaveLength(1);
76
+ (0, vitest_1.expect)(nodes[0].children).toHaveLength(2);
77
+ (0, vitest_1.expect)(nodes[0].children[0].text).toBe("Button 1");
78
+ (0, vitest_1.expect)(nodes[0].children[1].text).toBe("Button 2");
79
+ (0, vitest_1.expect)(nodes[0].children[0].depth).toBe(1);
80
+ });
81
+ (0, vitest_1.it)("filters system UI by default", () => {
82
+ const nodes = (0, ui_tree_parser_js_1.parseUiTree)(SYSTEM_UI_XML);
83
+ (0, vitest_1.expect)(nodes).toHaveLength(1);
84
+ (0, vitest_1.expect)(nodes[0].text).toBe("App Content");
85
+ });
86
+ (0, vitest_1.it)("includes system UI when requested", () => {
87
+ const nodes = (0, ui_tree_parser_js_1.parseUiTree)(SYSTEM_UI_XML, { includeSystemUI: true });
88
+ (0, vitest_1.expect)(nodes).toHaveLength(2);
89
+ });
90
+ (0, vitest_1.it)("respects maxDepth", () => {
91
+ const nodes = (0, ui_tree_parser_js_1.parseUiTree)(NESTED_XML, { maxDepth: 1 });
92
+ (0, vitest_1.expect)(nodes).toHaveLength(1);
93
+ // maxDepth=1 means depth 0 is included, children at depth 1 are included but not their children
94
+ (0, vitest_1.expect)(nodes[0].children).toHaveLength(2);
95
+ });
96
+ (0, vitest_1.it)("maxDepth=0 means unlimited", () => {
97
+ const nodes = (0, ui_tree_parser_js_1.parseUiTree)(NESTED_XML, { maxDepth: 0 });
98
+ (0, vitest_1.expect)(nodes[0].children).toHaveLength(2);
99
+ });
100
+ (0, vitest_1.it)("returns empty array for empty hierarchy", () => {
101
+ const xml = `<?xml version="1.0" encoding="UTF-8"?><hierarchy rotation="0"></hierarchy>`;
102
+ (0, vitest_1.expect)((0, ui_tree_parser_js_1.parseUiTree)(xml)).toEqual([]);
103
+ });
104
+ (0, vitest_1.it)("returns empty array for missing hierarchy", () => {
105
+ const xml = `<?xml version="1.0" encoding="UTF-8"?><root></root>`;
106
+ (0, vitest_1.expect)((0, ui_tree_parser_js_1.parseUiTree)(xml)).toEqual([]);
107
+ });
108
+ (0, vitest_1.it)("returns empty array for non-XML input", () => {
109
+ // fast-xml-parser is lenient — doesn't throw on all malformed input
110
+ const nodes = (0, ui_tree_parser_js_1.parseUiTree)("not xml at all");
111
+ (0, vitest_1.expect)(nodes).toEqual([]);
112
+ });
113
+ (0, vitest_1.it)("handles node with no bounds", () => {
114
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
115
+ <hierarchy rotation="0">
116
+ <node index="0" text="NoBounds" resource-id="" class="android.view.View"
117
+ package="com.example" content-desc="" checkable="false" checked="false"
118
+ clickable="false" enabled="true" focusable="false" focused="false"
119
+ scrollable="false" long-clickable="false" password="false"
120
+ selected="false" bounds="">
121
+ </node>
122
+ </hierarchy>`;
123
+ const nodes = (0, ui_tree_parser_js_1.parseUiTree)(xml);
124
+ (0, vitest_1.expect)(nodes).toHaveLength(1);
125
+ (0, vitest_1.expect)(nodes[0].bounds).toBeNull();
126
+ (0, vitest_1.expect)(nodes[0].visible).toBe(false);
127
+ });
128
+ (0, vitest_1.it)("filters invisible nodes with visibleOnly", () => {
129
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
130
+ <hierarchy rotation="0">
131
+ <node index="0" text="Visible" resource-id="" class="android.view.View"
132
+ package="com.example" content-desc="" checkable="false" checked="false"
133
+ clickable="false" enabled="true" focusable="false" focused="false"
134
+ scrollable="false" long-clickable="false" password="false"
135
+ selected="false" bounds="[0,0][100,100]">
136
+ </node>
137
+ <node index="1" text="Invisible" resource-id="" class="android.view.View"
138
+ package="com.example" content-desc="" checkable="false" checked="false"
139
+ clickable="false" enabled="true" focusable="false" focused="false"
140
+ scrollable="false" long-clickable="false" password="false"
141
+ selected="false" bounds="">
142
+ </node>
143
+ </hierarchy>`;
144
+ const nodes = (0, ui_tree_parser_js_1.parseUiTree)(xml, { visibleOnly: true });
145
+ (0, vitest_1.expect)(nodes).toHaveLength(1);
146
+ (0, vitest_1.expect)(nodes[0].text).toBe("Visible");
147
+ });
148
+ });
149
+ (0, vitest_1.describe)("flattenTree", () => {
150
+ (0, vitest_1.it)("flattens nested tree to array", () => {
151
+ const nodes = (0, ui_tree_parser_js_1.parseUiTree)(NESTED_XML);
152
+ const flat = (0, ui_tree_parser_js_1.flattenTree)(nodes);
153
+ (0, vitest_1.expect)(flat).toHaveLength(3); // root + 2 children
154
+ (0, vitest_1.expect)(flat[0].className).toBe("android.widget.FrameLayout");
155
+ (0, vitest_1.expect)(flat[1].text).toBe("Button 1");
156
+ (0, vitest_1.expect)(flat[2].text).toBe("Button 2");
157
+ });
158
+ (0, vitest_1.it)("handles empty array", () => {
159
+ (0, vitest_1.expect)((0, ui_tree_parser_js_1.flattenTree)([])).toEqual([]);
160
+ });
161
+ (0, vitest_1.it)("handles single node", () => {
162
+ const nodes = (0, ui_tree_parser_js_1.parseUiTree)(MINIMAL_XML);
163
+ const flat = (0, ui_tree_parser_js_1.flattenTree)(nodes);
164
+ (0, vitest_1.expect)(flat).toHaveLength(1);
165
+ });
166
+ });
167
+ //# sourceMappingURL=ui-tree-parser.test.js.map
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@sensaiorg/adapter-android",
3
+ "version": "0.1.0",
4
+ "description": "SensAI Android adapter — debug Android apps via ADB and on-device agent",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc -b",
9
+ "clean": "rm -rf dist .tsbuildinfo"
10
+ },
11
+ "license": "Apache-2.0",
12
+ "dependencies": {
13
+ "@sensaiorg/core": "0.1.0",
14
+ "fast-xml-parser": "^4.5.0"
15
+ },
16
+ "devDependencies": {
17
+ "@modelcontextprotocol/sdk": "^1.12.0",
18
+ "@types/node": "^22.0.0",
19
+ "typescript": "^5.7.0",
20
+ "zod": "^3.24.0"
21
+ }
22
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * AndroidAdapter — implements IPlatformAdapter for Android devices/emulators.
3
+ *
4
+ * Phase 1: All tools work via ADB commands (uiautomator, logcat, input, dumpsys)
5
+ * Phase 2: On-device agent provides faster UI tree, React Native inspection, network capture
6
+ */
7
+
8
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
+ import type {
10
+ IPlatformAdapter,
11
+ IUiProvider,
12
+ ILogProvider,
13
+ IInteractionProvider,
14
+ INetworkProvider,
15
+ IPerformanceProvider,
16
+ IScreenshotProvider,
17
+ IAppStateProvider,
18
+ PlatformCapabilities,
19
+ ConnectionStatus,
20
+ } from "@sensai/core";
21
+
22
+ import { ConnectionManager } from "./transport/connection-manager.js";
23
+ import { registerAllTools } from "./tools/index.js";
24
+
25
+ export interface AndroidAdapterConfig {
26
+ adbDevice?: string;
27
+ adbPath?: string;
28
+ agentPort?: number;
29
+ targetPackage?: string;
30
+ }
31
+
32
+ export class AndroidAdapter implements IPlatformAdapter {
33
+ readonly platform = "android" as const;
34
+ readonly displayName: string;
35
+ readonly capabilities: PlatformCapabilities = {
36
+ uiTree: true,
37
+ logs: true,
38
+ interaction: true,
39
+ network: false, // Phase 2 (agent needed)
40
+ performance: true,
41
+ screenshot: true,
42
+ appState: false, // Phase 2 (agent needed)
43
+ hotReload: true,
44
+ evalCode: false,
45
+ };
46
+
47
+ private readonly connectionManager: ConnectionManager;
48
+ private readonly config: Required<AndroidAdapterConfig>;
49
+
50
+ // Providers — implemented inline for now, will be extracted to separate classes
51
+ readonly ui: IUiProvider | null = null; // TODO: Extract from tools/ui-tree.ts
52
+ readonly logs: ILogProvider | null = null; // TODO: Extract from tools/logcat.ts
53
+ readonly interaction: IInteractionProvider | null = null; // TODO: Extract from tools/interaction.ts
54
+ readonly network: INetworkProvider | null = null;
55
+ readonly performance: IPerformanceProvider | null = null;
56
+ readonly screenshot: IScreenshotProvider | null = null;
57
+ readonly appState: IAppStateProvider | null = null;
58
+
59
+ constructor(config: AndroidAdapterConfig = {}) {
60
+ this.config = {
61
+ adbDevice: config.adbDevice ?? process.env.ADB_DEVICE ?? "emulator-5554",
62
+ adbPath: config.adbPath ?? process.env.ADB_PATH ?? "adb",
63
+ agentPort: config.agentPort ?? parseInt(process.env.DEBUG_AGENT_PORT ?? "8463", 10),
64
+ targetPackage: config.targetPackage ?? process.env.TARGET_PACKAGE ?? "com.chronocrew",
65
+ };
66
+
67
+ this.displayName = `Android (${this.config.adbDevice})`;
68
+
69
+ this.connectionManager = new ConnectionManager(
70
+ this.config.adbPath,
71
+ this.config.adbDevice,
72
+ this.config.agentPort,
73
+ );
74
+ }
75
+
76
+ async initialize(): Promise<ConnectionStatus> {
77
+ const status = await this.connectionManager.initialize();
78
+
79
+ // Update capabilities if agent is available
80
+ if (status.agent) {
81
+ this.capabilities.network = true;
82
+ this.capabilities.appState = true;
83
+ }
84
+
85
+ return {
86
+ platform: "android",
87
+ device: status.device,
88
+ connected: status.adb,
89
+ capabilities: this.capabilities,
90
+ details: {
91
+ adb: status.adb,
92
+ agent: status.agent,
93
+ targetPackage: this.config.targetPackage,
94
+ },
95
+ };
96
+ }
97
+
98
+ shutdown(): void {
99
+ this.connectionManager.shutdown();
100
+ }
101
+
102
+ isConnected(): boolean {
103
+ return this.connectionManager.getStatus().adb;
104
+ }
105
+
106
+ /**
107
+ * Get the ConnectionManager for direct tool access.
108
+ * Used during the migration period before full provider extraction.
109
+ */
110
+ getConnectionManager(): ConnectionManager {
111
+ return this.connectionManager;
112
+ }
113
+
114
+ /**
115
+ * Register Android-only MCP tools.
116
+ * These have no cross-platform equivalent.
117
+ */
118
+ registerPlatformTools(server: McpServer): void {
119
+ const cm = this.connectionManager;
120
+
121
+ // Register all 22 Android debugging tools
122
+ registerAllTools(server, cm);
123
+ }
124
+ }
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @sensai/adapter-android — Android platform adapter for SensAI.
3
+ *
4
+ * Provides 22 debugging tools via ADB (Phase 1) and on-device agent (Phase 2).
5
+ * This adapter wraps the original EmuDebug MCP server tools.
6
+ */
7
+
8
+ export { AndroidAdapter } from "./android-adapter.js";
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Accessibility Tool - Retrieve the Android accessibility tree.
3
+ *
4
+ * Uses `adb shell dumpsys accessibility` to get the accessibility service's
5
+ * view of the screen, which is richer than uiautomator for a11y auditing.
6
+ */
7
+
8
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
+ import type { ConnectionManager } from "../transport/connection-manager.js";
10
+
11
+ // A11yNode type reserved for Phase 2 structured accessibility tree parsing.
12
+
13
+ /**
14
+ * Parse the raw accessibility dump into a structured tree.
15
+ * The dump format is indentation-based with key: value pairs.
16
+ */
17
+ function parseAccessibilityDump(raw: string): { services: string[]; windows: string; nodeCount: number } {
18
+ const lines = raw.split("\n");
19
+ const services: string[] = [];
20
+ let nodeCount = 0;
21
+
22
+ for (const line of lines) {
23
+ if (line.includes("Service[")) {
24
+ const match = line.match(/Service\[(.+?)]/);
25
+ if (match) services.push(match[1]);
26
+ }
27
+ if (line.includes("className:") || line.includes("class:")) {
28
+ nodeCount++;
29
+ }
30
+ }
31
+
32
+ return {
33
+ services,
34
+ windows: raw,
35
+ nodeCount,
36
+ };
37
+ }
38
+
39
+ export function registerAccessibilityTools(server: McpServer, cm: ConnectionManager): void {
40
+ server.tool(
41
+ "get_accessibility",
42
+ "Get the Android accessibility tree for the current screen. Shows which elements are accessible, their roles, states, and actions. Useful for auditing screen reader compatibility.",
43
+ {},
44
+ async () => {
45
+ try {
46
+ // Try the agent first for a richer accessibility tree
47
+ if (cm.agent.isConnected()) {
48
+ try {
49
+ const result = await cm.agent.call("getAccessibilityTree");
50
+ return {
51
+ content: [{ type: "text" as const, text: JSON.stringify(result) }],
52
+ };
53
+ } catch {
54
+ // Fall through to ADB
55
+ }
56
+ }
57
+
58
+ // ADB fallback: use accessibility dumpsys
59
+ const dump = await cm.adb.shell("dumpsys accessibility");
60
+ const parsed = parseAccessibilityDump(dump);
61
+
62
+ // Also get window info for additional context
63
+ let windowInfo = "";
64
+ try {
65
+ windowInfo = await cm.adb.shell("dumpsys window windows | head -50");
66
+ } catch {
67
+ // Non-critical
68
+ }
69
+
70
+ const result = {
71
+ accessibilityServices: parsed.services,
72
+ estimatedNodeCount: parsed.nodeCount,
73
+ currentWindow: windowInfo.trim(),
74
+ rawDump: parsed.windows,
75
+ note: "For a structured accessibility tree, install the EmuDebug on-device agent (Phase 2).",
76
+ };
77
+
78
+ return {
79
+ content: [{ type: "text" as const, text: JSON.stringify(result) }],
80
+ };
81
+ } catch (err) {
82
+ return {
83
+ content: [
84
+ {
85
+ type: "text" as const,
86
+ text: `Error getting accessibility tree: ${err instanceof Error ? err.message : String(err)}`,
87
+ },
88
+ ],
89
+ isError: true,
90
+ };
91
+ }
92
+ },
93
+ );
94
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * ADB Passthrough Tool - Execute safe ADB commands on the connected device.
3
+ *
4
+ * Commands are validated against an allowlist before execution to prevent
5
+ * destructive operations. See util/safety.ts for the allowlist.
6
+ */
7
+
8
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
+ import { z } from "zod";
10
+ import type { ConnectionManager } from "../transport/connection-manager.js";
11
+ import { validateAdbCommand, parseCommandString } from "../util/safety.js";
12
+
13
+ export function registerAdbTools(server: McpServer, cm: ConnectionManager): void {
14
+ server.tool(
15
+ "run_adb",
16
+ "Execute an ADB command on the connected device. Commands are validated against a safety allowlist. Allowed: shell, pull, push, install, uninstall, logcat, bugreport, devices, forward, reverse. Blocked: root, remount, reboot-bootloader, sideload, and destructive shell commands.",
17
+ {
18
+ command: z
19
+ .string()
20
+ .describe(
21
+ "ADB command to execute (without the leading 'adb -s <device>' prefix). " +
22
+ "Example: 'shell ls /sdcard' or 'logcat -d -t 50'",
23
+ ),
24
+ timeoutMs: z.number().optional().describe("Timeout in milliseconds (default: 30000, max: 60000)"),
25
+ },
26
+ async (params) => {
27
+ try {
28
+ const args = parseCommandString(params.command);
29
+
30
+ // Validate against safety allowlist
31
+ const validation = validateAdbCommand(args);
32
+ if (!validation.allowed) {
33
+ return {
34
+ content: [
35
+ {
36
+ type: "text" as const,
37
+ text: JSON.stringify({
38
+ success: false,
39
+ error: "Command blocked by safety check",
40
+ reason: validation.reason,
41
+ }),
42
+ },
43
+ ],
44
+ };
45
+ }
46
+
47
+ const timeoutMs = Math.min(params.timeoutMs ?? 30_000, 60_000);
48
+ const output = await cm.adb.exec(args, timeoutMs);
49
+
50
+ return {
51
+ content: [
52
+ {
53
+ type: "text" as const,
54
+ text: JSON.stringify({
55
+ success: true,
56
+ command: `adb ${args.join(" ")}`,
57
+ output: output.trim(),
58
+ }),
59
+ },
60
+ ],
61
+ };
62
+ } catch (err) {
63
+ return {
64
+ content: [
65
+ {
66
+ type: "text" as const,
67
+ text: `ADB command error: ${err instanceof Error ? err.message : String(err)}`,
68
+ },
69
+ ],
70
+ isError: true,
71
+ };
72
+ }
73
+ },
74
+ );
75
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * App State Tool - Inspect the target application's internal state.
3
+ *
4
+ * Retrieves AsyncStorage, React Query cache, navigation state, auth tokens,
5
+ * theme, feature flags, and offline queue data from the running app.
6
+ *
7
+ * Requires the on-device agent for full data. In Phase 1, reads what is
8
+ * accessible via ADB (SharedPreferences, app data files).
9
+ */
10
+
11
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
12
+ import { z } from "zod";
13
+ import type { ConnectionManager } from "../transport/connection-manager.js";
14
+
15
+ /** State categories that can be requested. */
16
+ const STATE_CATEGORIES = [
17
+ "asyncStorage",
18
+ "queryCache",
19
+ "navigation",
20
+ "auth",
21
+ "theme",
22
+ "featureFlags",
23
+ "offlineQueue",
24
+ ] as const;
25
+
26
+ type StateCategory = (typeof STATE_CATEGORIES)[number];
27
+
28
+ export function registerAppStateTools(server: McpServer, cm: ConnectionManager): void {
29
+ server.tool(
30
+ "get_app_state",
31
+ "Inspect the target app's internal state including AsyncStorage, React Query cache, navigation state, auth info, theme, feature flags, and offline queue. Select specific categories or get all.",
32
+ {
33
+ categories: z
34
+ .array(z.enum(STATE_CATEGORIES))
35
+ .optional()
36
+ .describe("Which state categories to retrieve (default: all)"),
37
+ asyncStorageKeys: z
38
+ .array(z.string())
39
+ .optional()
40
+ .describe("Specific AsyncStorage keys to read (default: all)"),
41
+ queryCacheFilter: z.string().optional().describe("Filter query cache by key prefix"),
42
+ },
43
+ async (params) => {
44
+ const categories = (params.categories ?? [...STATE_CATEGORIES]) as StateCategory[];
45
+ const targetPackage = process.env.TARGET_PACKAGE ?? "com.emudebug.target";
46
+
47
+ // Phase 2: agent provides full app state
48
+ if (cm.agent.isConnected()) {
49
+ try {
50
+ const result = await cm.agent.call("getAppState", {
51
+ categories,
52
+ asyncStorageKeys: params.asyncStorageKeys,
53
+ queryCacheFilter: params.queryCacheFilter,
54
+ });
55
+
56
+ return {
57
+ content: [{ type: "text" as const, text: JSON.stringify(result) }],
58
+ };
59
+ } catch (err) {
60
+ return {
61
+ content: [
62
+ {
63
+ type: "text" as const,
64
+ text: `Agent error getting app state: ${err instanceof Error ? err.message : String(err)}. Falling back to ADB.`,
65
+ },
66
+ ],
67
+ isError: true,
68
+ };
69
+ }
70
+ }
71
+
72
+ // Phase 1: limited state via ADB
73
+ try {
74
+ const state: Record<string, unknown> = {
75
+ _phase1Note:
76
+ "Limited app state available via ADB. Install the on-device agent for full " +
77
+ "AsyncStorage, React Query cache, navigation, and auth state inspection.",
78
+ };
79
+
80
+ // AsyncStorage: RN stores in SQLite database (accessible if debuggable or rooted)
81
+ if (categories.includes("asyncStorage")) {
82
+ try {
83
+ const dbPath = `/data/data/${targetPackage}/databases/RKStorage`;
84
+ const output = await cm.adb.shell(
85
+ `run-as ${targetPackage} sqlite3 ${dbPath} "SELECT key, value FROM catalystLocalStorage LIMIT 50;"`,
86
+ );
87
+ const rows: Record<string, string> = {};
88
+ for (const line of output.trim().split("\n")) {
89
+ const sep = line.indexOf("|");
90
+ if (sep > 0) {
91
+ rows[line.slice(0, sep)] = line.slice(sep + 1);
92
+ }
93
+ }
94
+ state.asyncStorage = {
95
+ entries: rows,
96
+ count: Object.keys(rows).length,
97
+ note: "Retrieved via run-as + sqlite3 (requires debuggable app).",
98
+ };
99
+ } catch {
100
+ state.asyncStorage = {
101
+ error: "Cannot read AsyncStorage via ADB. App may not be debuggable.",
102
+ hint: "Use the on-device agent or ensure the app has android:debuggable=true.",
103
+ };
104
+ }
105
+ }
106
+
107
+ // SharedPreferences: accessible via run-as
108
+ if (categories.includes("featureFlags") || categories.includes("theme")) {
109
+ try {
110
+ const prefsDir = `/data/data/${targetPackage}/shared_prefs/`;
111
+ const files = await cm.adb.shell(`run-as ${targetPackage} ls ${prefsDir}`);
112
+ const prefsFiles = files.trim().split("\n").filter(Boolean);
113
+
114
+ const prefs: Record<string, string> = {};
115
+ for (const file of prefsFiles.slice(0, 5)) {
116
+ try {
117
+ const content = await cm.adb.shell(
118
+ `run-as ${targetPackage} cat ${prefsDir}${file}`,
119
+ );
120
+ prefs[file] = content.trim();
121
+ } catch {
122
+ prefs[file] = "(unreadable)";
123
+ }
124
+ }
125
+
126
+ if (categories.includes("featureFlags")) {
127
+ state.featureFlags = { sharedPreferences: prefs };
128
+ }
129
+ if (categories.includes("theme")) {
130
+ state.theme = { sharedPreferences: prefs };
131
+ }
132
+ } catch {
133
+ if (categories.includes("featureFlags")) {
134
+ state.featureFlags = { error: "Cannot read SharedPreferences via ADB." };
135
+ }
136
+ if (categories.includes("theme")) {
137
+ state.theme = { error: "Cannot read SharedPreferences via ADB." };
138
+ }
139
+ }
140
+ }
141
+
142
+ // Navigation: infer from activity stack
143
+ if (categories.includes("navigation")) {
144
+ try {
145
+ const activityDump = await cm.adb.shell(
146
+ `dumpsys activity activities | grep -A5 "mResumedActivity\\|topResumedActivity"`,
147
+ );
148
+ state.navigation = {
149
+ currentActivity: activityDump.trim(),
150
+ note: "Full React Navigation state requires the on-device agent.",
151
+ };
152
+ } catch {
153
+ state.navigation = { error: "Cannot determine current activity." };
154
+ }
155
+ }
156
+
157
+ // Auth: check for token files (surface-level only)
158
+ if (categories.includes("auth")) {
159
+ state.auth = {
160
+ note: "Auth token inspection requires the on-device agent. " +
161
+ "Check AsyncStorage for token keys or use get_logcat with auth-related tags.",
162
+ };
163
+ }
164
+
165
+ // Query cache and offline queue require the agent
166
+ if (categories.includes("queryCache")) {
167
+ state.queryCache = {
168
+ note: "React Query cache inspection requires the on-device agent.",
169
+ };
170
+ }
171
+ if (categories.includes("offlineQueue")) {
172
+ state.offlineQueue = {
173
+ note: "Offline queue inspection requires the on-device agent.",
174
+ };
175
+ }
176
+
177
+ return {
178
+ content: [{ type: "text" as const, text: JSON.stringify(state) }],
179
+ };
180
+ } catch (err) {
181
+ return {
182
+ content: [
183
+ {
184
+ type: "text" as const,
185
+ text: `Error getting app state: ${err instanceof Error ? err.message : String(err)}`,
186
+ },
187
+ ],
188
+ isError: true,
189
+ };
190
+ }
191
+ },
192
+ );
193
+ }