appium-mcp 1.4.0 → 1.6.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 +12 -0
- package/README.md +5 -0
- package/dist/tests/screenshot.test.d.ts +1 -0
- package/dist/tests/screenshot.test.js +212 -0
- package/dist/tests/screenshot.test.js.map +1 -0
- package/dist/tools/index.js +2 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/interactions/drag-and-drop.d.ts +2 -0
- package/dist/tools/interactions/drag-and-drop.js +173 -0
- package/dist/tools/interactions/drag-and-drop.js.map +1 -0
- package/dist/tools/interactions/screenshot.d.ts +18 -0
- package/dist/tools/interactions/screenshot.js +64 -37
- package/dist/tools/interactions/screenshot.js.map +1 -1
- package/package.json +1 -1
- package/src/tests/screenshot.test.ts +278 -0
- package/src/tools/README.md +1 -0
- package/src/tools/index.ts +2 -0
- package/src/tools/interactions/drag-and-drop.ts +229 -0
- package/src/tools/interactions/screenshot.ts +86 -42
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [1.6.0](https://github.com/appium/appium-mcp/compare/v1.5.0...v1.6.0) (2025-12-22)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
* **screenshot:** add configurable screenshot directory via SCREENSHOTS_DIR env var ([#54](https://github.com/appium/appium-mcp/issues/54)) ([f1957ad](https://github.com/appium/appium-mcp/commit/f1957ad0372d999db67f77b7b85a035546486b86))
|
|
6
|
+
|
|
7
|
+
## [1.5.0](https://github.com/appium/appium-mcp/compare/v1.4.0...v1.5.0) (2025-12-19)
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **interactions:** add drag and drop tool for element and coordinate-based drag operations ([#55](https://github.com/appium/appium-mcp/issues/55)) ([571942e](https://github.com/appium/appium-mcp/commit/571942ecbd2d7ef395cc8e6a8aaf158dcf0f54fb))
|
|
12
|
+
|
|
1
13
|
## [1.4.0](https://github.com/appium/appium-mcp/compare/v1.3.0...v1.4.0) (2025-12-07)
|
|
2
14
|
|
|
3
15
|
### Features
|
package/README.md
CHANGED
|
@@ -157,6 +157,10 @@ Create a `capabilities.json` file to define your device capabilities:
|
|
|
157
157
|
|
|
158
158
|
Set the `CAPABILITIES_CONFIG` environment variable to point to your configuration file.
|
|
159
159
|
|
|
160
|
+
### Screenshots
|
|
161
|
+
|
|
162
|
+
Set the `SCREENSHOTS_DIR` environment variable to specify where screenshots are saved. If not set, screenshots are saved to the current working directory. Supports both absolute and relative paths (relative paths are resolved from the current working directory). The directory is created automatically if it doesn't exist.
|
|
163
|
+
|
|
160
164
|
## 🎯 Available Tools
|
|
161
165
|
|
|
162
166
|
MCP Appium provides a comprehensive set of tools organized into the following categories:
|
|
@@ -193,6 +197,7 @@ MCP Appium provides a comprehensive set of tools organized into the following ca
|
|
|
193
197
|
| `appium_click` | Click on an element |
|
|
194
198
|
| `appium_double_tap` | Perform double tap on an element |
|
|
195
199
|
| `appium_long_press` | Perform a long press (press and hold) gesture on an element |
|
|
200
|
+
| `appium_drag_and_drop` | Perform a drag and drop gesture from a source location to a target location (supports element-to-element, element-to-coordinates, coordinates-to-element, and coordinates-to-coordinates) |
|
|
196
201
|
| `appium_set_value` | Enter text into an input field |
|
|
197
202
|
| `appium_get_text` | Get text content from an element |
|
|
198
203
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach, jest, } from '@jest/globals';
|
|
2
|
+
import { join, isAbsolute } from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* Local implementation of resolveScreenshotDir for testing.
|
|
5
|
+
* This mirrors the implementation in screenshot.ts to avoid importing
|
|
6
|
+
* the module which has heavy dependencies.
|
|
7
|
+
*/
|
|
8
|
+
function resolveScreenshotDir() {
|
|
9
|
+
const screenshotDir = process.env.SCREENSHOTS_DIR;
|
|
10
|
+
if (!screenshotDir) {
|
|
11
|
+
return process.cwd();
|
|
12
|
+
}
|
|
13
|
+
if (isAbsolute(screenshotDir)) {
|
|
14
|
+
return screenshotDir;
|
|
15
|
+
}
|
|
16
|
+
return join(process.cwd(), screenshotDir);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Local implementation of executeScreenshot for testing.
|
|
20
|
+
* This mirrors the implementation in screenshot.ts.
|
|
21
|
+
*/
|
|
22
|
+
async function executeScreenshot(deps) {
|
|
23
|
+
const driver = deps.getDriver();
|
|
24
|
+
if (!driver) {
|
|
25
|
+
throw new Error('No driver found');
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const screenshotBase64 = await driver.getScreenshot();
|
|
29
|
+
const screenshotBuffer = Buffer.from(screenshotBase64, 'base64');
|
|
30
|
+
const timestamp = deps.dateNow();
|
|
31
|
+
const filename = `screenshot_${timestamp}.png`;
|
|
32
|
+
const screenshotDir = deps.resolveScreenshotDir();
|
|
33
|
+
await deps.mkdir(screenshotDir, { recursive: true });
|
|
34
|
+
const filepath = join(screenshotDir, filename);
|
|
35
|
+
await deps.writeFile(filepath, screenshotBuffer);
|
|
36
|
+
return {
|
|
37
|
+
content: [
|
|
38
|
+
{
|
|
39
|
+
type: 'text',
|
|
40
|
+
text: `Screenshot saved successfully to: ${filename}`,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
return {
|
|
47
|
+
content: [
|
|
48
|
+
{
|
|
49
|
+
type: 'text',
|
|
50
|
+
text: `Failed to take screenshot. err: ${err.toString()}`,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
describe('resolveScreenshotDir', () => {
|
|
57
|
+
const originalEnv = process.env.SCREENSHOTS_DIR;
|
|
58
|
+
const cwd = process.cwd();
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
delete process.env.SCREENSHOTS_DIR;
|
|
61
|
+
});
|
|
62
|
+
afterEach(() => {
|
|
63
|
+
if (originalEnv !== undefined) {
|
|
64
|
+
process.env.SCREENSHOTS_DIR = originalEnv;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
delete process.env.SCREENSHOTS_DIR;
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
test('should return process.cwd() when SCREENSHOTS_DIR is not set', () => {
|
|
71
|
+
const result = resolveScreenshotDir();
|
|
72
|
+
expect(result).toBe(cwd);
|
|
73
|
+
});
|
|
74
|
+
test('should return process.cwd() when SCREENSHOTS_DIR is empty string', () => {
|
|
75
|
+
process.env.SCREENSHOTS_DIR = '';
|
|
76
|
+
const result = resolveScreenshotDir();
|
|
77
|
+
expect(result).toBe(cwd);
|
|
78
|
+
});
|
|
79
|
+
test('should return absolute path as-is when SCREENSHOTS_DIR is absolute', () => {
|
|
80
|
+
const absolutePath = '/tmp/screenshots';
|
|
81
|
+
process.env.SCREENSHOTS_DIR = absolutePath;
|
|
82
|
+
const result = resolveScreenshotDir();
|
|
83
|
+
expect(result).toBe(absolutePath);
|
|
84
|
+
});
|
|
85
|
+
test('should join relative path with process.cwd()', () => {
|
|
86
|
+
const relativePath = 'screenshots';
|
|
87
|
+
process.env.SCREENSHOTS_DIR = relativePath;
|
|
88
|
+
const result = resolveScreenshotDir();
|
|
89
|
+
expect(result).toBe(join(cwd, relativePath));
|
|
90
|
+
});
|
|
91
|
+
test('should handle nested relative paths', () => {
|
|
92
|
+
const relativePath = 'output/screenshots/test';
|
|
93
|
+
process.env.SCREENSHOTS_DIR = relativePath;
|
|
94
|
+
const result = resolveScreenshotDir();
|
|
95
|
+
expect(result).toBe(join(cwd, relativePath));
|
|
96
|
+
});
|
|
97
|
+
test('should handle relative path starting with ./', () => {
|
|
98
|
+
const relativePath = './screenshots';
|
|
99
|
+
process.env.SCREENSHOTS_DIR = relativePath;
|
|
100
|
+
const result = resolveScreenshotDir();
|
|
101
|
+
expect(result).toBe(join(cwd, relativePath));
|
|
102
|
+
});
|
|
103
|
+
test('should handle relative path with parent directory reference', () => {
|
|
104
|
+
const relativePath = '../screenshots';
|
|
105
|
+
process.env.SCREENSHOTS_DIR = relativePath;
|
|
106
|
+
const result = resolveScreenshotDir();
|
|
107
|
+
expect(result).toBe(join(cwd, relativePath));
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
describe('executeScreenshot', () => {
|
|
111
|
+
const mockBase64 = 'dGVzdA=='; // 'test' in base64
|
|
112
|
+
const mockTimestamp = 1234567890;
|
|
113
|
+
function createMockDeps(overrides = {}) {
|
|
114
|
+
return {
|
|
115
|
+
getDriver: jest.fn(() => ({
|
|
116
|
+
getScreenshot: jest.fn(() => Promise.resolve(mockBase64)),
|
|
117
|
+
})),
|
|
118
|
+
writeFile: jest.fn(() => Promise.resolve()),
|
|
119
|
+
mkdir: jest.fn(() => Promise.resolve()),
|
|
120
|
+
resolveScreenshotDir: jest.fn(() => '/mock/screenshots'),
|
|
121
|
+
dateNow: jest.fn(() => mockTimestamp),
|
|
122
|
+
...overrides,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
test('should throw error when no driver found', async () => {
|
|
126
|
+
const deps = createMockDeps({
|
|
127
|
+
getDriver: jest.fn(() => null),
|
|
128
|
+
});
|
|
129
|
+
await expect(executeScreenshot(deps)).rejects.toThrow('No driver found');
|
|
130
|
+
});
|
|
131
|
+
test('should return success content with filename', async () => {
|
|
132
|
+
const deps = createMockDeps();
|
|
133
|
+
const result = await executeScreenshot(deps);
|
|
134
|
+
expect(result).toEqual({
|
|
135
|
+
content: [
|
|
136
|
+
{
|
|
137
|
+
type: 'text',
|
|
138
|
+
text: `Screenshot saved successfully to: screenshot_${mockTimestamp}.png`,
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
test('should use resolved screenshot directory from SCREENSHOTS_DIR', async () => {
|
|
144
|
+
const customDir = '/custom/path/screenshots';
|
|
145
|
+
const deps = createMockDeps({
|
|
146
|
+
resolveScreenshotDir: jest.fn(() => customDir),
|
|
147
|
+
});
|
|
148
|
+
await executeScreenshot(deps);
|
|
149
|
+
expect(deps.mkdir).toHaveBeenCalledWith(customDir, { recursive: true });
|
|
150
|
+
expect(deps.writeFile).toHaveBeenCalledWith(join(customDir, `screenshot_${mockTimestamp}.png`), expect.any(Buffer));
|
|
151
|
+
});
|
|
152
|
+
test('should create directory with recursive option', async () => {
|
|
153
|
+
const deps = createMockDeps();
|
|
154
|
+
await executeScreenshot(deps);
|
|
155
|
+
expect(deps.mkdir).toHaveBeenCalledWith('/mock/screenshots', {
|
|
156
|
+
recursive: true,
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
test('should write screenshot buffer to correct filepath', async () => {
|
|
160
|
+
const deps = createMockDeps();
|
|
161
|
+
await executeScreenshot(deps);
|
|
162
|
+
expect(deps.writeFile).toHaveBeenCalledWith(`/mock/screenshots/screenshot_${mockTimestamp}.png`, Buffer.from(mockBase64, 'base64'));
|
|
163
|
+
});
|
|
164
|
+
test('should return error content when screenshot fails', async () => {
|
|
165
|
+
const errorMessage = 'Screenshot capture failed';
|
|
166
|
+
const deps = createMockDeps({
|
|
167
|
+
getDriver: jest.fn(() => ({
|
|
168
|
+
getScreenshot: jest.fn(() => Promise.reject(new Error(errorMessage))),
|
|
169
|
+
})),
|
|
170
|
+
});
|
|
171
|
+
const result = await executeScreenshot(deps);
|
|
172
|
+
expect(result).toEqual({
|
|
173
|
+
content: [
|
|
174
|
+
{
|
|
175
|
+
type: 'text',
|
|
176
|
+
text: `Failed to take screenshot. err: Error: ${errorMessage}`,
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
test('should return error content when mkdir fails', async () => {
|
|
182
|
+
const errorMessage = 'Permission denied';
|
|
183
|
+
const deps = createMockDeps({
|
|
184
|
+
mkdir: jest.fn(() => Promise.reject(new Error(errorMessage))),
|
|
185
|
+
});
|
|
186
|
+
const result = await executeScreenshot(deps);
|
|
187
|
+
expect(result).toEqual({
|
|
188
|
+
content: [
|
|
189
|
+
{
|
|
190
|
+
type: 'text',
|
|
191
|
+
text: `Failed to take screenshot. err: Error: ${errorMessage}`,
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
test('should return error content when writeFile fails', async () => {
|
|
197
|
+
const errorMessage = 'Disk full';
|
|
198
|
+
const deps = createMockDeps({
|
|
199
|
+
writeFile: jest.fn(() => Promise.reject(new Error(errorMessage))),
|
|
200
|
+
});
|
|
201
|
+
const result = await executeScreenshot(deps);
|
|
202
|
+
expect(result).toEqual({
|
|
203
|
+
content: [
|
|
204
|
+
{
|
|
205
|
+
type: 'text',
|
|
206
|
+
text: `Failed to take screenshot. err: Error: ${errorMessage}`,
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
//# sourceMappingURL=screenshot.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"screenshot.test.js","sourceRoot":"","sources":["../../src/tests/screenshot.test.ts"],"names":[],"mappings":"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"}
|
package/dist/tools/index.js
CHANGED
|
@@ -16,6 +16,7 @@ import findElement from './interactions/find.js';
|
|
|
16
16
|
import clickElement from './interactions/click.js';
|
|
17
17
|
import doubleTap from './interactions/double-tap.js';
|
|
18
18
|
import longPress from './interactions/long-press.js';
|
|
19
|
+
import dragAndDrop from './interactions/drag-and-drop.js';
|
|
19
20
|
import setValue from './interactions/set-value.js';
|
|
20
21
|
import getText from './interactions/get-text.js';
|
|
21
22
|
import getPageSource from './interactions/get-page-source.js';
|
|
@@ -107,6 +108,7 @@ export default function registerTools(server) {
|
|
|
107
108
|
clickElement(server);
|
|
108
109
|
doubleTap(server);
|
|
109
110
|
longPress(server);
|
|
111
|
+
dragAndDrop(server);
|
|
110
112
|
setValue(server);
|
|
111
113
|
getText(server);
|
|
112
114
|
getPageSource(server);
|
package/dist/tools/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getDriver, getPlatformName } from '../../session-store.js';
|
|
3
|
+
import { elementUUIDScheme } from '../../schema.js';
|
|
4
|
+
const DROP_PAUSE_DURATION_MS = 150;
|
|
5
|
+
async function performDragAndDrop(driver, startX, startY, endX, endY, duration, longPressDuration) {
|
|
6
|
+
await driver.performActions([
|
|
7
|
+
{
|
|
8
|
+
type: 'pointer',
|
|
9
|
+
id: 'finger1',
|
|
10
|
+
parameters: { pointerType: 'touch' },
|
|
11
|
+
actions: [
|
|
12
|
+
{ type: 'pointerMove', duration: 0, x: startX, y: startY },
|
|
13
|
+
{ type: 'pointerDown', button: 0 },
|
|
14
|
+
{ type: 'pause', duration: longPressDuration },
|
|
15
|
+
{ type: 'pointerMove', duration: duration, x: endX, y: endY },
|
|
16
|
+
{ type: 'pause', duration: DROP_PAUSE_DURATION_MS },
|
|
17
|
+
{ type: 'pointerUp', button: 0 },
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
]);
|
|
21
|
+
}
|
|
22
|
+
export default function dragAndDrop(server) {
|
|
23
|
+
const dragAndDropSchema = z.object({
|
|
24
|
+
sourceElementUUID: elementUUIDScheme
|
|
25
|
+
.trim()
|
|
26
|
+
.min(1)
|
|
27
|
+
.optional()
|
|
28
|
+
.describe('UUID of the source element to drag from. Either sourceElementUUID or sourceX/sourceY must be provided.'),
|
|
29
|
+
sourceX: z
|
|
30
|
+
.number()
|
|
31
|
+
.int()
|
|
32
|
+
.min(0)
|
|
33
|
+
.optional()
|
|
34
|
+
.describe('Starting X coordinate. Required if sourceElementUUID is not provided.'),
|
|
35
|
+
sourceY: z
|
|
36
|
+
.number()
|
|
37
|
+
.int()
|
|
38
|
+
.min(0)
|
|
39
|
+
.optional()
|
|
40
|
+
.describe('Starting Y coordinate. Required if sourceElementUUID is not provided.'),
|
|
41
|
+
targetElementUUID: elementUUIDScheme
|
|
42
|
+
.trim()
|
|
43
|
+
.min(1)
|
|
44
|
+
.optional()
|
|
45
|
+
.describe('UUID of the target element to drop on. Either targetElementUUID or targetX/targetY must be provided.'),
|
|
46
|
+
targetX: z
|
|
47
|
+
.number()
|
|
48
|
+
.int()
|
|
49
|
+
.min(0)
|
|
50
|
+
.optional()
|
|
51
|
+
.describe('Ending X coordinate. Required if targetElementUUID is not provided.'),
|
|
52
|
+
targetY: z
|
|
53
|
+
.number()
|
|
54
|
+
.int()
|
|
55
|
+
.min(0)
|
|
56
|
+
.optional()
|
|
57
|
+
.describe('Ending Y coordinate. Required if targetElementUUID is not provided.'),
|
|
58
|
+
duration: z
|
|
59
|
+
.number()
|
|
60
|
+
.int()
|
|
61
|
+
.min(100)
|
|
62
|
+
.max(5000)
|
|
63
|
+
.default(1200)
|
|
64
|
+
.optional()
|
|
65
|
+
.describe('Duration of the drag movement in milliseconds. Default is 1200ms.'),
|
|
66
|
+
longPressDuration: z
|
|
67
|
+
.number()
|
|
68
|
+
.int()
|
|
69
|
+
.min(400)
|
|
70
|
+
.max(2000)
|
|
71
|
+
.default(600)
|
|
72
|
+
.optional()
|
|
73
|
+
.describe('Duration of the long press before dragging in milliseconds. Default is 600ms.'),
|
|
74
|
+
});
|
|
75
|
+
server.addTool({
|
|
76
|
+
name: 'appium_drag_and_drop',
|
|
77
|
+
description: `Perform a drag and drop gesture from a source location to a target location.
|
|
78
|
+
The gesture includes:
|
|
79
|
+
1. Long press (default 600ms, configurable) on the source to initiate drag mode
|
|
80
|
+
2. While holding, drag to the target location
|
|
81
|
+
3. Release at the target to complete the drop
|
|
82
|
+
|
|
83
|
+
Supports four modes:
|
|
84
|
+
1. Element to Element: Drag from one element to another element
|
|
85
|
+
2. Element to Coordinates: Drag from an element to specific coordinates
|
|
86
|
+
3. Coordinates to Element: Drag from coordinates to an element
|
|
87
|
+
4. Coordinates to Coordinates: Drag from coordinates to coordinates
|
|
88
|
+
|
|
89
|
+
This is useful for reordering lists, moving items, drag-to-delete, and other drag interactions.`,
|
|
90
|
+
parameters: dragAndDropSchema,
|
|
91
|
+
annotations: {
|
|
92
|
+
readOnlyHint: false,
|
|
93
|
+
openWorldHint: false,
|
|
94
|
+
},
|
|
95
|
+
execute: async (args, context) => {
|
|
96
|
+
const driver = getDriver();
|
|
97
|
+
if (!driver) {
|
|
98
|
+
throw new Error('No driver found');
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
const platform = getPlatformName(driver);
|
|
102
|
+
const duration = args.duration || 1200;
|
|
103
|
+
const longPressDuration = args.longPressDuration || 600;
|
|
104
|
+
if (!args.sourceElementUUID &&
|
|
105
|
+
(args.sourceX === undefined || args.sourceY === undefined)) {
|
|
106
|
+
throw new Error('Either sourceElementUUID or both sourceX and sourceY must be provided.');
|
|
107
|
+
}
|
|
108
|
+
if (!args.targetElementUUID &&
|
|
109
|
+
(args.targetX === undefined || args.targetY === undefined)) {
|
|
110
|
+
throw new Error('Either targetElementUUID or both targetX and targetY must be provided.');
|
|
111
|
+
}
|
|
112
|
+
let startX, startY;
|
|
113
|
+
let endX, endY;
|
|
114
|
+
if (args.sourceElementUUID) {
|
|
115
|
+
const rect = await driver.getElementRect(args.sourceElementUUID);
|
|
116
|
+
startX = Math.floor(rect.x + rect.width / 2);
|
|
117
|
+
startY = Math.floor(rect.y + rect.height / 2);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
startX = args.sourceX;
|
|
121
|
+
startY = args.sourceY;
|
|
122
|
+
}
|
|
123
|
+
if (args.targetElementUUID) {
|
|
124
|
+
const rect = await driver.getElementRect(args.targetElementUUID);
|
|
125
|
+
endX = Math.floor(rect.x + rect.width / 2);
|
|
126
|
+
endY = Math.floor(rect.y + rect.height / 2);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
endX = args.targetX;
|
|
130
|
+
endY = args.targetY;
|
|
131
|
+
}
|
|
132
|
+
const { width, height } = await driver.getWindowSize();
|
|
133
|
+
if (startX < 0 || startX >= width || startY < 0 || startY >= height) {
|
|
134
|
+
throw new Error(`Source coordinates (${startX}, ${startY}) are out of screen bounds (${width}x${height})`);
|
|
135
|
+
}
|
|
136
|
+
if (endX < 0 || endX >= width || endY < 0 || endY >= height) {
|
|
137
|
+
throw new Error(`Target coordinates (${endX}, ${endY}) are out of screen bounds (${width}x${height})`);
|
|
138
|
+
}
|
|
139
|
+
if (platform === 'Android' || platform === 'iOS') {
|
|
140
|
+
await performDragAndDrop(driver, startX, startY, endX, endY, duration, longPressDuration);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
throw new Error(`Unsupported platform: ${platform}. Only Android and iOS are supported.`);
|
|
144
|
+
}
|
|
145
|
+
const sourceDesc = args.sourceElementUUID
|
|
146
|
+
? `element ${args.sourceElementUUID}`
|
|
147
|
+
: `coordinates (${startX}, ${startY})`;
|
|
148
|
+
const targetDesc = args.targetElementUUID
|
|
149
|
+
? `element ${args.targetElementUUID}`
|
|
150
|
+
: `coordinates (${endX}, ${endY})`;
|
|
151
|
+
return {
|
|
152
|
+
content: [
|
|
153
|
+
{
|
|
154
|
+
type: 'text',
|
|
155
|
+
text: `Successfully performed drag and drop from ${sourceDesc} to ${targetDesc}.`,
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
return {
|
|
162
|
+
content: [
|
|
163
|
+
{
|
|
164
|
+
type: 'text',
|
|
165
|
+
text: `Failed to perform drag and drop. Error: ${err.toString()}`,
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=drag-and-drop.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drag-and-drop.js","sourceRoot":"","sources":["../../../src/tools/interactions/drag-and-drop.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACpE,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEpD,MAAM,sBAAsB,GAAG,GAAG,CAAC;AAEnC,KAAK,UAAU,kBAAkB,CAC/B,MAAW,EACX,MAAc,EACd,MAAc,EACd,IAAY,EACZ,IAAY,EACZ,QAAgB,EAChB,iBAAyB;IAEzB,MAAM,MAAM,CAAC,cAAc,CAAC;QAC1B;YACE,IAAI,EAAE,SAAS;YACf,EAAE,EAAE,SAAS;YACb,UAAU,EAAE,EAAE,WAAW,EAAE,OAAO,EAAE;YACpC,OAAO,EAAE;gBACP,EAAE,IAAI,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE;gBAC1D,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,CAAC,EAAE;gBAClC,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAE;gBAC9C,EAAE,IAAI,EAAE,aAAa,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE;gBAC7D,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,sBAAsB,EAAE;gBACnD,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,CAAC,EAAE;aACjC;SACF;KACF,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,OAAO,UAAU,WAAW,CAAC,MAAe;IACjD,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAC;QACjC,iBAAiB,EAAE,iBAAiB;aACjC,IAAI,EAAE;aACN,GAAG,CAAC,CAAC,CAAC;aACN,QAAQ,EAAE;aACV,QAAQ,CACP,wGAAwG,CACzG;QACH,OAAO,EAAE,CAAC;aACP,MAAM,EAAE;aACR,GAAG,EAAE;aACL,GAAG,CAAC,CAAC,CAAC;aACN,QAAQ,EAAE;aACV,QAAQ,CACP,uEAAuE,CACxE;QACH,OAAO,EAAE,CAAC;aACP,MAAM,EAAE;aACR,GAAG,EAAE;aACL,GAAG,CAAC,CAAC,CAAC;aACN,QAAQ,EAAE;aACV,QAAQ,CACP,uEAAuE,CACxE;QACH,iBAAiB,EAAE,iBAAiB;aACjC,IAAI,EAAE;aACN,GAAG,CAAC,CAAC,CAAC;aACN,QAAQ,EAAE;aACV,QAAQ,CACP,sGAAsG,CACvG;QACH,OAAO,EAAE,CAAC;aACP,MAAM,EAAE;aACR,GAAG,EAAE;aACL,GAAG,CAAC,CAAC,CAAC;aACN,QAAQ,EAAE;aACV,QAAQ,CACP,qEAAqE,CACtE;QACH,OAAO,EAAE,CAAC;aACP,MAAM,EAAE;aACR,GAAG,EAAE;aACL,GAAG,CAAC,CAAC,CAAC;aACN,QAAQ,EAAE;aACV,QAAQ,CACP,qEAAqE,CACtE;QACH,QAAQ,EAAE,CAAC;aACR,MAAM,EAAE;aACR,GAAG,EAAE;aACL,GAAG,CAAC,GAAG,CAAC;aACR,GAAG,CAAC,IAAI,CAAC;aACT,OAAO,CAAC,IAAI,CAAC;aACb,QAAQ,EAAE;aACV,QAAQ,CACP,mEAAmE,CACpE;QACH,iBAAiB,EAAE,CAAC;aACjB,MAAM,EAAE;aACR,GAAG,EAAE;aACL,GAAG,CAAC,GAAG,CAAC;aACR,GAAG,CAAC,IAAI,CAAC;aACT,OAAO,CAAC,GAAG,CAAC;aACZ,QAAQ,EAAE;aACV,QAAQ,CACP,+EAA+E,CAChF;KACJ,CAAC,CAAC;IAEH,MAAM,CAAC,OAAO,CAAC;QACb,IAAI,EAAE,sBAAsB;QAC5B,WAAW,EAAE;;;;;;;;;;;;sGAYqF;QAClG,UAAU,EAAE,iBAAiB;QAC7B,WAAW,EAAE;YACX,YAAY,EAAE,KAAK;YACnB,aAAa,EAAE,KAAK;SACrB;QACD,OAAO,EAAE,KAAK,EAAE,IAAS,EAAE,OAAY,EAAgB,EAAE;YACvD,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;YAC3B,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAC;YACrC,CAAC;YAED,IAAI,CAAC;gBACH,MAAM,QAAQ,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;gBACzC,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC;gBACvC,MAAM,iBAAiB,GAAG,IAAI,CAAC,iBAAiB,IAAI,GAAG,CAAC;gBAExD,IACE,CAAC,IAAI,CAAC,iBAAiB;oBACvB,CAAC,IAAI,CAAC,OAAO,KAAK,SAAS,IAAI,IAAI,CAAC,OAAO,KAAK,SAAS,CAAC,EAC1D,CAAC;oBACD,MAAM,IAAI,KAAK,CACb,wEAAwE,CACzE,CAAC;gBACJ,CAAC;gBAED,IACE,CAAC,IAAI,CAAC,iBAAiB;oBACvB,CAAC,IAAI,CAAC,OAAO,KAAK,SAAS,IAAI,IAAI,CAAC,OAAO,KAAK,SAAS,CAAC,EAC1D,CAAC;oBACD,MAAM,IAAI,KAAK,CACb,wEAAwE,CACzE,CAAC;gBACJ,CAAC;gBAED,IAAI,MAAc,EAAE,MAAc,CAAC;gBACnC,IAAI,IAAY,EAAE,IAAY,CAAC;gBAE/B,IAAI,IAAI,CAAC,iBAAiB,EAAE,CAAC;oBAC3B,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;oBACjE,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;oBAC7C,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;gBAChD,CAAC;qBAAM,CAAC;oBACN,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC;oBACtB,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC;gBACxB,CAAC;gBAED,IAAI,IAAI,CAAC,iBAAiB,EAAE,CAAC;oBAC3B,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;oBACjE,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;oBAC3C,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;gBAC9C,CAAC;qBAAM,CAAC;oBACN,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC;oBACpB,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC;gBACtB,CAAC;gBAED,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,aAAa,EAAE,CAAC;gBACvD,IAAI,MAAM,GAAG,CAAC,IAAI,MAAM,IAAI,KAAK,IAAI,MAAM,GAAG,CAAC,IAAI,MAAM,IAAI,MAAM,EAAE,CAAC;oBACpE,MAAM,IAAI,KAAK,CACb,uBAAuB,MAAM,KAAK,MAAM,+BAA+B,KAAK,IAAI,MAAM,GAAG,CAC1F,CAAC;gBACJ,CAAC;gBACD,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,IAAI,KAAK,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,IAAI,MAAM,EAAE,CAAC;oBAC5D,MAAM,IAAI,KAAK,CACb,uBAAuB,IAAI,KAAK,IAAI,+BAA+B,KAAK,IAAI,MAAM,GAAG,CACtF,CAAC;gBACJ,CAAC;gBAED,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,KAAK,EAAE,CAAC;oBACjD,MAAM,kBAAkB,CACtB,MAAM,EACN,MAAM,EACN,MAAM,EACN,IAAI,EACJ,IAAI,EACJ,QAAQ,EACR,iBAAiB,CAClB,CAAC;gBACJ,CAAC;qBAAM,CAAC;oBACN,MAAM,IAAI,KAAK,CACb,yBAAyB,QAAQ,uCAAuC,CACzE,CAAC;gBACJ,CAAC;gBAED,MAAM,UAAU,GAAG,IAAI,CAAC,iBAAiB;oBACvC,CAAC,CAAC,WAAW,IAAI,CAAC,iBAAiB,EAAE;oBACrC,CAAC,CAAC,gBAAgB,MAAM,KAAK,MAAM,GAAG,CAAC;gBACzC,MAAM,UAAU,GAAG,IAAI,CAAC,iBAAiB;oBACvC,CAAC,CAAC,WAAW,IAAI,CAAC,iBAAiB,EAAE;oBACrC,CAAC,CAAC,gBAAgB,IAAI,KAAK,IAAI,GAAG,CAAC;gBAErC,OAAO;oBACL,OAAO,EAAE;wBACP;4BACE,IAAI,EAAE,MAAM;4BACZ,IAAI,EAAE,6CAA6C,UAAU,OAAO,UAAU,GAAG;yBAClF;qBACF;iBACF,CAAC;YACJ,CAAC;YAAC,OAAO,GAAQ,EAAE,CAAC;gBAClB,OAAO;oBACL,OAAO,EAAE;wBACP;4BACE,IAAI,EAAE,MAAM;4BACZ,IAAI,EAAE,2CAA2C,GAAG,CAAC,QAAQ,EAAE,EAAE;yBAClE;qBACF;iBACF,CAAC;YACJ,CAAC;QACH,CAAC;KACF,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -1,2 +1,20 @@
|
|
|
1
1
|
import { FastMCP } from 'fastmcp/dist/FastMCP.js';
|
|
2
|
+
import { writeFile, mkdir } from 'fs/promises';
|
|
3
|
+
/**
|
|
4
|
+
* Resolves the screenshot directory path.
|
|
5
|
+
* - If SCREENSHOTS_DIR is not set, returns process.cwd()
|
|
6
|
+
* - If SCREENSHOTS_DIR is absolute, returns it as-is
|
|
7
|
+
* - If SCREENSHOTS_DIR is relative, joins it with process.cwd()
|
|
8
|
+
*/
|
|
9
|
+
export declare function resolveScreenshotDir(): string;
|
|
10
|
+
export interface ScreenshotDeps {
|
|
11
|
+
getDriver: () => {
|
|
12
|
+
getScreenshot: () => Promise<string>;
|
|
13
|
+
} | null;
|
|
14
|
+
writeFile: typeof writeFile;
|
|
15
|
+
mkdir: typeof mkdir;
|
|
16
|
+
resolveScreenshotDir: typeof resolveScreenshotDir;
|
|
17
|
+
dateNow: () => number;
|
|
18
|
+
}
|
|
19
|
+
export declare function executeScreenshot(deps?: ScreenshotDeps): Promise<any>;
|
|
2
20
|
export default function screenshot(server: FastMCP): void;
|
|
@@ -1,6 +1,67 @@
|
|
|
1
1
|
import { getDriver } from '../../session-store.js';
|
|
2
|
-
import { writeFile } from 'fs/promises';
|
|
3
|
-
import { join } from 'path';
|
|
2
|
+
import { writeFile, mkdir } from 'fs/promises';
|
|
3
|
+
import { join, isAbsolute } from 'path';
|
|
4
|
+
/**
|
|
5
|
+
* Resolves the screenshot directory path.
|
|
6
|
+
* - If SCREENSHOTS_DIR is not set, returns process.cwd()
|
|
7
|
+
* - If SCREENSHOTS_DIR is absolute, returns it as-is
|
|
8
|
+
* - If SCREENSHOTS_DIR is relative, joins it with process.cwd()
|
|
9
|
+
*/
|
|
10
|
+
export function resolveScreenshotDir() {
|
|
11
|
+
const screenshotDir = process.env.SCREENSHOTS_DIR;
|
|
12
|
+
if (!screenshotDir) {
|
|
13
|
+
return process.cwd();
|
|
14
|
+
}
|
|
15
|
+
if (isAbsolute(screenshotDir)) {
|
|
16
|
+
return screenshotDir;
|
|
17
|
+
}
|
|
18
|
+
return join(process.cwd(), screenshotDir);
|
|
19
|
+
}
|
|
20
|
+
const defaultDeps = {
|
|
21
|
+
getDriver,
|
|
22
|
+
writeFile,
|
|
23
|
+
mkdir,
|
|
24
|
+
resolveScreenshotDir,
|
|
25
|
+
dateNow: () => Date.now(),
|
|
26
|
+
};
|
|
27
|
+
export async function executeScreenshot(deps = defaultDeps) {
|
|
28
|
+
const driver = deps.getDriver();
|
|
29
|
+
if (!driver) {
|
|
30
|
+
throw new Error('No driver found');
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const screenshotBase64 = await driver.getScreenshot();
|
|
34
|
+
// Convert base64 to buffer
|
|
35
|
+
const screenshotBuffer = Buffer.from(screenshotBase64, 'base64');
|
|
36
|
+
// Generate filename with timestamp
|
|
37
|
+
const timestamp = deps.dateNow();
|
|
38
|
+
const filename = `screenshot_${timestamp}.png`;
|
|
39
|
+
const screenshotDir = deps.resolveScreenshotDir();
|
|
40
|
+
// Create a directory if it doesn't exist
|
|
41
|
+
await deps.mkdir(screenshotDir, { recursive: true });
|
|
42
|
+
const filepath = join(screenshotDir, filename);
|
|
43
|
+
// Save screenshot to disk
|
|
44
|
+
await deps.writeFile(filepath, screenshotBuffer);
|
|
45
|
+
return {
|
|
46
|
+
content: [
|
|
47
|
+
{
|
|
48
|
+
type: 'text',
|
|
49
|
+
text: `Screenshot saved successfully to: ${filepath}`,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
return {
|
|
56
|
+
content: [
|
|
57
|
+
{
|
|
58
|
+
type: 'text',
|
|
59
|
+
text: `Failed to take screenshot. err: ${err.toString()}`,
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
4
65
|
export default function screenshot(server) {
|
|
5
66
|
server.addTool({
|
|
6
67
|
name: 'appium_screenshot',
|
|
@@ -9,41 +70,7 @@ export default function screenshot(server) {
|
|
|
9
70
|
readOnlyHint: false,
|
|
10
71
|
openWorldHint: false,
|
|
11
72
|
},
|
|
12
|
-
execute: async (
|
|
13
|
-
const driver = getDriver();
|
|
14
|
-
if (!driver) {
|
|
15
|
-
throw new Error('No driver found');
|
|
16
|
-
}
|
|
17
|
-
try {
|
|
18
|
-
const screenshotBase64 = await driver.getScreenshot();
|
|
19
|
-
// Convert base64 to buffer
|
|
20
|
-
const screenshotBuffer = Buffer.from(screenshotBase64, 'base64');
|
|
21
|
-
// Generate filename with timestamp
|
|
22
|
-
const timestamp = Date.now();
|
|
23
|
-
const filename = `screenshot_${timestamp}.png`;
|
|
24
|
-
const filepath = join(process.cwd(), filename);
|
|
25
|
-
// Save screenshot to disk
|
|
26
|
-
await writeFile(filepath, screenshotBuffer);
|
|
27
|
-
return {
|
|
28
|
-
content: [
|
|
29
|
-
{
|
|
30
|
-
type: 'text',
|
|
31
|
-
text: `Screenshot saved successfully to: ${filename}`,
|
|
32
|
-
},
|
|
33
|
-
],
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
catch (err) {
|
|
37
|
-
return {
|
|
38
|
-
content: [
|
|
39
|
-
{
|
|
40
|
-
type: 'text',
|
|
41
|
-
text: `Failed to take screenshot. err: ${err.toString()}`,
|
|
42
|
-
},
|
|
43
|
-
],
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
},
|
|
73
|
+
execute: async () => executeScreenshot(),
|
|
47
74
|
});
|
|
48
75
|
}
|
|
49
76
|
//# sourceMappingURL=screenshot.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"screenshot.js","sourceRoot":"","sources":["../../../src/tools/interactions/screenshot.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"screenshot.js","sourceRoot":"","sources":["../../../src/tools/interactions/screenshot.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAExC;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB;IAClC,MAAM,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC;IAElD,IAAI,CAAC,aAAa,EAAE,CAAC;QACnB,OAAO,OAAO,CAAC,GAAG,EAAE,CAAC;IACvB,CAAC;IAED,IAAI,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;QAC9B,OAAO,aAAa,CAAC;IACvB,CAAC;IAED,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,aAAa,CAAC,CAAC;AAC5C,CAAC;AAUD,MAAM,WAAW,GAAmB;IAClC,SAAS;IACT,SAAS;IACT,KAAK;IACL,oBAAoB;IACpB,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE;CAC1B,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,OAAuB,WAAW;IAElC,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;IAChC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAC;IACrC,CAAC;IAED,IAAI,CAAC;QACH,MAAM,gBAAgB,GAAG,MAAM,MAAM,CAAC,aAAa,EAAE,CAAC;QAEtD,2BAA2B;QAC3B,MAAM,gBAAgB,GAAG,MAAM,CAAC,IAAI,CAAC,gBAAgB,EAAE,QAAQ,CAAC,CAAC;QAEjE,mCAAmC;QACnC,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC;QACjC,MAAM,QAAQ,GAAG,cAAc,SAAS,MAAM,CAAC;QAC/C,MAAM,aAAa,GAAG,IAAI,CAAC,oBAAoB,EAAE,CAAC;QAElD,yCAAyC;QACzC,MAAM,IAAI,CAAC,KAAK,CAAC,aAAa,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAErD,MAAM,QAAQ,GAAG,IAAI,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAC;QAE/C,0BAA0B;QAC1B,MAAM,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAAC;QAEjD,OAAO;YACL,OAAO,EAAE;gBACP;oBACE,IAAI,EAAE,MAAM;oBACZ,IAAI,EAAE,qCAAqC,QAAQ,EAAE;iBACtD;aACF;SACF,CAAC;IACJ,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAClB,OAAO;YACL,OAAO,EAAE;gBACP;oBACE,IAAI,EAAE,MAAM;oBACZ,IAAI,EAAE,mCAAmC,GAAG,CAAC,QAAQ,EAAE,EAAE;iBAC1D;aACF;SACF,CAAC;IACJ,CAAC;AACH,CAAC;AAED,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,MAAe;IAChD,MAAM,CAAC,OAAO,CAAC;QACb,IAAI,EAAE,mBAAmB;QACzB,WAAW,EACT,iEAAiE;QACnE,WAAW,EAAE;YACX,YAAY,EAAE,KAAK;YACnB,aAAa,EAAE,KAAK;SACrB;QACD,OAAO,EAAE,KAAK,IAAkB,EAAE,CAAC,iBAAiB,EAAE;KACvD,CAAC,CAAC;AACL,CAAC"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import {
|
|
2
|
+
describe,
|
|
3
|
+
test,
|
|
4
|
+
expect,
|
|
5
|
+
beforeEach,
|
|
6
|
+
afterEach,
|
|
7
|
+
jest,
|
|
8
|
+
} from '@jest/globals';
|
|
9
|
+
import { join, isAbsolute } from 'path';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Local implementation of resolveScreenshotDir for testing.
|
|
13
|
+
* This mirrors the implementation in screenshot.ts to avoid importing
|
|
14
|
+
* the module which has heavy dependencies.
|
|
15
|
+
*/
|
|
16
|
+
function resolveScreenshotDir(): string {
|
|
17
|
+
const screenshotDir = process.env.SCREENSHOTS_DIR;
|
|
18
|
+
|
|
19
|
+
if (!screenshotDir) {
|
|
20
|
+
return process.cwd();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (isAbsolute(screenshotDir)) {
|
|
24
|
+
return screenshotDir;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return join(process.cwd(), screenshotDir);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Interface for screenshot dependencies (mirrors screenshot.ts).
|
|
32
|
+
*/
|
|
33
|
+
interface ScreenshotDeps {
|
|
34
|
+
getDriver: () => { getScreenshot: () => Promise<string> } | null;
|
|
35
|
+
writeFile: (path: string, data: Buffer) => Promise<void>;
|
|
36
|
+
mkdir: (path: string, options: { recursive: boolean }) => Promise<void>;
|
|
37
|
+
resolveScreenshotDir: () => string;
|
|
38
|
+
dateNow: () => number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Local implementation of executeScreenshot for testing.
|
|
43
|
+
* This mirrors the implementation in screenshot.ts.
|
|
44
|
+
*/
|
|
45
|
+
async function executeScreenshot(deps: ScreenshotDeps): Promise<any> {
|
|
46
|
+
const driver = deps.getDriver();
|
|
47
|
+
if (!driver) {
|
|
48
|
+
throw new Error('No driver found');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const screenshotBase64 = await driver.getScreenshot();
|
|
53
|
+
const screenshotBuffer = Buffer.from(screenshotBase64, 'base64');
|
|
54
|
+
const timestamp = deps.dateNow();
|
|
55
|
+
const filename = `screenshot_${timestamp}.png`;
|
|
56
|
+
const screenshotDir = deps.resolveScreenshotDir();
|
|
57
|
+
|
|
58
|
+
await deps.mkdir(screenshotDir, { recursive: true });
|
|
59
|
+
const filepath = join(screenshotDir, filename);
|
|
60
|
+
await deps.writeFile(filepath, screenshotBuffer);
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
content: [
|
|
64
|
+
{
|
|
65
|
+
type: 'text',
|
|
66
|
+
text: `Screenshot saved successfully to: ${filename}`,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
};
|
|
70
|
+
} catch (err: any) {
|
|
71
|
+
return {
|
|
72
|
+
content: [
|
|
73
|
+
{
|
|
74
|
+
type: 'text',
|
|
75
|
+
text: `Failed to take screenshot. err: ${err.toString()}`,
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
describe('resolveScreenshotDir', () => {
|
|
83
|
+
const originalEnv = process.env.SCREENSHOTS_DIR;
|
|
84
|
+
const cwd = process.cwd();
|
|
85
|
+
|
|
86
|
+
beforeEach(() => {
|
|
87
|
+
delete process.env.SCREENSHOTS_DIR;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
afterEach(() => {
|
|
91
|
+
if (originalEnv !== undefined) {
|
|
92
|
+
process.env.SCREENSHOTS_DIR = originalEnv;
|
|
93
|
+
} else {
|
|
94
|
+
delete process.env.SCREENSHOTS_DIR;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('should return process.cwd() when SCREENSHOTS_DIR is not set', () => {
|
|
99
|
+
const result = resolveScreenshotDir();
|
|
100
|
+
expect(result).toBe(cwd);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('should return process.cwd() when SCREENSHOTS_DIR is empty string', () => {
|
|
104
|
+
process.env.SCREENSHOTS_DIR = '';
|
|
105
|
+
const result = resolveScreenshotDir();
|
|
106
|
+
expect(result).toBe(cwd);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('should return absolute path as-is when SCREENSHOTS_DIR is absolute', () => {
|
|
110
|
+
const absolutePath = '/tmp/screenshots';
|
|
111
|
+
process.env.SCREENSHOTS_DIR = absolutePath;
|
|
112
|
+
const result = resolveScreenshotDir();
|
|
113
|
+
expect(result).toBe(absolutePath);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('should join relative path with process.cwd()', () => {
|
|
117
|
+
const relativePath = 'screenshots';
|
|
118
|
+
process.env.SCREENSHOTS_DIR = relativePath;
|
|
119
|
+
const result = resolveScreenshotDir();
|
|
120
|
+
expect(result).toBe(join(cwd, relativePath));
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('should handle nested relative paths', () => {
|
|
124
|
+
const relativePath = 'output/screenshots/test';
|
|
125
|
+
process.env.SCREENSHOTS_DIR = relativePath;
|
|
126
|
+
const result = resolveScreenshotDir();
|
|
127
|
+
expect(result).toBe(join(cwd, relativePath));
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('should handle relative path starting with ./', () => {
|
|
131
|
+
const relativePath = './screenshots';
|
|
132
|
+
process.env.SCREENSHOTS_DIR = relativePath;
|
|
133
|
+
const result = resolveScreenshotDir();
|
|
134
|
+
expect(result).toBe(join(cwd, relativePath));
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('should handle relative path with parent directory reference', () => {
|
|
138
|
+
const relativePath = '../screenshots';
|
|
139
|
+
process.env.SCREENSHOTS_DIR = relativePath;
|
|
140
|
+
const result = resolveScreenshotDir();
|
|
141
|
+
expect(result).toBe(join(cwd, relativePath));
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('executeScreenshot', () => {
|
|
146
|
+
const mockBase64 = 'dGVzdA=='; // 'test' in base64
|
|
147
|
+
const mockTimestamp = 1234567890;
|
|
148
|
+
|
|
149
|
+
function createMockDeps(
|
|
150
|
+
overrides: Partial<ScreenshotDeps> = {}
|
|
151
|
+
): ScreenshotDeps {
|
|
152
|
+
return {
|
|
153
|
+
getDriver: jest.fn(() => ({
|
|
154
|
+
getScreenshot: jest.fn(() => Promise.resolve(mockBase64)),
|
|
155
|
+
})) as any,
|
|
156
|
+
writeFile: jest.fn(() => Promise.resolve()) as any,
|
|
157
|
+
mkdir: jest.fn(() => Promise.resolve()) as any,
|
|
158
|
+
resolveScreenshotDir: jest.fn(() => '/mock/screenshots') as any,
|
|
159
|
+
dateNow: jest.fn(() => mockTimestamp) as any,
|
|
160
|
+
...overrides,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
test('should throw error when no driver found', async () => {
|
|
165
|
+
const deps = createMockDeps({
|
|
166
|
+
getDriver: jest.fn(() => null) as any,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
await expect(executeScreenshot(deps)).rejects.toThrow('No driver found');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('should return success content with filename', async () => {
|
|
173
|
+
const deps = createMockDeps();
|
|
174
|
+
|
|
175
|
+
const result = await executeScreenshot(deps);
|
|
176
|
+
|
|
177
|
+
expect(result).toEqual({
|
|
178
|
+
content: [
|
|
179
|
+
{
|
|
180
|
+
type: 'text',
|
|
181
|
+
text: `Screenshot saved successfully to: screenshot_${mockTimestamp}.png`,
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('should use resolved screenshot directory from SCREENSHOTS_DIR', async () => {
|
|
188
|
+
const customDir = '/custom/path/screenshots';
|
|
189
|
+
const deps = createMockDeps({
|
|
190
|
+
resolveScreenshotDir: jest.fn(() => customDir) as any,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
await executeScreenshot(deps);
|
|
194
|
+
|
|
195
|
+
expect(deps.mkdir).toHaveBeenCalledWith(customDir, { recursive: true });
|
|
196
|
+
expect(deps.writeFile).toHaveBeenCalledWith(
|
|
197
|
+
join(customDir, `screenshot_${mockTimestamp}.png`),
|
|
198
|
+
expect.any(Buffer)
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('should create directory with recursive option', async () => {
|
|
203
|
+
const deps = createMockDeps();
|
|
204
|
+
|
|
205
|
+
await executeScreenshot(deps);
|
|
206
|
+
|
|
207
|
+
expect(deps.mkdir).toHaveBeenCalledWith('/mock/screenshots', {
|
|
208
|
+
recursive: true,
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('should write screenshot buffer to correct filepath', async () => {
|
|
213
|
+
const deps = createMockDeps();
|
|
214
|
+
|
|
215
|
+
await executeScreenshot(deps);
|
|
216
|
+
|
|
217
|
+
expect(deps.writeFile).toHaveBeenCalledWith(
|
|
218
|
+
`/mock/screenshots/screenshot_${mockTimestamp}.png`,
|
|
219
|
+
Buffer.from(mockBase64, 'base64')
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('should return error content when screenshot fails', async () => {
|
|
224
|
+
const errorMessage = 'Screenshot capture failed';
|
|
225
|
+
const deps = createMockDeps({
|
|
226
|
+
getDriver: jest.fn(() => ({
|
|
227
|
+
getScreenshot: jest.fn(() => Promise.reject(new Error(errorMessage))),
|
|
228
|
+
})) as any,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const result = await executeScreenshot(deps);
|
|
232
|
+
|
|
233
|
+
expect(result).toEqual({
|
|
234
|
+
content: [
|
|
235
|
+
{
|
|
236
|
+
type: 'text',
|
|
237
|
+
text: `Failed to take screenshot. err: Error: ${errorMessage}`,
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('should return error content when mkdir fails', async () => {
|
|
244
|
+
const errorMessage = 'Permission denied';
|
|
245
|
+
const deps = createMockDeps({
|
|
246
|
+
mkdir: jest.fn(() => Promise.reject(new Error(errorMessage))) as any,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const result = await executeScreenshot(deps);
|
|
250
|
+
|
|
251
|
+
expect(result).toEqual({
|
|
252
|
+
content: [
|
|
253
|
+
{
|
|
254
|
+
type: 'text',
|
|
255
|
+
text: `Failed to take screenshot. err: Error: ${errorMessage}`,
|
|
256
|
+
},
|
|
257
|
+
],
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test('should return error content when writeFile fails', async () => {
|
|
262
|
+
const errorMessage = 'Disk full';
|
|
263
|
+
const deps = createMockDeps({
|
|
264
|
+
writeFile: jest.fn(() => Promise.reject(new Error(errorMessage))) as any,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const result = await executeScreenshot(deps);
|
|
268
|
+
|
|
269
|
+
expect(result).toEqual({
|
|
270
|
+
content: [
|
|
271
|
+
{
|
|
272
|
+
type: 'text',
|
|
273
|
+
text: `Failed to take screenshot. err: Error: ${errorMessage}`,
|
|
274
|
+
},
|
|
275
|
+
],
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
});
|
package/src/tools/README.md
CHANGED
|
@@ -29,6 +29,7 @@ This directory contains all MCP tools available in MCP Appium.
|
|
|
29
29
|
- `click.ts` - Click elements
|
|
30
30
|
- `double-tap.ts` - Double tap elements
|
|
31
31
|
- `long-press.ts` - Long press (press and hold) elements
|
|
32
|
+
- `drag-and-drop.ts` - Drag and drop elements or coordinates
|
|
32
33
|
- `set-value.ts` - Enter text
|
|
33
34
|
- `get-text.ts` - Get element text
|
|
34
35
|
- `get-page-source.ts` - Get page source (XML) from current screen
|
package/src/tools/index.ts
CHANGED
|
@@ -31,6 +31,7 @@ import findElement from './interactions/find.js';
|
|
|
31
31
|
import clickElement from './interactions/click.js';
|
|
32
32
|
import doubleTap from './interactions/double-tap.js';
|
|
33
33
|
import longPress from './interactions/long-press.js';
|
|
34
|
+
import dragAndDrop from './interactions/drag-and-drop.js';
|
|
34
35
|
import setValue from './interactions/set-value.js';
|
|
35
36
|
import getText from './interactions/get-text.js';
|
|
36
37
|
import getPageSource from './interactions/get-page-source.js';
|
|
@@ -131,6 +132,7 @@ export default function registerTools(server: FastMCP): void {
|
|
|
131
132
|
clickElement(server);
|
|
132
133
|
doubleTap(server);
|
|
133
134
|
longPress(server);
|
|
135
|
+
dragAndDrop(server);
|
|
134
136
|
setValue(server);
|
|
135
137
|
getText(server);
|
|
136
138
|
getPageSource(server);
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { FastMCP } from 'fastmcp/dist/FastMCP.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getDriver, getPlatformName } from '../../session-store.js';
|
|
4
|
+
import { elementUUIDScheme } from '../../schema.js';
|
|
5
|
+
|
|
6
|
+
const DROP_PAUSE_DURATION_MS = 150;
|
|
7
|
+
|
|
8
|
+
async function performDragAndDrop(
|
|
9
|
+
driver: any,
|
|
10
|
+
startX: number,
|
|
11
|
+
startY: number,
|
|
12
|
+
endX: number,
|
|
13
|
+
endY: number,
|
|
14
|
+
duration: number,
|
|
15
|
+
longPressDuration: number
|
|
16
|
+
): Promise<void> {
|
|
17
|
+
await driver.performActions([
|
|
18
|
+
{
|
|
19
|
+
type: 'pointer',
|
|
20
|
+
id: 'finger1',
|
|
21
|
+
parameters: { pointerType: 'touch' },
|
|
22
|
+
actions: [
|
|
23
|
+
{ type: 'pointerMove', duration: 0, x: startX, y: startY },
|
|
24
|
+
{ type: 'pointerDown', button: 0 },
|
|
25
|
+
{ type: 'pause', duration: longPressDuration },
|
|
26
|
+
{ type: 'pointerMove', duration: duration, x: endX, y: endY },
|
|
27
|
+
{ type: 'pause', duration: DROP_PAUSE_DURATION_MS },
|
|
28
|
+
{ type: 'pointerUp', button: 0 },
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default function dragAndDrop(server: FastMCP): void {
|
|
35
|
+
const dragAndDropSchema = z.object({
|
|
36
|
+
sourceElementUUID: elementUUIDScheme
|
|
37
|
+
.trim()
|
|
38
|
+
.min(1)
|
|
39
|
+
.optional()
|
|
40
|
+
.describe(
|
|
41
|
+
'UUID of the source element to drag from. Either sourceElementUUID or sourceX/sourceY must be provided.'
|
|
42
|
+
),
|
|
43
|
+
sourceX: z
|
|
44
|
+
.number()
|
|
45
|
+
.int()
|
|
46
|
+
.min(0)
|
|
47
|
+
.optional()
|
|
48
|
+
.describe(
|
|
49
|
+
'Starting X coordinate. Required if sourceElementUUID is not provided.'
|
|
50
|
+
),
|
|
51
|
+
sourceY: z
|
|
52
|
+
.number()
|
|
53
|
+
.int()
|
|
54
|
+
.min(0)
|
|
55
|
+
.optional()
|
|
56
|
+
.describe(
|
|
57
|
+
'Starting Y coordinate. Required if sourceElementUUID is not provided.'
|
|
58
|
+
),
|
|
59
|
+
targetElementUUID: elementUUIDScheme
|
|
60
|
+
.trim()
|
|
61
|
+
.min(1)
|
|
62
|
+
.optional()
|
|
63
|
+
.describe(
|
|
64
|
+
'UUID of the target element to drop on. Either targetElementUUID or targetX/targetY must be provided.'
|
|
65
|
+
),
|
|
66
|
+
targetX: z
|
|
67
|
+
.number()
|
|
68
|
+
.int()
|
|
69
|
+
.min(0)
|
|
70
|
+
.optional()
|
|
71
|
+
.describe(
|
|
72
|
+
'Ending X coordinate. Required if targetElementUUID is not provided.'
|
|
73
|
+
),
|
|
74
|
+
targetY: z
|
|
75
|
+
.number()
|
|
76
|
+
.int()
|
|
77
|
+
.min(0)
|
|
78
|
+
.optional()
|
|
79
|
+
.describe(
|
|
80
|
+
'Ending Y coordinate. Required if targetElementUUID is not provided.'
|
|
81
|
+
),
|
|
82
|
+
duration: z
|
|
83
|
+
.number()
|
|
84
|
+
.int()
|
|
85
|
+
.min(100)
|
|
86
|
+
.max(5000)
|
|
87
|
+
.default(1200)
|
|
88
|
+
.optional()
|
|
89
|
+
.describe(
|
|
90
|
+
'Duration of the drag movement in milliseconds. Default is 1200ms.'
|
|
91
|
+
),
|
|
92
|
+
longPressDuration: z
|
|
93
|
+
.number()
|
|
94
|
+
.int()
|
|
95
|
+
.min(400)
|
|
96
|
+
.max(2000)
|
|
97
|
+
.default(600)
|
|
98
|
+
.optional()
|
|
99
|
+
.describe(
|
|
100
|
+
'Duration of the long press before dragging in milliseconds. Default is 600ms.'
|
|
101
|
+
),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
server.addTool({
|
|
105
|
+
name: 'appium_drag_and_drop',
|
|
106
|
+
description: `Perform a drag and drop gesture from a source location to a target location.
|
|
107
|
+
The gesture includes:
|
|
108
|
+
1. Long press (default 600ms, configurable) on the source to initiate drag mode
|
|
109
|
+
2. While holding, drag to the target location
|
|
110
|
+
3. Release at the target to complete the drop
|
|
111
|
+
|
|
112
|
+
Supports four modes:
|
|
113
|
+
1. Element to Element: Drag from one element to another element
|
|
114
|
+
2. Element to Coordinates: Drag from an element to specific coordinates
|
|
115
|
+
3. Coordinates to Element: Drag from coordinates to an element
|
|
116
|
+
4. Coordinates to Coordinates: Drag from coordinates to coordinates
|
|
117
|
+
|
|
118
|
+
This is useful for reordering lists, moving items, drag-to-delete, and other drag interactions.`,
|
|
119
|
+
parameters: dragAndDropSchema,
|
|
120
|
+
annotations: {
|
|
121
|
+
readOnlyHint: false,
|
|
122
|
+
openWorldHint: false,
|
|
123
|
+
},
|
|
124
|
+
execute: async (args: any, context: any): Promise<any> => {
|
|
125
|
+
const driver = getDriver();
|
|
126
|
+
if (!driver) {
|
|
127
|
+
throw new Error('No driver found');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const platform = getPlatformName(driver);
|
|
132
|
+
const duration = args.duration || 1200;
|
|
133
|
+
const longPressDuration = args.longPressDuration || 600;
|
|
134
|
+
|
|
135
|
+
if (
|
|
136
|
+
!args.sourceElementUUID &&
|
|
137
|
+
(args.sourceX === undefined || args.sourceY === undefined)
|
|
138
|
+
) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
'Either sourceElementUUID or both sourceX and sourceY must be provided.'
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (
|
|
145
|
+
!args.targetElementUUID &&
|
|
146
|
+
(args.targetX === undefined || args.targetY === undefined)
|
|
147
|
+
) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
'Either targetElementUUID or both targetX and targetY must be provided.'
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let startX: number, startY: number;
|
|
154
|
+
let endX: number, endY: number;
|
|
155
|
+
|
|
156
|
+
if (args.sourceElementUUID) {
|
|
157
|
+
const rect = await driver.getElementRect(args.sourceElementUUID);
|
|
158
|
+
startX = Math.floor(rect.x + rect.width / 2);
|
|
159
|
+
startY = Math.floor(rect.y + rect.height / 2);
|
|
160
|
+
} else {
|
|
161
|
+
startX = args.sourceX;
|
|
162
|
+
startY = args.sourceY;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (args.targetElementUUID) {
|
|
166
|
+
const rect = await driver.getElementRect(args.targetElementUUID);
|
|
167
|
+
endX = Math.floor(rect.x + rect.width / 2);
|
|
168
|
+
endY = Math.floor(rect.y + rect.height / 2);
|
|
169
|
+
} else {
|
|
170
|
+
endX = args.targetX;
|
|
171
|
+
endY = args.targetY;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const { width, height } = await driver.getWindowSize();
|
|
175
|
+
if (startX < 0 || startX >= width || startY < 0 || startY >= height) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`Source coordinates (${startX}, ${startY}) are out of screen bounds (${width}x${height})`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
if (endX < 0 || endX >= width || endY < 0 || endY >= height) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
`Target coordinates (${endX}, ${endY}) are out of screen bounds (${width}x${height})`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (platform === 'Android' || platform === 'iOS') {
|
|
187
|
+
await performDragAndDrop(
|
|
188
|
+
driver,
|
|
189
|
+
startX,
|
|
190
|
+
startY,
|
|
191
|
+
endX,
|
|
192
|
+
endY,
|
|
193
|
+
duration,
|
|
194
|
+
longPressDuration
|
|
195
|
+
);
|
|
196
|
+
} else {
|
|
197
|
+
throw new Error(
|
|
198
|
+
`Unsupported platform: ${platform}. Only Android and iOS are supported.`
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const sourceDesc = args.sourceElementUUID
|
|
203
|
+
? `element ${args.sourceElementUUID}`
|
|
204
|
+
: `coordinates (${startX}, ${startY})`;
|
|
205
|
+
const targetDesc = args.targetElementUUID
|
|
206
|
+
? `element ${args.targetElementUUID}`
|
|
207
|
+
: `coordinates (${endX}, ${endY})`;
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
content: [
|
|
211
|
+
{
|
|
212
|
+
type: 'text',
|
|
213
|
+
text: `Successfully performed drag and drop from ${sourceDesc} to ${targetDesc}.`,
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
};
|
|
217
|
+
} catch (err: any) {
|
|
218
|
+
return {
|
|
219
|
+
content: [
|
|
220
|
+
{
|
|
221
|
+
type: 'text',
|
|
222
|
+
text: `Failed to perform drag and drop. Error: ${err.toString()}`,
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
}
|
|
@@ -1,8 +1,90 @@
|
|
|
1
1
|
import { FastMCP } from 'fastmcp/dist/FastMCP.js';
|
|
2
|
-
import { z } from 'zod';
|
|
3
2
|
import { getDriver } from '../../session-store.js';
|
|
4
|
-
import { writeFile } from 'fs/promises';
|
|
5
|
-
import { join } from 'path';
|
|
3
|
+
import { writeFile, mkdir } from 'fs/promises';
|
|
4
|
+
import { join, isAbsolute } from 'path';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Resolves the screenshot directory path.
|
|
8
|
+
* - If SCREENSHOTS_DIR is not set, returns process.cwd()
|
|
9
|
+
* - If SCREENSHOTS_DIR is absolute, returns it as-is
|
|
10
|
+
* - If SCREENSHOTS_DIR is relative, joins it with process.cwd()
|
|
11
|
+
*/
|
|
12
|
+
export function resolveScreenshotDir(): string {
|
|
13
|
+
const screenshotDir = process.env.SCREENSHOTS_DIR;
|
|
14
|
+
|
|
15
|
+
if (!screenshotDir) {
|
|
16
|
+
return process.cwd();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (isAbsolute(screenshotDir)) {
|
|
20
|
+
return screenshotDir;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return join(process.cwd(), screenshotDir);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ScreenshotDeps {
|
|
27
|
+
getDriver: () => { getScreenshot: () => Promise<string> } | null;
|
|
28
|
+
writeFile: typeof writeFile;
|
|
29
|
+
mkdir: typeof mkdir;
|
|
30
|
+
resolveScreenshotDir: typeof resolveScreenshotDir;
|
|
31
|
+
dateNow: () => number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const defaultDeps: ScreenshotDeps = {
|
|
35
|
+
getDriver,
|
|
36
|
+
writeFile,
|
|
37
|
+
mkdir,
|
|
38
|
+
resolveScreenshotDir,
|
|
39
|
+
dateNow: () => Date.now(),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export async function executeScreenshot(
|
|
43
|
+
deps: ScreenshotDeps = defaultDeps
|
|
44
|
+
): Promise<any> {
|
|
45
|
+
const driver = deps.getDriver();
|
|
46
|
+
if (!driver) {
|
|
47
|
+
throw new Error('No driver found');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const screenshotBase64 = await driver.getScreenshot();
|
|
52
|
+
|
|
53
|
+
// Convert base64 to buffer
|
|
54
|
+
const screenshotBuffer = Buffer.from(screenshotBase64, 'base64');
|
|
55
|
+
|
|
56
|
+
// Generate filename with timestamp
|
|
57
|
+
const timestamp = deps.dateNow();
|
|
58
|
+
const filename = `screenshot_${timestamp}.png`;
|
|
59
|
+
const screenshotDir = deps.resolveScreenshotDir();
|
|
60
|
+
|
|
61
|
+
// Create a directory if it doesn't exist
|
|
62
|
+
await deps.mkdir(screenshotDir, { recursive: true });
|
|
63
|
+
|
|
64
|
+
const filepath = join(screenshotDir, filename);
|
|
65
|
+
|
|
66
|
+
// Save screenshot to disk
|
|
67
|
+
await deps.writeFile(filepath, screenshotBuffer);
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
content: [
|
|
71
|
+
{
|
|
72
|
+
type: 'text',
|
|
73
|
+
text: `Screenshot saved successfully to: ${filepath}`,
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
} catch (err: any) {
|
|
78
|
+
return {
|
|
79
|
+
content: [
|
|
80
|
+
{
|
|
81
|
+
type: 'text',
|
|
82
|
+
text: `Failed to take screenshot. err: ${err.toString()}`,
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
6
88
|
|
|
7
89
|
export default function screenshot(server: FastMCP): void {
|
|
8
90
|
server.addTool({
|
|
@@ -13,44 +95,6 @@ export default function screenshot(server: FastMCP): void {
|
|
|
13
95
|
readOnlyHint: false,
|
|
14
96
|
openWorldHint: false,
|
|
15
97
|
},
|
|
16
|
-
execute: async (
|
|
17
|
-
const driver = getDriver();
|
|
18
|
-
if (!driver) {
|
|
19
|
-
throw new Error('No driver found');
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
const screenshotBase64 = await driver.getScreenshot();
|
|
24
|
-
|
|
25
|
-
// Convert base64 to buffer
|
|
26
|
-
const screenshotBuffer = Buffer.from(screenshotBase64, 'base64');
|
|
27
|
-
|
|
28
|
-
// Generate filename with timestamp
|
|
29
|
-
const timestamp = Date.now();
|
|
30
|
-
const filename = `screenshot_${timestamp}.png`;
|
|
31
|
-
const filepath = join(process.cwd(), filename);
|
|
32
|
-
|
|
33
|
-
// Save screenshot to disk
|
|
34
|
-
await writeFile(filepath, screenshotBuffer);
|
|
35
|
-
|
|
36
|
-
return {
|
|
37
|
-
content: [
|
|
38
|
-
{
|
|
39
|
-
type: 'text',
|
|
40
|
-
text: `Screenshot saved successfully to: ${filename}`,
|
|
41
|
-
},
|
|
42
|
-
],
|
|
43
|
-
};
|
|
44
|
-
} catch (err: any) {
|
|
45
|
-
return {
|
|
46
|
-
content: [
|
|
47
|
-
{
|
|
48
|
-
type: 'text',
|
|
49
|
-
text: `Failed to take screenshot. err: ${err.toString()}`,
|
|
50
|
-
},
|
|
51
|
-
],
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
},
|
|
98
|
+
execute: async (): Promise<any> => executeScreenshot(),
|
|
55
99
|
});
|
|
56
100
|
}
|