appium-mcp 1.71.5 → 1.72.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/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## [1.72.0](https://github.com/appium/appium-mcp/compare/v1.71.6...v1.72.0) (2026-05-01)
2
+
3
+ ### Features
4
+
5
+ * add APPIUM_MCP_ON_CLIENT_DISCONNECT toggle ([#306](https://github.com/appium/appium-mcp/issues/306)) ([b77cae4](https://github.com/appium/appium-mcp/commit/b77cae4fe6753599f4c04cfedbb6e1b362c23ed2))
6
+
7
+ ## [1.71.6](https://github.com/appium/appium-mcp/compare/v1.71.5...v1.71.6) (2026-05-01)
8
+
9
+ ### Bug Fixes
10
+
11
+ * add actions api to the java template ([#310](https://github.com/appium/appium-mcp/issues/310)) ([caebdd4](https://github.com/appium/appium-mcp/commit/caebdd41d21a51dbfaaa790952e2fc9ccc1e0c77))
12
+
1
13
  ## [1.71.5](https://github.com/appium/appium-mcp/compare/v1.71.4...v1.71.5) (2026-04-30)
2
14
 
3
15
  ### Bug Fixes
package/README.md CHANGED
@@ -143,6 +143,7 @@ This will automatically configure the MCP server for use with Claude Code. Make
143
143
  | `CAPABILITIES_CONFIG` | Optional | Absolute path to a `capabilities.json` file with per-platform capability presets |
144
144
  | `SCREENSHOTS_DIR` | Optional | Directory where screenshots and screen recordings are saved. Defaults to the current working directory |
145
145
  | `NO_UI` | Optional | Set to `true` or `1` to disable HTML UI components — faster responses, fewer tokens. See [NO_UI Mode](#no_ui-mode) |
146
+ | `APPIUM_MCP_ON_CLIENT_DISCONNECT` | Optional | Session cleanup when the MCP client disconnects: `delete_all` (default) deletes **MCP-owned** Appium sessions (`safeDeleteAllSessions`); `skip` keeps those sessions across disconnects (e.g. HTTP/stream clients that reconnect). Attached/remote sessions are not removed by this path. See [MCP disconnect behavior](#mcp-disconnect-behavior). |
146
147
  | `APPIUM_MCP_WDA_APP_PATH` | Optional | Absolute path to a pre-extracted `WebDriverAgentRunner-Runner.app` bundle. When set, `prepare_ios_simulator` skips all GitHub downloads and uses this bundle directly — useful in environments where external downloads are blocked |
147
148
  | `REMOTE_SERVER_URL_ALLOW_REGEX` | Optional | Regex pattern that remote Appium server URLs must match. Defaults to `^https?://` |
148
149
  | `AI_VISION_API_BASE_URL` | Required for AI Vision | Base URL of the OpenAI-compatible vision model API |
@@ -321,6 +322,14 @@ The following tools return lightweight text-only responses when NO_UI is enabled
321
322
  - ✅ Scripted automation where human interaction is not needed
322
323
  - ❌ Interactive debugging and exploration (keep UI enabled for better experience)
323
324
 
325
+ #### MCP disconnect behavior
326
+
327
+ By default (`APPIUM_MCP_ON_CLIENT_DISCONNECT` unset or `delete_all`), when the **MCP client disconnects**, this server **deletes every MCP-owned Appium session** (the same sessions `safeDeleteAllSessions` targets) so embedded drivers are not left running after a short-lived assistant run. **Attached** sessions (`ownership=attached`) are unchanged by this teardown.
328
+
329
+ HTTP and streamable MCP clients may **disconnect briefly** (reconnect, reload, proxy). If that tears down drivers you still need, set `APPIUM_MCP_ON_CLIENT_DISCONNECT` to `skip` in your MCP server `env` (same pattern as `NO_UI` above). With `skip`, sessions **survive** disconnect until you call `appium_session_management` with `action=delete`, or you stop the Appium server / process.
330
+
331
+ **Tradeoff:** `skip` can leave **orphaned sessions** on your Appium server if nothing cleans up — use it when disconnect is not the same as “automation finished.”
332
+
324
333
  ## 🎯 Available Tools
325
334
 
326
335
  MCP Appium provides a comprehensive set of tools organized into the following categories:
@@ -1 +1 @@
1
- {"version":3,"file":"template.d.ts","sourceRoot":"","sources":["../../../src/resources/java/template.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,CAAC,OAAO,UAAU,qBAAqB,CAAC,MAAM,EAAE,GAAG,QA+ExD"}
1
+ {"version":3,"file":"template.d.ts","sourceRoot":"","sources":["../../../src/resources/java/template.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,CAAC,OAAO,UAAU,qBAAqB,CAAC,MAAM,EAAE,GAAG,QA6GxD"}
@@ -7,6 +7,7 @@ export default function javaTemplatesResource(server) {
7
7
  name: 'Generate Code With Locators',
8
8
  description: `Generate code for the current page with locators which was retrieved from generate_locators tool.
9
9
  The code should be generated in the same priorty order as the locators were generated.
10
+ Always use the actions API code from the template for the gestures.
10
11
  For iOS the priority order is:
11
12
  'id',
12
13
  'accessibility id',
@@ -73,6 +74,35 @@ public class locators {
73
74
  public locators(AppiumDriver driver) {
74
75
  PageFactory.initElements(new AppiumFieldDecorator(driver, Duration.ofSeconds(10)), this);
75
76
  }
77
+
78
+ @Test
79
+ public void sliderTest() {
80
+ WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
81
+ wait.until(ExpectedConditions.elementToBeClickable(AppiumBy.accessibilityId("login"))).click();
82
+ wait.until(ExpectedConditions.elementToBeClickable(AppiumBy.accessibilityId("slider1"))).click();
83
+
84
+ try { Thread.sleep(1000); } catch (InterruptedException e) {}
85
+
86
+ WebElement slider = wait.until(ExpectedConditions.visibilityOfElementLocated(AppiumBy.accessibilityId("slider")));
87
+
88
+ Point location = slider.getLocation();
89
+ Dimension size = slider.getSize();
90
+
91
+ // Swipe right: from left side of the slider to the right side
92
+ int startX = location.getX() + (int)(size.getWidth() * 0.1);
93
+ int endX = location.getX() + (int)(size.getWidth() * 0.9);
94
+ int y = location.getY() + size.getHeight() / 2;
95
+
96
+ PointerInput finger = new PointerInput(PointerInput.Kind.TOUCH, "finger");
97
+ Sequence swipe = new Sequence(finger, 1);
98
+
99
+ swipe.addAction(finger.createPointerMove(Duration.ZERO, PointerInput.Origin.viewport(), startX, y));
100
+ swipe.addAction(finger.createPointerDown(PointerInput.MouseButton.LEFT.asArg()));
101
+ swipe.addAction(finger.createPointerMove(Duration.ofMillis(600), PointerInput.Origin.viewport(), endX, y));
102
+ swipe.addAction(finger.createPointerUp(PointerInput.MouseButton.LEFT.asArg()));
103
+
104
+ driver.perform(Collections.singletonList(swipe));
105
+ }
76
106
  }
77
107
  `,
78
108
  };
@@ -1 +1 @@
1
- {"version":3,"file":"template.js","sourceRoot":"","sources":["../../../src/resources/java/template.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,CAAC,OAAO,UAAU,qBAAqB,CAAC,MAAW;IACvD,MAAM,CAAC,WAAW,CAAC;QACjB,GAAG,EAAE,+BAA+B;QACpC,IAAI,EAAE,6BAA6B;QACnC,WAAW,EAAE;;;;;;;;;;;;;;;sHAeqG;QAClH,QAAQ,EAAE,YAAY;QACtB,KAAK,CAAC,IAAI;YACR,uDAAuD;YACvD,OAAO;gBACL,OAAO,EACL,uFAAuF;gBACzF,WAAW,EACT,2DAA2D;gBAC7D,IAAI,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA+CT;aACE,CAAC;QACJ,CAAC;KACF,CAAC,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"template.js","sourceRoot":"","sources":["../../../src/resources/java/template.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,CAAC,OAAO,UAAU,qBAAqB,CAAC,MAAW;IACvD,MAAM,CAAC,WAAW,CAAC;QACjB,GAAG,EAAE,+BAA+B;QACpC,IAAI,EAAE,6BAA6B;QACnC,WAAW,EAAE;;;;;;;;;;;;;;;;sHAgBqG;QAClH,QAAQ,EAAE,YAAY;QACtB,KAAK,CAAC,IAAI;YACR,uDAAuD;YACvD,OAAO;gBACL,OAAO,EACL,uFAAuF;gBACzF,WAAW,EACT,2DAA2D;gBAC7D,IAAI,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA4ET;aACE,CAAC;QACJ,CAAC;KACF,CAAC,CAAC;AACL,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAMlC,QAAA,MAAM,MAAM,+CAKV,CAAC;AAgCH,eAAe,MAAM,CAAC"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AA0BlC,QAAA,MAAM,MAAM,+CAKV,CAAC;AA2CH,eAAe,MAAM,CAAC"}
package/dist/server.js CHANGED
@@ -3,6 +3,21 @@ import registerTools from './tools/index.js';
3
3
  import registerResources from './resources/index.js';
4
4
  import { listSessions, safeDeleteAllSessions } from './session-store.js';
5
5
  import log from './logger.js';
6
+ /**
7
+ * MCP disconnect policy for Appium sessions tracked by this server.
8
+ * - delete_all (default): end every owned session when the MCP client disconnects (avoids leaked drivers).
9
+ * - skip: keep sessions across disconnects — needed for flaky HTTP/stream clients that reconnect briefly.
10
+ */
11
+ function disconnectSessionPolicyFromEnv() {
12
+ const raw = process.env.APPIUM_MCP_ON_CLIENT_DISCONNECT?.trim().toLowerCase();
13
+ if (raw === 'skip') {
14
+ return 'skip';
15
+ }
16
+ if (raw !== 'delete_all') {
17
+ log.warn(`APPIUM_MCP_ON_CLIENT_DISCONNECT="${raw}" is not recognized (expected delete_all or skip); defaulting to delete_all`);
18
+ }
19
+ return 'delete_all';
20
+ }
6
21
  const server = new FastMCP({
7
22
  name: 'MCP Appium',
8
23
  version: '1.0.0',
@@ -16,7 +31,13 @@ server.on('connect', (event) => {
16
31
  });
17
32
  server.on('disconnect', async (event) => {
18
33
  log.info('Client disconnected:', event.session);
34
+ const policy = disconnectSessionPolicyFromEnv();
19
35
  const ownedSessions = listSessions().filter((session) => session.ownership === 'owned');
36
+ if (ownedSessions.length > 0 && policy === 'skip') {
37
+ log.info(`${ownedSessions.length} owned session(s) retained after MCP disconnect ` +
38
+ '(APPIUM_MCP_ON_CLIENT_DISCONNECT=skip). Delete explicitly via appium_session_management (action=delete) when finished.');
39
+ return;
40
+ }
20
41
  if (ownedSessions.length > 0) {
21
42
  try {
22
43
  log.info(`${ownedSessions.length} owned session(s) detected on disconnect, cleaning up...`);
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,aAAa,MAAM,kBAAkB,CAAC;AAC7C,OAAO,iBAAiB,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AACzE,OAAO,GAAG,MAAM,aAAa,CAAC;AAE9B,MAAM,MAAM,GAAG,IAAI,OAAO,CAAC;IACzB,IAAI,EAAE,YAAY;IAClB,OAAO,EAAE,OAAO;IAChB,YAAY,EACV,mPAAmP;CACtP,CAAC,CAAC;AAEH,iBAAiB,CAAC,MAAM,CAAC,CAAC;AAC1B,aAAa,CAAC,MAAM,CAAC,CAAC;AAEtB,oDAAoD;AACpD,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,EAAE;IAC7B,GAAG,CAAC,IAAI,CAAC,mBAAmB,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;AAC/C,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,EAAE,CAAC,YAAY,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;IACtC,GAAG,CAAC,IAAI,CAAC,sBAAsB,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;IAChD,MAAM,aAAa,GAAG,YAAY,EAAE,CAAC,MAAM,CACzC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,SAAS,KAAK,OAAO,CAC3C,CAAC;IACF,IAAI,aAAa,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC7B,IAAI,CAAC;YACH,GAAG,CAAC,IAAI,CACN,GAAG,aAAa,CAAC,MAAM,0DAA0D,CAClF,CAAC;YACF,MAAM,YAAY,GAAG,MAAM,qBAAqB,EAAE,CAAC;YACnD,GAAG,CAAC,IAAI,CACN,GAAG,YAAY,oDAAoD,CACpE,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,GAAG,CAAC,KAAK,CAAC,0CAA0C,EAAE,KAAK,CAAC,CAAC;QAC/D,CAAC;IACH,CAAC;SAAM,CAAC;QACN,GAAG,CAAC,IAAI,CAAC,8CAA8C,CAAC,CAAC;IAC3D,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,eAAe,MAAM,CAAC"}
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,aAAa,MAAM,kBAAkB,CAAC;AAC7C,OAAO,iBAAiB,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AACzE,OAAO,GAAG,MAAM,aAAa,CAAC;AAI9B;;;;GAIG;AACH,SAAS,8BAA8B;IACrC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,+BAA+B,EAAE,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC9E,IAAI,GAAG,KAAK,MAAM,EAAE,CAAC;QACnB,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,IAAI,GAAG,KAAK,YAAY,EAAE,CAAC;QACzB,GAAG,CAAC,IAAI,CACN,oCAAoC,GAAG,6EAA6E,CACrH,CAAC;IACJ,CAAC;IACD,OAAO,YAAY,CAAC;AACtB,CAAC;AAED,MAAM,MAAM,GAAG,IAAI,OAAO,CAAC;IACzB,IAAI,EAAE,YAAY;IAClB,OAAO,EAAE,OAAO;IAChB,YAAY,EACV,mPAAmP;CACtP,CAAC,CAAC;AAEH,iBAAiB,CAAC,MAAM,CAAC,CAAC;AAC1B,aAAa,CAAC,MAAM,CAAC,CAAC;AAEtB,oDAAoD;AACpD,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,EAAE;IAC7B,GAAG,CAAC,IAAI,CAAC,mBAAmB,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;AAC/C,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,EAAE,CAAC,YAAY,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;IACtC,GAAG,CAAC,IAAI,CAAC,sBAAsB,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,8BAA8B,EAAE,CAAC;IAEhD,MAAM,aAAa,GAAG,YAAY,EAAE,CAAC,MAAM,CACzC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,SAAS,KAAK,OAAO,CAC3C,CAAC;IAEF,IAAI,aAAa,CAAC,MAAM,GAAG,CAAC,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;QAClD,GAAG,CAAC,IAAI,CACN,GAAG,aAAa,CAAC,MAAM,kDAAkD;YACvE,wHAAwH,CAC3H,CAAC;QACF,OAAO;IACT,CAAC;IAED,IAAI,aAAa,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC7B,IAAI,CAAC;YACH,GAAG,CAAC,IAAI,CACN,GAAG,aAAa,CAAC,MAAM,0DAA0D,CAClF,CAAC;YACF,MAAM,YAAY,GAAG,MAAM,qBAAqB,EAAE,CAAC;YACnD,GAAG,CAAC,IAAI,CACN,GAAG,YAAY,oDAAoD,CACpE,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,GAAG,CAAC,KAAK,CAAC,0CAA0C,EAAE,KAAK,CAAC,CAAC;QAC/D,CAAC;IACH,CAAC;SAAM,CAAC;QACN,GAAG,CAAC,IAAI,CAAC,8CAA8C,CAAC,CAAC;IAC3D,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,eAAe,MAAM,CAAC"}
@@ -1,3 +1,3 @@
1
- import { FastMCP } from 'fastmcp';
1
+ import type { FastMCP } from 'fastmcp';
2
2
  export default function generateTest(server: FastMCP): void;
3
3
  //# sourceMappingURL=generate-tests.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"generate-tests.d.ts","sourceRoot":"","sources":["../../../src/tools/test-generation/generate-tests.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAIlC,MAAM,CAAC,OAAO,UAAU,YAAY,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAoC1D"}
1
+ {"version":3,"file":"generate-tests.d.ts","sourceRoot":"","sources":["../../../src/tools/test-generation/generate-tests.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,OAAO,EAAE,MAAM,SAAS,CAAC;AAQtD,MAAM,CAAC,OAAO,UAAU,YAAY,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAgD1D"}
@@ -1,30 +1,42 @@
1
1
  import { z } from 'zod';
2
2
  import { textResult } from '../tool-response.js';
3
+ const generateTestSchema = z.object({
4
+ steps: z.array(z.string()).describe('The steps of the test'),
5
+ });
3
6
  export default function generateTest(server) {
4
- const generateTestSchema = z.object({
5
- steps: z.array(z.string()).describe('The steps of the test'),
6
- });
7
7
  const instructions = (params) => [
8
8
  `## Instructions`,
9
9
  `- You are an Appium test generator.`,
10
- `- You are given a scenario and you need to generate a appium test for it.`,
11
- `- Request user to select the platform first using select_device tool and create a session`,
12
- `- Use generate_locators tool to fetch all interactable elements from the current screen and use it to generate the tests`,
13
- `- Element can only be clicked only if it is clickable.`,
14
- `- Text can entered in the element only if it is focusable`,
15
- `- If any interaction on element is failed, retry again with a differnt possible locator in the hierrarchy`,
16
- `- Interact with the app using the tools provided and generate the test`,
17
- '- DO NOT generate test code based on the scenario alone. DO run steps one by one using the tools provided instead.',
18
- '- Only after all steps are completed, emit a Appium test based on message history',
19
- '- Save generated test file in the tests directory',
20
- `- Use generate://code-with-locators resource as reference for code generation`,
21
- `- Always call find_element_tool to retrieve the element UUID before interacting with the element`,
10
+ `- You are given a scenario and you need to generate an Appium test for it.`,
11
+ ``,
12
+ `### Ordered workflow (use these exact MCP tool names)`,
13
+ `1. select_device choose platform/device (local/embedded mode). Skip if the user already connected to a remote Appium URL (then use appium_session_management only).`,
14
+ `2. prepare_ios_simulator or appium_prepare_ios_real_device only when iOS local setup requires it; otherwise skip.`,
15
+ `3. appium_session_management with action=create start a driver session (match platform to select_device unless using remote server mode).`,
16
+ `4. appium_app_lifecycle or other session tools as needed (e.g. activate app, deep link) only if the scenario requires it.`,
17
+ `5. Discover the target element prefer appium_get_active_element when the focused field is enough; otherwise appium_find_element (strategy + selector, or ai_instruction when appropriate). Use generate_locators only for broad inspection/debugging, not for every step.`,
18
+ `6. Interact using the same tool names the server exposes, for example:`,
19
+ ` - appium_gesture with action=tap (or double_tap, long_press, scroll, swipe, scroll_to_element, pinch_zoom) — use the element id from appium_find_element when required`,
20
+ ` - appium_set_value type into an element; optionally appium_mobile_press_key for special keys`,
21
+ ` - appium_get_text / appium_get_element_attribute when assertions need visible state`,
22
+ `- After appium_find_element, the first line of the response is elementId:<uuid> (or an ai-element coordinate token). Pass that value as elementUUID to gestures or appium_set_value as documented in each tool.`,
23
+ ``,
24
+ `- Use generate_locators to fetch interactable elements when you need full-screen locator lists for code generation; pairing with generate://code-with-locators is fine for templates.`,
25
+ `- An element can only be clicked if it is clickable.`,
26
+ `- Text can only be entered into an element if it is focusable (or use appium_set_value with w3cActions when typing into the focused element).`,
27
+ `- If an interaction fails, retry with a different possible locator in the hierarchy.`,
28
+ `- Interact with the app using the tools provided, then generate the test.`,
29
+ `- DO NOT generate test code from the scenario alone. DO run steps one by one with the tools listed above.`,
30
+ `- Only after all steps are completed, emit an Appium/WebdriverIO-style test based on the message history.`,
31
+ `- Save the generated test file in the tests directory.`,
32
+ `- Use the generate://code-with-locators resource as reference for code generation.`,
33
+ `- Always call appium_find_element (or appium_get_active_element when sufficient) to obtain elementId before tapping or setting value on a specific element.`,
22
34
  `Steps:`,
23
35
  ...params.steps.map((step, index) => `- ${index + 1}. ${step}`),
24
36
  ].join('\n');
25
37
  server.addTool({
26
38
  name: 'appium_generate_tests',
27
- description: 'Generate tests for a given mobile app',
39
+ description: 'Generate tests for a mobile app: follow the returned instructions to drive the real session with MCP tools (select_device, appium_session_management, appium_find_element, appium_gesture, appium_set_value, etc.), then emit code. Use generate_locators only when you need a full locator snapshot; prefer appium_find_element for normal steps.',
28
40
  parameters: generateTestSchema,
29
41
  annotations: {
30
42
  readOnlyHint: false,
@@ -1 +1 @@
1
- {"version":3,"file":"generate-tests.js","sourceRoot":"","sources":["../../../src/tools/test-generation/generate-tests.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEjD,MAAM,CAAC,OAAO,UAAU,YAAY,CAAC,MAAe;IAClD,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;QAClC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,CAAC,uBAAuB,CAAC;KAC7D,CAAC,CAAC;IAEH,MAAM,YAAY,GAAG,CAAC,MAA2B,EAAE,EAAE,CACnD;QACE,iBAAiB;QACjB,qCAAqC;QACrC,2EAA2E;QAC3E,2FAA2F;QAC3F,0HAA0H;QAC1H,wDAAwD;QACxD,2DAA2D;QAC3D,2GAA2G;QAC3G,wEAAwE;QACxE,oHAAoH;QACpH,mFAAmF;QACnF,mDAAmD;QACnD,+EAA+E;QAC/E,kGAAkG;QAClG,QAAQ;QACR,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC;KAChE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAEf,MAAM,CAAC,OAAO,CAAC;QACb,IAAI,EAAE,uBAAuB;QAC7B,WAAW,EAAE,uCAAuC;QACpD,UAAU,EAAE,kBAAkB;QAC9B,WAAW,EAAE;YACX,YAAY,EAAE,KAAK;YACnB,aAAa,EAAE,KAAK;SACrB;QACD,OAAO,EAAE,KAAK,EAAE,IAAS,EAAE,QAAa,EAAgB,EAAE,CACxD,UAAU,CAAC,YAAY,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;KAClD,CAAC,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"generate-tests.js","sourceRoot":"","sources":["../../../src/tools/test-generation/generate-tests.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEjD,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,CAAC,uBAAuB,CAAC;CAC7D,CAAC,CAAC;AAEH,MAAM,CAAC,OAAO,UAAU,YAAY,CAAC,MAAe;IAClD,MAAM,YAAY,GAAG,CAAC,MAA2B,EAAE,EAAE,CACnD;QACE,iBAAiB;QACjB,qCAAqC;QACrC,4EAA4E;QAC5E,EAAE;QACF,uDAAuD;QACvD,uKAAuK;QACvK,qHAAqH;QACrH,6IAA6I;QAC7I,6HAA6H;QAC7H,6QAA6Q;QAC7Q,wEAAwE;QACxE,2KAA2K;QAC3K,mGAAmG;QACnG,wFAAwF;QACxF,iNAAiN;QACjN,EAAE;QACF,uLAAuL;QACvL,sDAAsD;QACtD,+IAA+I;QAC/I,sFAAsF;QACtF,2EAA2E;QAC3E,2GAA2G;QAC3G,2GAA2G;QAC3G,wDAAwD;QACxD,oFAAoF;QACpF,6JAA6J;QAC7J,QAAQ;QACR,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC;KAChE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAEf,MAAM,CAAC,OAAO,CAAC;QACb,IAAI,EAAE,uBAAuB;QAC7B,WAAW,EACT,oVAAoV;QACtV,UAAU,EAAE,kBAAkB;QAC9B,WAAW,EAAE;YACX,YAAY,EAAE,KAAK;YACnB,aAAa,EAAE,KAAK;SACrB;QACD,OAAO,EAAE,KAAK,EACZ,IAAwC,EACxC,QAA6C,EACrB,EAAE,CAC1B,UAAU,CAAC,YAAY,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;KAClD,CAAC,CAAC;AACL,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "appium-mcp",
3
3
  "mcpName": "io.github.appium/appium-mcp",
4
- "version": "1.71.5",
4
+ "version": "1.72.0",
5
5
  "type": "module",
6
6
  "repository": {
7
7
  "type": "git",
package/server.json CHANGED
@@ -3,12 +3,12 @@
3
3
  "name": "io.github.appium/appium-mcp",
4
4
  "title": "MCP Appium - Mobile Development and Automation Server",
5
5
  "description": "MCP server for Appium mobile automation on iOS and Android devices with test creation tools.",
6
- "version": "1.71.5",
6
+ "version": "1.72.0",
7
7
  "packages": [
8
8
  {
9
9
  "registryType": "npm",
10
10
  "identifier": "appium-mcp",
11
- "version": "1.71.5",
11
+ "version": "1.72.0",
12
12
  "transport": {
13
13
  "type": "stdio"
14
14
  }
@@ -7,6 +7,7 @@ export default function javaTemplatesResource(server: any) {
7
7
  name: 'Generate Code With Locators',
8
8
  description: `Generate code for the current page with locators which was retrieved from generate_locators tool.
9
9
  The code should be generated in the same priorty order as the locators were generated.
10
+ Always use the actions API code from the template for the gestures.
10
11
  For iOS the priority order is:
11
12
  'id',
12
13
  'accessibility id',
@@ -75,6 +76,35 @@ public class locators {
75
76
  public locators(AppiumDriver driver) {
76
77
  PageFactory.initElements(new AppiumFieldDecorator(driver, Duration.ofSeconds(10)), this);
77
78
  }
79
+
80
+ @Test
81
+ public void sliderTest() {
82
+ WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
83
+ wait.until(ExpectedConditions.elementToBeClickable(AppiumBy.accessibilityId("login"))).click();
84
+ wait.until(ExpectedConditions.elementToBeClickable(AppiumBy.accessibilityId("slider1"))).click();
85
+
86
+ try { Thread.sleep(1000); } catch (InterruptedException e) {}
87
+
88
+ WebElement slider = wait.until(ExpectedConditions.visibilityOfElementLocated(AppiumBy.accessibilityId("slider")));
89
+
90
+ Point location = slider.getLocation();
91
+ Dimension size = slider.getSize();
92
+
93
+ // Swipe right: from left side of the slider to the right side
94
+ int startX = location.getX() + (int)(size.getWidth() * 0.1);
95
+ int endX = location.getX() + (int)(size.getWidth() * 0.9);
96
+ int y = location.getY() + size.getHeight() / 2;
97
+
98
+ PointerInput finger = new PointerInput(PointerInput.Kind.TOUCH, "finger");
99
+ Sequence swipe = new Sequence(finger, 1);
100
+
101
+ swipe.addAction(finger.createPointerMove(Duration.ZERO, PointerInput.Origin.viewport(), startX, y));
102
+ swipe.addAction(finger.createPointerDown(PointerInput.MouseButton.LEFT.asArg()));
103
+ swipe.addAction(finger.createPointerMove(Duration.ofMillis(600), PointerInput.Origin.viewport(), endX, y));
104
+ swipe.addAction(finger.createPointerUp(PointerInput.MouseButton.LEFT.asArg()));
105
+
106
+ driver.perform(Collections.singletonList(swipe));
107
+ }
78
108
  }
79
109
  `,
80
110
  };
Binary file
package/src/server.ts CHANGED
@@ -4,6 +4,26 @@ import registerResources from './resources/index.js';
4
4
  import { listSessions, safeDeleteAllSessions } from './session-store.js';
5
5
  import log from './logger.js';
6
6
 
7
+ type DisconnectSessionPolicy = 'delete_all' | 'skip';
8
+
9
+ /**
10
+ * MCP disconnect policy for Appium sessions tracked by this server.
11
+ * - delete_all (default): end every owned session when the MCP client disconnects (avoids leaked drivers).
12
+ * - skip: keep sessions across disconnects — needed for flaky HTTP/stream clients that reconnect briefly.
13
+ */
14
+ function disconnectSessionPolicyFromEnv(): DisconnectSessionPolicy {
15
+ const raw = process.env.APPIUM_MCP_ON_CLIENT_DISCONNECT?.trim().toLowerCase();
16
+ if (raw === 'skip') {
17
+ return 'skip';
18
+ }
19
+ if (raw !== 'delete_all') {
20
+ log.warn(
21
+ `APPIUM_MCP_ON_CLIENT_DISCONNECT="${raw}" is not recognized (expected delete_all or skip); defaulting to delete_all`
22
+ );
23
+ }
24
+ return 'delete_all';
25
+ }
26
+
7
27
  const server = new FastMCP({
8
28
  name: 'MCP Appium',
9
29
  version: '1.0.0',
@@ -21,9 +41,20 @@ server.on('connect', (event) => {
21
41
 
22
42
  server.on('disconnect', async (event) => {
23
43
  log.info('Client disconnected:', event.session);
44
+ const policy = disconnectSessionPolicyFromEnv();
45
+
24
46
  const ownedSessions = listSessions().filter(
25
47
  (session) => session.ownership === 'owned'
26
48
  );
49
+
50
+ if (ownedSessions.length > 0 && policy === 'skip') {
51
+ log.info(
52
+ `${ownedSessions.length} owned session(s) retained after MCP disconnect ` +
53
+ '(APPIUM_MCP_ON_CLIENT_DISCONNECT=skip). Delete explicitly via appium_session_management (action=delete) when finished.'
54
+ );
55
+ return;
56
+ }
57
+
27
58
  if (ownedSessions.length > 0) {
28
59
  try {
29
60
  log.info(
@@ -1,41 +1,57 @@
1
- import { FastMCP } from 'fastmcp';
1
+ import type { ContentResult, FastMCP } from 'fastmcp';
2
2
  import { z } from 'zod';
3
3
  import { textResult } from '../tool-response.js';
4
4
 
5
- export default function generateTest(server: FastMCP): void {
6
- const generateTestSchema = z.object({
7
- steps: z.array(z.string()).describe('The steps of the test'),
8
- });
5
+ const generateTestSchema = z.object({
6
+ steps: z.array(z.string()).describe('The steps of the test'),
7
+ });
9
8
 
9
+ export default function generateTest(server: FastMCP): void {
10
10
  const instructions = (params: { steps: string[] }) =>
11
11
  [
12
12
  `## Instructions`,
13
13
  `- You are an Appium test generator.`,
14
- `- You are given a scenario and you need to generate a appium test for it.`,
15
- `- Request user to select the platform first using select_device tool and create a session`,
16
- `- Use generate_locators tool to fetch all interactable elements from the current screen and use it to generate the tests`,
17
- `- Element can only be clicked only if it is clickable.`,
18
- `- Text can entered in the element only if it is focusable`,
19
- `- If any interaction on element is failed, retry again with a differnt possible locator in the hierrarchy`,
20
- `- Interact with the app using the tools provided and generate the test`,
21
- '- DO NOT generate test code based on the scenario alone. DO run steps one by one using the tools provided instead.',
22
- '- Only after all steps are completed, emit a Appium test based on message history',
23
- '- Save generated test file in the tests directory',
24
- `- Use generate://code-with-locators resource as reference for code generation`,
25
- `- Always call find_element_tool to retrieve the element UUID before interacting with the element`,
14
+ `- You are given a scenario and you need to generate an Appium test for it.`,
15
+ ``,
16
+ `### Ordered workflow (use these exact MCP tool names)`,
17
+ `1. select_device choose platform/device (local/embedded mode). Skip if the user already connected to a remote Appium URL (then use appium_session_management only).`,
18
+ `2. prepare_ios_simulator or appium_prepare_ios_real_device only when iOS local setup requires it; otherwise skip.`,
19
+ `3. appium_session_management with action=create start a driver session (match platform to select_device unless using remote server mode).`,
20
+ `4. appium_app_lifecycle or other session tools as needed (e.g. activate app, deep link) only if the scenario requires it.`,
21
+ `5. Discover the target element prefer appium_get_active_element when the focused field is enough; otherwise appium_find_element (strategy + selector, or ai_instruction when appropriate). Use generate_locators only for broad inspection/debugging, not for every step.`,
22
+ `6. Interact using the same tool names the server exposes, for example:`,
23
+ ` - appium_gesture with action=tap (or double_tap, long_press, scroll, swipe, scroll_to_element, pinch_zoom) — use the element id from appium_find_element when required`,
24
+ ` - appium_set_value type into an element; optionally appium_mobile_press_key for special keys`,
25
+ ` - appium_get_text / appium_get_element_attribute when assertions need visible state`,
26
+ `- After appium_find_element, the first line of the response is elementId:<uuid> (or an ai-element coordinate token). Pass that value as elementUUID to gestures or appium_set_value as documented in each tool.`,
27
+ ``,
28
+ `- Use generate_locators to fetch interactable elements when you need full-screen locator lists for code generation; pairing with generate://code-with-locators is fine for templates.`,
29
+ `- An element can only be clicked if it is clickable.`,
30
+ `- Text can only be entered into an element if it is focusable (or use appium_set_value with w3cActions when typing into the focused element).`,
31
+ `- If an interaction fails, retry with a different possible locator in the hierarchy.`,
32
+ `- Interact with the app using the tools provided, then generate the test.`,
33
+ `- DO NOT generate test code from the scenario alone. DO run steps one by one with the tools listed above.`,
34
+ `- Only after all steps are completed, emit an Appium/WebdriverIO-style test based on the message history.`,
35
+ `- Save the generated test file in the tests directory.`,
36
+ `- Use the generate://code-with-locators resource as reference for code generation.`,
37
+ `- Always call appium_find_element (or appium_get_active_element when sufficient) to obtain elementId before tapping or setting value on a specific element.`,
26
38
  `Steps:`,
27
39
  ...params.steps.map((step, index) => `- ${index + 1}. ${step}`),
28
40
  ].join('\n');
29
41
 
30
42
  server.addTool({
31
43
  name: 'appium_generate_tests',
32
- description: 'Generate tests for a given mobile app',
44
+ description:
45
+ 'Generate tests for a mobile app: follow the returned instructions to drive the real session with MCP tools (select_device, appium_session_management, appium_find_element, appium_gesture, appium_set_value, etc.), then emit code. Use generate_locators only when you need a full locator snapshot; prefer appium_find_element for normal steps.',
33
46
  parameters: generateTestSchema,
34
47
  annotations: {
35
48
  readOnlyHint: false,
36
49
  openWorldHint: false,
37
50
  },
38
- execute: async (args: any, _context: any): Promise<any> =>
51
+ execute: async (
52
+ args: z.infer<typeof generateTestSchema>,
53
+ _context: Record<string, unknown> | undefined
54
+ ): Promise<ContentResult> =>
39
55
  textResult(instructions({ steps: args.steps })),
40
56
  });
41
57
  }