mobile-debug-mcp 0.24.5 → 0.24.7

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 (48) hide show
  1. package/.github/workflows/ci.yml +1 -3
  2. package/README.md +7 -0
  3. package/dist/manage/android.js +9 -5
  4. package/dist/manage/index.js +37 -23
  5. package/dist/manage/ios.js +12 -15
  6. package/dist/observe/index.js +2 -2
  7. package/dist/server/common.js +46 -0
  8. package/dist/server/tool-handlers.js +120 -33
  9. package/dist/server-core.js +1 -1
  10. package/dist/utils/android/utils.js +17 -5
  11. package/dist/utils/cli/idb/check-idb.js +1 -1
  12. package/docs/CHANGELOG.md +16 -8
  13. package/docs/tools/observe.md +2 -2
  14. package/eslint.config.js +2 -47
  15. package/package.json +7 -6
  16. package/src/manage/android.ts +22 -11
  17. package/src/manage/index.ts +37 -16
  18. package/src/manage/ios.ts +28 -15
  19. package/src/observe/index.ts +2 -2
  20. package/src/server/common.ts +50 -0
  21. package/src/server/tool-handlers.ts +136 -32
  22. package/src/server-core.ts +1 -1
  23. package/src/utils/android/utils.ts +18 -7
  24. package/src/utils/cli/idb/check-idb.ts +1 -1
  25. package/test/device/automated/observe/capture_screenshot.android.smoke.ts +1 -1
  26. package/test/device/automated/observe/capture_screenshot.ios.smoke.ts +1 -1
  27. package/test/device/automated/observe/get_logs.android.smoke.ts +1 -1
  28. package/test/device/automated/observe/get_logs.ios.smoke.ts +1 -1
  29. package/test/device/automated/observe/get_ui_tree.android.smoke.ts +1 -1
  30. package/test/device/automated/observe/get_ui_tree.ios.smoke.ts +1 -1
  31. package/test/device/manual/interact/app_lifecycle.manual.ts +3 -3
  32. package/test/device/manual/observe/capture_screenshot.manual.ts +2 -2
  33. package/test/device/manual/observe/get_logs.manual.ts +2 -2
  34. package/test/device/manual/observe/get_ui_tree.manual.ts +2 -2
  35. package/test/device/manual/observe/logstream.manual.ts +1 -1
  36. package/test/device/manual/observe/screen_fingerprint.manual.ts +2 -2
  37. package/test/unit/manage/scoped_env.test.ts +137 -0
  38. package/test/unit/server/capture_screenshot.test.ts +17 -0
  39. package/test/unit/server/common.test.ts +18 -0
  40. package/test/unit/server/contract.test.ts +3 -0
  41. package/test/unit/server/get_logs.test.ts +17 -0
  42. package/test/unit/server/get_network_activity.test.ts +17 -0
  43. package/test/unit/server/get_ui_tree.test.ts +17 -0
  44. package/test/unit/server/response_shapes.test.ts +18 -0
  45. package/test/unit/server/start_log_stream.test.ts +37 -0
  46. package/.eslintignore +0 -5
  47. package/.eslintrc.cjs +0 -18
  48. package/eslint.config.cjs +0 -36
@@ -0,0 +1,137 @@
1
+ import assert from 'assert'
2
+ import { ToolsManage } from '../../../src/manage/index.js'
3
+ import { AndroidManage } from '../../../src/manage/android.js'
4
+ import { iOSManage } from '../../../src/manage/ios.js'
5
+
6
+ async function run() {
7
+ const originalAndroidBuild = AndroidManage.prototype.build
8
+ const originalIOSBuild = iOSManage.prototype.build
9
+ const originalGradleWorkers = process.env.MCP_GRADLE_WORKERS
10
+ const originalGradleCache = process.env.MCP_GRADLE_CACHE
11
+ const originalForceCleanAndroid = process.env.MCP_FORCE_CLEAN_ANDROID
12
+ const originalDerivedData = process.env.MCP_DERIVED_DATA
13
+ const originalXcodeJobs = process.env.MCP_XCODE_JOBS
14
+ const originalForceCleanIOS = process.env.MCP_FORCE_CLEAN
15
+ const originalDestination = process.env.MCP_XCODE_DESTINATION_UDID
16
+
17
+ try {
18
+ process.env.MCP_GRADLE_WORKERS = 'ambient-workers'
19
+ process.env.MCP_GRADLE_CACHE = 'ambient-cache'
20
+ process.env.MCP_FORCE_CLEAN_ANDROID = 'ambient-force-android'
21
+ process.env.MCP_DERIVED_DATA = '/tmp/ambient-derived'
22
+ process.env.MCP_XCODE_JOBS = 'ambient-xcode-jobs'
23
+ process.env.MCP_FORCE_CLEAN = 'ambient-force-ios'
24
+ process.env.MCP_XCODE_DESTINATION_UDID = 'ambient-destination'
25
+
26
+ let androidCalls = 0
27
+ AndroidManage.prototype.build = async function (_projectPath: string, options?: { variant?: string, env?: Record<string, string | undefined> }) {
28
+ androidCalls += 1
29
+ if (androidCalls === 1) {
30
+ assert.strictEqual(options?.variant, 'assembleDebug')
31
+ assert.deepStrictEqual(options?.env, {
32
+ MCP_GRADLE_TASK: 'assembleDebug',
33
+ MCP_GRADLE_WORKERS: '3',
34
+ MCP_GRADLE_CACHE: '0',
35
+ MCP_FORCE_CLEAN_ANDROID: '1'
36
+ })
37
+ } else {
38
+ assert.strictEqual(options?.variant, 'assembleDebug')
39
+ assert.deepStrictEqual(options?.env, {
40
+ MCP_GRADLE_TASK: 'assembleDebug'
41
+ })
42
+ }
43
+ return { artifactPath: '/tmp/fake.apk' }
44
+ }
45
+
46
+ let iosCalls = 0
47
+ iOSManage.prototype.build = async function (_projectPath: string, options?: { variant?: string, workspace?: string, project?: string, scheme?: string, destinationUDID?: string, derivedDataPath?: string, buildJobs?: number, forceClean?: boolean, xcodeCmd?: string, env?: Record<string, string | undefined> }) {
48
+ iosCalls += 1
49
+ if (iosCalls === 1) {
50
+ assert.deepStrictEqual(options?.env, {
51
+ MCP_DERIVED_DATA: '/tmp/derived',
52
+ MCP_XCODE_JOBS: '4',
53
+ MCP_FORCE_CLEAN: '1',
54
+ MCP_XCODE_DESTINATION_UDID: 'booted'
55
+ })
56
+ } else if (iosCalls === 2) {
57
+ assert.deepStrictEqual(options?.env, {})
58
+ } else {
59
+ assert.deepStrictEqual(options?.env, {
60
+ MCP_FORCE_CLEAN: '0',
61
+ })
62
+ }
63
+ assert.strictEqual(options?.derivedDataPath, iosCalls === 1 ? '/tmp/derived' : undefined)
64
+ assert.strictEqual(options?.buildJobs, iosCalls === 1 ? 4 : undefined)
65
+ assert.strictEqual(options?.forceClean, iosCalls === 1 ? true : iosCalls === 3 ? false : undefined)
66
+ assert.strictEqual(options?.destinationUDID, iosCalls === 1 ? 'booted' : undefined)
67
+ return { artifactPath: '/tmp/Fake.app' }
68
+ }
69
+
70
+ await ToolsManage.build_android({
71
+ projectPath: '/tmp/project',
72
+ maxWorkers: 3,
73
+ gradleCache: false,
74
+ forceClean: true
75
+ })
76
+
77
+ await ToolsManage.build_ios({
78
+ projectPath: '/tmp/project',
79
+ derivedDataPath: '/tmp/derived',
80
+ buildJobs: 4,
81
+ forceClean: true,
82
+ destinationUDID: 'booted'
83
+ })
84
+
85
+ await ToolsManage.build_android({
86
+ projectPath: '/tmp/project'
87
+ })
88
+
89
+ await ToolsManage.build_ios({
90
+ projectPath: '/tmp/project'
91
+ })
92
+
93
+ await ToolsManage.build_ios({
94
+ projectPath: '/tmp/project',
95
+ forceClean: false
96
+ })
97
+
98
+ assert.strictEqual(process.env.MCP_GRADLE_WORKERS, 'ambient-workers')
99
+ assert.strictEqual(process.env.MCP_GRADLE_CACHE, 'ambient-cache')
100
+ assert.strictEqual(process.env.MCP_FORCE_CLEAN_ANDROID, 'ambient-force-android')
101
+ assert.strictEqual(process.env.MCP_DERIVED_DATA, '/tmp/ambient-derived')
102
+ assert.strictEqual(process.env.MCP_XCODE_JOBS, 'ambient-xcode-jobs')
103
+ assert.strictEqual(process.env.MCP_FORCE_CLEAN, 'ambient-force-ios')
104
+ assert.strictEqual(process.env.MCP_XCODE_DESTINATION_UDID, 'ambient-destination')
105
+
106
+ console.log('manage scoped env tests passed')
107
+ } finally {
108
+ AndroidManage.prototype.build = originalAndroidBuild
109
+ iOSManage.prototype.build = originalIOSBuild
110
+
111
+ if (originalGradleWorkers === undefined) delete process.env.MCP_GRADLE_WORKERS
112
+ else process.env.MCP_GRADLE_WORKERS = originalGradleWorkers
113
+
114
+ if (originalGradleCache === undefined) delete process.env.MCP_GRADLE_CACHE
115
+ else process.env.MCP_GRADLE_CACHE = originalGradleCache
116
+
117
+ if (originalForceCleanAndroid === undefined) delete process.env.MCP_FORCE_CLEAN_ANDROID
118
+ else process.env.MCP_FORCE_CLEAN_ANDROID = originalForceCleanAndroid
119
+
120
+ if (originalDerivedData === undefined) delete process.env.MCP_DERIVED_DATA
121
+ else process.env.MCP_DERIVED_DATA = originalDerivedData
122
+
123
+ if (originalXcodeJobs === undefined) delete process.env.MCP_XCODE_JOBS
124
+ else process.env.MCP_XCODE_JOBS = originalXcodeJobs
125
+
126
+ if (originalForceCleanIOS === undefined) delete process.env.MCP_FORCE_CLEAN
127
+ else process.env.MCP_FORCE_CLEAN = originalForceCleanIOS
128
+
129
+ if (originalDestination === undefined) delete process.env.MCP_XCODE_DESTINATION_UDID
130
+ else process.env.MCP_XCODE_DESTINATION_UDID = originalDestination
131
+ }
132
+ }
133
+
134
+ run().catch((error) => {
135
+ console.error(error)
136
+ process.exit(1)
137
+ })
@@ -0,0 +1,17 @@
1
+ import assert from 'assert'
2
+ import { handleToolCall } from '../../../src/server-core.js'
3
+
4
+ async function run() {
5
+ const result = await handleToolCall('capture_screenshot', {})
6
+ const payload = JSON.parse(result.content[0].text)
7
+
8
+ assert.strictEqual(payload.error.tool, 'capture_screenshot')
9
+ assert.match(payload.error.message, /Missing or invalid string argument: platform/)
10
+
11
+ console.log('capture_screenshot argument tests passed')
12
+ }
13
+
14
+ run().catch((error) => {
15
+ console.error(error)
16
+ process.exit(1)
17
+ })
@@ -0,0 +1,18 @@
1
+ import assert from 'assert'
2
+ import { requireBooleanArg } from '../../../src/server/common.js'
3
+
4
+ function run() {
5
+ assert.strictEqual(requireBooleanArg({ exact: true }, 'exact'), true)
6
+ assert.strictEqual(requireBooleanArg({ exact: false }, 'exact'), false)
7
+ assert.throws(() => requireBooleanArg({}, 'exact'), /Missing or invalid boolean argument: exact/)
8
+ assert.throws(() => requireBooleanArg({ exact: 'true' as unknown as boolean }, 'exact'), /Missing or invalid boolean argument: exact/)
9
+
10
+ console.log('server common tests passed')
11
+ }
12
+
13
+ try {
14
+ run()
15
+ } catch (error) {
16
+ console.error(error)
17
+ process.exit(1)
18
+ }
@@ -1,11 +1,14 @@
1
1
  import assert from 'assert'
2
+ import { readFileSync } from 'fs'
2
3
  import { handleToolCall, serverInfo, toolDefinitions } from '../../../src/server-core.js'
3
4
 
4
5
  async function run() {
6
+ const packageJson = JSON.parse(readFileSync(new URL('../../../package.json', import.meta.url), 'utf8'))
5
7
  const names = toolDefinitions.map((tool) => tool.name)
6
8
  const uniqueNames = new Set(names)
7
9
 
8
10
  assert.strictEqual(serverInfo.name, 'mobile-debug-mcp')
11
+ assert.strictEqual(serverInfo.version, packageJson.version, 'serverInfo version should match package.json')
9
12
  assert.strictEqual(names.length, uniqueNames.size, 'tool names should be unique')
10
13
  assert(names.includes('wait_for_ui'))
11
14
  assert(names.includes('expect_screen'))
@@ -0,0 +1,17 @@
1
+ import assert from 'assert'
2
+ import { handleToolCall } from '../../../src/server-core.js'
3
+
4
+ async function run() {
5
+ const result = await handleToolCall('get_logs', {})
6
+ const payload = JSON.parse(result.content[0].text)
7
+
8
+ assert.strictEqual(payload.error.tool, 'get_logs')
9
+ assert.match(payload.error.message, /Missing or invalid string argument: platform/)
10
+
11
+ console.log('get_logs argument tests passed')
12
+ }
13
+
14
+ run().catch((error) => {
15
+ console.error(error)
16
+ process.exit(1)
17
+ })
@@ -0,0 +1,17 @@
1
+ import assert from 'assert'
2
+ import { handleToolCall } from '../../../src/server-core.js'
3
+
4
+ async function run() {
5
+ const result = await handleToolCall('get_network_activity', {})
6
+ const payload = JSON.parse(result.content[0].text)
7
+
8
+ assert.strictEqual(payload.error.tool, 'get_network_activity')
9
+ assert.match(payload.error.message, /Missing or invalid string argument: platform/)
10
+
11
+ console.log('get_network_activity argument tests passed')
12
+ }
13
+
14
+ run().catch((error) => {
15
+ console.error(error)
16
+ process.exit(1)
17
+ })
@@ -0,0 +1,17 @@
1
+ import assert from 'assert'
2
+ import { handleToolCall } from '../../../src/server-core.js'
3
+
4
+ async function run() {
5
+ const result = await handleToolCall('get_ui_tree', {})
6
+ const payload = JSON.parse(result.content[0].text)
7
+
8
+ assert.strictEqual(payload.error.tool, 'get_ui_tree')
9
+ assert.match(payload.error.message, /Missing or invalid string argument: platform/)
10
+
11
+ console.log('get_ui_tree argument tests passed')
12
+ }
13
+
14
+ run().catch((error) => {
15
+ console.error(error)
16
+ process.exit(1)
17
+ })
@@ -32,6 +32,15 @@ async function run() {
32
32
  assert.strictEqual(installPayload.output, 'Success')
33
33
  assert.strictEqual(installPayload.device.id, 'emulator-5554')
34
34
 
35
+ const missingBuildResponse = await handleToolCall('build_app', { projectPath: '/tmp/project' })
36
+ const missingBuildPayload = JSON.parse((missingBuildResponse as any).content[0].text)
37
+ assert.deepStrictEqual(missingBuildPayload, {
38
+ error: {
39
+ tool: 'build_app',
40
+ message: 'Missing or invalid string argument: platform'
41
+ }
42
+ })
43
+
35
44
  ;(ToolsInteract as any).waitForUIHandler = async () => ({
36
45
  status: 'success',
37
46
  matched: 1,
@@ -156,6 +165,15 @@ async function run() {
156
165
  assert.match(objectTapPayload.error.message, /"code": "E_CUSTOM"/)
157
166
  assert.match(objectTapPayload.error.message, /"field": "value"/)
158
167
 
168
+ const missingArgResponse = await handleToolCall('tap', { platform: 'android', x: 1 })
169
+ const missingArgPayload = JSON.parse((missingArgResponse as any).content[0].text)
170
+ assert.deepStrictEqual(missingArgPayload, {
171
+ error: {
172
+ tool: 'tap',
173
+ message: 'Missing or invalid number argument: y'
174
+ }
175
+ })
176
+
159
177
  ;(ToolsObserve as any).captureScreenshotHandler = async () => ({
160
178
  device: { platform: 'ios', id: 'booted', osVersion: '18.0', model: 'Simulator', simulator: true },
161
179
  screenshot: Buffer.from('png-data').toString('base64'),
@@ -0,0 +1,37 @@
1
+ import assert from 'assert'
2
+ import { handleToolCall } from '../../../src/server-core.js'
3
+ import { ToolsObserve } from '../../../src/observe/index.js'
4
+
5
+ async function run() {
6
+ const originalStartLogStreamHandler = (ToolsObserve as any).startLogStreamHandler
7
+
8
+ try {
9
+ let captured: any = null
10
+ ;(ToolsObserve as any).startLogStreamHandler = async (args: any) => {
11
+ captured = args
12
+ return { sessionId: 'session-1', started: true }
13
+ }
14
+
15
+ const result = await handleToolCall('start_log_stream', { packageName: 'com.example.app' })
16
+ const payload = JSON.parse(result.content[0].text)
17
+
18
+ assert.deepStrictEqual(captured, {
19
+ platform: 'android',
20
+ packageName: 'com.example.app',
21
+ level: 'error',
22
+ sessionId: undefined,
23
+ deviceId: undefined
24
+ })
25
+ assert.strictEqual(payload.sessionId, 'session-1')
26
+ assert.strictEqual(payload.started, true)
27
+
28
+ console.log('start_log_stream default tests passed')
29
+ } finally {
30
+ ;(ToolsObserve as any).startLogStreamHandler = originalStartLogStreamHandler
31
+ }
32
+ }
33
+
34
+ run().catch((error) => {
35
+ console.error(error)
36
+ process.exit(1)
37
+ })
package/.eslintignore DELETED
@@ -1,5 +0,0 @@
1
- node_modules/
2
- dist/
3
- .env
4
- .vscode/
5
- coverage/
package/.eslintrc.cjs DELETED
@@ -1,18 +0,0 @@
1
- module.exports = {
2
- root: true,
3
- parser: '@typescript-eslint/parser',
4
- parserOptions: {
5
- ecmaVersion: 2020,
6
- sourceType: 'module',
7
- project: './tsconfig.json'
8
- },
9
- plugins: ['@typescript-eslint', 'unused-imports'],
10
- rules: {
11
- // Use plugin to error on unused imports and provide autofix where possible
12
- 'unused-imports/no-unused-imports': 'error',
13
- 'unused-imports/no-unused-vars': ['error', { vars: 'all', args: 'after-used', ignoreRestSiblings: true }],
14
- // Disable the default TS rule to avoid duplicate warnings
15
- '@typescript-eslint/no-unused-vars': 'off'
16
- },
17
- ignorePatterns: ['dist/', 'node_modules/', '.git/']
18
- }
package/eslint.config.cjs DELETED
@@ -1,36 +0,0 @@
1
- module.exports = [
2
- // Files/directories to ignore
3
- {
4
- ignores: [
5
- 'dist/',
6
- 'node_modules/',
7
- '.git/',
8
- '.vscode/',
9
- 'coverage/',
10
- '.env'
11
- ]
12
- },
13
- // Apply rules to JS/TS source and tests
14
- {
15
- files: ['src/**/*.ts', 'test/**/*.ts', 'src/**/*.js', 'test/**/*.js'],
16
- languageOptions: {
17
- parser: require.resolve('@typescript-eslint/parser'),
18
- parserOptions: {
19
- ecmaVersion: 2020,
20
- sourceType: 'module',
21
- project: './tsconfig.json'
22
- }
23
- },
24
- plugins: {
25
- '@typescript-eslint': require('@typescript-eslint/eslint-plugin'),
26
- 'unused-imports': require('eslint-plugin-unused-imports')
27
- },
28
- rules: {
29
- // Use plugin to error on unused imports and provide autofix where possible
30
- 'unused-imports/no-unused-imports': 'error',
31
- 'unused-imports/no-unused-vars': ['error', { vars: 'all', args: 'after-used', ignoreRestSiblings: true }],
32
- // Disable the default TS rule to avoid duplicate warnings
33
- '@typescript-eslint/no-unused-vars': 'off'
34
- }
35
- }
36
- ]