@nbakka/mcp-appium 1.0.28 → 2.0.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/LICENSE +201 -0
- package/README.md +1 -1
- package/lib/android.js +270 -0
- package/lib/image-utils.js +64 -0
- package/lib/index.js +17 -0
- package/lib/ios.js +175 -0
- package/lib/iphone-simulator.js +182 -0
- package/lib/logger.js +22 -0
- package/lib/png.js +19 -0
- package/lib/robot.js +9 -0
- package/lib/server.js +226 -0
- package/lib/webdriver-agent.js +231 -0
- package/package.json +46 -18
- package/bin/mcp-appium.js +0 -57
- package/src/lib/server.js +0 -237
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WebDriverAgent = void 0;
|
|
4
|
+
const robot_1 = require("./robot");
|
|
5
|
+
class WebDriverAgent {
|
|
6
|
+
host;
|
|
7
|
+
port;
|
|
8
|
+
constructor(host, port) {
|
|
9
|
+
this.host = host;
|
|
10
|
+
this.port = port;
|
|
11
|
+
}
|
|
12
|
+
async isRunning() {
|
|
13
|
+
const url = `http://${this.host}:${this.port}/status`;
|
|
14
|
+
try {
|
|
15
|
+
const response = await fetch(url);
|
|
16
|
+
return response.status === 200;
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
console.error(`Failed to connect to WebDriverAgent: ${error}`);
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async createSession() {
|
|
24
|
+
const url = `http://${this.host}:${this.port}/session`;
|
|
25
|
+
const response = await fetch(url, {
|
|
26
|
+
method: "POST",
|
|
27
|
+
headers: {
|
|
28
|
+
"Content-Type": "application/json",
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify({ capabilities: { alwaysMatch: { platformName: "iOS" } } }),
|
|
31
|
+
});
|
|
32
|
+
const json = await response.json();
|
|
33
|
+
return json.value.sessionId;
|
|
34
|
+
}
|
|
35
|
+
async deleteSession(sessionId) {
|
|
36
|
+
const url = `http://${this.host}:${this.port}/session/${sessionId}`;
|
|
37
|
+
const response = await fetch(url, { method: "DELETE" });
|
|
38
|
+
return response.json();
|
|
39
|
+
}
|
|
40
|
+
async withinSession(fn) {
|
|
41
|
+
const sessionId = await this.createSession();
|
|
42
|
+
const url = `http://${this.host}:${this.port}/session/${sessionId}`;
|
|
43
|
+
const result = await fn(url);
|
|
44
|
+
await this.deleteSession(sessionId);
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
async getScreenSize() {
|
|
48
|
+
return this.withinSession(async (sessionUrl) => {
|
|
49
|
+
const url = `${sessionUrl}/wda/screen`;
|
|
50
|
+
const response = await fetch(url);
|
|
51
|
+
const json = await response.json();
|
|
52
|
+
return {
|
|
53
|
+
width: json.value.screenSize.width,
|
|
54
|
+
height: json.value.screenSize.height,
|
|
55
|
+
scale: json.value.scale || 1,
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
async sendKeys(keys) {
|
|
60
|
+
await this.withinSession(async (sessionUrl) => {
|
|
61
|
+
const url = `${sessionUrl}/wda/keys`;
|
|
62
|
+
await fetch(url, {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: {
|
|
65
|
+
"Content-Type": "application/json",
|
|
66
|
+
},
|
|
67
|
+
body: JSON.stringify({ value: [keys] }),
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
async pressButton(button) {
|
|
72
|
+
const _map = {
|
|
73
|
+
"HOME": "home",
|
|
74
|
+
"VOLUME_UP": "volumeup",
|
|
75
|
+
"VOLUME_DOWN": "volumedown",
|
|
76
|
+
};
|
|
77
|
+
if (button === "ENTER") {
|
|
78
|
+
await this.sendKeys("\n");
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// Type assertion to check if button is a key of _map
|
|
82
|
+
if (!(button in _map)) {
|
|
83
|
+
throw new robot_1.ActionableError(`Button "${button}" is not supported`);
|
|
84
|
+
}
|
|
85
|
+
await this.withinSession(async (sessionUrl) => {
|
|
86
|
+
const url = `${sessionUrl}/wda/pressButton`;
|
|
87
|
+
const response = await fetch(url, {
|
|
88
|
+
method: "POST",
|
|
89
|
+
headers: {
|
|
90
|
+
"Content-Type": "application/json",
|
|
91
|
+
},
|
|
92
|
+
body: JSON.stringify({
|
|
93
|
+
name: button,
|
|
94
|
+
}),
|
|
95
|
+
});
|
|
96
|
+
return response.json();
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
async tap(x, y) {
|
|
100
|
+
await this.withinSession(async (sessionUrl) => {
|
|
101
|
+
const url = `${sessionUrl}/actions`;
|
|
102
|
+
await fetch(url, {
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers: {
|
|
105
|
+
"Content-Type": "application/json",
|
|
106
|
+
},
|
|
107
|
+
body: JSON.stringify({
|
|
108
|
+
actions: [
|
|
109
|
+
{
|
|
110
|
+
type: "pointer",
|
|
111
|
+
id: "finger1",
|
|
112
|
+
parameters: { pointerType: "touch" },
|
|
113
|
+
actions: [
|
|
114
|
+
{ type: "pointerMove", duration: 0, x, y },
|
|
115
|
+
{ type: "pointerDown", button: 0 },
|
|
116
|
+
{ type: "pause", duration: 100 },
|
|
117
|
+
{ type: "pointerUp", button: 0 }
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
}),
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
isVisible(rect) {
|
|
126
|
+
return rect.x >= 0 && rect.y >= 0;
|
|
127
|
+
}
|
|
128
|
+
filterSourceElements(source) {
|
|
129
|
+
const output = [];
|
|
130
|
+
const acceptedTypes = ["TextField", "Button", "Switch", "Icon", "SearchField", "StaticText", "Image"];
|
|
131
|
+
if (acceptedTypes.includes(source.type)) {
|
|
132
|
+
if (source.isVisible === "1" && this.isVisible(source.rect)) {
|
|
133
|
+
if (source.label !== null || source.name !== null) {
|
|
134
|
+
output.push({
|
|
135
|
+
type: source.type,
|
|
136
|
+
label: source.label,
|
|
137
|
+
name: source.name,
|
|
138
|
+
value: source.value,
|
|
139
|
+
rect: {
|
|
140
|
+
x: source.rect.x,
|
|
141
|
+
y: source.rect.y,
|
|
142
|
+
width: source.rect.width,
|
|
143
|
+
height: source.rect.height,
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (source.children) {
|
|
150
|
+
for (const child of source.children) {
|
|
151
|
+
output.push(...this.filterSourceElements(child));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return output;
|
|
155
|
+
}
|
|
156
|
+
async getPageSource() {
|
|
157
|
+
const url = `http://${this.host}:${this.port}/source/?format=json`;
|
|
158
|
+
const response = await fetch(url);
|
|
159
|
+
const json = await response.json();
|
|
160
|
+
return json;
|
|
161
|
+
}
|
|
162
|
+
async getElementsOnScreen() {
|
|
163
|
+
const source = await this.getPageSource();
|
|
164
|
+
return this.filterSourceElements(source.value);
|
|
165
|
+
}
|
|
166
|
+
async openUrl(url) {
|
|
167
|
+
await this.withinSession(async (sessionUrl) => {
|
|
168
|
+
await fetch(`${sessionUrl}/url`, {
|
|
169
|
+
method: "POST",
|
|
170
|
+
body: JSON.stringify({ url }),
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
async swipe(direction) {
|
|
175
|
+
await this.withinSession(async (sessionUrl) => {
|
|
176
|
+
const x0 = 200;
|
|
177
|
+
let y0 = 600;
|
|
178
|
+
const x1 = 200;
|
|
179
|
+
let y1 = 200;
|
|
180
|
+
if (direction === "up") {
|
|
181
|
+
const tmp = y0;
|
|
182
|
+
y0 = y1;
|
|
183
|
+
y1 = tmp;
|
|
184
|
+
}
|
|
185
|
+
const url = `${sessionUrl}/actions`;
|
|
186
|
+
await fetch(url, {
|
|
187
|
+
method: "POST",
|
|
188
|
+
headers: {
|
|
189
|
+
"Content-Type": "application/json",
|
|
190
|
+
},
|
|
191
|
+
body: JSON.stringify({
|
|
192
|
+
actions: [
|
|
193
|
+
{
|
|
194
|
+
type: "pointer",
|
|
195
|
+
id: "finger1",
|
|
196
|
+
parameters: { pointerType: "touch" },
|
|
197
|
+
actions: [
|
|
198
|
+
{ type: "pointerMove", duration: 0, x: x0, y: y0 },
|
|
199
|
+
{ type: "pointerDown", button: 0 },
|
|
200
|
+
{ type: "pointerMove", duration: 0, x: x1, y: y1 },
|
|
201
|
+
{ type: "pause", duration: 1000 },
|
|
202
|
+
{ type: "pointerUp", button: 0 }
|
|
203
|
+
]
|
|
204
|
+
}
|
|
205
|
+
]
|
|
206
|
+
}),
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
async setOrientation(orientation) {
|
|
211
|
+
await this.withinSession(async (sessionUrl) => {
|
|
212
|
+
const url = `${sessionUrl}/orientation`;
|
|
213
|
+
await fetch(url, {
|
|
214
|
+
method: "POST",
|
|
215
|
+
headers: { "Content-Type": "application/json" },
|
|
216
|
+
body: JSON.stringify({
|
|
217
|
+
orientation: orientation.toUpperCase()
|
|
218
|
+
})
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
async getOrientation() {
|
|
223
|
+
return this.withinSession(async (sessionUrl) => {
|
|
224
|
+
const url = `${sessionUrl}/orientation`;
|
|
225
|
+
const response = await fetch(url);
|
|
226
|
+
const json = await response.json();
|
|
227
|
+
return json.value.toLowerCase();
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
exports.WebDriverAgent = WebDriverAgent;
|
package/package.json
CHANGED
|
@@ -1,24 +1,52 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nbakka/mcp-appium",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
"bin": {
|
|
8
|
-
"mcp-appium": "./src/lib/server.js"
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Appium MCP",
|
|
5
|
+
"engines": {
|
|
6
|
+
"node": ">=18"
|
|
9
7
|
},
|
|
8
|
+
"license": "Apache-2.0",
|
|
10
9
|
"scripts": {
|
|
11
|
-
"
|
|
12
|
-
"
|
|
10
|
+
"build": "tsc && chmod +x lib/index.js",
|
|
11
|
+
"lint": "eslint .",
|
|
12
|
+
"test": "nyc mocha --require ts-node/register test/*.ts",
|
|
13
|
+
"watch": "tsc --watch",
|
|
14
|
+
"clean": "rm -rf lib",
|
|
15
|
+
"prepare": "husky"
|
|
13
16
|
},
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
"files": [
|
|
18
|
+
"lib"
|
|
19
|
+
],
|
|
17
20
|
"dependencies": {
|
|
18
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
19
|
-
|
|
20
|
-
"zod": "^3.
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
"@modelcontextprotocol/sdk": "^1.6.1",
|
|
22
|
+
"fast-xml-parser": "^5.0.9",
|
|
23
|
+
"zod-to-json-schema": "^3.24.4"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@eslint/eslintrc": "^3.2.0",
|
|
27
|
+
"@eslint/js": "^9.19.0",
|
|
28
|
+
"@stylistic/eslint-plugin": "^3.0.1",
|
|
29
|
+
"@types/mocha": "^10.0.10",
|
|
30
|
+
"@types/node": "^22.13.10",
|
|
31
|
+
"@typescript-eslint/eslint-plugin": "^8.28.0",
|
|
32
|
+
"@typescript-eslint/parser": "^8.26.1",
|
|
33
|
+
"@typescript-eslint/utils": "^8.26.1",
|
|
34
|
+
"eslint": "^9.19.0",
|
|
35
|
+
"eslint-plugin": "^1.0.1",
|
|
36
|
+
"eslint-plugin-import": "^2.31.0",
|
|
37
|
+
"eslint-plugin-notice": "^1.0.0",
|
|
38
|
+
"husky": "^9.1.7",
|
|
39
|
+
"nyc": "^17.1.0",
|
|
40
|
+
"mocha": "^11.1.0",
|
|
41
|
+
"ts-node": "^10.9.2",
|
|
42
|
+
"typescript": "^5.8.2"
|
|
43
|
+
},
|
|
44
|
+
"main": "index.js",
|
|
45
|
+
"bin": {
|
|
46
|
+
"mcp-server-mobile": "lib/index.js"
|
|
47
|
+
},
|
|
48
|
+
"directories": {
|
|
49
|
+
"lib": "lib"
|
|
50
|
+
},
|
|
51
|
+
"author": "nbakka"
|
|
52
|
+
}
|
package/bin/mcp-appium.js
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
|
-
import { dirname, resolve } from 'path';
|
|
5
|
-
import { spawn } from 'child_process';
|
|
6
|
-
|
|
7
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
-
const __dirname = dirname(__filename);
|
|
9
|
-
|
|
10
|
-
const serverPath = resolve(__dirname, '../src/lib/server.js');
|
|
11
|
-
|
|
12
|
-
// Start the server
|
|
13
|
-
const child = spawn('node', [serverPath], {
|
|
14
|
-
stdio: 'inherit'
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
child.on('error', (error) => {
|
|
18
|
-
console.error(`Error starting server: ${error.message}`);
|
|
19
|
-
process.exit(1);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
// Handle process termination
|
|
23
|
-
process.on('SIGTERM', () => {
|
|
24
|
-
child.kill('SIGTERM');
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
process.on('SIGINT', () => {
|
|
28
|
-
child.kill('SIGINT');
|
|
29
|
-
});#!/usr/bin/env node
|
|
30
|
-
|
|
31
|
-
import { fileURLToPath } from 'url';
|
|
32
|
-
import { dirname, resolve } from 'path';
|
|
33
|
-
import { spawn } from 'child_process';
|
|
34
|
-
|
|
35
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
36
|
-
const __dirname = dirname(__filename);
|
|
37
|
-
|
|
38
|
-
const serverPath = resolve(__dirname, '../src/lib/server.js');
|
|
39
|
-
|
|
40
|
-
// Start the server
|
|
41
|
-
const child = spawn('node', [serverPath], {
|
|
42
|
-
stdio: 'inherit'
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
child.on('error', (error) => {
|
|
46
|
-
console.error(`Error starting server: ${error.message}`);
|
|
47
|
-
process.exit(1);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
// Handle process termination
|
|
51
|
-
process.on('SIGTERM', () => {
|
|
52
|
-
child.kill('SIGTERM');
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
process.on('SIGINT', () => {
|
|
56
|
-
child.kill('SIGINT');
|
|
57
|
-
});
|
package/src/lib/server.js
DELETED
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
-
import { z } from "zod";
|
|
6
|
-
import axios from "axios";
|
|
7
|
-
import { exec } from "child_process";
|
|
8
|
-
import { parseStringPromise } from "xml2js";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const APPIUM_URL = "http://127.0.0.1:4723"; // Corrected endpoint for Appium 2.x
|
|
12
|
-
|
|
13
|
-
const server = new McpServer({ name: "MCP Appium JSONWire", version: "1.0.0" });
|
|
14
|
-
const state = { sessionId: null };
|
|
15
|
-
|
|
16
|
-
// Helper to extract element ID
|
|
17
|
-
function extractElementId(element) {
|
|
18
|
-
return element.ELEMENT || element["element-6066-11e4-a52e-4f735466cecf"];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function delay(ms) {
|
|
22
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Start Session tool
|
|
26
|
-
server.tool(
|
|
27
|
-
"start_session",
|
|
28
|
-
"Start Appium session with capabilities",
|
|
29
|
-
{
|
|
30
|
-
capabilities: z.object({
|
|
31
|
-
platformName: z.string(),
|
|
32
|
-
udid: z.string(),
|
|
33
|
-
automationName: z.string(),
|
|
34
|
-
app: z.string().optional(),
|
|
35
|
-
}),
|
|
36
|
-
},
|
|
37
|
-
async ({ capabilities }) => {
|
|
38
|
-
try {
|
|
39
|
-
// Construct capabilities strictly with prefixes
|
|
40
|
-
const alwaysMatch = {
|
|
41
|
-
platformName: capabilities.platformName,
|
|
42
|
-
"appium:udid": capabilities.udid,
|
|
43
|
-
"appium:automationName": capabilities.automationName,
|
|
44
|
-
};
|
|
45
|
-
if (capabilities.app) {
|
|
46
|
-
alwaysMatch["appium:app"] = capabilities.app;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const payload = {
|
|
50
|
-
capabilities: {
|
|
51
|
-
firstMatch: [{}],
|
|
52
|
-
alwaysMatch,
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
const response = await axios.post(`${APPIUM_URL}/session`, payload);
|
|
57
|
-
state.sessionId = response.data.value.sessionId;
|
|
58
|
-
state.deviceId = capabilities.udid;
|
|
59
|
-
await delay(10000);
|
|
60
|
-
return { content: [{ type: "text", text: `Session started: ${state.sessionId}` }] };
|
|
61
|
-
} catch (e) {
|
|
62
|
-
return { content: [{ type: "text", text: `Error starting session: ${e.message}` }] };
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
// Tap tool
|
|
69
|
-
server.tool(
|
|
70
|
-
"tap",
|
|
71
|
-
"Tap element by locator",
|
|
72
|
-
{
|
|
73
|
-
by: z.enum(["id", "accessibility id", "xpath", "class name", "name"]),
|
|
74
|
-
value: z.string(),
|
|
75
|
-
},
|
|
76
|
-
async ({ by, value }) => {
|
|
77
|
-
if (!state.sessionId) return { content: [{ type: "text", text: "No active session" }] };
|
|
78
|
-
try {
|
|
79
|
-
const findResp = await axios.post(`${APPIUM_URL}/session/${state.sessionId}/element`, { using: by, value });
|
|
80
|
-
const elementId = extractElementId(findResp.data.value);
|
|
81
|
-
await axios.post(`${APPIUM_URL}/session/${state.sessionId}/element/${elementId}/click`);
|
|
82
|
-
return { content: [{ type: "text", text: "Element tapped" }] };
|
|
83
|
-
} catch (e) {
|
|
84
|
-
return { content: [{ type: "text", text: `Error tapping element: ${e.message}` }] };
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
// Swipe tool
|
|
90
|
-
server.tool(
|
|
91
|
-
"swipe",
|
|
92
|
-
"Swipe from start to end coordinates",
|
|
93
|
-
{
|
|
94
|
-
startX: z.number(),
|
|
95
|
-
startY: z.number(),
|
|
96
|
-
endX: z.number(),
|
|
97
|
-
endY: z.number(),
|
|
98
|
-
duration: z.number().optional(),
|
|
99
|
-
},
|
|
100
|
-
async ({ startX, startY, endX, endY, duration = 800 }) => {
|
|
101
|
-
if (!state.sessionId) return { content: [{ type: "text", text: "No active session" }] };
|
|
102
|
-
try {
|
|
103
|
-
const actions = [
|
|
104
|
-
{ action: "press", options: { x: startX, y: startY } },
|
|
105
|
-
{ action: "wait", options: { ms: duration } },
|
|
106
|
-
{ action: "moveTo", options: { x: endX, y: endY } },
|
|
107
|
-
{ action: "release", options: {} },
|
|
108
|
-
];
|
|
109
|
-
await axios.post(`${APPIUM_URL}/session/${state.sessionId}/touch/perform`, { actions });
|
|
110
|
-
return { content: [{ type: "text", text: `Swiped from (${startX},${startY}) to (${endX},${endY})` }] };
|
|
111
|
-
} catch (e) {
|
|
112
|
-
return { content: [{ type: "text", text: `Error swiping: ${e.message}` }] };
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
// Open Deep Link
|
|
118
|
-
server.tool(
|
|
119
|
-
"open_deep_link_adb",
|
|
120
|
-
"Open deep link on device via ADB shell",
|
|
121
|
-
{
|
|
122
|
-
deepLink: z.string(),
|
|
123
|
-
},
|
|
124
|
-
async ({ deepLink }) => {
|
|
125
|
-
if (!state.deviceId) {
|
|
126
|
-
return { content: [{ type: "text", text: "No connected device found" }] };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const command = `adb -s ${state.deviceId} shell am start -a android.intent.action.VIEW -d "${deepLink}"`;
|
|
130
|
-
|
|
131
|
-
return new Promise((resolve) => {
|
|
132
|
-
exec(command, (error, stdout, stderr) => {
|
|
133
|
-
if (error) {
|
|
134
|
-
resolve({ content: [{ type: "text", text: `Error executing ADB command: ${error.message}` }] });
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
if (stderr) {
|
|
138
|
-
resolve({ content: [{ type: "text", text: `ADB stderr: ${stderr}` }] });
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
resolve({ content: [{ type: "text", text: `Deep link opened via ADB: ${deepLink}` }] });
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
// Get visible text elements including duplicates
|
|
148
|
-
server.tool(
|
|
149
|
-
"get_visible_text_elements",
|
|
150
|
-
"Get all visible texts from screen XML source including duplicates",
|
|
151
|
-
{},
|
|
152
|
-
async () => {
|
|
153
|
-
if (!state.sessionId) return { content: [{ type: "text", text: "No active session" }] };
|
|
154
|
-
try {
|
|
155
|
-
const response = await axios.get(`${APPIUM_URL}/session/${state.sessionId}/source`);
|
|
156
|
-
const xml = response.data;
|
|
157
|
-
console.log(response.data);
|
|
158
|
-
const parsed = await parseStringPromise(xml, { explicitArray: false, mergeAttrs: true });
|
|
159
|
-
|
|
160
|
-
function collectTextsFromXml(node, texts = []) {
|
|
161
|
-
if (!node) return texts;
|
|
162
|
-
|
|
163
|
-
// If array, recurse each element
|
|
164
|
-
if (Array.isArray(node)) {
|
|
165
|
-
node.forEach(child => collectTextsFromXml(child, texts));
|
|
166
|
-
return texts;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// If node has 'text' attribute with non-empty value, collect it
|
|
170
|
-
if (node.text && node.text.trim() !== "") {
|
|
171
|
-
texts.push(node.text.trim());
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Recurse into all children nodes
|
|
175
|
-
for (const key in node) {
|
|
176
|
-
if (key !== "text" && typeof node[key] === "object") {
|
|
177
|
-
collectTextsFromXml(node[key], texts);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
return texts;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const allTexts = collectTextsFromXml(parsed);
|
|
184
|
-
return { content: [{ type: "json", json: allTexts }] };
|
|
185
|
-
} catch (e) {
|
|
186
|
-
return { content: [{ type: "text", text: `Error fetching or parsing source XML: ${e.message}` }] };
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
);
|
|
190
|
-
|
|
191
|
-
// Click element by text with optional index (default 0)
|
|
192
|
-
server.tool(
|
|
193
|
-
"click_by_text",
|
|
194
|
-
"Click element by visible text with optional index",
|
|
195
|
-
{
|
|
196
|
-
text: z.string(),
|
|
197
|
-
index: z.number().optional().default(0),
|
|
198
|
-
},
|
|
199
|
-
async ({ text, index }) => {
|
|
200
|
-
if (!state.sessionId) return { content: [{ type: "text", text: "No active session" }] };
|
|
201
|
-
try {
|
|
202
|
-
const xpath = `(//*[normalize-space(@text)='${text}'])[${index + 1}]`;
|
|
203
|
-
const findResp = await axios.post(`${APPIUM_URL}/session/${state.sessionId}/element`, { using: "xpath", value: xpath });
|
|
204
|
-
const elementId = extractElementId(findResp.data.value);
|
|
205
|
-
await axios.post(`${APPIUM_URL}/session/${state.sessionId}/element/${elementId}/click`);
|
|
206
|
-
return { content: [{ type: "text", text: `Clicked element with text: "${text}" at index ${index}` }] };
|
|
207
|
-
} catch (e) {
|
|
208
|
-
return { content: [{ type: "text", text: `Error clicking element: ${e.message}` }] };
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
);
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
// Close Session tool
|
|
218
|
-
server.tool(
|
|
219
|
-
"close_session",
|
|
220
|
-
"Close the current Appium session",
|
|
221
|
-
{},
|
|
222
|
-
async () => {
|
|
223
|
-
if (!state.sessionId) return { content: [{ type: "text", text: "No active session" }] };
|
|
224
|
-
try {
|
|
225
|
-
await axios.delete(`${APPIUM_URL}/session/${state.sessionId}`);
|
|
226
|
-
const oldSession = state.sessionId;
|
|
227
|
-
state.sessionId = null;
|
|
228
|
-
return { content: [{ type: "text", text: `Session ${oldSession} closed` }] };
|
|
229
|
-
} catch (e) {
|
|
230
|
-
return { content: [{ type: "text", text: `Error closing session: ${e.message}` }] };
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
);
|
|
234
|
-
|
|
235
|
-
// Connect MCP server to stdio
|
|
236
|
-
const transport = new StdioServerTransport();
|
|
237
|
-
await server.connect(transport);
|