|
1 | 1 | import { type McpServerProxy } from '@expo/mcp-tunnel'; |
2 | | -import fs from 'node:fs'; |
3 | 2 | import { z } from 'zod'; |
4 | | -import { $, tmpfile, within } from 'zx'; |
| 3 | +import { $, within } from 'zx'; |
5 | 4 |
|
6 | 5 | import { AutomationFactory } from '../automation/AutomationFactory.js'; |
7 | 6 | import { createLogCollector } from '../develop/LogCollectorFactory.js'; |
8 | 7 | import { findDevServerUrlAsync, openDevtoolsAsync } from '../develop/devtools.js'; |
9 | | -import { resizeImageToMaxSizeAsync } from '../imageUtils.js'; |
10 | 8 | import { isExpoRouterProject } from '../project.js'; |
| 9 | +import { addAutomationTools } from './tools/automation.js'; |
11 | 10 |
|
12 | 11 | export function addMcpTools(server: McpServerProxy, projectRoot: string) { |
13 | 12 | const isRouterProject = isExpoRouterProject(projectRoot); |
@@ -142,147 +141,5 @@ export function addMcpTools(server: McpServerProxy, projectRoot: string) { |
142 | 141 | } |
143 | 142 | ); |
144 | 143 |
|
145 | | - //#region automation tools |
146 | | - |
147 | | - server.registerTool( |
148 | | - 'automation_tap', |
149 | | - { |
150 | | - title: 'Tap on device', |
151 | | - description: 'Tap on the device at the given coordinates', |
152 | | - inputSchema: { |
153 | | - projectRoot: z.string(), |
154 | | - platform: z.enum(['android', 'ios']).optional(), |
155 | | - x: z.number(), |
156 | | - y: z.number(), |
157 | | - }, |
158 | | - }, |
159 | | - async ({ projectRoot, platform: platformParam, x, y }) => { |
160 | | - const platform = platformParam ?? (await AutomationFactory.guessCurrentPlatformAsync()); |
161 | | - const deviceId = await AutomationFactory.getBootedDeviceIdAsync(platform); |
162 | | - const appId = await AutomationFactory.getAppIdAsync({ projectRoot, platform, deviceId }); |
163 | | - const automation = AutomationFactory.create(platform, { |
164 | | - appId, |
165 | | - deviceId, |
166 | | - }); |
167 | | - const result = await automation.tapAsync({ x, y }); |
168 | | - return { content: [{ type: 'text', text: JSON.stringify(result) }] }; |
169 | | - } |
170 | | - ); |
171 | | - |
172 | | - server.registerTool( |
173 | | - 'automation_take_screenshot', |
174 | | - { |
175 | | - title: 'Take screenshot of the app', |
176 | | - description: |
177 | | - 'Take screenshot of the app. This is useful to verify the app is visually correct', |
178 | | - inputSchema: { |
179 | | - projectRoot: z.string(), |
180 | | - platform: z.enum(['android', 'ios']).optional(), |
181 | | - }, |
182 | | - }, |
183 | | - async ({ projectRoot, platform: platformParam }) => { |
184 | | - const platform = platformParam ?? (await AutomationFactory.guessCurrentPlatformAsync()); |
185 | | - const deviceId = await AutomationFactory.getBootedDeviceIdAsync(platform); |
186 | | - const appId = await AutomationFactory.getAppIdAsync({ projectRoot, platform, deviceId }); |
187 | | - const outputPath = `${tmpfile()}.png`; |
188 | | - try { |
189 | | - const automation = AutomationFactory.create(platform, { |
190 | | - appId, |
191 | | - deviceId, |
192 | | - }); |
193 | | - await automation.takeFullScreenshotAsync({ outputPath }); |
194 | | - const { buffer } = await resizeImageToMaxSizeAsync(outputPath); |
195 | | - return { |
196 | | - content: [{ type: 'image', data: buffer.toString('base64'), mimeType: 'image/jpeg' }], |
197 | | - }; |
198 | | - } finally { |
199 | | - await fs.promises.rm(outputPath, { force: true }); |
200 | | - } |
201 | | - } |
202 | | - ); |
203 | | - |
204 | | - server.registerTool( |
205 | | - 'automation_find_view_by_testid', |
206 | | - { |
207 | | - title: 'Find view properties by react-native testID', |
208 | | - description: |
209 | | - 'Find view and dump its properties by react-native testID. This is useful to verify the view is rendered correctly', |
210 | | - inputSchema: { |
211 | | - projectRoot: z.string(), |
212 | | - platform: z.enum(['android', 'ios']).optional(), |
213 | | - testID: z.string(), |
214 | | - }, |
215 | | - }, |
216 | | - async ({ projectRoot, platform: platformParam, testID }) => { |
217 | | - const platform = platformParam ?? (await AutomationFactory.guessCurrentPlatformAsync()); |
218 | | - const deviceId = await AutomationFactory.getBootedDeviceIdAsync(platform); |
219 | | - const appId = await AutomationFactory.getAppIdAsync({ projectRoot, platform, deviceId }); |
220 | | - const automation = AutomationFactory.create(platform, { |
221 | | - appId, |
222 | | - deviceId, |
223 | | - }); |
224 | | - const result = await automation.findViewByTestIDAsync(testID); |
225 | | - return { content: [{ type: 'text', text: JSON.stringify(result) }] }; |
226 | | - } |
227 | | - ); |
228 | | - |
229 | | - server.registerTool( |
230 | | - 'automation_tap_by_testid', |
231 | | - { |
232 | | - title: 'Tap on the view by react-native testID', |
233 | | - description: |
234 | | - 'Tap on the view specified by react-native testID. This is useful to interact with the view', |
235 | | - inputSchema: { |
236 | | - projectRoot: z.string(), |
237 | | - platform: z.enum(['android', 'ios']).optional(), |
238 | | - testID: z.string(), |
239 | | - }, |
240 | | - }, |
241 | | - async ({ projectRoot, platform: platformParam, testID }) => { |
242 | | - const platform = platformParam ?? (await AutomationFactory.guessCurrentPlatformAsync()); |
243 | | - const deviceId = await AutomationFactory.getBootedDeviceIdAsync(platform); |
244 | | - const appId = await AutomationFactory.getAppIdAsync({ projectRoot, platform, deviceId }); |
245 | | - const automation = AutomationFactory.create(platform, { |
246 | | - appId, |
247 | | - deviceId, |
248 | | - }); |
249 | | - const result = await automation.tapByTestIDAsync(testID); |
250 | | - return { content: [{ type: 'text', text: JSON.stringify(result) }] }; |
251 | | - } |
252 | | - ); |
253 | | - |
254 | | - server.registerTool( |
255 | | - 'automation_take_screenshot_by_testid', |
256 | | - { |
257 | | - title: 'Take screenshot of the app by react-native testID', |
258 | | - description: |
259 | | - 'Take screenshot of the app by react-native testID. This is useful to verify the view is rendered correctly', |
260 | | - inputSchema: { |
261 | | - projectRoot: z.string(), |
262 | | - platform: z.enum(['android', 'ios']).optional(), |
263 | | - testID: z.string(), |
264 | | - }, |
265 | | - }, |
266 | | - async ({ projectRoot, platform: platformParam, testID }) => { |
267 | | - const platform = platformParam ?? (await AutomationFactory.guessCurrentPlatformAsync()); |
268 | | - const deviceId = await AutomationFactory.getBootedDeviceIdAsync(platform); |
269 | | - const appId = await AutomationFactory.getAppIdAsync({ projectRoot, platform, deviceId }); |
270 | | - const outputPath = `${tmpfile()}.png`; |
271 | | - try { |
272 | | - const automation = AutomationFactory.create(platform, { |
273 | | - appId, |
274 | | - deviceId, |
275 | | - }); |
276 | | - await automation.taksScreenshotByTestIDAsync({ testID, outputPath }); |
277 | | - const { buffer } = await resizeImageToMaxSizeAsync(outputPath); |
278 | | - return { |
279 | | - content: [{ type: 'image', data: buffer.toString('base64'), mimeType: 'image/jpeg' }], |
280 | | - }; |
281 | | - } finally { |
282 | | - await fs.promises.rm(outputPath, { force: true }); |
283 | | - } |
284 | | - } |
285 | | - ); |
286 | | - |
287 | | - //#endregion automation tools |
| 144 | + addAutomationTools(server, projectRoot); |
288 | 145 | } |
0 commit comments