@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.
- package/dist/android-adapter.d.ts.map +1 -0
- package/dist/android-adapter.js +89 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/tools/accessibility.d.ts.map +1 -0
- package/dist/tools/accessibility.js +85 -0
- package/dist/tools/adb.d.ts.map +1 -0
- package/dist/tools/adb.js +66 -0
- package/dist/tools/app-state.d.ts.map +1 -0
- package/dist/tools/app-state.js +173 -0
- package/dist/tools/diagnose.d.ts.map +1 -0
- package/dist/tools/diagnose.js +128 -0
- package/dist/tools/hot-reload.d.ts.map +1 -0
- package/dist/tools/hot-reload.js +97 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +66 -0
- package/dist/tools/interaction.d.ts.map +1 -0
- package/dist/tools/interaction.js +395 -0
- package/dist/tools/logcat.d.ts.map +1 -0
- package/dist/tools/logcat.js +216 -0
- package/dist/tools/network.d.ts.map +1 -0
- package/dist/tools/network.js +123 -0
- package/dist/tools/performance.d.ts.map +1 -0
- package/dist/tools/performance.js +143 -0
- package/dist/tools/recording.d.ts.map +1 -0
- package/dist/tools/recording.js +102 -0
- package/dist/tools/rn-tools.d.ts.map +1 -0
- package/dist/tools/rn-tools.js +120 -0
- package/dist/tools/smart-actions.d.ts.map +1 -0
- package/dist/tools/smart-actions.js +506 -0
- package/dist/tools/ui-tree.d.ts.map +1 -0
- package/dist/tools/ui-tree.js +226 -0
- package/dist/transport/adb-client.d.ts.map +1 -0
- package/dist/transport/adb-client.js +124 -0
- package/dist/transport/adb-client.test.d.ts.map +1 -0
- package/dist/transport/adb-client.test.js +153 -0
- package/dist/transport/agent-client.d.ts.map +1 -0
- package/dist/transport/agent-client.js +157 -0
- package/dist/transport/agent-client.test.d.ts.map +1 -0
- package/dist/transport/agent-client.test.js +199 -0
- package/dist/transport/connection-manager.d.ts.map +1 -0
- package/dist/transport/connection-manager.js +119 -0
- package/dist/util/logcat-parser.d.ts.map +1 -0
- package/dist/util/logcat-parser.js +79 -0
- package/dist/util/safety.d.ts.map +1 -0
- package/dist/util/safety.js +132 -0
- package/dist/util/safety.test.d.ts.map +1 -0
- package/dist/util/safety.test.js +205 -0
- package/dist/util/text-extractor.d.ts.map +1 -0
- package/dist/util/text-extractor.js +71 -0
- package/dist/util/ui-tree-cache.d.ts.map +1 -0
- package/dist/util/ui-tree-cache.js +46 -0
- package/dist/util/ui-tree-cache.test.d.ts.map +1 -0
- package/dist/util/ui-tree-cache.test.js +84 -0
- package/dist/util/ui-tree-parser.d.ts.map +1 -0
- package/dist/util/ui-tree-parser.js +123 -0
- package/dist/util/ui-tree-parser.test.d.ts.map +1 -0
- package/dist/util/ui-tree-parser.test.js +167 -0
- package/package.json +22 -0
- package/src/android-adapter.ts +124 -0
- package/src/index.ts +8 -0
- package/src/tools/accessibility.ts +94 -0
- package/src/tools/adb.ts +75 -0
- package/src/tools/app-state.ts +193 -0
- package/src/tools/diagnose.ts +146 -0
- package/src/tools/hot-reload.ts +103 -0
- package/src/tools/index.ts +66 -0
- package/src/tools/interaction.ts +448 -0
- package/src/tools/logcat.ts +252 -0
- package/src/tools/network.ts +145 -0
- package/src/tools/performance.ts +169 -0
- package/src/tools/recording.ts +123 -0
- package/src/tools/rn-tools.ts +143 -0
- package/src/tools/smart-actions.ts +593 -0
- package/src/tools/ui-tree.ts +258 -0
- package/src/transport/adb-client.test.ts +228 -0
- package/src/transport/adb-client.ts +139 -0
- package/src/transport/agent-client.test.ts +267 -0
- package/src/transport/agent-client.ts +188 -0
- package/src/transport/connection-manager.ts +140 -0
- package/src/util/logcat-parser.ts +94 -0
- package/src/util/safety.test.ts +251 -0
- package/src/util/safety.ts +143 -0
- package/src/util/text-extractor.ts +87 -0
- package/src/util/ui-tree-cache.test.ts +105 -0
- package/src/util/ui-tree-cache.ts +54 -0
- package/src/util/ui-tree-parser.test.ts +182 -0
- package/src/util/ui-tree-parser.ts +169 -0
- package/tsconfig.json +11 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Network Tool - Inspect HTTP network traffic from the target app.
|
|
3
|
+
*
|
|
4
|
+
* Supports two modes:
|
|
5
|
+
* - "history": Retrieve past network requests (from agent buffer)
|
|
6
|
+
* - "capture": Start/stop live capture with filters
|
|
7
|
+
*
|
|
8
|
+
* Phase 1 fallback: parse logcat for OkHttp/Retrofit logging.
|
|
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
|
+
import { parseLogcat, filterByGrep } from "../util/logcat-parser.js";
|
|
15
|
+
|
|
16
|
+
export function registerNetworkTools(server: McpServer, cm: ConnectionManager): void {
|
|
17
|
+
server.tool(
|
|
18
|
+
"get_network",
|
|
19
|
+
"Inspect HTTP network traffic from the target app. In 'history' mode, returns recent requests. In 'capture' mode, starts/stops live capture. Supports URL and status code filtering.",
|
|
20
|
+
{
|
|
21
|
+
mode: z
|
|
22
|
+
.enum(["history", "capture_start", "capture_stop"])
|
|
23
|
+
.describe("history = past requests, capture_start = begin live capture, capture_stop = end and return captured"),
|
|
24
|
+
urlFilter: z.string().optional().describe("Only include requests matching this URL pattern (substring)"),
|
|
25
|
+
statusFilter: z
|
|
26
|
+
.number()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe("Only include responses with this HTTP status code"),
|
|
29
|
+
maxEntries: z.number().optional().describe("Maximum entries to return (default: 50)"),
|
|
30
|
+
includeBody: z.boolean().optional().describe("Include request/response bodies (default: false, can be large)"),
|
|
31
|
+
},
|
|
32
|
+
async (params) => {
|
|
33
|
+
const maxEntries = params.maxEntries ?? 50;
|
|
34
|
+
|
|
35
|
+
// Phase 2: full network interception via agent
|
|
36
|
+
if (cm.agent.isConnected()) {
|
|
37
|
+
try {
|
|
38
|
+
const result = await cm.agent.call("getNetwork", {
|
|
39
|
+
mode: params.mode,
|
|
40
|
+
urlFilter: params.urlFilter,
|
|
41
|
+
statusFilter: params.statusFilter,
|
|
42
|
+
maxEntries,
|
|
43
|
+
includeBody: params.includeBody ?? false,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
content: [{ type: "text" as const, text: JSON.stringify(result) }],
|
|
48
|
+
};
|
|
49
|
+
} catch (err) {
|
|
50
|
+
return {
|
|
51
|
+
content: [
|
|
52
|
+
{
|
|
53
|
+
type: "text" as const,
|
|
54
|
+
text: `Agent network error: ${err instanceof Error ? err.message : String(err)}. Falling back to logcat.`,
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
isError: true,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Phase 1: parse logcat for network-related logs
|
|
63
|
+
if (params.mode === "capture_start" || params.mode === "capture_stop") {
|
|
64
|
+
return {
|
|
65
|
+
content: [
|
|
66
|
+
{
|
|
67
|
+
type: "text" as const,
|
|
68
|
+
text: JSON.stringify({
|
|
69
|
+
error: "agent_not_connected",
|
|
70
|
+
message:
|
|
71
|
+
"Live network capture requires the on-device agent (Phase 2). " +
|
|
72
|
+
"Use mode='history' to see network logs from logcat.",
|
|
73
|
+
}),
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
// Look for OkHttp, Retrofit, and generic HTTP logs
|
|
81
|
+
const raw = await cm.adb.shell("logcat -d -v threadtime", 15_000);
|
|
82
|
+
let entries = parseLogcat(raw);
|
|
83
|
+
|
|
84
|
+
// Filter for network-related tags
|
|
85
|
+
const networkTags = ["OkHttp", "Retrofit", "HttpURLConnection", "NetworkClient", "API", "Volley"];
|
|
86
|
+
entries = entries.filter((e) =>
|
|
87
|
+
networkTags.some((tag) => e.tag.includes(tag)) ||
|
|
88
|
+
e.message.includes("HTTP") ||
|
|
89
|
+
e.message.includes("http://") ||
|
|
90
|
+
e.message.includes("https://"),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// Apply URL filter
|
|
94
|
+
if (params.urlFilter) {
|
|
95
|
+
entries = filterByGrep(entries, params.urlFilter);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Apply status filter by searching for the status code in the message
|
|
99
|
+
if (params.statusFilter) {
|
|
100
|
+
entries = entries.filter((e) =>
|
|
101
|
+
e.message.includes(String(params.statusFilter)),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const limited = entries.slice(-maxEntries);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
content: [
|
|
109
|
+
{
|
|
110
|
+
type: "text" as const,
|
|
111
|
+
text: JSON.stringify(
|
|
112
|
+
{
|
|
113
|
+
phase1Mode: true,
|
|
114
|
+
note:
|
|
115
|
+
"Network data extracted from logcat. For full request/response inspection " +
|
|
116
|
+
"with headers and bodies, install the on-device agent (Phase 2). " +
|
|
117
|
+
"Ensure your app uses OkHttp logging interceptor for better logcat data.",
|
|
118
|
+
totalMatches: entries.length,
|
|
119
|
+
returned: limited.length,
|
|
120
|
+
entries: limited.map((e) => ({
|
|
121
|
+
timestamp: e.timestamp,
|
|
122
|
+
tag: e.tag,
|
|
123
|
+
message: e.message,
|
|
124
|
+
})),
|
|
125
|
+
},
|
|
126
|
+
null,
|
|
127
|
+
2,
|
|
128
|
+
),
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
};
|
|
132
|
+
} catch (err) {
|
|
133
|
+
return {
|
|
134
|
+
content: [
|
|
135
|
+
{
|
|
136
|
+
type: "text" as const,
|
|
137
|
+
text: `Error getting network data: ${err instanceof Error ? err.message : String(err)}`,
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
isError: true,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance Tool - Monitor app performance metrics.
|
|
3
|
+
*
|
|
4
|
+
* Collects FPS, memory usage, CPU usage, and (via agent) React Native
|
|
5
|
+
* bridge metrics. Phase 1 uses ADB dumpsys; Phase 2 adds real-time
|
|
6
|
+
* frame timing and bridge throughput.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import type { ConnectionManager } from "../transport/connection-manager.js";
|
|
12
|
+
|
|
13
|
+
/** Parsed memory info from dumpsys meminfo. */
|
|
14
|
+
interface MemoryInfo {
|
|
15
|
+
totalPss: number;
|
|
16
|
+
javaHeap: number;
|
|
17
|
+
nativeHeap: number;
|
|
18
|
+
code: number;
|
|
19
|
+
stack: number;
|
|
20
|
+
graphics: number;
|
|
21
|
+
summary: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Parse `dumpsys meminfo <package>` output. */
|
|
25
|
+
function parseMeminfo(raw: string): MemoryInfo {
|
|
26
|
+
const parseLine = (label: string): number => {
|
|
27
|
+
const re = new RegExp(`${label}[:\\s]+(\\d+)`, "i");
|
|
28
|
+
const match = raw.match(re);
|
|
29
|
+
return match ? parseInt(match[1], 10) : 0;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
totalPss: parseLine("TOTAL PSS"),
|
|
34
|
+
javaHeap: parseLine("Java Heap"),
|
|
35
|
+
nativeHeap: parseLine("Native Heap"),
|
|
36
|
+
code: parseLine("Code"),
|
|
37
|
+
stack: parseLine("Stack"),
|
|
38
|
+
graphics: parseLine("Graphics"),
|
|
39
|
+
summary: raw.split("\n").slice(0, 20).join("\n"),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function registerPerformanceTools(server: McpServer, cm: ConnectionManager): void {
|
|
44
|
+
server.tool(
|
|
45
|
+
"get_performance",
|
|
46
|
+
"Get performance metrics for the target app: memory usage, CPU, frame rate (FPS), and bridge metrics. Uses ADB dumpsys in Phase 1; on-device agent provides real-time FPS and bridge throughput in Phase 2.",
|
|
47
|
+
{
|
|
48
|
+
metrics: z
|
|
49
|
+
.array(z.enum(["fps", "memory", "cpu", "bridge"]))
|
|
50
|
+
.optional()
|
|
51
|
+
.describe("Which metrics to collect (default: all)"),
|
|
52
|
+
durationSec: z
|
|
53
|
+
.number()
|
|
54
|
+
.optional()
|
|
55
|
+
.describe("Duration for FPS/CPU sampling in seconds (default: 2, max: 10)"),
|
|
56
|
+
},
|
|
57
|
+
async (params) => {
|
|
58
|
+
const metrics = params.metrics ?? ["fps", "memory", "cpu", "bridge"];
|
|
59
|
+
const targetPackage = process.env.TARGET_PACKAGE ?? "com.emudebug.target";
|
|
60
|
+
const durationSec = Math.min(params.durationSec ?? 2, 10);
|
|
61
|
+
|
|
62
|
+
// Phase 2: agent provides rich metrics
|
|
63
|
+
if (cm.agent.isConnected()) {
|
|
64
|
+
try {
|
|
65
|
+
const result = await cm.agent.call("getPerformance", {
|
|
66
|
+
metrics,
|
|
67
|
+
durationSec,
|
|
68
|
+
});
|
|
69
|
+
return {
|
|
70
|
+
content: [{ type: "text" as const, text: JSON.stringify(result) }],
|
|
71
|
+
};
|
|
72
|
+
} catch {
|
|
73
|
+
// Fall through to ADB
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Phase 1: ADB-based metrics
|
|
78
|
+
const result: Record<string, unknown> = {};
|
|
79
|
+
|
|
80
|
+
// Memory via dumpsys
|
|
81
|
+
if (metrics.includes("memory")) {
|
|
82
|
+
try {
|
|
83
|
+
const raw = await cm.adb.shell(`dumpsys meminfo ${targetPackage}`);
|
|
84
|
+
result.memory = parseMeminfo(raw);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
result.memory = {
|
|
87
|
+
error: `Failed to get memory info: ${err instanceof Error ? err.message : String(err)}`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// CPU via top (single snapshot)
|
|
93
|
+
if (metrics.includes("cpu")) {
|
|
94
|
+
try {
|
|
95
|
+
const raw = await cm.adb.shell(
|
|
96
|
+
`top -b -n 1 -d ${durationSec} | grep -i "${targetPackage}"`,
|
|
97
|
+
);
|
|
98
|
+
const lines = raw.trim().split("\n").filter(Boolean);
|
|
99
|
+
const cpuEntries = lines.map((line) => {
|
|
100
|
+
const parts = line.trim().split(/\s+/);
|
|
101
|
+
return {
|
|
102
|
+
pid: parts[0],
|
|
103
|
+
cpu: parts.length > 8 ? parts[8] : "?",
|
|
104
|
+
mem: parts.length > 9 ? parts[9] : "?",
|
|
105
|
+
raw: line.trim(),
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
result.cpu = { processes: cpuEntries };
|
|
109
|
+
} catch (err) {
|
|
110
|
+
result.cpu = {
|
|
111
|
+
error: `Failed to get CPU info: ${err instanceof Error ? err.message : String(err)}`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// FPS via dumpsys gfxinfo
|
|
117
|
+
if (metrics.includes("fps")) {
|
|
118
|
+
try {
|
|
119
|
+
// Reset gfxinfo, wait, then read
|
|
120
|
+
await cm.adb.shell(`dumpsys gfxinfo ${targetPackage} reset`);
|
|
121
|
+
// Brief wait for frame data to accumulate
|
|
122
|
+
await new Promise((r) => setTimeout(r, durationSec * 1000));
|
|
123
|
+
const raw = await cm.adb.shell(`dumpsys gfxinfo ${targetPackage}`);
|
|
124
|
+
|
|
125
|
+
// Parse frame stats
|
|
126
|
+
const totalFramesMatch = raw.match(/Total frames rendered:\s*(\d+)/);
|
|
127
|
+
const jankyMatch = raw.match(/Janky frames:\s*(\d+)/);
|
|
128
|
+
const p50Match = raw.match(/50th percentile:\s*(\d+)ms/);
|
|
129
|
+
const p90Match = raw.match(/90th percentile:\s*(\d+)ms/);
|
|
130
|
+
const p95Match = raw.match(/95th percentile:\s*(\d+)ms/);
|
|
131
|
+
const p99Match = raw.match(/99th percentile:\s*(\d+)ms/);
|
|
132
|
+
|
|
133
|
+
const totalFrames = totalFramesMatch ? parseInt(totalFramesMatch[1], 10) : 0;
|
|
134
|
+
const jankyFrames = jankyMatch ? parseInt(jankyMatch[1], 10) : 0;
|
|
135
|
+
|
|
136
|
+
result.fps = {
|
|
137
|
+
totalFrames,
|
|
138
|
+
jankyFrames,
|
|
139
|
+
jankyPercentage: totalFrames > 0 ? ((jankyFrames / totalFrames) * 100).toFixed(1) + "%" : "N/A",
|
|
140
|
+
percentiles: {
|
|
141
|
+
p50: p50Match ? `${p50Match[1]}ms` : "N/A",
|
|
142
|
+
p90: p90Match ? `${p90Match[1]}ms` : "N/A",
|
|
143
|
+
p95: p95Match ? `${p95Match[1]}ms` : "N/A",
|
|
144
|
+
p99: p99Match ? `${p99Match[1]}ms` : "N/A",
|
|
145
|
+
},
|
|
146
|
+
sampleDurationSec: durationSec,
|
|
147
|
+
note: "For real-time FPS monitoring, install the on-device agent (Phase 2).",
|
|
148
|
+
};
|
|
149
|
+
} catch (err) {
|
|
150
|
+
result.fps = {
|
|
151
|
+
error: `Failed to get FPS data: ${err instanceof Error ? err.message : String(err)}`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Bridge metrics require agent
|
|
157
|
+
if (metrics.includes("bridge")) {
|
|
158
|
+
result.bridge = {
|
|
159
|
+
note: "React Native bridge metrics require the on-device agent (Phase 2). " +
|
|
160
|
+
"Use get_rn_bridge for logcat-based bridge inspection.",
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
content: [{ type: "text" as const, text: JSON.stringify(result) }],
|
|
166
|
+
};
|
|
167
|
+
},
|
|
168
|
+
);
|
|
169
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recording Tools - Screen recording for Android devices.
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - start_recording: Begin screen recording
|
|
6
|
+
* - stop_recording: Stop and retrieve recording as base64 MP4
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import type { ConnectionManager } from "../transport/connection-manager.js";
|
|
12
|
+
import { readFile, unlink } from "node:fs/promises";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
|
|
16
|
+
/** Track active recording state. */
|
|
17
|
+
let recordingActive = false;
|
|
18
|
+
const DEVICE_RECORDING_PATH = "/sdcard/sensai_recording.mp4";
|
|
19
|
+
|
|
20
|
+
export function registerRecordingTools(server: McpServer, cm: ConnectionManager): void {
|
|
21
|
+
/**
|
|
22
|
+
* start_recording - Begin screen recording on the Android device.
|
|
23
|
+
*/
|
|
24
|
+
server.tool(
|
|
25
|
+
"start_recording",
|
|
26
|
+
"Start recording the Android device screen. Recording runs in the background. Use stop_recording to finish and retrieve the video. Max duration 180 seconds.",
|
|
27
|
+
{
|
|
28
|
+
maxDurationSec: z.number().optional().describe("Maximum recording duration in seconds (default: 60, max: 180)"),
|
|
29
|
+
},
|
|
30
|
+
async ({ maxDurationSec }) => {
|
|
31
|
+
if (recordingActive) {
|
|
32
|
+
return {
|
|
33
|
+
content: [{ type: "text" as const, text: JSON.stringify({ error: "Recording already in progress. Call stop_recording first." }) }],
|
|
34
|
+
isError: true,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const duration = Math.min(maxDurationSec ?? 60, 180);
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
// Remove any leftover recording file
|
|
42
|
+
await cm.adb.shell(`rm -f ${DEVICE_RECORDING_PATH}`).catch(() => {});
|
|
43
|
+
|
|
44
|
+
// Start recording in background (screenrecord exits after duration or when killed)
|
|
45
|
+
// We run it detached so it doesn't block
|
|
46
|
+
cm.adb.shell(`screenrecord --time-limit ${duration} ${DEVICE_RECORDING_PATH}`).catch(() => {
|
|
47
|
+
// Recording finished or was killed — expected
|
|
48
|
+
recordingActive = false;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
recordingActive = true;
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
content: [{
|
|
55
|
+
type: "text" as const,
|
|
56
|
+
text: JSON.stringify({ ok: true, maxDurationSec: duration, path: DEVICE_RECORDING_PATH }),
|
|
57
|
+
}],
|
|
58
|
+
};
|
|
59
|
+
} catch (err) {
|
|
60
|
+
recordingActive = false;
|
|
61
|
+
return {
|
|
62
|
+
content: [{ type: "text" as const, text: `start_recording failed: ${err instanceof Error ? err.message : String(err)}` }],
|
|
63
|
+
isError: true,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* stop_recording - Stop the active recording and retrieve the video file.
|
|
71
|
+
*/
|
|
72
|
+
server.tool(
|
|
73
|
+
"stop_recording",
|
|
74
|
+
"Stop the active screen recording and retrieve the MP4 video. Returns the video as base64 data, or saves to a local path if specified.",
|
|
75
|
+
{
|
|
76
|
+
savePath: z.string().optional().describe("Local path to save the MP4 file (optional). If not provided, returns base64."),
|
|
77
|
+
},
|
|
78
|
+
async ({ savePath }) => {
|
|
79
|
+
try {
|
|
80
|
+
// Kill screenrecord process (this causes it to finalize the MP4)
|
|
81
|
+
await cm.adb.shell("pkill -SIGINT screenrecord").catch(() => {});
|
|
82
|
+
|
|
83
|
+
// Wait for file to be finalized
|
|
84
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
85
|
+
recordingActive = false;
|
|
86
|
+
|
|
87
|
+
// Pull the file from device
|
|
88
|
+
const localPath = savePath ?? join(tmpdir(), `sensai-recording-${Date.now()}.mp4`);
|
|
89
|
+
await cm.adb.pull(DEVICE_RECORDING_PATH, localPath);
|
|
90
|
+
|
|
91
|
+
// Clean up device file
|
|
92
|
+
await cm.adb.shell(`rm -f ${DEVICE_RECORDING_PATH}`).catch(() => {});
|
|
93
|
+
|
|
94
|
+
if (savePath) {
|
|
95
|
+
// User wants it saved to a path
|
|
96
|
+
return {
|
|
97
|
+
content: [{
|
|
98
|
+
type: "text" as const,
|
|
99
|
+
text: JSON.stringify({ ok: true, savedTo: localPath }),
|
|
100
|
+
}],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Return as base64
|
|
105
|
+
const data = await readFile(localPath);
|
|
106
|
+
await unlink(localPath).catch(() => {});
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
content: [{
|
|
110
|
+
type: "text" as const,
|
|
111
|
+
text: JSON.stringify({ ok: true, format: "mp4", sizeBytes: data.length, base64: data.toString("base64") }),
|
|
112
|
+
}],
|
|
113
|
+
};
|
|
114
|
+
} catch (err) {
|
|
115
|
+
recordingActive = false;
|
|
116
|
+
return {
|
|
117
|
+
content: [{ type: "text" as const, text: `stop_recording failed: ${err instanceof Error ? err.message : String(err)}` }],
|
|
118
|
+
isError: true,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Native Tools - Inspect RN component tree and bridge activity.
|
|
3
|
+
*
|
|
4
|
+
* These tools require the on-device agent (Phase 2) for full functionality.
|
|
5
|
+
* In Phase 1, they provide guidance on what data will be available once
|
|
6
|
+
* the agent is installed.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import type { ConnectionManager } from "../transport/connection-manager.js";
|
|
12
|
+
|
|
13
|
+
/** Message returned when the agent is required but not connected. */
|
|
14
|
+
const AGENT_REQUIRED_MSG = (tool: string) =>
|
|
15
|
+
JSON.stringify({
|
|
16
|
+
error: "agent_not_connected",
|
|
17
|
+
tool,
|
|
18
|
+
message:
|
|
19
|
+
"This tool requires the EmuDebug on-device agent (Phase 2). " +
|
|
20
|
+
"The agent provides deep React Native introspection by hooking into " +
|
|
21
|
+
"the Hermes JS engine and React fiber tree. " +
|
|
22
|
+
"Use ADB-based tools (get_ui_tree, get_logcat) for Phase 1 debugging.",
|
|
23
|
+
alternatives: [
|
|
24
|
+
"get_ui_tree - View hierarchy via uiautomator",
|
|
25
|
+
"get_screen_text - All visible text on screen",
|
|
26
|
+
"get_logcat - Filter logs by ReactNativeJS tag",
|
|
27
|
+
"get_crash_info - JS exceptions from logcat",
|
|
28
|
+
],
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export function registerRnTools(server: McpServer, cm: ConnectionManager): void {
|
|
32
|
+
/**
|
|
33
|
+
* get_rn_component_tree - Get the React Native component tree via Hermes CDP.
|
|
34
|
+
*/
|
|
35
|
+
server.tool(
|
|
36
|
+
"get_rn_component_tree",
|
|
37
|
+
"Get the React Native component tree from the Hermes engine. Shows component names, props, state, and hooks. Requires on-device agent (Phase 2); falls back to guidance in Phase 1.",
|
|
38
|
+
{
|
|
39
|
+
componentFilter: z.string().optional().describe("Filter by component name (substring match)"),
|
|
40
|
+
maxDepth: z.number().optional().describe("Maximum tree depth to return"),
|
|
41
|
+
includeProps: z.boolean().optional().describe("Include component props (default: true)"),
|
|
42
|
+
includeState: z.boolean().optional().describe("Include component state (default: true)"),
|
|
43
|
+
},
|
|
44
|
+
async (params) => {
|
|
45
|
+
if (!cm.agent.isConnected()) {
|
|
46
|
+
return {
|
|
47
|
+
content: [{ type: "text" as const, text: AGENT_REQUIRED_MSG("get_rn_component_tree") }],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const result = await cm.agent.call("getRnComponentTree", {
|
|
53
|
+
componentFilter: params.componentFilter,
|
|
54
|
+
maxDepth: params.maxDepth ?? 0,
|
|
55
|
+
includeProps: params.includeProps ?? true,
|
|
56
|
+
includeState: params.includeState ?? true,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
content: [{ type: "text" as const, text: JSON.stringify(result) }],
|
|
61
|
+
};
|
|
62
|
+
} catch (err) {
|
|
63
|
+
return {
|
|
64
|
+
content: [
|
|
65
|
+
{
|
|
66
|
+
type: "text" as const,
|
|
67
|
+
text: `Error getting RN component tree: ${err instanceof Error ? err.message : String(err)}`,
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
isError: true,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* get_rn_bridge - Inspect React Native bridge / TurboModule activity.
|
|
78
|
+
*/
|
|
79
|
+
server.tool(
|
|
80
|
+
"get_rn_bridge",
|
|
81
|
+
"Inspect React Native TurboModule calls and bridge traffic. Shows recent native module invocations and their parameters. Requires on-device agent (Phase 2).",
|
|
82
|
+
{
|
|
83
|
+
moduleName: z.string().optional().describe("Filter by native module name"),
|
|
84
|
+
maxEntries: z.number().optional().describe("Maximum entries to return (default: 50)"),
|
|
85
|
+
includeArgs: z.boolean().optional().describe("Include call arguments (default: true)"),
|
|
86
|
+
},
|
|
87
|
+
async (params) => {
|
|
88
|
+
if (!cm.agent.isConnected()) {
|
|
89
|
+
// In Phase 1, we can at least check logcat for bridge-related messages
|
|
90
|
+
try {
|
|
91
|
+
const bridgeLogs = await cm.adb.shell(
|
|
92
|
+
"logcat -d -v threadtime | grep -i 'turbo\\|bridge\\|nativemodule' | tail -30",
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
content: [
|
|
97
|
+
{
|
|
98
|
+
type: "text" as const,
|
|
99
|
+
text: JSON.stringify(
|
|
100
|
+
{
|
|
101
|
+
phase1Mode: true,
|
|
102
|
+
message:
|
|
103
|
+
"Full bridge inspection requires the on-device agent (Phase 2). " +
|
|
104
|
+
"Showing bridge-related logcat entries as a fallback.",
|
|
105
|
+
logEntries: bridgeLogs.trim().split("\n").filter(Boolean),
|
|
106
|
+
},
|
|
107
|
+
null,
|
|
108
|
+
2,
|
|
109
|
+
),
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
};
|
|
113
|
+
} catch {
|
|
114
|
+
return {
|
|
115
|
+
content: [{ type: "text" as const, text: AGENT_REQUIRED_MSG("get_rn_bridge") }],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const result = await cm.agent.call("getRnBridge", {
|
|
122
|
+
moduleName: params.moduleName,
|
|
123
|
+
maxEntries: params.maxEntries ?? 50,
|
|
124
|
+
includeArgs: params.includeArgs ?? true,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
content: [{ type: "text" as const, text: JSON.stringify(result) }],
|
|
129
|
+
};
|
|
130
|
+
} catch (err) {
|
|
131
|
+
return {
|
|
132
|
+
content: [
|
|
133
|
+
{
|
|
134
|
+
type: "text" as const,
|
|
135
|
+
text: `Error getting bridge data: ${err instanceof Error ? err.message : String(err)}`,
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
isError: true,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
}
|