appium-mcp 1.86.2 → 1.86.4

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.
Files changed (34) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/ai-finder/vision-finder.d.ts.map +1 -1
  3. package/dist/ai-finder/vision-finder.js +3 -4
  4. package/dist/ai-finder/vision-finder.js.map +1 -1
  5. package/dist/documentation.js +4 -4
  6. package/dist/documentation.js.map +1 -1
  7. package/dist/tools/interactions/screen-recording.js +3 -3
  8. package/dist/tools/interactions/screen-recording.js.map +1 -1
  9. package/dist/tools/interactions/screenshot.d.ts +4 -3
  10. package/dist/tools/interactions/screenshot.d.ts.map +1 -1
  11. package/dist/tools/interactions/screenshot.js +3 -4
  12. package/dist/tools/interactions/screenshot.js.map +1 -1
  13. package/dist/tools/ios/prepare-ios-real-device.d.ts.map +1 -1
  14. package/dist/tools/ios/prepare-ios-real-device.js +21 -28
  15. package/dist/tools/ios/prepare-ios-real-device.js.map +1 -1
  16. package/dist/tools/ios/prepare-ios-simulator.d.ts.map +1 -1
  17. package/dist/tools/ios/prepare-ios-simulator.js +8 -16
  18. package/dist/tools/ios/prepare-ios-simulator.js.map +1 -1
  19. package/dist/tools/session/create-session.d.ts.map +1 -1
  20. package/dist/tools/session/create-session.js +5 -4
  21. package/dist/tools/session/create-session.js.map +1 -1
  22. package/dist/ui/mcp-ui-utils.d.ts.map +1 -1
  23. package/dist/ui/mcp-ui-utils.js +53 -28
  24. package/dist/ui/mcp-ui-utils.js.map +1 -1
  25. package/package.json +1 -1
  26. package/server.json +2 -2
  27. package/src/ai-finder/vision-finder.ts +3 -4
  28. package/src/documentation.ts +4 -4
  29. package/src/tools/interactions/screen-recording.ts +3 -3
  30. package/src/tools/interactions/screenshot.ts +8 -6
  31. package/src/tools/ios/prepare-ios-real-device.ts +21 -27
  32. package/src/tools/ios/prepare-ios-simulator.ts +8 -15
  33. package/src/tools/session/create-session.ts +7 -4
  34. package/src/ui/mcp-ui-utils.ts +59 -30
@@ -8,10 +8,8 @@ import { z } from 'zod';
8
8
  import { exec } from 'node:child_process';
9
9
  import { promisify } from 'node:util';
10
10
  import path from 'node:path';
11
- import { access, mkdir, unlink, readdir, stat, rm } from 'node:fs/promises';
12
- import { constants } from 'node:fs';
13
11
  import os from 'node:os';
14
- import { net, plist, zip } from '@appium/support';
12
+ import { fs, net, plist, zip } from '@appium/support';
15
13
  import { Simctl } from 'node-simctl';
16
14
  import { IOSManager } from '../../devicemanager/ios-manager.js';
17
15
  import log from '../../logger.js';
@@ -44,19 +42,14 @@ interface WDAState {
44
42
  }
45
43
 
46
44
  async function fileExists(filePath: string): Promise<boolean> {
47
- try {
48
- await access(filePath, constants.F_OK);
49
- return true;
50
- } catch {
51
- return false;
52
- }
45
+ return await fs.hasAccess(filePath);
53
46
  }
54
47
 
55
48
  // ── WDA download helpers ──
56
49
 
57
50
  async function cleanupFile(filePath: string): Promise<void> {
58
51
  try {
59
- await unlink(filePath);
52
+ await fs.unlink(filePath);
60
53
  } catch (err: any) {
61
54
  if (err.code !== 'ENOENT') {
62
55
  throw err;
@@ -105,11 +98,11 @@ async function getLatestWDAVersionFromCache(): Promise<string | null> {
105
98
  return null;
106
99
  }
107
100
 
108
- const entries = await readdir(wdaCacheDir);
101
+ const entries = await fs.readdir(wdaCacheDir);
109
102
  const versions = await Promise.all(
110
103
  entries.map(async (dir) => {
111
104
  const dirPath = path.join(wdaCacheDir, dir);
112
- const stats = await stat(dirPath);
105
+ const stats = await fs.stat(dirPath);
113
106
  return stats.isDirectory() ? dir : null;
114
107
  })
115
108
  );
@@ -263,11 +256,11 @@ async function resolveWdaAppPath(
263
256
 
264
257
  // Clean any prior (possibly partial) extraction before downloading
265
258
  if (await fileExists(extractDir)) {
266
- await rm(extractDir, { recursive: true, force: true });
259
+ await fs.rimraf(extractDir);
267
260
  }
268
261
 
269
- await mkdir(versionCacheDir, { recursive: true });
270
- await mkdir(extractDir, { recursive: true });
262
+ await fs.mkdirp(versionCacheDir);
263
+ await fs.mkdirp(extractDir);
271
264
 
272
265
  const downloadUrl = `https://github.com/appium/WebDriverAgent/releases/download/v${wdaVersion}/${artifactPrefix}-Build-Sim-${archStr}.zip`;
273
266
  log.info(`Downloading prebuilt WDA v${wdaVersion}...`);
@@ -1,5 +1,4 @@
1
- import { access, readFile } from 'node:fs/promises';
2
- import { constants } from 'node:fs';
1
+ import { fs } from '@appium/support';
3
2
  import { URL } from 'node:url';
4
3
  import { getPortFromUrl } from '../../utils/url.js';
5
4
  import { AndroidUiautomator2Driver } from 'appium-uiautomator2-driver';
@@ -452,8 +451,12 @@ async function loadCapabilitiesConfig(): Promise<CapabilitiesConfig> {
452
451
  }
453
452
 
454
453
  try {
455
- await access(configPath, constants.F_OK);
456
- const configContent = await readFile(configPath, 'utf8');
454
+ if (!(await fs.hasAccess(configPath))) {
455
+ throw new Error(
456
+ `Capabilities config does not exist or is not accessible: ${configPath}`
457
+ );
458
+ }
459
+ const configContent = await fs.readFile(configPath, 'utf8');
457
460
  return JSON.parse(configContent);
458
461
  } catch (error: unknown) {
459
462
  log.warn(`Failed to parse capabilities config: ${toolErrorMessage(error)}`);
@@ -59,23 +59,29 @@ export function createDevicePickerUI(
59
59
  : 'Android Devices';
60
60
 
61
61
  const deviceCards = devices
62
- .map(
63
- (device, index) => `
64
- <div class="device-card" data-udid="${device.udid}" data-index="${index}">
62
+ .map((device, index) => {
63
+ const udid = String(device.udid);
64
+ const name = device.name || udid;
65
+ const state = device.state ? String(device.state) : undefined;
66
+ const type = device.type ? String(device.type) : undefined;
67
+ const stateClass = state ? sanitizeClassName(state) : '';
68
+
69
+ return `
70
+ <div class="device-card" data-udid="${escapeHtml(udid)}" data-index="${index}">
65
71
  <div class="device-header">
66
- <h3>${device.name || device.udid}</h3>
67
- ${device.state ? `<span class="device-state ${device.state.toLowerCase()}">${device.state}</span>` : ''}
72
+ <h3>${escapeHtml(name)}</h3>
73
+ ${state ? `<span class="device-state ${stateClass}">${escapeHtml(state)}</span>` : ''}
68
74
  </div>
69
75
  <div class="device-details">
70
- <p><strong>UDID:</strong> <code>${device.udid}</code></p>
71
- ${device.type ? `<p><strong>Type:</strong> ${device.type}</p>` : ''}
76
+ <p><strong>UDID:</strong> <code>${escapeHtml(udid)}</code></p>
77
+ ${type ? `<p><strong>Type:</strong> ${escapeHtml(type)}</p>` : ''}
72
78
  </div>
73
- <button class="select-device-btn" onclick="selectDevice('${device.udid}')">
79
+ <button class="select-device-btn" data-udid="${escapeHtml(udid)}" onclick="selectDevice(this.dataset.udid)">
74
80
  Select Device
75
81
  </button>
76
82
  </div>
77
- `
78
- )
83
+ `;
84
+ })
79
85
  .join('');
80
86
 
81
87
  return `
@@ -215,8 +221,8 @@ export function createDevicePickerUI(
215
221
  payload: {
216
222
  intent: 'select-device',
217
223
  params: {
218
- platform: '${platform}',
219
- ${deviceType ? `iosDeviceType: '${deviceType}',` : ''}
224
+ platform: ${escapeScriptValue(platform)},
225
+ ${deviceType ? `iosDeviceType: ${escapeScriptValue(deviceType)},` : ''}
220
226
  deviceUdid: udid
221
227
  }
222
228
  }
@@ -247,6 +253,8 @@ export function createScreenshotViewerUI(
247
253
  screenshotBase64: string,
248
254
  filepath: string
249
255
  ): string {
256
+ const downloadFilename = filepath.split('/').pop() || 'screenshot.png';
257
+
250
258
  return `
251
259
  <!DOCTYPE html>
252
260
  <html lang="en">
@@ -370,7 +378,7 @@ export function createScreenshotViewerUI(
370
378
  <div class="toolbar">
371
379
  <div class="toolbar-left">
372
380
  <span style="font-size: 14px; font-weight: 500;">📸 Screenshot</span>
373
- <span class="filepath">${filepath}</span>
381
+ <span class="filepath">${escapeHtml(filepath)}</span>
374
382
  </div>
375
383
  <div class="toolbar-right">
376
384
  <button class="btn btn-secondary" onclick="downloadScreenshot()">Download</button>
@@ -378,7 +386,7 @@ export function createScreenshotViewerUI(
378
386
  </div>
379
387
  </div>
380
388
  <div class="image-container" id="imageContainer">
381
- <img src="data:image/png;base64,${screenshotBase64}"
389
+ <img src="data:image/png;base64,${escapeHtml(screenshotBase64)}"
382
390
  alt="Screenshot"
383
391
  class="screenshot-img"
384
392
  id="screenshotImg"
@@ -420,7 +428,7 @@ export function createScreenshotViewerUI(
420
428
  function downloadScreenshot() {
421
429
  const link = document.createElement('a');
422
430
  link.href = img.src;
423
- link.download = '${filepath.split('/').pop() || 'screenshot.png'}';
431
+ link.download = ${escapeScriptValue(downloadFilename)};
424
432
  link.click();
425
433
  }
426
434
 
@@ -470,6 +478,9 @@ export function createSessionDashboardUI(sessionInfo: {
470
478
  sessionIdStr.length > 8
471
479
  ? `${sessionIdStr.substring(0, 8)}...`
472
480
  : sessionIdStr;
481
+ const deviceName = sessionInfo.deviceName;
482
+ const platformVersion = sessionInfo.platformVersion;
483
+ const udid = sessionInfo.udid;
473
484
 
474
485
  return `
475
486
  <!DOCTYPE html>
@@ -590,42 +601,42 @@ export function createSessionDashboardUI(sessionInfo: {
590
601
  <div class="info-grid">
591
602
  <div class="info-card">
592
603
  <label>Session ID</label>
593
- <value>${sessionIdDisplay}</value>
604
+ <value>${escapeHtml(sessionIdDisplay)}</value>
594
605
  </div>
595
606
  <div class="info-card">
596
607
  <label>Platform</label>
597
- <value>${sessionInfo.platform}</value>
608
+ <value>${escapeHtml(sessionInfo.platform)}</value>
598
609
  </div>
599
610
  <div class="info-card">
600
611
  <label>Automation</label>
601
- <value>${sessionInfo.automationName}</value>
612
+ <value>${escapeHtml(sessionInfo.automationName)}</value>
602
613
  </div>
603
614
  ${
604
- sessionInfo.deviceName
615
+ deviceName
605
616
  ? `
606
617
  <div class="info-card">
607
618
  <label>Device</label>
608
- <value>${sessionInfo.deviceName}</value>
619
+ <value>${escapeHtml(deviceName)}</value>
609
620
  </div>
610
621
  `
611
622
  : ''
612
623
  }
613
624
  ${
614
- sessionInfo.platformVersion
625
+ platformVersion
615
626
  ? `
616
627
  <div class="info-card">
617
628
  <label>Platform Version</label>
618
- <value>${sessionInfo.platformVersion}</value>
629
+ <value>${escapeHtml(platformVersion)}</value>
619
630
  </div>
620
631
  `
621
632
  : ''
622
633
  }
623
634
  ${
624
- sessionInfo.udid
635
+ udid
625
636
  ? `
626
637
  <div class="info-card">
627
638
  <label>UDID</label>
628
- <value><code style="font-size: 12px;">${sessionInfo.udid}</code></value>
639
+ <value><code style="font-size: 12px;">${escapeHtml(udid)}</code></value>
629
640
  </div>
630
641
  `
631
642
  : ''
@@ -1260,17 +1271,17 @@ export function createAppListUI(
1260
1271
  const appCards = apps
1261
1272
  .map(
1262
1273
  (app) => `
1263
- <div class="app-card" data-package="${app.packageName}">
1274
+ <div class="app-card" data-package="${escapeHtml(app.packageName)}">
1264
1275
  <div class="app-header">
1265
- <h3>${app.appName || app.packageName}</h3>
1276
+ <h3>${escapeHtml(app.appName || app.packageName)}</h3>
1266
1277
  </div>
1267
1278
  <div class="app-details">
1268
- <p><strong>Package:</strong> <code>${app.packageName}</code></p>
1279
+ <p><strong>Package:</strong> <code>${escapeHtml(app.packageName)}</code></p>
1269
1280
  </div>
1270
1281
  <div class="app-actions">
1271
- <button class="btn btn-primary" onclick="activateApp('${app.packageName}')">Activate</button>
1272
- <button class="btn btn-secondary" onclick="terminateApp('${app.packageName}')">Terminate</button>
1273
- <button class="btn btn-danger" onclick="uninstallApp('${app.packageName}')">Uninstall</button>
1282
+ <button class="btn btn-primary" data-package="${escapeHtml(app.packageName)}" onclick="activateApp(this.dataset.package)">Activate</button>
1283
+ <button class="btn btn-secondary" data-package="${escapeHtml(app.packageName)}" onclick="terminateApp(this.dataset.package)">Terminate</button>
1284
+ <button class="btn btn-danger" data-package="${escapeHtml(app.packageName)}" onclick="uninstallApp(this.dataset.package)">Uninstall</button>
1274
1285
  </div>
1275
1286
  </div>
1276
1287
  `
@@ -1652,3 +1663,21 @@ function escapeHtml(value: unknown): string {
1652
1663
  .replace(/"/g, '&quot;')
1653
1664
  .replace(/'/g, '&#039;');
1654
1665
  }
1666
+
1667
+ function escapeScriptValue(value: unknown): string {
1668
+ return JSON.stringify(String(value))
1669
+ .replace(/</g, '\\u003C')
1670
+ .replace(/>/g, '\\u003E')
1671
+ .replace(/&/g, '\\u0026')
1672
+ .replace(/\u2028/g, '\\u2028')
1673
+ .replace(/\u2029/g, '\\u2029');
1674
+ }
1675
+
1676
+ function sanitizeClassName(value: unknown): string {
1677
+ return escapeHtml(
1678
+ String(value)
1679
+ .toLowerCase()
1680
+ .replace(/[^a-z0-9_-]+/g, '-')
1681
+ .replace(/^-+|-+$/g, '')
1682
+ );
1683
+ }