@rvaim/deveco-mobile-mcp 0.1.0-rvaim.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.
@@ -0,0 +1,155 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.isScalingAvailable = exports.isImageMagickInstalled = exports.isSipsInstalled = exports.Image = exports.ImageTransformer = void 0;
7
+ const child_process_1 = require("child_process");
8
+ const node_os_1 = __importDefault(require("node:os"));
9
+ const node_fs_1 = __importDefault(require("node:fs"));
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const logger_1 = require("./logger");
12
+ const DEFAULT_JPEG_QUALITY = 75;
13
+ class ImageTransformer {
14
+ buffer;
15
+ newWidth = 0;
16
+ newFormat = "png";
17
+ jpegOptions = { quality: DEFAULT_JPEG_QUALITY };
18
+ constructor(buffer) {
19
+ this.buffer = buffer;
20
+ }
21
+ resize(width) {
22
+ this.newWidth = width;
23
+ return this;
24
+ }
25
+ jpeg(options) {
26
+ this.newFormat = "jpg";
27
+ this.jpegOptions = options;
28
+ return this;
29
+ }
30
+ png() {
31
+ this.newFormat = "png";
32
+ return this;
33
+ }
34
+ toBuffer() {
35
+ if ((0, exports.isSipsInstalled)()) {
36
+ try {
37
+ return this.toBufferWithSips();
38
+ }
39
+ catch (error) {
40
+ (0, logger_1.trace)(`Sips failed, falling back to ImageMagick: ${error}`);
41
+ }
42
+ }
43
+ try {
44
+ return this.toBufferWithImageMagick();
45
+ }
46
+ catch (error) {
47
+ (0, logger_1.trace)(`ImageMagick failed: ${error}`);
48
+ throw new Error("Image scaling unavailable (requires Sips or ImageMagick).");
49
+ }
50
+ }
51
+ qualityToSips(q) {
52
+ if (q >= 90) {
53
+ return "best";
54
+ }
55
+ if (q >= 75) {
56
+ return "high";
57
+ }
58
+ if (q >= 50) {
59
+ return "normal";
60
+ }
61
+ return "low";
62
+ }
63
+ toBufferWithSips() {
64
+ const tempDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "image-"));
65
+ const inputFile = node_path_1.default.join(tempDir, "input");
66
+ const outputFile = node_path_1.default.join(tempDir, `output.${this.newFormat === "jpg" ? "jpg" : "png"}`);
67
+ try {
68
+ node_fs_1.default.writeFileSync(inputFile, this.buffer);
69
+ const args = ["-s", "format", this.newFormat === "jpg" ? "jpeg" : "png"];
70
+ if (this.newFormat === "jpg") {
71
+ args.push("-s", "formatOptions", this.qualityToSips(this.jpegOptions.quality));
72
+ }
73
+ args.push("-Z", `${this.newWidth}`);
74
+ args.push("--out", outputFile);
75
+ args.push(inputFile);
76
+ (0, logger_1.trace)(`Running sips command: /usr/bin/sips ${args.join(" ")}`);
77
+ const proc = (0, child_process_1.spawnSync)("/usr/bin/sips", args, {
78
+ maxBuffer: 8 * 1024 * 1024
79
+ });
80
+ if (proc.status !== 0) {
81
+ throw new Error(`Sips failed with status ${proc.status}`);
82
+ }
83
+ const outputBuffer = node_fs_1.default.readFileSync(outputFile);
84
+ (0, logger_1.trace)("Sips returned buffer of size: " + outputBuffer.length);
85
+ return outputBuffer;
86
+ }
87
+ finally {
88
+ try {
89
+ node_fs_1.default.rmSync(tempDir, { recursive: true, force: true });
90
+ }
91
+ catch (error) {
92
+ // Ignore cleanup errors
93
+ }
94
+ }
95
+ }
96
+ toBufferWithImageMagick() {
97
+ const magickArgs = ["-", "-resize", `${this.newWidth}x`, "-quality", `${this.jpegOptions.quality}`, `${this.newFormat}:-`];
98
+ (0, logger_1.trace)(`Running magick command: magick ${magickArgs.join(" ")}`);
99
+ const proc = (0, child_process_1.spawnSync)("magick", magickArgs, {
100
+ maxBuffer: 8 * 1024 * 1024,
101
+ input: this.buffer
102
+ });
103
+ return proc.stdout;
104
+ }
105
+ }
106
+ exports.ImageTransformer = ImageTransformer;
107
+ class Image {
108
+ buffer;
109
+ constructor(buffer) {
110
+ this.buffer = buffer;
111
+ }
112
+ static fromBuffer(buffer) {
113
+ return new Image(buffer);
114
+ }
115
+ resize(width) {
116
+ return new ImageTransformer(this.buffer).resize(width);
117
+ }
118
+ jpeg(options) {
119
+ return new ImageTransformer(this.buffer).jpeg(options);
120
+ }
121
+ }
122
+ exports.Image = Image;
123
+ const isDarwin = () => {
124
+ return node_os_1.default.platform() === "darwin";
125
+ };
126
+ const isSipsInstalled = () => {
127
+ if (!isDarwin()) {
128
+ return false;
129
+ }
130
+ try {
131
+ (0, child_process_1.execFileSync)("/usr/bin/sips", ["--version"]);
132
+ return true;
133
+ }
134
+ catch (error) {
135
+ return false;
136
+ }
137
+ };
138
+ exports.isSipsInstalled = isSipsInstalled;
139
+ const isImageMagickInstalled = () => {
140
+ try {
141
+ return (0, child_process_1.execFileSync)("magick", ["--version"])
142
+ .toString()
143
+ .split("\n")
144
+ .filter(line => line.includes("Version: ImageMagick"))
145
+ .length > 0;
146
+ }
147
+ catch (error) {
148
+ return false;
149
+ }
150
+ };
151
+ exports.isImageMagickInstalled = isImageMagickInstalled;
152
+ const isScalingAvailable = () => {
153
+ return (0, exports.isImageMagickInstalled)() || (0, exports.isSipsInstalled)();
154
+ };
155
+ exports.isScalingAvailable = isScalingAvailable;
package/lib/index.js ADDED
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const sse_js_1 = require("@modelcontextprotocol/sdk/server/sse.js");
8
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
9
+ const server_1 = require("./server");
10
+ const logger_1 = require("./logger");
11
+ const express_1 = __importDefault(require("express"));
12
+ const commander_1 = require("commander");
13
+ const startSseServer = async (port) => {
14
+ const app = (0, express_1.default)();
15
+ const server = (0, server_1.createMcpServer)();
16
+ let transport = null;
17
+ app.post("/mcp", (req, res) => {
18
+ if (transport) {
19
+ transport.handlePostMessage(req, res);
20
+ }
21
+ });
22
+ app.get("/mcp", (req, res) => {
23
+ if (transport) {
24
+ transport.close();
25
+ }
26
+ transport = new sse_js_1.SSEServerTransport("/mcp", res);
27
+ server.connect(transport);
28
+ });
29
+ app.listen(port, () => {
30
+ (0, logger_1.error)(`mobile-mcp ${(0, server_1.getAgentVersion)()} sse server listening on http://localhost:${port}/mcp`);
31
+ });
32
+ };
33
+ const startStdioServer = async () => {
34
+ try {
35
+ const transport = new stdio_js_1.StdioServerTransport();
36
+ const server = (0, server_1.createMcpServer)();
37
+ await server.connect(transport);
38
+ (0, logger_1.error)("mobile-mcp server running on stdio");
39
+ }
40
+ catch (err) {
41
+ console.error("Fatal error in main():", err);
42
+ (0, logger_1.error)("Fatal error in main(): " + JSON.stringify(err.stack));
43
+ process.exit(1);
44
+ }
45
+ };
46
+ const main = async () => {
47
+ commander_1.program
48
+ .version((0, server_1.getAgentVersion)())
49
+ .option("--port <port>", "Start SSE server on this port")
50
+ .option("--stdio", "Start stdio server (default)")
51
+ .parse(process.argv);
52
+ const options = commander_1.program.opts();
53
+ if (options.port) {
54
+ await startSseServer(+options.port);
55
+ }
56
+ else {
57
+ await startStdioServer();
58
+ }
59
+ };
60
+ main().then();
package/lib/ios.js ADDED
@@ -0,0 +1,282 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.IosManager = exports.IosRobot = void 0;
4
+ const node_net_1 = require("node:net");
5
+ const node_child_process_1 = require("node:child_process");
6
+ const webdriver_agent_1 = require("./webdriver-agent");
7
+ const robot_1 = require("./robot");
8
+ const utils_1 = require("./utils");
9
+ const WDA_PORT = 8100;
10
+ const IOS_TUNNEL_PORT = 60105;
11
+ const getGoIosPath = () => {
12
+ if (process.env.GO_IOS_PATH) {
13
+ return process.env.GO_IOS_PATH;
14
+ }
15
+ // fallback to go-ios in PATH via `npm install -g go-ios`
16
+ return "ios";
17
+ };
18
+ class IosRobot {
19
+ deviceId;
20
+ constructor(deviceId) {
21
+ this.deviceId = deviceId;
22
+ }
23
+ isListeningOnPort(port) {
24
+ return new Promise((resolve, reject) => {
25
+ const client = new node_net_1.Socket();
26
+ client.connect(port, "localhost", () => {
27
+ client.destroy();
28
+ resolve(true);
29
+ });
30
+ client.on("error", (err) => {
31
+ resolve(false);
32
+ });
33
+ });
34
+ }
35
+ async isTunnelRunning() {
36
+ return await this.isListeningOnPort(IOS_TUNNEL_PORT);
37
+ }
38
+ async isWdaForwardRunning() {
39
+ return await this.isListeningOnPort(WDA_PORT);
40
+ }
41
+ async assertTunnelRunning() {
42
+ if (await this.isTunnelRequired()) {
43
+ if (!(await this.isTunnelRunning())) {
44
+ throw new robot_1.ActionableError("iOS tunnel is not running, please see https://github.com/mobile-next/mobile-mcp/wiki/");
45
+ }
46
+ }
47
+ }
48
+ async wda() {
49
+ await this.assertTunnelRunning();
50
+ if (!(await this.isWdaForwardRunning())) {
51
+ throw new robot_1.ActionableError("Port forwarding to WebDriverAgent is not running (tunnel okay), please see https://github.com/mobile-next/mobile-mcp/wiki/");
52
+ }
53
+ const wda = new webdriver_agent_1.WebDriverAgent("localhost", WDA_PORT);
54
+ if (!(await wda.isRunning())) {
55
+ throw new robot_1.ActionableError("WebDriverAgent is not running on device (tunnel okay, port forwarding okay), please see https://github.com/mobile-next/mobile-mcp/wiki/");
56
+ }
57
+ return wda;
58
+ }
59
+ async ios(...args) {
60
+ return (0, node_child_process_1.execFileSync)(getGoIosPath(), ["--udid", this.deviceId, ...args], {}).toString();
61
+ }
62
+ async getIosVersion() {
63
+ const output = await this.ios("info");
64
+ const json = JSON.parse(output);
65
+ return json.ProductVersion;
66
+ }
67
+ async isTunnelRequired() {
68
+ const version = await this.getIosVersion();
69
+ const args = version.split(".");
70
+ return parseInt(args[0], 10) >= 17;
71
+ }
72
+ async getScreenSize() {
73
+ const wda = await this.wda();
74
+ return await wda.getScreenSize();
75
+ }
76
+ async swipe(direction) {
77
+ const wda = await this.wda();
78
+ await wda.swipe(direction);
79
+ }
80
+ async swipeFromCoordinate(x, y, direction, distance) {
81
+ const wda = await this.wda();
82
+ await wda.swipeFromCoordinate(x, y, direction, distance);
83
+ }
84
+ async listApps() {
85
+ await this.assertTunnelRunning();
86
+ const output = await this.ios("apps", "--all", "--list");
87
+ return output
88
+ .split("\n")
89
+ .map(line => {
90
+ const [packageName, appName] = line.split(" ");
91
+ return {
92
+ packageName,
93
+ appName,
94
+ };
95
+ });
96
+ }
97
+ async launchApp(packageName, locale) {
98
+ (0, utils_1.validatePackageName)(packageName);
99
+ await this.assertTunnelRunning();
100
+ const args = ["launch", packageName];
101
+ if (locale) {
102
+ (0, utils_1.validateLocale)(locale);
103
+ const locales = locale.split(",").map(l => l.trim());
104
+ args.push("-AppleLanguages", `(${locales.join(", ")})`);
105
+ args.push("-AppleLocale", locales[0]);
106
+ }
107
+ await this.ios(...args);
108
+ }
109
+ async terminateApp(packageName) {
110
+ (0, utils_1.validatePackageName)(packageName);
111
+ await this.assertTunnelRunning();
112
+ await this.ios("kill", packageName);
113
+ }
114
+ async installApp(path) {
115
+ await this.assertTunnelRunning();
116
+ try {
117
+ await this.ios("install", "--path", path);
118
+ }
119
+ catch (error) {
120
+ const stdout = error.stdout ? error.stdout.toString() : "";
121
+ const stderr = error.stderr ? error.stderr.toString() : "";
122
+ const output = (stdout + stderr).trim();
123
+ throw new robot_1.ActionableError(output || error.message);
124
+ }
125
+ }
126
+ async uninstallApp(bundleId) {
127
+ await this.assertTunnelRunning();
128
+ try {
129
+ await this.ios("uninstall", "--bundleid", bundleId);
130
+ }
131
+ catch (error) {
132
+ const stdout = error.stdout ? error.stdout.toString() : "";
133
+ const stderr = error.stderr ? error.stderr.toString() : "";
134
+ const output = (stdout + stderr).trim();
135
+ throw new robot_1.ActionableError(output || error.message);
136
+ }
137
+ }
138
+ async openUrl(url) {
139
+ const wda = await this.wda();
140
+ await wda.openUrl(url);
141
+ }
142
+ async sendKeys(text) {
143
+ const wda = await this.wda();
144
+ await wda.sendKeys(text);
145
+ }
146
+ async pressButton(button) {
147
+ const wda = await this.wda();
148
+ await wda.pressButton(button);
149
+ }
150
+ async tap(x, y) {
151
+ const wda = await this.wda();
152
+ await wda.tap(x, y);
153
+ }
154
+ async doubleTap(x, y) {
155
+ const wda = await this.wda();
156
+ await wda.doubleTap(x, y);
157
+ }
158
+ async longPress(x, y, duration) {
159
+ const wda = await this.wda();
160
+ await wda.longPress(x, y, duration);
161
+ }
162
+ async getElementsOnScreen() {
163
+ const wda = await this.wda();
164
+ return await wda.getElementsOnScreen();
165
+ }
166
+ async getScreenshot() {
167
+ const wda = await this.wda();
168
+ return await wda.getScreenshot();
169
+ /* alternative:
170
+ await this.assertTunnelRunning();
171
+ const tmpFilename = path.join(tmpdir(), `screenshot-${randomBytes(8).toString("hex")}.png`);
172
+ await this.ios("screenshot", "--output", tmpFilename);
173
+ const buffer = readFileSync(tmpFilename);
174
+ unlinkSync(tmpFilename);
175
+ return buffer;
176
+ */
177
+ }
178
+ async setOrientation(orientation) {
179
+ const wda = await this.wda();
180
+ await wda.setOrientation(orientation);
181
+ }
182
+ async getOrientation() {
183
+ const wda = await this.wda();
184
+ return await wda.getOrientation();
185
+ }
186
+ /**
187
+ * 获取设备日志 (iOS syslog)
188
+ * @param options - 可选的过滤选项
189
+ * @param options.bundleName - 按日志标签/进程名过滤
190
+ * @param options.pid - 按进程 ID 过滤
191
+ * @param options.level - 最低日志级别 (Default/Info/Debug/Error/Fault)
192
+ * @param options.lines - 获取最近 N 行日志 (默认: 100)
193
+ * @param options.clear - 获取前清除日志缓冲区
194
+ */
195
+ async getDeviceLogs(options) {
196
+ // iOS 使用 go-ios 的 syslog 命令获取日志
197
+ // 注意:go-ios 的 syslog 功能有限,这里提供基本实现
198
+ try {
199
+ const args = ["syslog", "--udid", this.deviceId];
200
+ // 添加行数限制 (go-ios 可能不支持,这里作为备用)
201
+ const lines = options?.lines || 100;
202
+ // 获取日志
203
+ const output = (0, node_child_process_1.execFileSync)(getGoIosPath(), args, {
204
+ maxBuffer: 1024 * 1024 * 8,
205
+ timeout: 10000,
206
+ }).toString();
207
+ // 按行分割并过滤
208
+ let lines_array = output.split("\n");
209
+ // 按标签过滤
210
+ if (options?.bundleName) {
211
+ lines_array = lines_array.filter(line => line.includes(options.bundleName));
212
+ }
213
+ // 按 PID 过滤
214
+ if (options?.pid) {
215
+ lines_array = lines_array.filter(line => line.includes(`[${options.pid}]`));
216
+ }
217
+ // 按级别过滤
218
+ if (options?.level) {
219
+ lines_array = lines_array.filter(line => line.toLowerCase().includes(options.level.toLowerCase()));
220
+ }
221
+ // 限制行数
222
+ return lines_array.slice(-lines).join("\n");
223
+ }
224
+ catch (error) {
225
+ throw new robot_1.ActionableError(`Failed to get device logs: ${error.message}`);
226
+ }
227
+ }
228
+ }
229
+ exports.IosRobot = IosRobot;
230
+ class IosManager {
231
+ isGoIosInstalled() {
232
+ try {
233
+ const output = (0, node_child_process_1.execFileSync)(getGoIosPath(), ["version"], { stdio: ["pipe", "pipe", "ignore"] }).toString();
234
+ const json = JSON.parse(output);
235
+ return json.version !== undefined && (json.version.startsWith("v") || json.version === "local-build");
236
+ }
237
+ catch (error) {
238
+ return false;
239
+ }
240
+ }
241
+ getDeviceName(deviceId) {
242
+ const output = (0, node_child_process_1.execFileSync)(getGoIosPath(), ["info", "--udid", deviceId]).toString();
243
+ const json = JSON.parse(output);
244
+ return json.DeviceName;
245
+ }
246
+ getDeviceInfo(deviceId) {
247
+ const output = (0, node_child_process_1.execFileSync)(getGoIosPath(), ["info", "--udid", deviceId]).toString();
248
+ const json = JSON.parse(output);
249
+ return json;
250
+ }
251
+ listDevices() {
252
+ if (!this.isGoIosInstalled()) {
253
+ console.error("go-ios is not installed, no physical iOS devices can be detected");
254
+ return [];
255
+ }
256
+ const output = (0, node_child_process_1.execFileSync)(getGoIosPath(), ["list"]).toString();
257
+ const json = JSON.parse(output);
258
+ const devices = json.deviceList.map(device => ({
259
+ deviceId: device,
260
+ deviceName: this.getDeviceName(device),
261
+ }));
262
+ return devices;
263
+ }
264
+ listDevicesWithDetails() {
265
+ if (!this.isGoIosInstalled()) {
266
+ console.error("go-ios is not installed, no physical iOS devices can be detected");
267
+ return [];
268
+ }
269
+ const output = (0, node_child_process_1.execFileSync)(getGoIosPath(), ["list"]).toString();
270
+ const json = JSON.parse(output);
271
+ const devices = json.deviceList.map(device => {
272
+ const info = this.getDeviceInfo(device);
273
+ return {
274
+ deviceId: device,
275
+ deviceName: info.DeviceName,
276
+ version: info.ProductVersion,
277
+ };
278
+ });
279
+ return devices;
280
+ }
281
+ }
282
+ exports.IosManager = IosManager;