Skip to content

packages

Installed-app management on a connected device: listing (adb shell pm list packages), installing (adb install), uninstalling (adb shell pm uninstall), and making an already-installed package available to another Android user (adb shell pm install-existing). Exposes semantic install/ uninstall options rather than raw adb/pm flags — see install_apk's Args for how each option maps to Android's supported installation behavior. Split APK/APK-bundle/APEX installation, staged installs/install sessions, install-location management, and package enable/disable/suspend aren't implemented yet.

adb_automation_mcp.modules.packages.tools

Module-level, statically-introspectable tool functions for the packages module.

Kept as plain top-level functions, never closures, so that documentation tooling and the registry meta-test can both introspect them directly.

get_package_info(ctx: Context, serial: str, package_name: str) -> PackageInfo async

Curated snapshot of an installed package: adb shell dumpsys package <pkg>.

Parses the stable, automation-relevant fields out of dumpsys — version, UID, install/update times, code path, data dir, installer, flags, per-user enabled/installed state, and requested vs. granted permissions — instead of returning the (very large, version-variable) raw dump. Fields dumpsys doesn't report on a given Android version come back null / empty, never an error.

Parameters:

Name Type Description Default
serial str

The target device's adb serial (see list_connected_devices).

required
package_name str

The application id to inspect, e.g. "com.example.app".

required

Returns:

Type Description
PackageInfo

A PackageInfo: package_name, uid, version_code / version_name, min_sdk / target_sdk, code_path, data_dir, installer_package_name, first_install_time / last_update_time, is_system, flags, users (per Android user: installed, enabled_state, stopped, hidden, suspended), requested_permissions, and granted_permissions (every permission with granted=true anywhere in the dump — install-time and runtime, across users).

Error handling

An empty package_name raises INVALID_ARGUMENT before any adb call. An unknown serial raises DEVICE_NOT_FOUND; an unreachable adb binary raises ADB_UNAVAILABLE. A package dumpsys has no record of raises PACKAGE_NOT_FOUND (dumpsys says "Unable to find package" and still exits 0 — this tool turns that into the error).

Example

Called with serial="emulator-5554", package_name="com.example.thirdparty". A typical response:

{
  "status": "success",
  "message": "com.example.thirdparty 4.5.0 (user) on emulator-5554: 3 granted / 4 requested permissions, 1 user(s).",
  "data": {
    "serial": "emulator-5554",
    "package_name": "com.example.thirdparty",
    "uid": 10234,
    "version_code": 4500,
    "version_name": "4.5.0",
    "min_sdk": 24,
    "target_sdk": 34,
    "code_path": "/data/app/~~kQ7d==/com.example.thirdparty-Ab3c==",
    "data_dir": "/data/user/0/com.example.thirdparty",
    "installer_package_name": "com.android.vending",
    "first_install_time": "2026-09-01 12:00:00",
    "last_update_time": "2026-09-03 08:30:00",
    "is_system": false,
    "flags": ["HAS_CODE", "ALLOW_CLEAR_USER_DATA", "ALLOW_BACKUP"],
    "users": [
      {"user_id": 0, "installed": true, "enabled_state": "enabled",
       "stopped": false, "hidden": false, "suspended": false}
    ],
    "requested_permissions": [
      "android.permission.INTERNET", "android.permission.ACCESS_NETWORK_STATE",
      "android.permission.CAMERA", "android.permission.ACCESS_FINE_LOCATION"
    ],
    "granted_permissions": [
      "android.permission.INTERNET", "android.permission.ACCESS_NETWORK_STATE",
      "android.permission.CAMERA"
    ]
  },
  "error": null
}
Source code in src/adb_automation_mcp/modules/packages/tools.py
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
@category("read")
async def get_package_info(ctx: Context, serial: str, package_name: str) -> PackageInfo:
    """Curated snapshot of an installed package: `adb shell dumpsys package <pkg>`.

    Parses the stable, automation-relevant fields out of dumpsys — version, UID,
    install/update times, code path, data dir, installer, flags, per-user
    enabled/installed state, and requested vs. granted permissions — instead of
    returning the (very large, version-variable) raw dump. Fields dumpsys
    doesn't report on a given Android version come back null / empty, never an
    error.

    Args:
        serial: The target device's adb serial (see list_connected_devices).
        package_name: The application id to inspect, e.g. "com.example.app".

    Returns:
        A PackageInfo: package_name, uid, version_code / version_name,
        min_sdk / target_sdk, code_path, data_dir, installer_package_name,
        first_install_time / last_update_time, is_system, flags, users (per
        Android user: installed, enabled_state, stopped, hidden, suspended),
        requested_permissions, and granted_permissions (every permission with
        granted=true anywhere in the dump — install-time and runtime, across
        users).

    Error handling:
        An empty package_name raises INVALID_ARGUMENT before any adb call. An
        unknown serial raises DEVICE_NOT_FOUND; an unreachable adb binary raises
        ADB_UNAVAILABLE. A package dumpsys has no record of raises
        PACKAGE_NOT_FOUND (dumpsys says "Unable to find package" and still exits
        0 — this tool turns that into the error).

    Example:
        Called with serial="emulator-5554", package_name="com.example.thirdparty".
        A typical response:

        ```json
        {
          "status": "success",
          "message": "com.example.thirdparty 4.5.0 (user) on emulator-5554: 3 granted / 4 requested permissions, 1 user(s).",
          "data": {
            "serial": "emulator-5554",
            "package_name": "com.example.thirdparty",
            "uid": 10234,
            "version_code": 4500,
            "version_name": "4.5.0",
            "min_sdk": 24,
            "target_sdk": 34,
            "code_path": "/data/app/~~kQ7d==/com.example.thirdparty-Ab3c==",
            "data_dir": "/data/user/0/com.example.thirdparty",
            "installer_package_name": "com.android.vending",
            "first_install_time": "2026-09-01 12:00:00",
            "last_update_time": "2026-09-03 08:30:00",
            "is_system": false,
            "flags": ["HAS_CODE", "ALLOW_CLEAR_USER_DATA", "ALLOW_BACKUP"],
            "users": [
              {"user_id": 0, "installed": true, "enabled_state": "enabled",
               "stopped": false, "hidden": false, "suspended": false}
            ],
            "requested_permissions": [
              "android.permission.INTERNET", "android.permission.ACCESS_NETWORK_STATE",
              "android.permission.CAMERA", "android.permission.ACCESS_FINE_LOCATION"
            ],
            "granted_permissions": [
              "android.permission.INTERNET", "android.permission.ACCESS_NETWORK_STATE",
              "android.permission.CAMERA"
            ]
          },
          "error": null
        }
        ```
    """
    services = cast("dict[str, object]", ctx.lifespan_context["services"])
    packages = cast(PackagesService, services["packages"])
    return await packages.get_package_info(serial, package_name)

get_package_path(ctx: Context, serial: str, package_name: str, user_id: int | None = None) -> PackagePathInfo async

Resolve an installed package's on-device APK paths: adb shell pm path.

Use this before pulling or analyzing an app's APKs — it returns the base APK plus any split APKs (split_config.*) for a split-installed app, so a follow-up pull can fetch every piece.

Parameters:

Name Type Description Default
serial str

The target device's adb serial (see list_connected_devices).

required
package_name str

The application id to resolve, e.g. "com.example.app".

required
user_id int | None

Resolve against one Android user's view (--user ID, see list_users). Omit to use pm's default user.

None

Returns:

Type Description
PackagePathInfo

The serial, package_name, user_id, and paths — every APK path pm reported, in its order. base_apk is the base APK (identified by a trailing "/base.apk", else the first path), and split_apks is the rest; for a monolithic install split_apks is empty and base_apk is the single path.

Error handling

An empty package_name or negative user_id raises INVALID_ARGUMENT before any adb call. An unknown serial raises DEVICE_NOT_FOUND; an unreachable adb binary raises ADB_UNAVAILABLE. A package that isn't installed (or isn't installed for the requested user) raises PACKAGE_NOT_FOUND — pm path reports both the same way. A recognizably bad user scope raises USER_NOT_FOUND where pm says so.

Example

Called with serial="emulator-5554", package_name="com.android.car.settings". A typical response:

{
  "status": "success",
  "message": "com.android.car.settings on emulator-5554: 1 APK.",
  "data": {
    "serial": "emulator-5554",
    "package_name": "com.android.car.settings",
    "user_id": null,
    "paths": ["/system/priv-app/CarSettings/CarSettings.apk"],
    "base_apk": "/system/priv-app/CarSettings/CarSettings.apk",
    "split_apks": []
  },
  "error": null
}
Source code in src/adb_automation_mcp/modules/packages/tools.py
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
@category("read")
async def get_package_path(
    ctx: Context, serial: str, package_name: str, user_id: int | None = None
) -> PackagePathInfo:
    """Resolve an installed package's on-device APK paths: `adb shell pm path`.

    Use this before pulling or analyzing an app's APKs — it returns the base
    APK plus any split APKs (`split_config.*`) for a split-installed app, so a
    follow-up pull can fetch every piece.

    Args:
        serial: The target device's adb serial (see list_connected_devices).
        package_name: The application id to resolve, e.g. "com.example.app".
        user_id: Resolve against one Android user's view (`--user ID`, see
            list_users). Omit to use pm's default user.

    Returns:
        The serial, package_name, user_id, and paths — every APK path pm
        reported, in its order. base_apk is the base APK (identified by a
        trailing "/base.apk", else the first path), and split_apks is the
        rest; for a monolithic install split_apks is empty and base_apk is the
        single path.

    Error handling:
        An empty package_name or negative user_id raises INVALID_ARGUMENT
        before any adb call. An unknown serial raises DEVICE_NOT_FOUND; an
        unreachable adb binary raises ADB_UNAVAILABLE. A package that isn't
        installed (or isn't installed for the requested user) raises
        PACKAGE_NOT_FOUND — `pm path` reports both the same way. A recognizably
        bad user scope raises USER_NOT_FOUND where pm says so.

    Example:
        Called with serial="emulator-5554", package_name="com.android.car.settings".
        A typical response:

        ```json
        {
          "status": "success",
          "message": "com.android.car.settings on emulator-5554: 1 APK.",
          "data": {
            "serial": "emulator-5554",
            "package_name": "com.android.car.settings",
            "user_id": null,
            "paths": ["/system/priv-app/CarSettings/CarSettings.apk"],
            "base_apk": "/system/priv-app/CarSettings/CarSettings.apk",
            "split_apks": []
          },
          "error": null
        }
        ```
    """
    services = cast("dict[str, object]", ctx.lifespan_context["services"])
    packages = cast(PackagesService, services["packages"])
    return await packages.get_package_path(serial, package_name, user_id=user_id)

install_apk(ctx: Context, serial: str, apk_path: str, user_id: int | None = None, replace_existing: bool = False, allow_downgrade: bool = False, grant_runtime_permissions: bool = False, allow_test_packages: bool = False, force_sdk: bool = False) -> InstallResult async

Install an APK on a connected device (adb install).

Exposes semantic installation options rather than raw adb/pm flags. Android's Package Manager remains the final authority on whether a requested installation is legal — these options select supported installation behavior, they never bypass signature checks, Package Manager restrictions, or (absent the matching option) SDK/downgrade restrictions.

Parameters:

Name Type Description Default
serial str

The target device's adb serial (see list_connected_devices).

required
apk_path str

Path to the APK to install, resolved by adb itself (a host path when running against a real device/emulator). Must not be empty.

required
user_id int | None

Install the APK for one Android user (--user ID, see list_users) instead of adb's default target.

None
replace_existing bool

Replace/reinstall an already-installed version of this package (-r) instead of failing when one is present.

False
allow_downgrade bool

Allow installing a lower versionCode than what's currently installed (-d), where Android's Package Manager permits it (documented as debuggable-package behavior — this does not override a build's own downgrade restrictions).

False
grant_runtime_permissions bool

Grant all of the app's declared runtime permissions automatically at install time (-g).

False
allow_test_packages bool

Allow installing an APK built with android:testOnly="true" (-t), which a plain install rejects.

False
force_sdk bool

Ask the Package Manager to override its usual minSdkVersion/targetSdkVersion compatibility check (--force-sdk). Android still enforces every other install check normally.

False

Returns:

Type Description
InstallResult

Whether the install succeeded, the requested apk_path and user_id, and which semantic options were requested (not raw flags).

Error handling

Raises DeviceNotFoundError if serial doesn't match a connected device. Raises AndroidRejectionError when adb install reports a "Failure [REASON]" outcome — e.g. a signature mismatch, a downgrade/SDK/test-package restriction the requested options didn't cover, or insufficient storage — REASON is included in the error details. Raises InvalidArgumentError for an empty apk_path or a negative user_id. Any other non-zero exit surfaces as BackendError.

Example

Called with serial="emulator-5554", apk_path="/tmp/app-debug.apk", replace_existing=True. A typical response:

{
  "status": "success",
  "message": "Installed /tmp/app-debug.apk on emulator-5554.",
  "data": {
    "serial": "emulator-5554",
    "apk_path": "/tmp/app-debug.apk",
    "user_id": null,
    "replace_existing": true,
    "allow_downgrade": false,
    "grant_runtime_permissions": false,
    "allow_test_packages": false,
    "force_sdk": false,
    "success": true,
    "output": "Performing Streamed Install\nSuccess\n"
  },
  "error": null
}
Source code in src/adb_automation_mcp/modules/packages/tools.py
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
@category("write")
async def install_apk(
    ctx: Context,
    serial: str,
    apk_path: str,
    user_id: int | None = None,
    replace_existing: bool = False,
    allow_downgrade: bool = False,
    grant_runtime_permissions: bool = False,
    allow_test_packages: bool = False,
    force_sdk: bool = False,
) -> InstallResult:
    """Install an APK on a connected device (`adb install`).

    Exposes semantic installation options rather than raw adb/pm flags.
    Android's Package Manager remains the final authority on whether a
    requested installation is legal — these options select supported
    installation behavior, they never bypass signature checks, Package
    Manager restrictions, or (absent the matching option) SDK/downgrade
    restrictions.

    Args:
        serial: The target device's adb serial (see list_connected_devices).
        apk_path: Path to the APK to install, resolved by adb itself (a
            host path when running against a real device/emulator). Must
            not be empty.
        user_id: Install the APK for one Android user (`--user ID`, see
            list_users) instead of adb's default target.
        replace_existing: Replace/reinstall an already-installed version of
            this package (`-r`) instead of failing when one is present.
        allow_downgrade: Allow installing a lower versionCode than what's
            currently installed (`-d`), where Android's Package Manager
            permits it (documented as debuggable-package behavior — this
            does not override a build's own downgrade restrictions).
        grant_runtime_permissions: Grant all of the app's declared runtime
            permissions automatically at install time (`-g`).
        allow_test_packages: Allow installing an APK built with
            `android:testOnly="true"` (`-t`), which a plain install rejects.
        force_sdk: Ask the Package Manager to override its usual
            minSdkVersion/targetSdkVersion compatibility check
            (`--force-sdk`). Android still enforces every other install
            check normally.

    Returns:
        Whether the install succeeded, the requested apk_path and user_id,
        and which semantic options were requested (not raw flags).

    Error handling:
        Raises DeviceNotFoundError if serial doesn't match a connected
        device. Raises AndroidRejectionError when adb install reports a
        "Failure [REASON]" outcome — e.g. a signature mismatch, a
        downgrade/SDK/test-package restriction the requested options
        didn't cover, or insufficient storage — REASON is included in the
        error details. Raises InvalidArgumentError for an empty apk_path or
        a negative user_id. Any other non-zero exit surfaces as
        BackendError.

    Example:
        Called with serial="emulator-5554",
        apk_path="/tmp/app-debug.apk", replace_existing=True. A typical
        response:

        ```json
        {
          "status": "success",
          "message": "Installed /tmp/app-debug.apk on emulator-5554.",
          "data": {
            "serial": "emulator-5554",
            "apk_path": "/tmp/app-debug.apk",
            "user_id": null,
            "replace_existing": true,
            "allow_downgrade": false,
            "grant_runtime_permissions": false,
            "allow_test_packages": false,
            "force_sdk": false,
            "success": true,
            "output": "Performing Streamed Install\\nSuccess\\n"
          },
          "error": null
        }
        ```
    """
    services = cast("dict[str, object]", ctx.lifespan_context["services"])
    packages = cast(PackagesService, services["packages"])
    return await packages.install_apk(
        serial,
        apk_path,
        user_id=user_id,
        replace_existing=replace_existing,
        allow_downgrade=allow_downgrade,
        grant_runtime_permissions=grant_runtime_permissions,
        allow_test_packages=allow_test_packages,
        force_sdk=force_sdk,
    )

install_existing_for_user(ctx: Context, serial: str, package_name: str, user_id: int) -> InstallExistingResult async

Make an already-installed package available to another Android user (adb shell pm install-existing --user USER_ID PACKAGE).

This is distinct from install_apk: it never touches an APK file, it only extends an app that's already present on the device to another user's view of it (e.g. a work profile or a secondary user on a multi-user/automotive build).

Parameters:

Name Type Description Default
serial str

The target device's adb serial (see list_connected_devices).

required
package_name str

The already-installed package to make available. Must not be empty.

required
user_id int

The Android user to make the package available to (see list_users).

required

Returns:

Type Description
InstallExistingResult

The package name, target user, and whether the operation succeeded. Requesting this for a user the package is already available to is not an error — pm install-existing is idempotent.

Error handling

Raises DeviceNotFoundError if serial doesn't match a connected device. Raises PackageNotFoundError when package_name isn't installed on the device at all. Raises AndroidRejectionError for any other on-device Package Manager rejection. Raises InvalidArgumentError for an empty package_name or a negative user_id. Any other non-zero exit surfaces as BackendError.

Note: pm install-existing does NOT validate the target user — verified live that a non-existent user_id (e.g. 42 on a single-user device) still returns "Package installed for user: 42" and exit 0. This tool reports that as success; it cannot surface a bogus user_id as an error because pm itself doesn't. Use list_users first if the id must be known-good.

Example

Called with serial="emulator-5554", package_name="com.example.app", user_id=10. A typical response:

{
  "status": "success",
  "message": "Made com.example.app available for user 10 on emulator-5554.",
  "data": {
    "serial": "emulator-5554",
    "package_name": "com.example.app",
    "user_id": 10,
    "success": true,
    "output": "Package com.example.app installed for user: 10\n"
  },
  "error": null
}
Source code in src/adb_automation_mcp/modules/packages/tools.py
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
@category("write")
async def install_existing_for_user(
    ctx: Context, serial: str, package_name: str, user_id: int
) -> InstallExistingResult:
    """Make an already-installed package available to another Android user
    (`adb shell pm install-existing --user USER_ID PACKAGE`).

    This is distinct from install_apk: it never touches an APK file, it
    only extends an app that's already present on the device to another
    user's view of it (e.g. a work profile or a secondary user on a
    multi-user/automotive build).

    Args:
        serial: The target device's adb serial (see list_connected_devices).
        package_name: The already-installed package to make available.
            Must not be empty.
        user_id: The Android user to make the package available to (see
            list_users).

    Returns:
        The package name, target user, and whether the operation
        succeeded. Requesting this for a user the package is already
        available to is not an error — pm install-existing is idempotent.

    Error handling:
        Raises DeviceNotFoundError if serial doesn't match a connected
        device. Raises PackageNotFoundError when package_name isn't
        installed on the device at all. Raises AndroidRejectionError for
        any other on-device Package Manager rejection. Raises
        InvalidArgumentError for an empty package_name or a negative
        user_id. Any other non-zero exit surfaces as BackendError.

        Note: `pm install-existing` does NOT validate the target user —
        verified live that a non-existent user_id (e.g. 42 on a
        single-user device) still returns "Package <name> installed for
        user: 42" and exit 0. This tool reports that as success; it cannot
        surface a bogus user_id as an error because pm itself doesn't.
        Use list_users first if the id must be known-good.

    Example:
        Called with serial="emulator-5554",
        package_name="com.example.app", user_id=10. A typical response:

        ```json
        {
          "status": "success",
          "message": "Made com.example.app available for user 10 on emulator-5554.",
          "data": {
            "serial": "emulator-5554",
            "package_name": "com.example.app",
            "user_id": 10,
            "success": true,
            "output": "Package com.example.app installed for user: 10\\n"
          },
          "error": null
        }
        ```
    """
    services = cast("dict[str, object]", ctx.lifespan_context["services"])
    packages = cast(PackagesService, services["packages"])
    return await packages.install_existing_for_user(serial, package_name, user_id)

list_packages(ctx: Context, serial: str, user_id: int | None = None, package_filter: PackageFilter | None = None) -> PackageList async

List installed Android packages on a device: adb shell pm list packages.

Returns parsed package names only, not pm's raw text output.

Parameters:

Name Type Description Default
serial str

The target device's adb serial (see list_connected_devices).

required
user_id int | None

Restrict the listing to one Android user's package view (--user ID, see list_users). Omit to use pm's default user.

None
package_filter PackageFilter | None

Restrict to "system" packages (-s) or "third_party" packages (-3) — pm's own mutually exclusive filter flags. Omit to list every package regardless of origin.

None

Returns:

Type Description
PackageList

The serial and every matching package name. An empty list is a normal result (e.g. no third-party apps installed), not an error.

Error handling

Propagates the same way most tools do (unlike check_adb_available): if the adb binary itself can't be found or is unresponsive, or the serial doesn't match a connected device, that surfaces as an actual tool error.

Example

Called with serial="emulator-5554", package_filter="third_party". A typical response:

{
  "status": "success",
  "message": "2 packages on emulator-5554.",
  "data": {
    "serial": "emulator-5554",
    "packages": ["com.example.app", "com.example.other"]
  },
  "error": null
}
Source code in src/adb_automation_mcp/modules/packages/tools.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
@category("read")
async def list_packages(
    ctx: Context,
    serial: str,
    user_id: int | None = None,
    package_filter: PackageFilter | None = None,
) -> PackageList:
    """List installed Android packages on a device: `adb shell pm list packages`.

    Returns parsed package names only, not pm's raw text output.

    Args:
        serial: The target device's adb serial (see list_connected_devices).
        user_id: Restrict the listing to one Android user's package view
            (`--user ID`, see list_users). Omit to use pm's default user.
        package_filter: Restrict to "system" packages (`-s`) or
            "third_party" packages (`-3`) — pm's own mutually exclusive
            filter flags. Omit to list every package regardless of origin.

    Returns:
        The serial and every matching package name. An empty list is a
        normal result (e.g. no third-party apps installed), not an error.

    Error handling:
        Propagates the same way most tools do (unlike check_adb_available): if
        the adb binary itself can't be found or is unresponsive, or the
        serial doesn't match a connected device, that surfaces as an actual
        tool error.

    Example:
        Called with serial="emulator-5554", package_filter="third_party". A
        typical response:

        ```json
        {
          "status": "success",
          "message": "2 packages on emulator-5554.",
          "data": {
            "serial": "emulator-5554",
            "packages": ["com.example.app", "com.example.other"]
          },
          "error": null
        }
        ```
    """
    services = cast("dict[str, object]", ctx.lifespan_context["services"])
    packages = cast(PackagesService, services["packages"])
    return await packages.list_packages(serial, user_id=user_id, package_filter=package_filter)

set_package_enabled_state(ctx: Context, serial: str, package_name: str, state: PackageEnabledState, component: str | None = None, user_id: int | None = None) -> PackageEnabledStateResult async

Enable or disable an app, or one of its components: adb shell pm enable|disable|....

Use this to turn a package (or a specific Activity / Service / Receiver / Provider) on or off for automation — e.g. disable a receiver, observe the behavior change, then set it back to "default". Reversible: "default" clears any override this tool (or anything else) set.

Parameters:

Name Type Description Default
serial str

The target device's adb serial (see list_connected_devices).

required
package_name str

The application id, e.g. "com.example.app".

required
state PackageEnabledState

The state to set. One of "enabled", "disabled", "disabled_user" (disable only for the given/current user, the safe choice for an app package — plain "disabled" is often refused for system apps), or "default" (clear any override).

required
component str | None

Optional class name of a single component to target instead of the whole package — either relative (".MyReceiver") or fully qualified ("com.example.app.MyReceiver"). No spaces or "/".

None
user_id int | None

Apply for one Android user (--user ID, see list_users). Omit for pm's default/current user.

None

Returns:

Type Description
PackageEnabledStateResult

The serial, package_name, component (null for a whole-package change), the resolved target string ("pkg" or "pkg/component"), user_id, requested_state, and new_state — the state pm confirmed, normalized to the same names as the request ("disabled_user", not "disabled-user").

Error handling

An empty package_name, unknown state, negative user_id, or malformed component raises INVALID_ARGUMENT before any adb call. An unknown serial raises DEVICE_NOT_FOUND; an unreachable adb binary raises ADB_UNAVAILABLE. A package pm doesn't know raises PACKAGE_NOT_FOUND. pm refusing the change — a protected/system package, or a component the shell isn't allowed to touch (pm reports an absent component the same way) — raises PERMISSION_DENIED.

Example

Called with serial="emulator-5554", package_name="com.example.app", state="disabled_user". A typical response:

{
  "status": "success",
  "message": "com.example.app on emulator-5554 is now 'disabled_user'.",
  "data": {
    "serial": "emulator-5554",
    "package_name": "com.example.app",
    "component": null,
    "target": "com.example.app",
    "user_id": null,
    "requested_state": "disabled_user",
    "new_state": "disabled_user",
    "success": true
  },
  "error": null
}
Source code in src/adb_automation_mcp/modules/packages/tools.py
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
@category("write")
async def set_package_enabled_state(
    ctx: Context,
    serial: str,
    package_name: str,
    state: PackageEnabledState,
    component: str | None = None,
    user_id: int | None = None,
) -> PackageEnabledStateResult:
    """Enable or disable an app, or one of its components: `adb shell pm enable|disable|...`.

    Use this to turn a package (or a specific Activity / Service / Receiver /
    Provider) on or off for automation — e.g. disable a receiver, observe the
    behavior change, then set it back to "default". Reversible: "default" clears
    any override this tool (or anything else) set.

    Args:
        serial: The target device's adb serial (see list_connected_devices).
        package_name: The application id, e.g. "com.example.app".
        state: The state to set. One of "enabled", "disabled", "disabled_user"
            (disable only for the given/current user, the safe choice for an
            app package — plain "disabled" is often refused for system apps),
            or "default" (clear any override).
        component: Optional class name of a single component to target instead
            of the whole package — either relative (".MyReceiver") or fully
            qualified ("com.example.app.MyReceiver"). No spaces or "/".
        user_id: Apply for one Android user (`--user ID`, see list_users). Omit
            for pm's default/current user.

    Returns:
        The serial, package_name, component (null for a whole-package change),
        the resolved target string ("pkg" or "pkg/component"), user_id,
        requested_state, and new_state — the state pm confirmed, normalized to
        the same names as the request ("disabled_user", not "disabled-user").

    Error handling:
        An empty package_name, unknown state, negative user_id, or malformed
        component raises INVALID_ARGUMENT before any adb call. An unknown serial
        raises DEVICE_NOT_FOUND; an unreachable adb binary raises
        ADB_UNAVAILABLE. A package pm doesn't know raises PACKAGE_NOT_FOUND. pm
        refusing the change — a protected/system package, or a component the
        shell isn't allowed to touch (pm reports an absent component the same
        way) — raises PERMISSION_DENIED.

    Example:
        Called with serial="emulator-5554", package_name="com.example.app",
        state="disabled_user". A typical response:

        ```json
        {
          "status": "success",
          "message": "com.example.app on emulator-5554 is now 'disabled_user'.",
          "data": {
            "serial": "emulator-5554",
            "package_name": "com.example.app",
            "component": null,
            "target": "com.example.app",
            "user_id": null,
            "requested_state": "disabled_user",
            "new_state": "disabled_user",
            "success": true
          },
          "error": null
        }
        ```
    """
    services = cast("dict[str, object]", ctx.lifespan_context["services"])
    packages = cast(PackagesService, services["packages"])
    return await packages.set_package_enabled_state(
        serial, package_name, state, component=component, user_id=user_id
    )

uninstall_package(ctx: Context, serial: str, package_name: str, user_id: int | None = None, keep_data: bool = False, version_code: int | None = None) -> UninstallResult async

Uninstall a package on a connected device (adb shell pm uninstall).

Parameters:

Name Type Description Default
serial str

The target device's adb serial (see list_connected_devices).

required
package_name str

The package to uninstall. Must not be empty.

required
user_id int | None

Uninstall the package for one Android user (--user ID, see list_users) rather than the device's normal (unscoped) uninstall behavior. Removing it for one user never removes it for every user — omit user_id for that instead.

None
keep_data bool

Request that Android preserve the package's data/cache (-k) rather than wiping it. This is what pm's -k provides, not a stronger guarantee beyond what Package Manager itself does.

False
version_code int | None

Only uninstall if the installed package's version code matches (--versionCode CODE). Omit to uninstall regardless of version.

None

Returns:

Type Description
UninstallResult

The package name, target user (if any), whether data retention was requested, and whether the uninstall succeeded.

Error handling

Raises DeviceNotFoundError if serial doesn't match a connected device. Raises PackageNotFoundError when the package isn't installed at all, or isn't installed for the targeted user. Raises UserNotFoundError when user_id doesn't correspond to an Android user on the device. Raises AndroidRejectionError for any other on-device Package Manager rejection (e.g. a version_code that doesn't match the installed package). Raises InvalidArgumentError for an empty package_name, a negative user_id, or a non-positive version_code. Any other non-zero exit surfaces as BackendError.

Example

Called with serial="emulator-5554", package_name="com.example.app", user_id=10. A typical response:

{
  "status": "success",
  "message": "Uninstalled com.example.app on emulator-5554 for user 10.",
  "data": {
    "serial": "emulator-5554",
    "package_name": "com.example.app",
    "user_id": 10,
    "keep_data": false,
    "version_code": null,
    "success": true,
    "output": "Success\n"
  },
  "error": null
}
Source code in src/adb_automation_mcp/modules/packages/tools.py
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
@category("destructive")
async def uninstall_package(
    ctx: Context,
    serial: str,
    package_name: str,
    user_id: int | None = None,
    keep_data: bool = False,
    version_code: int | None = None,
) -> UninstallResult:
    """Uninstall a package on a connected device (`adb shell pm uninstall`).

    Args:
        serial: The target device's adb serial (see list_connected_devices).
        package_name: The package to uninstall. Must not be empty.
        user_id: Uninstall the package for one Android user (`--user ID`,
            see list_users) rather than the device's normal (unscoped)
            uninstall behavior. Removing it for one user never removes it
            for every user — omit user_id for that instead.
        keep_data: Request that Android preserve the package's data/cache
            (`-k`) rather than wiping it. This is what pm's `-k` provides,
            not a stronger guarantee beyond what Package Manager itself
            does.
        version_code: Only uninstall if the installed package's version
            code matches (`--versionCode CODE`). Omit to uninstall
            regardless of version.

    Returns:
        The package name, target user (if any), whether data retention was
        requested, and whether the uninstall succeeded.

    Error handling:
        Raises DeviceNotFoundError if serial doesn't match a connected
        device. Raises PackageNotFoundError when the package isn't
        installed at all, or isn't installed for the targeted user.
        Raises UserNotFoundError when user_id doesn't correspond to an
        Android user on the device. Raises AndroidRejectionError for any
        other on-device Package Manager rejection (e.g. a version_code
        that doesn't match the installed package). Raises
        InvalidArgumentError for an empty package_name, a negative
        user_id, or a non-positive version_code. Any other non-zero exit
        surfaces as BackendError.

    Example:
        Called with serial="emulator-5554",
        package_name="com.example.app", user_id=10. A typical response:

        ```json
        {
          "status": "success",
          "message": "Uninstalled com.example.app on emulator-5554 for user 10.",
          "data": {
            "serial": "emulator-5554",
            "package_name": "com.example.app",
            "user_id": 10,
            "keep_data": false,
            "version_code": null,
            "success": true,
            "output": "Success\\n"
          },
          "error": null
        }
        ```
    """
    services = cast("dict[str, object]", ctx.lifespan_context["services"])
    packages = cast(PackagesService, services["packages"])
    return await packages.uninstall_package(
        serial, package_name, user_id=user_id, keep_data=keep_data, version_code=version_code
    )