@0xhayd3n/fling 0.6.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/adb.d.ts +18 -0
- package/dist/adb.js +109 -0
- package/dist/adb.js.map +1 -0
- package/dist/apkFinder.d.ts +19 -0
- package/dist/apkFinder.js +113 -0
- package/dist/apkFinder.js.map +1 -0
- package/dist/apkResolver.d.ts +14 -0
- package/dist/apkResolver.js +55 -0
- package/dist/apkResolver.js.map +1 -0
- package/dist/config.d.ts +44 -0
- package/dist/config.js +113 -0
- package/dist/config.js.map +1 -0
- package/dist/devices.d.ts +24 -0
- package/dist/devices.js +143 -0
- package/dist/devices.js.map +1 -0
- package/dist/errors.d.ts +13 -0
- package/dist/errors.js +22 -0
- package/dist/errors.js.map +1 -0
- package/dist/featureFlags.d.ts +6 -0
- package/dist/featureFlags.js +11 -0
- package/dist/featureFlags.js.map +1 -0
- package/dist/gradle.d.ts +39 -0
- package/dist/gradle.js +129 -0
- package/dist/gradle.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +76 -0
- package/dist/index.js.map +1 -0
- package/dist/mdns.d.ts +19 -0
- package/dist/mdns.js +68 -0
- package/dist/mdns.js.map +1 -0
- package/dist/pairing.d.ts +52 -0
- package/dist/pairing.js +212 -0
- package/dist/pairing.js.map +1 -0
- package/dist/qrText.d.ts +5 -0
- package/dist/qrText.js +8 -0
- package/dist/qrText.js.map +1 -0
- package/dist/schemas.d.ts +31 -0
- package/dist/schemas.js +26 -0
- package/dist/schemas.js.map +1 -0
- package/dist/shellFraming.d.ts +28 -0
- package/dist/shellFraming.js +43 -0
- package/dist/shellFraming.js.map +1 -0
- package/dist/shellPool.d.ts +54 -0
- package/dist/shellPool.js +181 -0
- package/dist/shellPool.js.map +1 -0
- package/dist/toolResult.d.ts +10 -0
- package/dist/toolResult.js +11 -0
- package/dist/toolResult.js.map +1 -0
- package/dist/tools/build-app.d.ts +2 -0
- package/dist/tools/build-app.js +74 -0
- package/dist/tools/build-app.js.map +1 -0
- package/dist/tools/deploy-and-run.d.ts +2 -0
- package/dist/tools/deploy-and-run.js +195 -0
- package/dist/tools/deploy-and-run.js.map +1 -0
- package/dist/tools/device-state.d.ts +11 -0
- package/dist/tools/device-state.js +140 -0
- package/dist/tools/device-state.js.map +1 -0
- package/dist/tools/dismiss-dialog.d.ts +15 -0
- package/dist/tools/dismiss-dialog.js +89 -0
- package/dist/tools/dismiss-dialog.js.map +1 -0
- package/dist/tools/dump-ui.d.ts +4 -0
- package/dist/tools/dump-ui.js +93 -0
- package/dist/tools/dump-ui.js.map +1 -0
- package/dist/tools/find-on-screen.d.ts +27 -0
- package/dist/tools/find-on-screen.js +92 -0
- package/dist/tools/find-on-screen.js.map +1 -0
- package/dist/tools/install-app.d.ts +23 -0
- package/dist/tools/install-app.js +127 -0
- package/dist/tools/install-app.js.map +1 -0
- package/dist/tools/launch-and-wait.d.ts +27 -0
- package/dist/tools/launch-and-wait.js +103 -0
- package/dist/tools/launch-and-wait.js.map +1 -0
- package/dist/tools/launch-app.d.ts +20 -0
- package/dist/tools/launch-app.js +131 -0
- package/dist/tools/launch-app.js.map +1 -0
- package/dist/tools/launch-settings.d.ts +42 -0
- package/dist/tools/launch-settings.js +211 -0
- package/dist/tools/launch-settings.js.map +1 -0
- package/dist/tools/list-devices.d.ts +2 -0
- package/dist/tools/list-devices.js +35 -0
- package/dist/tools/list-devices.js.map +1 -0
- package/dist/tools/long-press-by-text.d.ts +3 -0
- package/dist/tools/long-press-by-text.js +99 -0
- package/dist/tools/long-press-by-text.js.map +1 -0
- package/dist/tools/open-setting.d.ts +55 -0
- package/dist/tools/open-setting.js +257 -0
- package/dist/tools/open-setting.js.map +1 -0
- package/dist/tools/read-logs.d.ts +2 -0
- package/dist/tools/read-logs.js +147 -0
- package/dist/tools/read-logs.js.map +1 -0
- package/dist/tools/screenshot-with-ui.d.ts +21 -0
- package/dist/tools/screenshot-with-ui.js +74 -0
- package/dist/tools/screenshot-with-ui.js.map +1 -0
- package/dist/tools/screenshot.d.ts +8 -0
- package/dist/tools/screenshot.js +97 -0
- package/dist/tools/screenshot.js.map +1 -0
- package/dist/tools/scroll-until-visible.d.ts +23 -0
- package/dist/tools/scroll-until-visible.js +138 -0
- package/dist/tools/scroll-until-visible.js.map +1 -0
- package/dist/tools/start-pair-qr.d.ts +11 -0
- package/dist/tools/start-pair-qr.js +62 -0
- package/dist/tools/start-pair-qr.js.map +1 -0
- package/dist/tools/stop-app.d.ts +2 -0
- package/dist/tools/stop-app.js +63 -0
- package/dist/tools/stop-app.js.map +1 -0
- package/dist/tools/tap-by-content-desc.d.ts +17 -0
- package/dist/tools/tap-by-content-desc.js +97 -0
- package/dist/tools/tap-by-content-desc.js.map +1 -0
- package/dist/tools/tap-by-resource-id.d.ts +17 -0
- package/dist/tools/tap-by-resource-id.js +89 -0
- package/dist/tools/tap-by-resource-id.js.map +1 -0
- package/dist/tools/tap-by-text.d.ts +40 -0
- package/dist/tools/tap-by-text.js +180 -0
- package/dist/tools/tap-by-text.js.map +1 -0
- package/dist/tools/tap-text-verified.d.ts +37 -0
- package/dist/tools/tap-text-verified.js +111 -0
- package/dist/tools/tap-text-verified.js.map +1 -0
- package/dist/tools/uninstall-app.d.ts +2 -0
- package/dist/tools/uninstall-app.js +96 -0
- package/dist/tools/uninstall-app.js.map +1 -0
- package/dist/tools/wait-for-pair.d.ts +29 -0
- package/dist/tools/wait-for-pair.js +132 -0
- package/dist/tools/wait-for-pair.js.map +1 -0
- package/dist/tools/wait-for.d.ts +26 -0
- package/dist/tools/wait-for.js +109 -0
- package/dist/tools/wait-for.js.map +1 -0
- package/dist/uiDump.d.ts +34 -0
- package/dist/uiDump.js +108 -0
- package/dist/uiDump.js.map +1 -0
- package/dist/uiSelector.d.ts +21 -0
- package/dist/uiSelector.js +62 -0
- package/dist/uiSelector.js.map +1 -0
- package/package.json +67 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { type UiNode } from "../uiDump.js";
|
|
3
|
+
import { type Selector } from "../uiSelector.js";
|
|
4
|
+
export declare function buildSwipeArgs(deviceArgs: string[], direction: "up" | "down", screenWidth: number, screenHeight: number): string[];
|
|
5
|
+
export interface ScrollAndSearchOptions {
|
|
6
|
+
maxScrolls: number;
|
|
7
|
+
}
|
|
8
|
+
export interface ScrollAndSearchResult {
|
|
9
|
+
found: boolean;
|
|
10
|
+
scrolls_performed: number;
|
|
11
|
+
bounds?: {
|
|
12
|
+
x1: number;
|
|
13
|
+
y1: number;
|
|
14
|
+
x2: number;
|
|
15
|
+
y2: number;
|
|
16
|
+
};
|
|
17
|
+
center?: {
|
|
18
|
+
x: number;
|
|
19
|
+
y: number;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export declare function scrollAndSearch(dumpFn: () => Promise<UiNode[]>, swipeFn: () => Promise<void>, selector: Selector, options: ScrollAndSearchOptions): Promise<ScrollAndSearchResult>;
|
|
23
|
+
export declare function registerScrollUntilVisible(server: McpServer): void;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { fetchUiDump } from "../uiDump.js";
|
|
3
|
+
import { findNodes } from "../uiSelector.js";
|
|
4
|
+
import { runAdb } from "../adb.js";
|
|
5
|
+
import { resolveDeviceArgs } from "../devices.js";
|
|
6
|
+
import { deviceIdInput } from "../schemas.js";
|
|
7
|
+
import { toolErrorFrom } from "../toolResult.js";
|
|
8
|
+
const SWIPE_DURATION_MS = 500;
|
|
9
|
+
export function buildSwipeArgs(deviceArgs, direction, screenWidth, screenHeight) {
|
|
10
|
+
const x = Math.round(screenWidth / 2);
|
|
11
|
+
const high = Math.round(screenHeight * 0.8);
|
|
12
|
+
const low = Math.round(screenHeight * 0.2);
|
|
13
|
+
const [y1, y2] = direction === "down" ? [high, low] : [low, high];
|
|
14
|
+
return [
|
|
15
|
+
...deviceArgs,
|
|
16
|
+
"shell",
|
|
17
|
+
"input",
|
|
18
|
+
"swipe",
|
|
19
|
+
String(x),
|
|
20
|
+
String(y1),
|
|
21
|
+
String(x),
|
|
22
|
+
String(y2),
|
|
23
|
+
String(SWIPE_DURATION_MS),
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
export async function scrollAndSearch(dumpFn, swipeFn, selector, options) {
|
|
27
|
+
let scrolls = 0;
|
|
28
|
+
for (let i = 0; i <= options.maxScrolls; i++) {
|
|
29
|
+
const nodes = await dumpFn();
|
|
30
|
+
const matches = findNodes(nodes, selector);
|
|
31
|
+
if (matches.length > 0) {
|
|
32
|
+
return {
|
|
33
|
+
found: true,
|
|
34
|
+
scrolls_performed: scrolls,
|
|
35
|
+
bounds: matches[0].bounds,
|
|
36
|
+
center: matches[0].center,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (scrolls >= options.maxScrolls)
|
|
40
|
+
break;
|
|
41
|
+
await swipeFn();
|
|
42
|
+
scrolls++;
|
|
43
|
+
}
|
|
44
|
+
return { found: false, scrolls_performed: scrolls };
|
|
45
|
+
}
|
|
46
|
+
async function getScreenSize(deviceArgs) {
|
|
47
|
+
// `wm size` returns "Physical size: WxH" and possibly "Override size: WxH".
|
|
48
|
+
// Override wins when present (matches what uiautomator sees).
|
|
49
|
+
const { stdout } = await runAdb([...deviceArgs, "shell", "wm", "size"]);
|
|
50
|
+
const lines = stdout.split(/\r?\n/);
|
|
51
|
+
const override = lines.find((l) => /^Override size:/i.test(l));
|
|
52
|
+
const physical = lines.find((l) => /^Physical size:/i.test(l));
|
|
53
|
+
const line = override ?? physical;
|
|
54
|
+
if (!line) {
|
|
55
|
+
// Fallback: assume the resolution this device showed in dump_ui — 1220x2712.
|
|
56
|
+
return { width: 1220, height: 2712 };
|
|
57
|
+
}
|
|
58
|
+
const m = /(\d+)x(\d+)/.exec(line);
|
|
59
|
+
if (!m)
|
|
60
|
+
return { width: 1220, height: 2712 };
|
|
61
|
+
return { width: Number(m[1]), height: Number(m[2]) };
|
|
62
|
+
}
|
|
63
|
+
export function registerScrollUntilVisible(server) {
|
|
64
|
+
server.registerTool("scroll_until_visible", {
|
|
65
|
+
title: "Scroll the screen until a UI element becomes visible",
|
|
66
|
+
description: "Dump UI; if the selector matches, return immediately. Otherwise swipe " +
|
|
67
|
+
"in the given direction (default \"down\") and redump, up to max_scrolls " +
|
|
68
|
+
"times. Returns {found: false} when exhausted — NOT an error. The AI " +
|
|
69
|
+
"decides whether to keep trying or change strategy.",
|
|
70
|
+
inputSchema: {
|
|
71
|
+
by: z
|
|
72
|
+
.enum(["text", "resource_id", "content_desc"])
|
|
73
|
+
.describe("Which attribute to match on."),
|
|
74
|
+
value: z.string().min(1).describe("Value to look up."),
|
|
75
|
+
exact: z
|
|
76
|
+
.boolean()
|
|
77
|
+
.optional()
|
|
78
|
+
.default(false)
|
|
79
|
+
.describe("Exact match (ignored for resource_id which is always exact)."),
|
|
80
|
+
direction: z
|
|
81
|
+
.enum(["up", "down"])
|
|
82
|
+
.optional()
|
|
83
|
+
.default("down")
|
|
84
|
+
.describe("Which way to swipe between dumps."),
|
|
85
|
+
max_scrolls: z
|
|
86
|
+
.number()
|
|
87
|
+
.int()
|
|
88
|
+
.positive()
|
|
89
|
+
.max(50)
|
|
90
|
+
.optional()
|
|
91
|
+
.default(10)
|
|
92
|
+
.describe("Stop after this many swipes (default 10)."),
|
|
93
|
+
device_id: deviceIdInput,
|
|
94
|
+
},
|
|
95
|
+
outputSchema: {
|
|
96
|
+
device_id: z.string(),
|
|
97
|
+
found: z.boolean(),
|
|
98
|
+
scrolls_performed: z.number().int().nonnegative(),
|
|
99
|
+
bounds: z
|
|
100
|
+
.object({
|
|
101
|
+
x1: z.number(),
|
|
102
|
+
y1: z.number(),
|
|
103
|
+
x2: z.number(),
|
|
104
|
+
y2: z.number(),
|
|
105
|
+
})
|
|
106
|
+
.optional(),
|
|
107
|
+
center: z
|
|
108
|
+
.object({ x: z.number(), y: z.number() })
|
|
109
|
+
.optional(),
|
|
110
|
+
},
|
|
111
|
+
annotations: {
|
|
112
|
+
readOnlyHint: false,
|
|
113
|
+
destructiveHint: false,
|
|
114
|
+
idempotentHint: false,
|
|
115
|
+
openWorldHint: false,
|
|
116
|
+
},
|
|
117
|
+
}, async ({ by, value, exact, direction, max_scrolls, device_id }) => {
|
|
118
|
+
try {
|
|
119
|
+
const { args: deviceArgs, serial } = await resolveDeviceArgs(device_id);
|
|
120
|
+
const selector = by === "resource_id" ? { by, value } : { by, value, exact };
|
|
121
|
+
const { width, height } = await getScreenSize(deviceArgs);
|
|
122
|
+
const result = await scrollAndSearch(async () => (await fetchUiDump(deviceArgs)).nodes, async () => {
|
|
123
|
+
await runAdb(buildSwipeArgs(deviceArgs, direction, width, height));
|
|
124
|
+
}, selector, { maxScrolls: max_scrolls });
|
|
125
|
+
const msg = result.found
|
|
126
|
+
? `Found ${by}="${value}" after ${result.scrolls_performed} scroll${result.scrolls_performed === 1 ? "" : "s"} on ${serial}.`
|
|
127
|
+
: `Did not find ${by}="${value}" after ${result.scrolls_performed} scrolls on ${serial}.`;
|
|
128
|
+
return {
|
|
129
|
+
content: [{ type: "text", text: msg }],
|
|
130
|
+
structuredContent: { device_id: serial, ...result },
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
return toolErrorFrom(err);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=scroll-until-visible.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scroll-until-visible.js","sourceRoot":"","sources":["../../src/tools/scroll-until-visible.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
interface PendingSession {
|
|
3
|
+
password: string;
|
|
4
|
+
expiresAt: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function consumeSession(serviceName: string): PendingSession | undefined;
|
|
7
|
+
export declare function __pendingForTest(): Map<string, PendingSession>;
|
|
8
|
+
export declare function clearPending(): void;
|
|
9
|
+
export declare function registerSessionForTest(serviceName: string, password: string, expiresAt: number): void;
|
|
10
|
+
export declare function registerStartPairQr(server: McpServer): void;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { buildQrText } from "../qrText.js";
|
|
4
|
+
import { toolErrorFrom } from "../toolResult.js";
|
|
5
|
+
const pending = new Map();
|
|
6
|
+
function pruneExpired() {
|
|
7
|
+
const now = Date.now();
|
|
8
|
+
for (const [k, v] of pending.entries()) {
|
|
9
|
+
if (v.expiresAt <= now)
|
|
10
|
+
pending.delete(k);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function consumeSession(serviceName) {
|
|
14
|
+
pruneExpired();
|
|
15
|
+
const s = pending.get(serviceName);
|
|
16
|
+
if (!s)
|
|
17
|
+
return undefined;
|
|
18
|
+
pending.delete(serviceName);
|
|
19
|
+
return s;
|
|
20
|
+
}
|
|
21
|
+
export function __pendingForTest() {
|
|
22
|
+
pruneExpired();
|
|
23
|
+
return pending;
|
|
24
|
+
}
|
|
25
|
+
export function clearPending() {
|
|
26
|
+
pending.clear();
|
|
27
|
+
}
|
|
28
|
+
export function registerSessionForTest(serviceName, password, expiresAt) {
|
|
29
|
+
pending.set(serviceName, { password, expiresAt });
|
|
30
|
+
}
|
|
31
|
+
const DEFAULT_EXPIRES_IN_MS = 60_000;
|
|
32
|
+
export function registerStartPairQr(server) {
|
|
33
|
+
server.registerTool("start_pair_qr", {
|
|
34
|
+
title: "Generate a wireless-ADB pairing QR",
|
|
35
|
+
description: "Generate a QR-code payload (WIFI:T:ADB;S:<name>;P:<password>;;) for Android's " +
|
|
36
|
+
"Wireless debugging → Pair device with QR code flow. Returns the QR text plus the " +
|
|
37
|
+
"service_name to pass to wait_for_pair. The QR expires in 60 seconds.",
|
|
38
|
+
inputSchema: {},
|
|
39
|
+
outputSchema: {
|
|
40
|
+
qr_text: z.string(),
|
|
41
|
+
service_name: z.string(),
|
|
42
|
+
expires_at_ms: z.number().int(),
|
|
43
|
+
},
|
|
44
|
+
annotations: { readOnlyHint: false, openWorldHint: false },
|
|
45
|
+
}, async () => {
|
|
46
|
+
try {
|
|
47
|
+
const serviceName = `fling-debug-${randomBytes(2).toString("hex")}`;
|
|
48
|
+
const password = randomBytes(12).toString("base64").replace(/[=+/]/g, "");
|
|
49
|
+
const expiresAt = Date.now() + DEFAULT_EXPIRES_IN_MS;
|
|
50
|
+
const qrText = buildQrText({ serviceName, password });
|
|
51
|
+
pending.set(serviceName, { password, expiresAt });
|
|
52
|
+
return {
|
|
53
|
+
content: [{ type: "text", text: `QR payload: ${qrText}\nService name: ${serviceName}\nExpires in ~60s.` }],
|
|
54
|
+
structuredContent: { qr_text: qrText, service_name: serviceName, expires_at_ms: expiresAt },
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
return toolErrorFrom(err);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=start-pair-qr.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"start-pair-qr.js","sourceRoot":"","sources":["../../src/tools/start-pair-qr.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { runAdb } from "../adb.js";
|
|
3
|
+
import { loadFlingConfig } from "../config.js";
|
|
4
|
+
import { resolveDeviceArgs } from "../devices.js";
|
|
5
|
+
import { FlingError } from "../errors.js";
|
|
6
|
+
import { deviceIdInput } from "../schemas.js";
|
|
7
|
+
import { toolErrorFrom } from "../toolResult.js";
|
|
8
|
+
import { validatePackage } from "./launch-app.js";
|
|
9
|
+
export function registerStopApp(server) {
|
|
10
|
+
server.registerTool("stop_app", {
|
|
11
|
+
title: "Force-stop an Android app",
|
|
12
|
+
description: "Issue `adb shell am force-stop <package>` to terminate every process of the named app. " +
|
|
13
|
+
"Force-stopping an already-stopped app is a no-op (idempotent). " +
|
|
14
|
+
"When `package_name` is omitted, falls back to fling.config.json packageName.",
|
|
15
|
+
inputSchema: {
|
|
16
|
+
package_name: z
|
|
17
|
+
.string()
|
|
18
|
+
.min(1)
|
|
19
|
+
.optional()
|
|
20
|
+
.describe("App package name. Optional if set in fling.config.json."),
|
|
21
|
+
device_id: deviceIdInput,
|
|
22
|
+
cwd: z
|
|
23
|
+
.string()
|
|
24
|
+
.optional()
|
|
25
|
+
.describe("Starting directory for config lookup. Defaults to the MCP server's cwd."),
|
|
26
|
+
},
|
|
27
|
+
outputSchema: {
|
|
28
|
+
device_id: z.string(),
|
|
29
|
+
package_name: z.string(),
|
|
30
|
+
message: z.string(),
|
|
31
|
+
},
|
|
32
|
+
annotations: {
|
|
33
|
+
readOnlyHint: false,
|
|
34
|
+
destructiveHint: true,
|
|
35
|
+
idempotentHint: true,
|
|
36
|
+
openWorldHint: false,
|
|
37
|
+
},
|
|
38
|
+
}, async ({ package_name, device_id, cwd }) => {
|
|
39
|
+
try {
|
|
40
|
+
const loaded = await loadFlingConfig(cwd ?? process.cwd());
|
|
41
|
+
const pkg = package_name ?? loaded.config.packageName;
|
|
42
|
+
if (!pkg) {
|
|
43
|
+
throw new FlingError("CONFIG_MISSING", "stop_app needs a package_name (argument or config.packageName).");
|
|
44
|
+
}
|
|
45
|
+
validatePackage(pkg);
|
|
46
|
+
const { args: deviceArgs, serial } = await resolveDeviceArgs(device_id);
|
|
47
|
+
await runAdb([...deviceArgs, "shell", "am", "force-stop", pkg]);
|
|
48
|
+
// `am force-stop` exits 0 and prints nothing whether the app was
|
|
49
|
+
// running, stopped, or never installed. The success message reflects
|
|
50
|
+
// that — we sent the signal; we can't claim a process was actually
|
|
51
|
+
// killed without a separate `pidof` check.
|
|
52
|
+
const text = `Force-stop signal sent to ${pkg} on ${serial} (no-op if the app wasn't running or isn't installed).`;
|
|
53
|
+
return {
|
|
54
|
+
content: [{ type: "text", text }],
|
|
55
|
+
structuredContent: { device_id: serial, package_name: pkg, message: text },
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
return toolErrorFrom(err);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=stop-app.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stop-app.js","sourceRoot":"","sources":["../../src/tools/stop-app.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACnC,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAElD,MAAM,UAAU,eAAe,CAAC,MAAiB;IAC/C,MAAM,CAAC,YAAY,CACjB,UAAU,EACV;QACE,KAAK,EAAE,2BAA2B;QAClC,WAAW,EACT,yFAAyF;YACzF,iEAAiE;YACjE,8EAA8E;QAChF,WAAW,EAAE;YACX,YAAY,EAAE,CAAC;iBACZ,MAAM,EAAE;iBACR,GAAG,CAAC,CAAC,CAAC;iBACN,QAAQ,EAAE;iBACV,QAAQ,CAAC,yDAAyD,CAAC;YACtE,SAAS,EAAE,aAAa;YACxB,GAAG,EAAE,CAAC;iBACH,MAAM,EAAE;iBACR,QAAQ,EAAE;iBACV,QAAQ,CAAC,yEAAyE,CAAC;SACvF;QACD,YAAY,EAAE;YACZ,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;YACrB,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE;YACxB,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;SACpB;QACD,WAAW,EAAE;YACX,YAAY,EAAE,KAAK;YACnB,eAAe,EAAE,IAAI;YACrB,cAAc,EAAE,IAAI;YACpB,aAAa,EAAE,KAAK;SACrB;KACF,EACD,KAAK,EAAE,EAAE,YAAY,EAAE,SAAS,EAAE,GAAG,EAAE,EAAE,EAAE;QACzC,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;YAC3D,MAAM,GAAG,GAAG,YAAY,IAAI,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC;YACtD,IAAI,CAAC,GAAG,EAAE,CAAC;gBACT,MAAM,IAAI,UAAU,CAClB,gBAAgB,EAChB,iEAAiE,CAClE,CAAC;YACJ,CAAC;YACD,eAAe,CAAC,GAAG,CAAC,CAAC;YAErB,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,MAAM,iBAAiB,CAAC,SAAS,CAAC,CAAC;YACxE,MAAM,MAAM,CAAC,CAAC,GAAG,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,GAAG,CAAC,CAAC,CAAC;YAEhE,iEAAiE;YACjE,qEAAqE;YACrE,mEAAmE;YACnE,2CAA2C;YAC3C,MAAM,IAAI,GAAG,6BAA6B,GAAG,OAAO,MAAM,wDAAwD,CAAC;YACnH,OAAO;gBACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,CAAC;gBAC1C,iBAAiB,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE;aAC3E,CAAC;QACJ,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,aAAa,CAAC,GAAG,CAAC,CAAC;QAC5B,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { type UiNode } from "../uiDump.js";
|
|
3
|
+
export interface ContentDescTapTarget {
|
|
4
|
+
tap_x: number;
|
|
5
|
+
tap_y: number;
|
|
6
|
+
bounds: {
|
|
7
|
+
x1: number;
|
|
8
|
+
y1: number;
|
|
9
|
+
x2: number;
|
|
10
|
+
y2: number;
|
|
11
|
+
};
|
|
12
|
+
matched_content_desc: string;
|
|
13
|
+
fell_back_to_match: boolean;
|
|
14
|
+
candidates_count: number;
|
|
15
|
+
}
|
|
16
|
+
export declare function selectTapByContentDesc(nodes: UiNode[], contentDesc: string, exact: boolean): ContentDescTapTarget | null;
|
|
17
|
+
export declare function registerTapByContentDesc(server: McpServer): void;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { fetchUiDump } from "../uiDump.js";
|
|
3
|
+
import { findNodes, pickBest } from "../uiSelector.js";
|
|
4
|
+
import { runAdb } from "../adb.js";
|
|
5
|
+
import { resolveDeviceArgs } from "../devices.js";
|
|
6
|
+
import { FlingError } from "../errors.js";
|
|
7
|
+
import { deviceIdInput } from "../schemas.js";
|
|
8
|
+
import { toolErrorFrom } from "../toolResult.js";
|
|
9
|
+
import { buildTapArgs } from "./tap-by-text.js";
|
|
10
|
+
export function selectTapByContentDesc(nodes, contentDesc, exact) {
|
|
11
|
+
const matches = findNodes(nodes, {
|
|
12
|
+
by: "content_desc",
|
|
13
|
+
value: contentDesc,
|
|
14
|
+
exact,
|
|
15
|
+
});
|
|
16
|
+
if (matches.length === 0)
|
|
17
|
+
return null;
|
|
18
|
+
const best = pickBest(matches, nodes);
|
|
19
|
+
if (!best)
|
|
20
|
+
return null;
|
|
21
|
+
return {
|
|
22
|
+
tap_x: best.node.center.x,
|
|
23
|
+
tap_y: best.node.center.y,
|
|
24
|
+
bounds: best.node.bounds,
|
|
25
|
+
matched_content_desc: matches[0].content_desc,
|
|
26
|
+
fell_back_to_match: best.fellBackToMatch,
|
|
27
|
+
candidates_count: matches.length,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function registerTapByContentDesc(server) {
|
|
31
|
+
server.registerTool("tap_by_content_desc", {
|
|
32
|
+
title: "Tap a UI element by accessibility label (content-desc)",
|
|
33
|
+
description: "Dump UI, find the first node whose content-desc matches, and tap the " +
|
|
34
|
+
"smallest clickable container whose bounds contain it. Substring matching " +
|
|
35
|
+
"is case-sensitive by default; pass exact:true for strict equality. " +
|
|
36
|
+
"Used for icon buttons with no visible text (Search, Back, Account).",
|
|
37
|
+
inputSchema: {
|
|
38
|
+
content_desc: z
|
|
39
|
+
.string()
|
|
40
|
+
.min(1)
|
|
41
|
+
.describe("Accessibility label to match."),
|
|
42
|
+
exact: z
|
|
43
|
+
.boolean()
|
|
44
|
+
.optional()
|
|
45
|
+
.default(false)
|
|
46
|
+
.describe("Require equality instead of case-sensitive substring."),
|
|
47
|
+
device_id: deviceIdInput,
|
|
48
|
+
},
|
|
49
|
+
outputSchema: {
|
|
50
|
+
device_id: z.string(),
|
|
51
|
+
matched_content_desc: z.string(),
|
|
52
|
+
bounds: z.object({
|
|
53
|
+
x1: z.number(),
|
|
54
|
+
y1: z.number(),
|
|
55
|
+
x2: z.number(),
|
|
56
|
+
y2: z.number(),
|
|
57
|
+
}),
|
|
58
|
+
tap_x: z.number(),
|
|
59
|
+
tap_y: z.number(),
|
|
60
|
+
candidates_count: z.number().int().nonnegative(),
|
|
61
|
+
fell_back_to_match: z.boolean(),
|
|
62
|
+
},
|
|
63
|
+
annotations: {
|
|
64
|
+
readOnlyHint: false,
|
|
65
|
+
destructiveHint: false,
|
|
66
|
+
idempotentHint: false,
|
|
67
|
+
openWorldHint: false,
|
|
68
|
+
},
|
|
69
|
+
}, async ({ content_desc, exact, device_id }) => {
|
|
70
|
+
try {
|
|
71
|
+
const { args: deviceArgs, serial } = await resolveDeviceArgs(device_id);
|
|
72
|
+
const { nodes } = await fetchUiDump(deviceArgs);
|
|
73
|
+
const target = selectTapByContentDesc(nodes, content_desc, exact);
|
|
74
|
+
if (!target) {
|
|
75
|
+
throw new FlingError("UI_ELEMENT_NOT_FOUND", `No clickable element found for content_desc="${content_desc}" on ${serial}.`);
|
|
76
|
+
}
|
|
77
|
+
await runAdb(buildTapArgs(deviceArgs, target.tap_x, target.tap_y));
|
|
78
|
+
const msg = `Tapped content_desc="${target.matched_content_desc}" at (${target.tap_x}, ${target.tap_y}) on ${serial}.`;
|
|
79
|
+
return {
|
|
80
|
+
content: [{ type: "text", text: msg }],
|
|
81
|
+
structuredContent: {
|
|
82
|
+
device_id: serial,
|
|
83
|
+
matched_content_desc: target.matched_content_desc,
|
|
84
|
+
bounds: target.bounds,
|
|
85
|
+
tap_x: target.tap_x,
|
|
86
|
+
tap_y: target.tap_y,
|
|
87
|
+
candidates_count: target.candidates_count,
|
|
88
|
+
fell_back_to_match: target.fell_back_to_match,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
return toolErrorFrom(err);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=tap-by-content-desc.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tap-by-content-desc.js","sourceRoot":"","sources":["../../src/tools/tap-by-content-desc.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { type UiNode } from "../uiDump.js";
|
|
3
|
+
export interface ResourceIdTapTarget {
|
|
4
|
+
tap_x: number;
|
|
5
|
+
tap_y: number;
|
|
6
|
+
bounds: {
|
|
7
|
+
x1: number;
|
|
8
|
+
y1: number;
|
|
9
|
+
x2: number;
|
|
10
|
+
y2: number;
|
|
11
|
+
};
|
|
12
|
+
matched_resource_id: string;
|
|
13
|
+
fell_back_to_match: boolean;
|
|
14
|
+
candidates_count: number;
|
|
15
|
+
}
|
|
16
|
+
export declare function selectTapByResourceId(nodes: UiNode[], resourceId: string): ResourceIdTapTarget | null;
|
|
17
|
+
export declare function registerTapByResourceId(server: McpServer): void;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { fetchUiDump } from "../uiDump.js";
|
|
3
|
+
import { findNodes, pickBest } from "../uiSelector.js";
|
|
4
|
+
import { runAdb } from "../adb.js";
|
|
5
|
+
import { resolveDeviceArgs } from "../devices.js";
|
|
6
|
+
import { FlingError } from "../errors.js";
|
|
7
|
+
import { deviceIdInput } from "../schemas.js";
|
|
8
|
+
import { toolErrorFrom } from "../toolResult.js";
|
|
9
|
+
import { buildTapArgs } from "./tap-by-text.js";
|
|
10
|
+
export function selectTapByResourceId(nodes, resourceId) {
|
|
11
|
+
const matches = findNodes(nodes, { by: "resource_id", value: resourceId });
|
|
12
|
+
if (matches.length === 0)
|
|
13
|
+
return null;
|
|
14
|
+
const best = pickBest(matches, nodes);
|
|
15
|
+
if (!best)
|
|
16
|
+
return null;
|
|
17
|
+
return {
|
|
18
|
+
tap_x: best.node.center.x,
|
|
19
|
+
tap_y: best.node.center.y,
|
|
20
|
+
bounds: best.node.bounds,
|
|
21
|
+
matched_resource_id: matches[0].resource_id,
|
|
22
|
+
fell_back_to_match: best.fellBackToMatch,
|
|
23
|
+
candidates_count: matches.length,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function registerTapByResourceId(server) {
|
|
27
|
+
server.registerTool("tap_by_resource_id", {
|
|
28
|
+
title: "Tap a UI element by its Android resource ID",
|
|
29
|
+
description: "Dump UI, find the node with the exact resource_id, and tap the smallest " +
|
|
30
|
+
"clickable container whose bounds contain it. Resource IDs are unique by " +
|
|
31
|
+
"construction — typically 0 or 1 match. Most robust selector when you " +
|
|
32
|
+
"know the id (survives localization changes). No scroll_into_view; " +
|
|
33
|
+
"compose with scroll_until_visible if the element is off-screen.",
|
|
34
|
+
inputSchema: {
|
|
35
|
+
resource_id: z
|
|
36
|
+
.string()
|
|
37
|
+
.min(1)
|
|
38
|
+
.describe("Full Android resource ID (e.g. com.google.android.apps.photos:id/searchbar)."),
|
|
39
|
+
device_id: deviceIdInput,
|
|
40
|
+
},
|
|
41
|
+
outputSchema: {
|
|
42
|
+
device_id: z.string(),
|
|
43
|
+
matched_resource_id: z.string(),
|
|
44
|
+
bounds: z.object({
|
|
45
|
+
x1: z.number(),
|
|
46
|
+
y1: z.number(),
|
|
47
|
+
x2: z.number(),
|
|
48
|
+
y2: z.number(),
|
|
49
|
+
}),
|
|
50
|
+
tap_x: z.number(),
|
|
51
|
+
tap_y: z.number(),
|
|
52
|
+
candidates_count: z.number().int().nonnegative(),
|
|
53
|
+
fell_back_to_match: z.boolean(),
|
|
54
|
+
},
|
|
55
|
+
annotations: {
|
|
56
|
+
readOnlyHint: false,
|
|
57
|
+
destructiveHint: false,
|
|
58
|
+
idempotentHint: false,
|
|
59
|
+
openWorldHint: false,
|
|
60
|
+
},
|
|
61
|
+
}, async ({ resource_id, device_id }) => {
|
|
62
|
+
try {
|
|
63
|
+
const { args: deviceArgs, serial } = await resolveDeviceArgs(device_id);
|
|
64
|
+
const { nodes } = await fetchUiDump(deviceArgs);
|
|
65
|
+
const target = selectTapByResourceId(nodes, resource_id);
|
|
66
|
+
if (!target) {
|
|
67
|
+
throw new FlingError("UI_ELEMENT_NOT_FOUND", `No clickable element found for resource_id="${resource_id}" on ${serial}.`);
|
|
68
|
+
}
|
|
69
|
+
await runAdb(buildTapArgs(deviceArgs, target.tap_x, target.tap_y));
|
|
70
|
+
const msg = `Tapped resource_id="${target.matched_resource_id}" at (${target.tap_x}, ${target.tap_y}) on ${serial}.`;
|
|
71
|
+
return {
|
|
72
|
+
content: [{ type: "text", text: msg }],
|
|
73
|
+
structuredContent: {
|
|
74
|
+
device_id: serial,
|
|
75
|
+
matched_resource_id: target.matched_resource_id,
|
|
76
|
+
bounds: target.bounds,
|
|
77
|
+
tap_x: target.tap_x,
|
|
78
|
+
tap_y: target.tap_y,
|
|
79
|
+
candidates_count: target.candidates_count,
|
|
80
|
+
fell_back_to_match: target.fell_back_to_match,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
return toolErrorFrom(err);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=tap-by-resource-id.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tap-by-resource-id.js","sourceRoot":"","sources":["../../src/tools/tap-by-resource-id.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { type UiNode } from "../uiDump.js";
|
|
3
|
+
export declare function buildTapArgs(deviceArgs: string[], x: number, y: number): string[];
|
|
4
|
+
/**
|
|
5
|
+
* Build the adb argv for a long-press at (x, y) held for `durationMs`. A
|
|
6
|
+
* long-press is a zero-length swipe — start and end coordinates are
|
|
7
|
+
* identical; only the duration matters.
|
|
8
|
+
*/
|
|
9
|
+
export declare function buildLongPressArgs(deviceArgs: string[], x: number, y: number, durationMs: number): string[];
|
|
10
|
+
export interface TapDispatchInput {
|
|
11
|
+
deviceArgs: string[];
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
holdMs?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Pick between a plain tap and a long-press based on holdMs. Returns the
|
|
18
|
+
* adb argv to run. Extracted so the dispatch decision is unit-testable
|
|
19
|
+
* without driving a real device.
|
|
20
|
+
*/
|
|
21
|
+
export declare function tapDispatchArgs(input: TapDispatchInput): string[];
|
|
22
|
+
export interface TapTarget {
|
|
23
|
+
tap_x: number;
|
|
24
|
+
tap_y: number;
|
|
25
|
+
bounds: {
|
|
26
|
+
x1: number;
|
|
27
|
+
y1: number;
|
|
28
|
+
x2: number;
|
|
29
|
+
y2: number;
|
|
30
|
+
};
|
|
31
|
+
matched_text: string;
|
|
32
|
+
fell_back_to_match: boolean;
|
|
33
|
+
candidates_count: number;
|
|
34
|
+
}
|
|
35
|
+
export declare function selectTapTarget(nodes: UiNode[], selector: {
|
|
36
|
+
by: "text";
|
|
37
|
+
value: string;
|
|
38
|
+
exact?: boolean;
|
|
39
|
+
}): TapTarget | null;
|
|
40
|
+
export declare function registerTapByText(server: McpServer): void;
|