Skip to main content
Glama

gwagent

Let an AI agent use Gmail, Google Calendar and Google Drive on an Android phone or emulator, through adb, the way a person would: by driving the apps' own UI (Gmail, Drive, Calendar app screens) and, where Android allows it, Android's calendar content provider.

It is meant for a Google Workspace account that can only be used from the phone (no API access): the agent gets about 150 structured operations, each returning JSON, instead of raw taps.

  • One registry, two front ends: every operation is a Python function declared with @op; the CLI (gwagent ...) and the MCP server (gwagent mcp) are generated from it.

  • Structured results: rows with indexes, events with ids, files with paths, so an agent can chain calls (search -> read -> reply, list -> download, events -> update one occurrence).

  • Safety built in: destructive or outward actions need confirm=true; index-based targets are checked against the list they came from; a Gmail send is never repeated blind.

  • Fast path: a small on-phone server (a .dex run with app_process, not an installed app) makes UI dumps take ~50 ms instead of 4-10 s and lets any Unicode text be typed.

Documents:

File

For

AGENT_GUIDE.md

An AI agent that receives this system: workflows, ids, errors, etiquette

docs/OPERATIONS.md

Every operation with its parameters (generated from the registry)

PORTING.md

Moving the system to another machine or phone; scripts/bootstrap.sh

DEVELOPING.md

Rules for adding or changing operations

Requirements

  • macOS or Linux host with Python >= 3.12 and uv.

  • Android platform-tools (adb) on PATH, or pointed to with GWAGENT_ADB.

  • An Android device reachable by adb: the Android emulator with a Google Play system image (developed on Android 15 / API 35, x86_64), or a real phone with USB debugging enabled. The phone's UI language must be English (ops match English labels).

  • A Google (Workspace) account signed in on the phone, with Gmail, Calendar and Drive installed. A Workspace account usually forces a screen lock: gwagent unlocks it with a PIN (see below).

  • Optional: pdftotext (poppler) on the host, for drive.read_text on PDFs.

  • To rebuild the on-phone server only: a JDK and Android build-tools (see dumpserver/build.sh).

Related MCP server: DroidPilot

Install

git clone <this repo> gwagent && cd gwagent
uv sync                                  # creates .venv with gwagent and its dependencies
uv run gwagent device.status --pretty    # check that the phone is reachable

uv run gwagent ... works from the checkout. To have a gwagent command everywhere: uv tool install --editable ..

On a new machine or phone, follow PORTING.md and run scripts/bootstrap.sh.

Configuration (environment variables)

All configuration comes from the environment; nothing personal is stored in the repository.

Variable

Default

Meaning

GWAGENT_SERIAL

emulator-5554

adb serial of the phone to drive

GWAGENT_ADB

adb on PATH, then ~/android-ufficio-sdk/platform-tools/adb, ~/Library/Android/sdk/platform-tools/adb

adb binary

GWAGENT_PIN

-

Screen-lock PIN (prefer the PIN file)

GWAGENT_PIN_FILE

~/.android-ufficio-pin

File holding the PIN (mode 600); never print or commit it

GWAGENT_OUT_DIR

~/gwagent-downloads

Where downloads, attachments and screenshots go on the host

GWAGENT_STATE_DIR

~/.cache/gwagent

Host state: last Gmail list, last Drive location, last Gmail action (for undo)

GWAGENT_TZ

Europe/Rome

Time zone for calendar output and for times given without an offset

GWAGENT_CALENDAR_ID

primary calendar

Default calendar id for calendar.create

GWAGENT_LOCK_FILE

/tmp/gwagent-ui.lock

UI lock file (see UI lock)

GWAGENT_LOCK_TIMEOUT

300

Seconds to wait for the UI lock before failing with busy

GWAGENT_NO_LOCK

unset

1 disables the UI lock: unit tests only, never with a real phone

GWAGENT_SLEEP_AFTER

unset

1 turns the screen off after every UI op

GWAGENT_KEEPALIVE

1

Keep the screen awake during long ops; 0 disables

GWAGENT_AUTO_RECOVER

1

Wait for an offline phone and repeat retry-safe ops once; 0 disables

GWAGENT_BOOT_TIMEOUT

300

Max seconds to wait for the phone to come back

GWAGENT_RECOVER_CMD

unset

Operator hook (shell command) run once when the phone stays unreachable for 90 s after adb reconnect offline and adb -s <serial> reconnect did not help, e.g. a command that restarts the emulator (see PORTING.md); gets GWAGENT_SERIAL and a fresh GWAGENT_BOOT_TIMEOUT. Without it gwagent never restarts the emulator

GWAGENT_RECOVER_CMD_TIMEOUT

120

Max seconds the GWAGENT_RECOVER_CMD hook may run

GWAGENT_FAST_DUMP

1

0 disables the on-phone server (falls back to uiautomator dump / input)

GWAGENT_FAST_IDLE

45

Seconds after which an idle on-phone server exits

GWAGENT_FAST_START_TIMEOUT

30

Seconds to wait for the on-phone server to start

GWAGENT_FAST_BUSY_TIMEOUT

25

Seconds of silence before the server is considered stuck and restarted

GWAGENT_TRACE

unset

1 prints every adb call and server command with its duration on stderr

GWAGENT_BUILD_SDK

~/.cache/gwagent-sdk

Android SDK used by dumpserver/build.sh

Test-only variables: GWAGENT_LIVE=1 / GWAGENT_OUTWARD=1 (same as --live / --outward), GWAGENT_REGEN_MANIFEST=1 and GWAGENT_REGEN_OPS_DOC=1 (regenerate golden files), GWAGENT_PRIVACY_TERMS (extra comma-separated terms for the fixture privacy scan). GWAGENT_DUMP_NONCE is set by gwagent itself when it starts the on-phone server.

CLI

Output is always one JSON object on stdout; the exit code is 0 on success and an error-specific code otherwise (see docs/OPERATIONS.md#error-codes).

uv run gwagent list                       # every operation (name, summary, ui, confirm, params)
uv run gwagent list --app gmail --names   # just the names of one app's ops
uv run gwagent describe gmail.search      # one op's full description and JSON schema
uv run gwagent gmail.search --query "is:unread" --limit 5
uv run gwagent gmail search --query "is:unread"            # app and action as two words
uv run gwagent call gmail.read '{"index": 0, "expect_subject": "Weekly report"}'
uv run gwagent gmail.send --to alice@example.com --subject Hi --body "Ciao" --confirm
uv run gwagent calendar.events --period week --pretty
uv run gwagent mcp                        # MCP server on stdio

Flags: --name value, --name=value, bare --flag (true), --no-flag (false); a repeated flag becomes a list (--to a@example.com --to b@example.com, or --to a@example.com,b@example.com, or a JSON list). --args-json '{...}' merges a JSON object; --pretty indents the output.

Result envelope:

{"ok": true, "op": "gmail.search", "result": {"rows": [...]}, "elapsed_s": 6.1}
{"ok": false, "op": "gmail.reply", "error": "not_found", "message": "...", "elapsed_s": 3.2}

MCP server

gwagent mcp runs a stdio MCP server exposing every op as a tool; the tool name is the op name with . replaced by _ (gmail.search -> gmail_search). Tool results are the same JSON envelope as the CLI.

Claude Code:

claude mcp add gwagent -- uv --directory /path/to/gwagent run gwagent mcp

Any MCP client (Claude Desktop claude_desktop_config.json, a project .mcp.json, ...):

{
  "mcpServers": {
    "gwagent": {
      "command": "uv",
      "args": ["--directory", "/path/to/gwagent", "run", "gwagent", "mcp"],
      "env": {
        "GWAGENT_SERIAL": "emulator-5554",
        "GWAGENT_PIN_FILE": "/Users/you/.android-ufficio-pin"
      }
    }
  }
}

Give the agent AGENT_GUIDE.md (e.g. reference it from the client's project instructions). Ops can take tens of seconds; set the client's tool timeout to at least 5 minutes (MCP_TOOL_TIMEOUT=300000 for Claude Code), since a UI op may also wait for the UI lock.

How it works

src/gwagent/
  adb.py, device.py   adb wrapper; Device: unlock, tap/swipe/type, dump -> Screen, wait_for, recover
  ui.py               Screen/Node/Selector: parsing and querying UI dumps
  fastdump.py         client of the on-phone server (assets/gwagent-dump.dex)
  lock.py             the inter-process UI lock
  registry.py         @op, Context, call() -> {"ok": ...} envelope, offline recovery
  cli.py, mcp_server.py   generated front ends
  ops/                gmail*.py, calendar.py (provider), calendar_ui.py (app), drive*.py, device.py
dumpserver/           Java source of the on-phone server; build.sh -> src/gwagent/assets/gwagent-dump.dex

Fast path: the on-phone server

Device.dump/tap/swipe/key/type_text go through a persistent server on the phone that holds one UiAutomation connection. It is a .dex pushed to /data/local/tmp and started with app_process (shell user); it is not an APK and nothing is installed. A UI dump takes ~20-60 ms instead of 4-10 s, a tap a few ms instead of ~1 s, and type_text sets text through accessibility, so any Unicode works in every field. If the server is unavailable, everything falls back to uiautomator dump and adb shell input automatically. It exits after 45 s idle and is restarted on demand (also after an emulator restart).

Transport and trust:

  • The server listens on an abstract unix socket (not a TCP port) with a random name gwagent-dump-<16 hex> chosen at each start; the host reaches it through adb forward tcp:<port> localabstract:<name>.

  • Server checks the client: only peers with uid 2000 (shell, which is what adbd forwards as) or 0 are served (getPeerCredentials); other apps on the phone are refused.

  • Client checks the server: the abstract namespace has no permissions, so an app could bind a name and pose as the server. Each start gets a random nonce (passed to the server in its environment); the name and nonce are kept in /data/local/tmp/gwagent-dump.token (mode 600, shell only). The client accepts the server only if version answers "<PROTO> <nonce>".

  • The protocol is versioned (fastdump.PROTO = DumpServer.PROTO, currently 5); a running server with another version is replaced. Run one gwagent version per phone.

Safety model

  • confirm: ops that send, delete, share or are otherwise outward-facing or irreversible are marked requires_confirm; without confirm=true they fail with confirmation_required and do nothing. Some ops need it only in some cases (calendar.create / calendar.update on events with guests, since Google e-mails them).

  • Guards against acting on the wrong item: the last Gmail list (folder/query and each row's sender, subject, date) is remembered on the host. An op given a bare index re-opens that list and checks the row is still the same conversation; moved rows are found again, vanished ones fail with not_found before anything is touched. expect_subject (Gmail read/reply/forward/ attachments/...) and match (bulk actions) add explicit checks. Drive ops resolve names in the remembered location, not in whatever the screen shows. drive.empty_trash requires only_if_all_match or all=true.

  • Recipients are verified on the compose screen before Send: any unexpected or missing address keeps the mail as a draft and raises an error.

  • Send is never re-sent blind: when Gmail's reaction to Send is unclear, the mail is looked up in Outbox / Sent; if that cannot tell, the op fails with send_state_unknown and the caller must check the Sent folder before any retry. Ops with confirm are never repeated automatically, not even after the phone went offline.

  • Calendar out of office (calendar.out_of_office) only declines new invitations by default (decline="only_new"); meetings already accepted are declined (and their organizers notified) only with decline="new_and_existing".

  • UI lock: only one program drives the phone at a time (see below).

  • Live tests only create objects whose name or subject contains AGT-TEST, send mail only to the phone's own account, add no guests to test events, and delete what they create. Tests that change account settings skip when the user has that setting in use and restore it afterwards.

  • Privacy: the account is a real office account. No mail, calendar or Drive content, no screenshots or UI dumps of the phone, and no account address may be sent to cloud LLMs or committed; test fixtures are scrubbed (tests/unit/test_fixture_privacy.py scans them). The PIN is never printed, logged or committed. Run the agent that uses gwagent on a model the data owner has approved for this content.

  • No APKs: nothing is installed on the phone. The only on-phone code is the dump server .dex run with app_process. (device.install_adb_keyboard exists for other phones only.)

UI lock

Every ui=true op holds an exclusive flock on /tmp/gwagent-ui.lock (override GWAGENT_LOCK_FILE) for its whole duration; it waits up to GWAGENT_LOCK_TIMEOUT seconds (default 300), then fails with busy (exit 7). ui=false ops (calendar provider, file transfers) do not take it. Other programs that touch the phone's UI must take the same lock; the exact contract and a wrapper for shell commands are in docs/OPERATIONS.md#ui-lock-contract.

Tests

Level

Command

Needs

Unit

GWAGENT_NO_LOCK=1 uv run pytest tests/unit -q

nothing (FakeAdb + scrubbed fixtures), ~seconds

Live

uv run pytest tests/live --live -q

the phone; creates and deletes AGT-TEST objects

Live + outward

uv run pytest tests/live --live --outward -q

the phone; also sends mail to the account itself and changes settings (restored)

Quality

uv run python scripts/quality_ratchet.py

no new broad except in src/

The whole suite (uv run pytest --live --outward) takes several hours. Never run two live sessions at once: there is only one phone. One module at a time: uv run pytest tests/live/test_gmail_read_live.py --live --outward -q.

Golden files checked by unit tests: tests/fixtures/mcp_manifest.json (the MCP tool list, regenerate with GWAGENT_REGEN_MANIFEST=1 uv run pytest tests/unit/test_mcp_manifest.py -q) and docs/OPERATIONS.md (regenerate with uv run python scripts/gen_ops_doc.py).

Limitations

  • Speed: ops drive the real UI; typical ops take ~5-35 s, long lists and uploads minutes. Host performance matters most: on macOS the emulator must run as an interactive process (see PORTING.md).

  • English UI only; one phone, one account, one gwagent version per phone.

  • Google Docs/Sheets/Slides cannot be created or edited (their apps are not installed and installing apps is not allowed): drive.create_doc fails with app_missing. Native Google files can only be read; a Google Sheet is read through Drive's export, which is a PDF (table structure lost). The Drive app has no "Manage versions".

  • drive.read_text with method="viewer" only sees the pages the viewer rendered (usually the first); the default download method needs pdftotext for PDFs.

  • Drive search filters use search operators (type:, owner:, after:, before:, is:starred); the app's filter chips are not used. drive.set_role / drive.unshare could only be live-tested on their guards (single-account phone).

  • Gmail dates are parsed from screen text (English formats, phone time zone): date_iso next to the raw value. Gmail bulk actions beyond one screen need all=true with a query or folder.

  • gmail.undo recalls a sent mail only while Gmail still shows the Undo snackbar (seconds).

  • Gemini in Gmail depends on the account's licence (gemini_unavailable otherwise).

  • Calendar: working locations, Out of office and Focus time are not in the provider (they are set, read and deleted through the app). Avoid rapid successive edits of one event (an edit made while the previous one uploads can be overwritten by the server copy; check pending_sync). A recurring series that never synced has no instances until it syncs.

  • Google Tasks are only reachable through the Calendar app UI.

Attribution

Parts of the on-phone server and a few host helpers are adapted from google/artemis, Copyright 2026 Google LLC, licensed under the Apache License 2.0. Adapted code carries a comment saying so; the list of adapted files is in NOTICE and the license text in LICENSES/Apache-2.0.txt. Artemis itself is not used or run.

License

Apache License 2.0: see LICENSE. Third-party attributions are in NOTICE.

Available Tools

156 tools
app_intentC

Start an activity with an arbitrary intent (am start).

Inputs: data, mime, action, extras, +2 more.

app_launchB

Bring an app to the foreground.

Returns the focused window (package, activity, window) plus state: front (the app has the focus), covered (the app is in front under a dialog of another package, e.g. a permission prompt), other (another app is in front) or unknown.

Inputs: package (required), restart, activity.

app_stopB

Force-stop an app.

Inputs: package (required).

calendar_add_meetA

Add a Google Meet video conference to one of your events (the provider cannot create one).

Guests are not e-mailed about the change ("Don't send" is chosen if asked). Returns the Meet link once the provider has it.

Inputs: event_id (required), scope, start, timeout.

calendar_attendee_addA

Invite people to an event (Google e-mails them). Returns the attendee list.

Requires confirm=true (outward-facing or destructive).

Inputs: emails (required), event_id (required), confirm, optional.

calendar_attendee_removeA

Remove guests from an event (Google may e-mail them a cancellation). Returns what is left.

Requires confirm=true (outward-facing or destructive).

Inputs: emails (required), event_id (required), confirm.

calendar_calendarsA

List the calendars on the phone (id, name, account, colour, access level, visible, sync).

Use the id (or the name) as the calendar parameter of other calendar ops. primary marks the account's own calendar (default target of calendar.create).

Inputs: visible_only, writable_only.

calendar_createB

Create an event (synced to Google Calendar). Returns the new event (see calendar.get).

Inputs: start (required), title (required), end, color, +11 more.

calendar_deleteA

Delete an event (the whole series for recurring events, or one occurrence). Synced to Google.

  • An event you organise that has guests: Google cancels it for everybody and e-mails them.

  • An invitation from someone else: it is removed from your calendar only; Google records it as declined (the organizer can see that). To decline explicitly, use calendar.respond.

  • With instance_start, only that occurrence of a recurring event is canceled (the series stays; guests of a series you organise are told about that occurrence).

Requires confirm=true (outward-facing or destructive).

Inputs: event_id (required), confirm, instance_start.

calendar_delete_specialA

Delete a working location, Out of office or Focus time entry of one day in the Calendar app.

These entries are not in the calendar provider (calendar.delete cannot see them). For a working location only that day is removed (your weekly setting stays for other days; the day then has no working location). A multi-day Out of office is deleted as a whole.

Requires confirm=true (outward-facing or destructive).

Inputs: date (required), entry (required), title, confirm.

calendar_eventsA

List event occurrences in a time range (recurring events are expanded into instances).

Range: period, or start + (end | days). With nothing given: from today 00:00 for 7 days. Covers every calendar synced to the phone, including calendars hidden in the app (unless include_hidden=false). Calendars with sync off have no events on the phone: with include_unsynced=true they are synced for the duration of the call (slow: about a minute) and their sync is turned off again afterwards.

Inputs: end, days, limit, query, +9 more.

calendar_focus_timeA

Create a Focus time block in the Calendar app (colleagues see it; meetings can be auto-declined).

Not in the calendar provider: read back from the app, removed with calendar.delete_special.

Requires confirm=true (outward-facing or destructive).

Inputs: start (required), end, title, confirm, +4 more.

calendar_free_busyA

Busy blocks (merged) and the free gaps between them in a time range.

Counts events marked busy (and tentative, unless disabled), skipping canceled events and events you declined. All-day events are ignored unless include_all_day. Every calendar synced to the phone counts (hidden ones too, unless include_hidden=false); calendars with sync off are listed in unsynced_calendars, or synced for the call with include_unsynced.

Inputs: end, days, start, period, +7 more.

calendar_getA

Get one event (whole series for recurring events) with attendees and reminders.

Inputs: event_id (required).

calendar_nextB

The next upcoming (or currently running) event occurrences, soonest first.

Inputs: days, count, calendar, include_hidden, +3 more.

calendar_openA

Show a date in the Calendar app (optionally switching view) and read what is visible.

Returns the view, the toolbar title and the visible items (or, in month view, the number of items of each day). Use calendar.view_screen to read whole days in detail.

Inputs: date, view.

calendar_open_eventA

Open an event in the Calendar app by provider id and read its screen.

Returns what the app shows: when, recurrence, Google Meet link and dial-in, location, notifications, calendar, guests with their answers, notes and your RSVP state (rsvp.available, rsvp.response = yes/no/maybe/null). The event must be synced with Google.

Inputs: event_id (required), start, stay_open.

calendar_out_of_officeA

Create an Out of office block in the Calendar app (colleagues see it; meetings can be auto-declined).

Google shows it to people who look at your calendar and, with auto_decline, declines new meeting invitations in that time, sending message to the organizers. Meetings you already accepted are declined too only when asked explicitly with decline='new_and_existing' (it notifies their organizers); the default is 'only_new'. Out of office is not in the calendar provider: it is read back from the app and removed with calendar.delete_special.

Requires confirm=true (outward-facing or destructive).

Inputs: start (required), end, title, all_day, +4 more.

calendar_reminder_addB

Add a reminder to an event. Returns the event's reminders.

Inputs: minutes (required), event_id (required), method.

calendar_reminder_removeA

Remove reminders from an event (by minutes, by reminder_id, or all). Returns what is left.

Inputs: event_id (required), all, minutes, reminder_id.

calendar_remove_meetA

Remove the Google Meet conference from one of your events (the link stops working).

Requires confirm=true (outward-facing or destructive).

Inputs: event_id (required), scope, start, confirm, +1 more.

calendar_respondA

Answer an invitation: set your own attendee status (the organizer is notified by Google).

Requires confirm=true (outward-facing or destructive).

Inputs: status (required), event_id (required), confirm, instance_start.

calendar_rsvpA

Answer an invitation with the Calendar app's Yes / No / Maybe buttons (organizer is notified).

Unlike calendar.respond (provider), this goes through Google's servers at once and can answer a single occurrence of a recurring event (scope 'this'). The result is checked on the provider after the app syncs. Read the current answer first with calendar.open_event.

Requires confirm=true (outward-facing or destructive).

Inputs: event_id (required), response (required), scope, start, +2 more.

calendar_set_calendarA

Show/hide a calendar in the Calendar app, or turn its sync to the phone on/off.

Hidden calendars are still synced (calendar.events includes them). A calendar with sync off has no events on the phone; turning sync on downloads them (run calendar.sync_background). These are phone settings of your calendar list, not changes to the events.

Inputs: calendar (required), sync, visible.

calendar_set_working_locationA

Set your working location (Home, Office or any place) for one day or a range of days.

Colleagues see it in Calendar/Gmail/Chat. It applies to these days only (your weekly working-location settings stay); to undo, set the previous place again or use calendar.delete_special (the day then has no working location). Read with calendar.working_location.

Requires confirm=true (outward-facing or destructive).

Inputs: date (required), place (required), confirm, end_date.

calendar_syncA

Force a Calendar sync with Google and wait for it to finish.

New server events then show in the provider (calendar.events) and local changes are uploaded. method auto (default): Android's sync manager first (fast, no screen), the app's Refresh if no sync started; background: sync manager only (same as calendar.sync_background); ui: the app's Refresh (drawer). The sync start is verified on Android's sync manager.

Inputs: wait, method, timeout.

calendar_sync_backgroundA

Sync the calendar with Google without the screen (Android sync manager, requestsync).

Pushes local changes (create/update/delete/respond) and pulls server changes; works with the phone locked and the screen off. calendar.sync does the same and falls back to the app.

Inputs: wait, timeout.

calendar_task_completeA

Mark a Google Task as completed (or not completed again with undo=true).

Inputs: title (required), list, undo, occurrence.

calendar_task_createC

Create a Google Task (shown in Calendar on its date).

Inputs: title (required), date, list, time, +2 more.

calendar_task_deleteA

Delete a Google Task.

Requires confirm=true (outward-facing or destructive).

Inputs: title (required), list, confirm, completed, +1 more.

calendar_task_list_createB

Create a new Google Tasks list (it becomes the list shown).

Inputs: list (required).

calendar_task_list_deleteA

Delete a Google Tasks list and all its tasks (the default list cannot be deleted).

Requires confirm=true (outward-facing or destructive).

Inputs: list (required), confirm.

calendar_task_listsC

The Google Tasks lists (names usable as list in the other task ops).

Args:

Inputs: no named parameters.

calendar_tasksA

List Google Tasks of one list as shown in the Calendar app (tasks are not in the provider).

Each task: title, due (as shown, e.g. 'Tomorrow, 9:15 AM'), completed, starred, notes, subtask (true for a subtask, listed right after its parent). Tasks are addressed by title (+ occurrence when several share it) in the other task ops.

Inputs: list, include_completed.

calendar_task_updateA

Edit a Google Task: rename, change/remove its date and time, notes, subtasks, star, list.

Returns the task as it is now (title, list, notes, due, subtasks, starred) plus occurrence, so the new identity (new_title + occurrence) can be used in later calls.

Inputs: title (required), date, list, time, +8 more.

calendar_updateA

Change fields of an event (whole series for recurring events, or one occurrence). Omitted fields are kept.

Changing only start keeps the duration; only end/duration_minutes keeps the start. Pass an empty string to clear location/description; rrule='none' turns a series into a single event (its first occurrence). With instance_start only that occurrence of a recurring event changes (e.g. move tomorrow's stand-up): the result is the occurrence's own event (recurrence_exception_of = the series id).

Guests: changing title, time, location, description or recurrence of an event you organise that has guests makes Google e-mail them the update, so it needs confirm=true. Invitations from someone else cannot be changed (only your availability/colour, and reminders with calendar.reminder_*): answer them with calendar.respond / calendar.rsvp.

Avoid rapid successive edits of the same event: an edit made while the sync adapter is uploading the previous one can be overwritten by the server copy (check pending_sync).

Inputs: event_id (required), end, color, rrule, +10 more.

calendar_view_screenA

Read what the Calendar app shows for one or more days, as structured rows (schedule view).

Shows things the provider lacks: working locations, tasks, the RSVP shown for each event. Rows: kind (event / working_location / task / out_of_office / focus_time), title, all_day, time, start, end (HH:MM), response (Not responded / No / Maybe..., absent when you accepted or organise it), location, calendar (absent for your own calendar), color, and event_id/instance_id matched from the provider (use them with calendar.open_event / calendar.rsvp). If the provider has events the app does not show yet, the app is refreshed and read again; those still missing are listed in not_shown_event_ids.

Inputs: date, days, include, match_ids.

calendar_working_locationB

Your working location (Office, Home, ...) for each day, as shown by the Calendar app.

Working locations are not in the provider. Days without one are reported with place null.

Inputs: date, days.

device_accountsB

Accounts registered on the phone (name and type).

Inputs: no named parameters.

device_install_adb_keyboardA

Install ADBKeyboard (not needed: the on-phone server types any Unicode; the office phone forbids APK installs).

Requires confirm=true (outward-facing or destructive).

Inputs: apk_path (required), confirm.

device_lsB

List files in a phone directory.

Inputs: remote_dir.

device_pullA

Copy a file from the phone to this computer (default: the gwagent downloads dir).

Inputs: remote_path (required), local_path.

device_pushB

Copy a file from this computer to the phone (default: the phone's Download folder).

Inputs: local_path (required), remote_path.

device_rmA

Delete a file from the phone's shared storage (/sdcard only).

Requires confirm=true (outward-facing or destructive).

Inputs: remote_path (required), confirm.

device_shellA

Run a raw adb shell command on the phone (escape hatch for anything not covered).

Inputs: command (required), timeout.

device_sleepA

Turn the screen off (re-locks the phone). Use when done, to save CPU on the host.

Inputs: home.

device_statusB

Phone status: adb state, Android version, screen on/locked, foreground app, app versions.

Inputs: no named parameters.

device_unlockA

Wake the screen and unlock it with the configured PIN (every UI op does this automatically).

Inputs: no named parameters.

drive_cdA

Enter a folder: by path relative to the current location ("A/B/C", ".." = parent, leading "/" = from My Drive) or by index from the last list. Returns the folder's items.

Inputs: path, index, limit.

drive_copyC

Make a copy of a file ("Make a copy"); the copy is named "Copy of ".

Drive is left in the destination folder, where the copy is checked.

Inputs: name, path, index, occurrence, +1 more.

drive_create_docA

Create a new Google Docs/Sheets/Slides file (Create new > Google Docs...).

Drive hands the creation to the Docs/Sheets/Slides app: when that app is not installed (as on this phone) Drive opens the Play Store instead and this op fails with app_missing without creating anything.

Inputs: kind, path.

drive_delete_foreverA

Permanently delete an item that is in the Trash (cannot be undone).

Requires confirm=true (outward-facing or destructive).

Inputs: name, index, confirm, occurrence.

drive_downloadA

Download a file with Drive's "Download" (Google Docs/Sheets/Slides are exported by the app, e.g. a Google Doc as PDF) into the phone's Download folder, then copy it to this computer.

Inputs: name, path, index, timeout, +3 more.

drive_empty_trashA

Permanently delete everything in the Trash ("Empty trash"; cannot be undone).

Give only_if_all_match or all=true (invalid_argument otherwise, nothing touched): the Trash can hold items the user trashed by hand, so emptying it unconditionally must be explicit.

Requires confirm=true (outward-facing or destructive).

Inputs: all, confirm, only_if_all_match.

drive_infoA

Details and activity of an item ('View information'): type, size, location, created, modified (by whom), sharing summary, recent activity.

Inputs: name, path, index, occurrence, +1 more.

drive_listA

List the items of the current Drive location (folder, section or search results).

Scrolls and de-duplicates; switches grid view to list view (richer info). Indexes are usable by drive.cd / drive.info / drive.download / drive.read_text.

Inputs: path, limit, start, section.

drive_locationB

Where Drive is now: screen kind, title, sort order, view mode and remembered folder path.

Inputs: no named parameters.

drive_menuA

List the actions Drive offers for an item (its 'More actions' menu), without running any.

Inputs: name, path, index, occurrence.

drive_mkdirB

Create a new folder in the current location (or in path).

Inputs: name (required), path.

drive_moveB

Move a file or folder to another folder of My Drive.

Inputs: destination (required), name, path, index, +1 more.

drive_offlineB

Make a file available offline on the phone (enable=true) or remove offline access.

Inputs: name, path, index, enable, +1 more.

drive_openC

Open a Drive section and list its first items.

Inputs: limit, section.

drive_read_textA

Read the text of a file (PDF pages, document, slides, sheets, plain text).

'download' downloads/exports the file and extracts its text on this computer (nothing is left on the phone); 'viewer' opens it in Drive's viewer and reads the text it exposes; 'auto' tries download, then the viewer.

Inputs: name, path, index, method, +3 more.

drive_refreshA

Pull-to-refresh the current Drive list (re-sync from the server) and list it again.

Inputs: limit.

drive_renameC

Rename a file or folder.

Inputs: new_name (required), name, path, index, +1 more.

drive_restoreA

Restore an item from the Trash to its original folder.

Inputs: name, index, occurrence.

drive_set_roleA

Change the role of a person who already has access to a file or folder (Manage access).

Requires confirm=true (outward-facing or destructive).

Inputs: role (required), email (required), name, path, +3 more.

drive_shareB

Share a file or folder with people (Share > add people, role, notify, Send).

Requires confirm=true (outward-facing or destructive).

Inputs: emails (required), name, path, role, +5 more.

drive_sharingA

Read an item's sharing (changes nothing): people with access (name, email, role), general access ('Restricted'...) and the item's link and file id.

Inputs: name, path, index, copy_link, +1 more.

drive_sortA

Change the sort order of the current list (Drive remembers it per view).

Inputs: by, reverse.

drive_starC

Add a file or folder to Starred.

Inputs: name, path, index, occurrence.

drive_storageB

Drive storage used, and optionally the files using the most storage.

Inputs: limit.

drive_trashA

Move a file or folder to the Trash (restorable with drive.restore for 30 days).

Requires confirm=true (outward-facing or destructive).

Inputs: name, path, index, confirm, +1 more.

drive_unshareA

Remove people's access to a file or folder (Manage access > person > Remove).

Every address must currently have access (else not_found and nothing is changed); the owner cannot be removed.

Requires confirm=true (outward-facing or destructive).

Inputs: emails (required), name, path, index, +2 more.

drive_unstarB

Remove a file or folder from Starred.

Inputs: name, path, index, occurrence.

drive_upB

Go to the parent folder (or leave a section/search back to the main screen).

Inputs: limit.

drive_uploadA

Upload a file from this computer into a Drive folder (Create new > Upload).

The file is copied to the phone, picked in the system file picker, uploaded under title into the folder, then the phone copy is removed. Waits until the new item shows in the folder and the upload bar no longer reports progress.

Inputs: local_path (required), path, title, timeout.

drive_upload_filesB

Upload several files from this computer into one Drive folder (one after the other).

Inputs: local_paths (required), path, timeout.

drive_upload_folderA

Upload a folder from this computer: creates a Drive folder and uploads its files and sub-folders into it (recursively; hidden files are skipped).

Inputs: local_dir (required), name, path, timeout, +1 more.

drive_viewA

Switch the current Drive list between list and grid view.

Inputs: mode.

gmail_archiveB

Archive conversations (remove them from the Inbox; they stay in All mail).

Inputs: all, index, match, query, +3 more.

gmail_attachmentsC

List the attachments of a conversation (name, type, size, message index).

Inputs: index, query, folder, stay_open, +2 more.

gmail_backA

Close the open conversation (or dialog/full view) and return to the mail list.

Inputs: to_inbox.

gmail_block_senderA

Block the sender of a message: their future mail goes to Spam (message menu > Block "sender").

Gmail blocks at once (no dialog); gmail.unblock_sender reverts it. Not offered for your own messages.

Requires confirm=true (outward-facing or destructive).

Inputs: index, query, folder, confirm, +4 more.

gmail_cancel_scheduledA

Cancel a scheduled send: the message is not sent and becomes a draft again (see gmail.drafts).

Inputs: index, query, expect_subject.

gmail_compose_stateA

Describe the compose window currently open (mode, recipients, subject, body, attachments).

Inputs: no named parameters.

gmail_create_labelB

Create a new Gmail label (drawer -> Create label).

Inputs: name (required).

gmail_delete_draftA

Delete (discard) a draft: index from gmail.drafts, optionally with the same query.

Requires confirm=true (outward-facing or destructive).

Inputs: index, query, confirm, expect_subject.

gmail_delete_foreverA

Permanently delete conversations that are in the Trash or Spam (cannot be undone).

Only conversations listed in the Trash or Spam folder (default: Trash), or in a search restricted to them ('in:trash ...' / 'in:spam ...'), can be targeted. Some Gmail builds (e.g. Workspace accounts) offer no per-conversation 'Delete forever'; the op then fails with not_found and gmail.empty is the only way to purge.

Requires confirm=true (outward-facing or destructive).

Inputs: all, index, match, query, +4 more.

gmail_delete_labelA

Delete a user label (Gmail settings -> Manage labels). Conversations keep existing without it.

Requires confirm=true (outward-facing or destructive).

Inputs: name (required), confirm.

gmail_discardA

Close the compose window currently open WITHOUT sending it (its text is thrown away).

Requires confirm=true (outward-facing or destructive).

Inputs: confirm.

gmail_download_attachmentA

Download attachment(s) of a conversation to the phone's Download folder and copy them to this computer.

Choose the attachment with name (case-insensitive substring) or attachment_index (from gmail.attachments), or all=true; with none of them the conversation must have exactly one attachment.

Inputs: all, name, index, query, +7 more.

gmail_draftA

Create a new draft (not sent): recipients, subject, Unicode body, attachments.

Inputs: cc, to, bcc, body, +5 more.

gmail_draftsB

List drafts (Drafts folder, or drafts matching a Gmail query). Indexes feed gmail.send_draft.

Inputs: limit, query.

gmail_edit_draftA

Change an existing draft (recipients, subject, body, attachments) and save it again.

The draft keeps its thread (reply drafts stay replies). Fields left out are unchanged. With append=false (default) given recipients REPLACE the field and body replaces the whole editable body; with append=true recipients are added and body is added at the end.

Inputs: cc, to, bcc, body, +7 more.

gmail_emptyA

Empty the Trash or the Spam folder: EVERY conversation in it is permanently deleted.

Returns how many conversations Gmail said it would delete ('count').

Requires confirm=true (outward-facing or destructive).

Inputs: folder, confirm.

gmail_foldersA

List Gmail folders (inbox categories, system folders) and user labels with unread counts.

Counts are what the drawer shows: unread conversations for most folders, the total for Drafts/Outbox/Scheduled, and 'N new' for inbox categories.

Inputs: no named parameters.

gmail_forwardA

Forward the last message of a conversation, or message_index (with its attachments), to new recipients.

Send is never repeated blind: when Gmail's reaction to Send is unclear the mail is looked up in Outbox / Sent; if that cannot tell, the op fails with error 'send_state_unknown' and nothing is re-sent (check gmail.list folder='sent' before any retry). A result may carry verified_in='outbox' | 'sent' when the send was confirmed that way.

Requires confirm=true (outward-facing or destructive).

Inputs: cc, to, bcc, body, +9 more.

gmail_geminiA

Ask Gemini in Gmail and return its answer text.

Without index/query/folder Gemini is asked from the mail list (questions about the mailbox, e.g. 'unread mail from Alice this week'); with them the conversation is opened first and the question is about that conversation. Raises gemini_unavailable when the account has no Gemini in Gmail.

Inputs: prompt (required), index, query, folder, +2 more.

gmail_importantC

Mark conversations as important.

Inputs: all, index, match, query, +3 more.

gmail_labelB

Apply and/or remove labels on conversations ('Label as' dialog; 'Inbox' is a label too).

The result lists the labels offered by the dialog ('labels').

Inputs: add, all, index, match, +5 more.

gmail_labelsA

List the user labels (and system folders) available to gmail.label / gmail.move.

Inputs: no named parameters.

gmail_listA

List conversations of the current list (folder, label or search results) or of a given folder.

Rows have an 'index' (0 = top of the list) usable with gmail.read / gmail.reply / gmail.archive ... The list and its rows are remembered: a follow-up op given only an index re-opens this list if the phone shows another one, and refuses (not_found) when the row at that index is no longer the same conversation (it is looked up nearby if it just moved).

Inputs: limit, start, folder.

gmail_mark_readB

Mark conversations as read (changed=false if they already were).

Inputs: all, index, match, query, +3 more.

gmail_mark_unreadB

Mark conversations as unread (an open conversation is closed, as in the app).

Inputs: all, index, match, query, +3 more.

gmail_message_actionsA

List the actions a message's own menu offers (Reply all, Forward, Block "x", Report spam ...).

Read-only: tells whether gmail.block_sender / gmail.unblock_sender / gmail.unsubscribe / gmail.report_phishing apply to this message.

Inputs: index, query, folder, stay_open, +3 more.

gmail_moveA

Move conversations to another folder: an inbox category (Primary, Promotions...), a label, or the Inbox.

'Move to' is offered on folder lists (and, as 'Move to Inbox', for trashed/archived search results); it is not available on ordinary search results - use gmail.label there.

Inputs: to (required), all, index, match, +4 more.

gmail_muteA

Mute conversations: they are archived and later replies skip the Inbox (find them with 'is:muted').

Inputs: all, index, match, query, +3 more.

gmail_not_importantC

Mark conversations as not important.

Inputs: all, index, match, query, +3 more.

gmail_not_spamB

Report conversations as not spam: they go back to the Inbox (default list: the Spam folder).

Inputs: all, index, match, query, +3 more.

gmail_openB

Open Gmail on a folder or label (Inbox, Starred, Sent, Drafts, All mail, Spam, Trash, a user label...).

Inputs: folder.

gmail_readA

Read a whole conversation: subject, labels, and every message (from, to, cc, date, full body, attachments).

Opens row index of the current list (or of query search results / folder); with no index, reads the conversation already open. Collapsed messages are expanded and the whole thread is scrolled. Opening an unread conversation marks it read unless keep_unread=true.

Inputs: full, index, query, folder, +4 more.

gmail_refreshA

Sync mail now: pull-to-refresh on the current list (goes to the list first).

Inputs: no named parameters.

gmail_rename_labelA

Rename a user label (Gmail settings -> Manage labels -> label -> Name). Conversations keep it.

Inputs: name (required), new_name (required).

gmail_replyA

Reply to the sender of the last message of a conversation (or of message_index).

Send is never repeated blind: when Gmail's reaction to Send is unclear the mail is looked up in Outbox / Sent; if that cannot tell, the op fails with error 'send_state_unknown' and nothing is re-sent (check gmail.list folder='sent' before any retry). A result may carry verified_in='outbox' | 'sent' when the send was confirmed that way.

Requires confirm=true (outward-facing or destructive).

Inputs: cc, to, bcc, body, +9 more.

gmail_reply_allA

Reply to everyone on the last message of a conversation (or of message_index).

Send is never repeated blind: when Gmail's reaction to Send is unclear the mail is looked up in Outbox / Sent; if that cannot tell, the op fails with error 'send_state_unknown' and nothing is re-sent (check gmail.list folder='sent' before any retry). A result may carry verified_in='outbox' | 'sent' when the send was confirmed that way.

Requires confirm=true (outward-facing or destructive).

Inputs: cc, to, bcc, body, +9 more.

gmail_report_phishingA

Report a message as phishing to Google (irreversible), when Gmail offers it for that message.

Not every message has the entry: the op then fails with not_found and lists the actions the message menu offers (see gmail.message_actions); use gmail.spam to report spam instead.

Requires confirm=true (outward-facing or destructive).

Inputs: index, query, folder, confirm, +3 more.

gmail_restoreA

Bring conversations back to the Inbox from the Trash, Spam or the archive (All mail / search).

Uses 'Move to Inbox' / 'Move to' -> Inbox when offered, else adds the Inbox label.

Inputs: all, index, match, query, +3 more.

gmail_save_attachment_to_driveA

Save an attachment of a conversation into Google Drive (the tile's 'Save to Drive').

Choose the attachment with name (case-insensitive substring) or attachment_index (from gmail.attachments); with neither, the conversation must have exactly one attachment.

Inputs: name, path, index, query, +6 more.

gmail_scheduledA

List mail scheduled to be sent later (the 'Scheduled' folder). Indexes feed gmail.cancel_scheduled.

Inputs: limit, query.

gmail_scrollA

Scroll the current Gmail list or open conversation; 'top' returns to the top of the list.

On a list, the rows now visible are returned with their absolute 'index' (usable by gmail.read / gmail.reply / gmail.archive...) when the list is the one last returned by gmail.list / gmail.search / gmail.scroll; 'indexed' tells whether they could be numbered (direction=top always can). On an open conversation the visible messages are returned.

Inputs: times, direction.

gmail_sendA

Send a new e-mail (any Unicode text) with optional Cc, Bcc, attachments, From alias, or schedule it.

The account's signature is added after the body by Gmail (signature=false removes it). Recipients are verified on the compose screen before sending; on any mismatch nothing is sent. Scheduled mail waits in the 'Scheduled' folder (gmail.scheduled, gmail.cancel_scheduled).

Send is never repeated blind: when Gmail's reaction to Send is unclear the mail is looked up in Outbox / Sent; if that cannot tell, the op fails with error 'send_state_unknown' and nothing is re-sent (check gmail.list folder='sent' before any retry). A result may carry verified_in='outbox' | 'sent' when the send was confirmed that way; a scheduled send counts only once Gmail confirms it or it is found in 'Scheduled' (verified_in='scheduled').

Gmail offers 'Undo' for about 7 s after 'Sent' (undo_available tells whether it was still on screen when the op returned; gmail.undo uses it). undo=true taps that Undo within this op, the moment Gmail shows it: the result then has undone='send' and saved_as_draft=true only when the mail came back as a draft and is not in Outbox / Sent; undone=false (with undo_error) means it was sent anyway.

Requires confirm=true (outward-facing or destructive).

Inputs: to (required), cc, bcc, body, +8 more.

gmail_send_draftA

Send an existing draft (index from gmail.drafts, optionally with the same query).

Send is never repeated blind: when Gmail's reaction to Send is unclear the mail is looked up in Outbox / Sent; if that cannot tell, the op fails with error 'send_state_unknown' and nothing is re-sent (check gmail.list folder='sent' before any retry). A result may carry verified_in='outbox' | 'sent' when the send was confirmed that way; a scheduled send counts only once Gmail confirms it or it is found in 'Scheduled' (verified_in='scheduled').

Requires confirm=true (outward-facing or destructive).

Inputs: index, query, confirm, schedule, +1 more.

gmail_set_signatureA

Set the mobile signature added to mail sent from the phone ('' removes it).

Inputs: text (required).

gmail_set_vacationA

Turn the vacation responder (out-of-office auto-reply) on or off and set its dates and text.

While on, Gmail answers incoming mail automatically (outward-facing: needs confirm). Fields left out keep their current value. The result is the state read back from the screen.

Requires confirm=true (outward-facing or destructive).

Inputs: enabled (required), confirm, message, subject, +4 more.

gmail_signatureB

Read the mobile signature Gmail adds to mail sent from the phone ('' = none: the web signature is used).

Inputs: no named parameters.

gmail_snoozeA

Snooze conversations until a preset moment or a chosen date and time (gmail.unsnooze reverts).

Gmail offers only the presets that make sense now (e.g. no 'later_today' in the evening); an unavailable preset fails with not_found listing the available ones.

Inputs: all, date, time, index, +6 more.

gmail_spamA

Report conversations as spam (moved to Spam; the report is sent to Google).

Requires confirm=true (outward-facing or destructive).

Inputs: all, index, match, query, +4 more.

gmail_starC

Star conversations.

Inputs: all, index, match, query, +3 more.

gmail_summarizeB

Summarize a conversation with Gemini in Gmail ('Summarize this email').

Raises gemini_unavailable when the account has no Gemini in Gmail.

Inputs: index, query, folder, timeout, +1 more.

gmail_trashA

Delete conversations (move them to the Trash; gmail.restore or gmail.undo bring them back).

Requires confirm=true (outward-facing or destructive).

Inputs: all, index, match, query, +4 more.

gmail_unblock_senderB

Unblock the sender of a message (banner 'Unblock sender' or message menu > Unblock).

A sender that is not blocked is left alone (was_blocked=false).

Inputs: index, query, folder, stay_open, +3 more.

gmail_undoA

Undo the last Gmail action: tap 'Undo' on its snackbar if still shown, else revert it.

After gmail.send / reply / forward / send_draft it recalls the mail while Gmail still shows 'Undo' (about 7 s after 'Sent'; the send result's undo_available says whether it still did): undone='send' only when the message is back in Drafts and not in Outbox / Sent. Later it fails (not_found, the mail was sent). To be sure to recall a send, use gmail.send undo=true.

Reverting runs the opposite action on the same conversations (found again by subject): archive/trash/spam -> back to the Inbox, star <-> unstar, read <-> unread, label add <-> remove, mute -> unmute, snooze -> unsnooze, move -> move back (when it was done from a folder list).

Inputs: max_age_s.

gmail_unmuteA

Unmute conversations (e.g. query='is:muted'); they stay archived (use gmail.restore for the Inbox).

Inputs: all, index, match, query, +3 more.

gmail_unread_countB

Number of unread conversations in a folder or label, as shown in the Gmail drawer.

Inputs: folder.

gmail_unsnoozeA

Unsnooze conversations: they return to the Inbox now (default list: the Snoozed folder).

Inputs: all, index, match, query, +3 more.

gmail_unstarC

Remove the star from conversations.

Inputs: all, index, match, query, +3 more.

gmail_unsubscribeA

Unsubscribe from a mailing list / newsletter with Gmail's own Unsubscribe (outward, irreversible).

Gmail offers it only for messages with a recognised unsubscribe header; otherwise the op fails with not_found and lists the available actions.

Requires confirm=true (outward-facing or destructive).

Inputs: index, query, folder, confirm, +3 more.

gmail_vacationA

Read the vacation responder (out-of-office auto-reply): on/off, first/last day, subject, message.

Inputs: no named parameters.

meta_list_opsA

List every available operation with its parameters (JSON schema).

Inputs: app.

ui_double_tapA

Double-tap by coordinates or on the first element matching a selector (e.g. zoom in an image or PDF preview). Without the on-phone server a slow phone may see two single taps.

Inputs: x, y, rid, desc, +5 more.

ui_dumpB

Describe the current screen: foreground app and visible elements (text, desc, id, bounds, flags).

Inputs: raw, all_nodes.

ui_findC

Find elements on screen matching a selector (optionally wait up to timeout seconds).

Inputs: rid, desc, text, timeout, +3 more.

ui_global_actionA

Perform a system action like the navigation buttons or the status bar do.

lock_screen turns the screen off and locks the phone (the next UI op unlocks it); take_screenshot saves an image in the phone's gallery (prefer ui.screenshot); power_dialog needs the on-phone server.

Inputs: action (required).

ui_keyA

Press a key: BACK, HOME, ENTER, APP_SWITCH, DEL, TAB, DPAD_DOWN, MENU, SEARCH, or any KEYCODE_*.

Inputs: key (required).

ui_screenshotB

Save a PNG screenshot of the phone screen on this computer (look at it to see the UI).

Inputs: local_path.

ui_scrollA

Scroll the main content ('down' shows what is below, 'up' goes back toward the top).

Inputs: times, amount, direction.

ui_swipeC

Raw swipe gesture between two points.

Inputs: x1 (required), x2 (required), y1 (required), y2 (required), +1 more.

ui_tapB

Tap by coordinates, or tap the first element matching a selector (waits for it).

Inputs: x, y, rid, desc, +6 more.

ui_typeB

Type text into the focused field.

Inputs: text (required), clear, enter.

ui_waitB

Wait until an element appears (or disappears with gone=true).

Inputs: rid, desc, gone, text, +4 more.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    B3.2/5.0

    Scored across 156 tools

    Disambiguation3/5

    Tools are grouped by domain and mostly distinct, but many near-neighbors require close reading, such as calendar_respond vs calendar_rsvp, calendar_sync vs calendar_sync_background, gmail_move vs gmail_label, and gmail_delete_forever vs gmail_empty. Descriptions clarify most boundaries, but the large surface creates real misselection risk.

    Naming Consistency4/5

    All names use snake_case with a clear domain prefix (calendar_, drive_, gmail_, ui_, device_, app_), giving predictable grouping. The pattern is not strictly verb_noun throughout—several names are noun/state-oriented such as drive_location, gmail_compose_state, and calendar_calendars—but consistency is mostly maintained.

    Tool Count1/5

    At 156 tools, this is far beyond the well-scoped 3-15 range and exceeds the 50-tool threshold for extreme mismatch. Even though the server spans multiple app domains, the surface is too large to navigate efficiently as a coherent tool set.

    Completeness5/5

    Coverage across Gmail, Calendar, Drive, UI automation, and device control is extensive, including CRUD, sharing, labels, drafts, sync, tasks, and escape hatches. No obvious dead ends are apparent; unsupported edge cases can generally be reached through device_shell.

    Maintenance

    ActivityMaintained
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      D
      maintenance
      Enables full Android control from any AI agent via 7 MCP tools, including screen capture, touch interaction, app management, and system control.
      43 PyPI
      MIT
    • A
      license
      A
      quality
      D
      maintenance
      Drive an Android phone over ADB and expose it to AI agents via MCP, enabling natural language-driven interaction for native app testing.
      13
      MIT
    • A
      license
      Not graded
      quality
      C
      maintenance
      Enables AI agents to control Android devices via ADB, providing tools for screen capture, tap/swipe/input, app management, and diagnostics. Works locally over USB or WiFi and integrates with MCP clients like Claude and Cursor.
      1
      MIT