@nbakka/mcp-appium 1.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/README.md +1 -0
- package/bin/mcp-appium.js +74 -0
- package/package.json +34 -0
- package/src/lib/server.js +115 -0
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# mcp-appium
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import yargs from "yargs";
|
|
3
|
+
import { hideBin } from "yargs/helpers";
|
|
4
|
+
import MCPClient from "@modelcontextprotocol/sdk/client/mcp.js";
|
|
5
|
+
|
|
6
|
+
async function main() {
|
|
7
|
+
const argv = await yargs(hideBin(process.argv))
|
|
8
|
+
.option("url", {
|
|
9
|
+
alias: "u",
|
|
10
|
+
type: "string",
|
|
11
|
+
demandOption: true,
|
|
12
|
+
describe: "MCP server base URL",
|
|
13
|
+
})
|
|
14
|
+
.command("start_session", "Start Appium session", (yargs) => {
|
|
15
|
+
yargs
|
|
16
|
+
.option("platformName", { type: "string", demandOption: true })
|
|
17
|
+
.option("deviceName", { type: "string", demandOption: true })
|
|
18
|
+
.option("app", { type: "string" })
|
|
19
|
+
.option("automationName", { type: "string" });
|
|
20
|
+
})
|
|
21
|
+
.command("tap <by> <value>", "Tap element", (yargs) => {
|
|
22
|
+
yargs.positional("by", { type: "string" }).positional("value", { type: "string" });
|
|
23
|
+
})
|
|
24
|
+
.command(
|
|
25
|
+
"swipe <startX> <startY> <endX> <endY> [duration]",
|
|
26
|
+
"Swipe gesture",
|
|
27
|
+
(yargs) => {
|
|
28
|
+
yargs
|
|
29
|
+
.positional("startX", { type: "number" })
|
|
30
|
+
.positional("startY", { type: "number" })
|
|
31
|
+
.positional("endX", { type: "number" })
|
|
32
|
+
.positional("endY", { type: "number" })
|
|
33
|
+
.positional("duration", { type: "number", default: 800 });
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
.demandCommand(1, "You must provide a command")
|
|
37
|
+
.help().argv;
|
|
38
|
+
|
|
39
|
+
const client = new MCPClient(argv.url);
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const cmd = argv._[0];
|
|
43
|
+
let result;
|
|
44
|
+
|
|
45
|
+
if (cmd === "start_session") {
|
|
46
|
+
const caps = {
|
|
47
|
+
platformName: argv.platformName,
|
|
48
|
+
deviceName: argv.deviceName,
|
|
49
|
+
app: argv.app,
|
|
50
|
+
automationName: argv.automationName,
|
|
51
|
+
};
|
|
52
|
+
result = await client.callTool("start_session", { capabilities: caps });
|
|
53
|
+
} else if (cmd === "tap") {
|
|
54
|
+
result = await client.callTool("tap", { by: argv.by, value: argv.value });
|
|
55
|
+
} else if (cmd === "swipe") {
|
|
56
|
+
result = await client.callTool("swipe", {
|
|
57
|
+
startX: argv.startX,
|
|
58
|
+
startY: argv.startY,
|
|
59
|
+
endX: argv.endX,
|
|
60
|
+
endY: argv.endY,
|
|
61
|
+
duration: argv.duration,
|
|
62
|
+
});
|
|
63
|
+
} else {
|
|
64
|
+
throw new Error(`Unknown command: ${cmd}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log(result.content.map((c) => c.text).join("\n"));
|
|
68
|
+
} catch (e) {
|
|
69
|
+
console.error("Error:", e.message);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nbakka/mcp-appium",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for Appium using JSON Wire Protocol",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/lib/server.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mcp-appium": "./bin/mcp-appium.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node src/lib/server.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"src",
|
|
15
|
+
"bin",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@modelcontextprotocol/sdk": "^1.7.0",
|
|
21
|
+
"axios": "^1.4.0",
|
|
22
|
+
"zod": "^3.22.2",
|
|
23
|
+
"yargs": "^17.7.2"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"author": "Your Name <youremail@example.com>",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/nbakka/mcp-appium.git"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import axios from "axios";
|
|
5
|
+
|
|
6
|
+
const APPIUM_URL = "http://127.0.0.1:4723/wd/hub";
|
|
7
|
+
|
|
8
|
+
const server = new McpServer({ name: "MCP Appium JSONWire", version: "1.0.0" });
|
|
9
|
+
|
|
10
|
+
const state = { sessionId: null };
|
|
11
|
+
|
|
12
|
+
// Helper to extract element ID
|
|
13
|
+
function extractElementId(element) {
|
|
14
|
+
return element.ELEMENT || element["element-6066-11e4-a52e-4f735466cecf"];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Start Session tool
|
|
18
|
+
server.tool(
|
|
19
|
+
"start_session",
|
|
20
|
+
"Start Appium session with capabilities",
|
|
21
|
+
{
|
|
22
|
+
capabilities: z.object({
|
|
23
|
+
platformName: z.string(),
|
|
24
|
+
deviceName: z.string(),
|
|
25
|
+
app: z.string().optional(),
|
|
26
|
+
automationName: z.string().optional(),
|
|
27
|
+
}),
|
|
28
|
+
},
|
|
29
|
+
async ({ capabilities }) => {
|
|
30
|
+
try {
|
|
31
|
+
const payload = {
|
|
32
|
+
capabilities: {
|
|
33
|
+
firstMatch: [{}],
|
|
34
|
+
alwaysMatch: capabilities,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
const response = await axios.post(`${APPIUM_URL}/session`, payload);
|
|
38
|
+
state.sessionId = response.data.sessionId;
|
|
39
|
+
return { content: [{ type: "text", text: `Session started: ${state.sessionId}` }] };
|
|
40
|
+
} catch (e) {
|
|
41
|
+
return { content: [{ type: "text", text: `Error starting session: ${e.message}` }] };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// Tap tool
|
|
47
|
+
server.tool(
|
|
48
|
+
"tap",
|
|
49
|
+
"Tap element by locator",
|
|
50
|
+
{
|
|
51
|
+
by: z.enum(["id", "accessibility id", "xpath", "class name", "name"]),
|
|
52
|
+
value: z.string(),
|
|
53
|
+
},
|
|
54
|
+
async ({ by, value }) => {
|
|
55
|
+
if (!state.sessionId) return { content: [{ type: "text", text: "No active session" }] };
|
|
56
|
+
try {
|
|
57
|
+
const findResp = await axios.post(`${APPIUM_URL}/session/${state.sessionId}/element`, { using: by, value });
|
|
58
|
+
const elementId = extractElementId(findResp.data.value);
|
|
59
|
+
await axios.post(`${APPIUM_URL}/session/${state.sessionId}/element/${elementId}/click`);
|
|
60
|
+
return { content: [{ type: "text", text: "Element tapped" }] };
|
|
61
|
+
} catch (e) {
|
|
62
|
+
return { content: [{ type: "text", text: `Error tapping element: ${e.message}` }] };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// Swipe tool
|
|
68
|
+
server.tool(
|
|
69
|
+
"swipe",
|
|
70
|
+
"Swipe from start to end coordinates",
|
|
71
|
+
{
|
|
72
|
+
startX: z.number(),
|
|
73
|
+
startY: z.number(),
|
|
74
|
+
endX: z.number(),
|
|
75
|
+
endY: z.number(),
|
|
76
|
+
duration: z.number().optional(),
|
|
77
|
+
},
|
|
78
|
+
async ({ startX, startY, endX, endY, duration = 800 }) => {
|
|
79
|
+
if (!state.sessionId) return { content: [{ type: "text", text: "No active session" }] };
|
|
80
|
+
try {
|
|
81
|
+
const actions = [
|
|
82
|
+
{ action: "press", options: { x: startX, y: startY } },
|
|
83
|
+
{ action: "wait", options: { ms: duration } },
|
|
84
|
+
{ action: "moveTo", options: { x: endX, y: endY } },
|
|
85
|
+
{ action: "release", options: {} },
|
|
86
|
+
];
|
|
87
|
+
await axios.post(`${APPIUM_URL}/session/${state.sessionId}/touch/perform`, { actions });
|
|
88
|
+
return { content: [{ type: "text", text: `Swiped from (${startX},${startY}) to (${endX},${endY})` }] };
|
|
89
|
+
} catch (e) {
|
|
90
|
+
return { content: [{ type: "text", text: `Error swiping: ${e.message}` }] };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Close Session tool
|
|
96
|
+
server.tool(
|
|
97
|
+
"close_session",
|
|
98
|
+
"Close the current Appium session",
|
|
99
|
+
{},
|
|
100
|
+
async () => {
|
|
101
|
+
if (!state.sessionId) return { content: [{ type: "text", text: "No active session" }] };
|
|
102
|
+
try {
|
|
103
|
+
await axios.delete(`${APPIUM_URL}/session/${state.sessionId}`);
|
|
104
|
+
const oldSession = state.sessionId;
|
|
105
|
+
state.sessionId = null;
|
|
106
|
+
return { content: [{ type: "text", text: `Session ${oldSession} closed` }] };
|
|
107
|
+
} catch (e) {
|
|
108
|
+
return { content: [{ type: "text", text: `Error closing session: ${e.message}` }] };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// Connect MCP server to stdio
|
|
114
|
+
const transport = new StdioServerTransport();
|
|
115
|
+
await server.connect(transport);
|