Skip to main content
Glama
gwsadm-mcp

gwsadm-mcp

by shigechika

Server Configuration

Describes the environment variables required to run the server.

NameRequiredDescriptionDefault
GWSADM_CONFIGNoPath to the INI configuration file. Defaults to ~/.config/gwsadm-mcp/config.ini~/.config/gwsadm-mcp/config.ini

Instructions

Guidance the server publishes about itself, which clients place ahead of the tool catalog so the model reads it before choosing anything.

This server publishes no instructions, or was last inspected before Glama recorded them.

Capabilities

Features and capabilities supported by this server

Protocol revision2025-11-25

CapabilityDetails
tools
{
  "listChanged": false
}
prompts
{
  "listChanged": false
}
resources
{
  "subscribe": false,
  "listChanged": false
}
experimental
{}

Tools

Functions exposed to the LLM to take actions

NameDescription
health_checkA

Report service status, version, and per-domain auth/API reachability.

Always returns the same keys: status (healthy / degraded / error), service, version, config, and domains (per-domain auth result plus gmail: ok / api_disabled / no_grant / error, with gmail_detail on an unexpected error -- whether the per-user Gmail tools can work there; it does not affect status), sites (the optional GWSADM_SITES_INI table: configured / ranges / error) and geo (the optional GWSADM_IP2ASN dataset: configured / home_countries / age_days / error / warning); both are informational and do not affect status. Lightweight: one 1-item login query and one users.getProfile per domain.

login_auditA

Audit the login log: Google-auto-disabled accounts, suspicious logins, failure top-N.

Answers "did Google itself decide something was wrong with an account here, and with which account?" — the question a "my mail suddenly stopped working" ticket usually turns out to be. account_disabled_* is Google reporting that it locked an account (leaked password, hijacking, spamming); suspicious_login and gov_attack_warning are warnings without a lock.

In both sections user is the account the event is ABOUT, not an actor who did something: Google raised these itself, so the actor is Google and the account is read from the event's affected_email_address. Treat a hit as evidence about that account, and reach for get_user next for its current state.

An account_disabled_spamming entry means Google observed outbound spam, which is a compromise finding, not a delivery problem — the account was almost certainly being used by someone else. The IdP upstream is a separate system and is NOT disabled by this: an account locked here can still authenticate there until it is disabled there too.

Combine with a Directory suspended-users snapshot (Phase 2) for current state. Each section carries capped (window not fully scanned) — treat counts as lower bounds then.

gmail_usage_reportA

Daily Gmail send/receive counts per domain (Admin SDK customerUsageReports).

Answers "how much Gmail traffic did this domain send/receive on day X" -- a customer-level daily counter, NOT a per-user or per-message breakdown (the Reports API has no such thing; login_audit's activity stream is the per-event alternative for that, at the cost of no volume total).

Each day's fetch is independent: a domain missing the DWD scope reports one whole-domain error (there is no point retrying days times for an identical scope failure), but a single date's fetch failing for some other reason (Google's processing for that day not finished yet is a known lag on this API family; a transient error) only skips that one date, recorded in date_errors -- the rest of the window still comes back. The per-domain result also carries capped -- true if ANY date's fetch hit the (rare, since a single day's customer report is normally one record) pagination limit -- so a truncated day's counters are never mistaken for the complete picture.

Requires the admin.reports.usage.readonly DWD scope, granted PER SERVICE ACCOUNT CLIENT ID in the Admin console (Security > API controls > Domain-wide delegation) -- a DIFFERENT scope from admin.reports.audit.readonly (the one login_audit, drive_external_sharing, drive_doc_activity, shared_drive_membership_changes and daily_brief use), even though both live under the same Admin SDK Reports API. Having one does not imply the other; grant this one separately.

Read-only: only customerUsageReports().get() is issued, restricted to the two Gmail counters this tool reports via the API's own parameters filter (:data:GMAIL_USAGE_PARAMETERS) rather than pulling every application's counters.

Args: days: How many days back to report, ending YESTERDAY (not today -- the current day's data is not final until it ends) in the Reports API's own UTC-8:00/Pacific-Standard-Time date anchor. Must be 1..:data:MAX_USAGE_REPORT_DAYS -- 0/negative would otherwise silently return a well-formed, error-free empty result indistinguishable from "queried correctly, zero Gmail traffic that window", and an unbounded value drives an unbounded (domain x date) fan-out (one blocking API call per task, even though they run concurrently). domain: Configured [domain.*] section to report on. Default: all configured domains.

suspended_accountsA

Snapshot of currently suspended Google Workspace accounts, per domain.

A suspended-but-still-provisioned account is a common attack surface: an account disabled in Google may remain enabled in a downstream IdP (e.g. KeyCloak), where a password-spray attacker can still authenticate through it. Cross-reference this list against the IdP to find and disable such gaps.

Unlike login_audit (which reports the event of Google disabling an account within a time window), this is current state — every account suspended right now, regardless of when. Read-only (Directory API users().list with query=isSuspended=true). Requires the admin.directory.user.readonly DWD scope; a domain missing that grant degrades to {"error": ...} for that domain only. capped is set when max_pages was hit before the listing was exhausted.

Coverage is per configured domain (Directory domain= filter), unlike the customer-wide Reports tools — every domain you want covered (e.g. a separate student domain) must have its own [domain.*] config section, or its suspended accounts are not listed.

Args: domain: Restrict to one configured domain (default: all). max_pages: Page cap (500 accounts/page); capped=true means more exist.

get_userA

Look up ONE named account's current state — the "why can't this person sign in" tool.

Answers a helpdesk ticket that already names the exact address: is the account suspended (and for what reason, since when), archived, enrolled in or enforced into 2-step verification, when did it last log in, which org unit is it in, is a password change pending. One Directory API request, no pagination.

Use this — not suspended_accounts — whenever the address is known. That tool lists only accounts that ARE suspended, so it can never confirm that a given address is not suspended, and once that list exceeds its page cap absence stops being evidence either way — after spending far more API calls than this. suspended_accounts is for the domain-wide sweep it is actually named for.

This directory is downstream of the identity provider, not the master. Read the answer as "what Google Workspace currently believes about this account" and compare it against the IdP's own record, which is authoritative for who the account is. A disagreement is usually drift on this side rather than a mistyped address — an account the IdP still authenticates can be suspended or archived here, and an address the IdP does not assert at all will simply come back found: false.

An address that names no account returns found: false with no state fields. That is a normal, expected answer — a typo'd or long-deleted address — and is itself the diagnostic result, NOT a failure. A missing DWD scope, a rejected credential or a transient API failure is reported as {"error": ...} instead. The two are deliberately distinct: never read found: false as "the lookup did not work", and never read an error as evidence about whether the account exists.

Read-only (Directory API users().get; this tool never changes the account). Requires the admin.directory.user.readonly DWD scope — the same one suspended_accounts uses, so a tenant already running that tool needs no additional grant.

Args: username: Exact user email, passed through as the Directory API userKey (primary or alias address both work on Google's side; the returned email is the account's canonical primary one). domain: Configured [domain.*] section to route the lookup through. Default: resolved from the username's suffix. Set it explicitly when the address uses an alias/secondary domain that has no config section of its own (common when copying addresses from mail headers or IdP logs).

user_oauth_tokensA

List third-party OAuth apps one user has granted account access to.

Account-compromise triage tool for the case login_audit and suspended_accounts are both blind to: a malicious app used a previously-granted OAuth token to read/delete mail or Drive files without ever generating a fresh login event. Check each entry's scopes for Gmail/Drive access on an unrecognized client_id/display_text — Google's own apps (e.g. iOS/Android account sync) show up too and are normal noise.

Read-only (Directory API tokens().list; never tokens().delete()). Requires the admin.directory.user.security DWD scope — distinct from admin.directory.user.readonly used by suspended_accounts; a domain missing that grant returns {"error": ...}. No pagination: the API returns a user's full grant list in one response.

Args: username: Exact user email, passed through as the Directory API userKey (primary or alias address both work on Google's side). domain: Configured [domain.*] section to route the lookup through. Default: resolved from the username's suffix. Set it explicitly when the address uses an alias/secondary domain that has no config section of its own (common when copying addresses from mail headers or IdP logs).

user_activityA

Everything ONE account did across Workspace in a window, newest first.

The first question after an upstream identity provider reports an account as compromised: did the attacker get into Google, and what did they do there? login_audit cannot answer it — it is a domain-wide view of the events Google itself raised, so it shows neither one named account's logins nor the logins Google did not flag. This tool asks the Reports API for that one account (userKey=<username>, server-side) across several applications at once and merges the answers.

Read flags first. It holds the events that change the account's security posture or mark it as under attack, regardless of how much ordinary activity surrounds them: a second factor enrolled or disabled (2sv_enroll survives a password reset — it is the attacker's way back in), recovery email/phone changes, a password change, outbound mail forwarding to another domain, Google's own risky_sensitive_action_* verdicts, a login Google marked is_suspicious, and every OAuth authorize (client and scopes — note that a service account impersonating the user through domain-wide delegation, this server's own Gmail tools included, appears here too, as an authorize whose client_id is the service account's numeric id). Google's account_disabled_* / suspicious_login / gov_attack_warning verdicts are flags too, but they are NOT served under the account's own userKey (their actor is Google), so they are fetched separately the way login_audit does and reported under the synthetic application login_verdicts — its event_errors names any verdict probe that failed. ips lists the distinct source addresses with first/last seen and which applications each touched — the input to ip_activity for the "did the same source reach other accounts" question. timeline is the merged event list (Drive read/edit/download noise is counted under applications.drive.events but kept off the timeline), cut at max_events with timeline_truncated saying so. Counts on applications, ips and the timeline are raw deliveries: an event the API delivers under two applications counts twice there and once in flags.

Coverage is explicit: window.start is the time actually asked for, each application reports its own capped (more pages existed) or error (that application failed — the others still answer), and capped at the top is their union, a failed application included. Reports data for the current day can lag by hours, so an empty window right after an incident is "not visible yet", not "nothing happened". A missing DWD scope or a rejected credential is a whole-call {"error": ...}.

Read-only (Reports API activities().list only, the same admin.reports.audit.readonly DWD scope as login_audit).

Args: username: Exact user email, passed through as the Reports API userKey (primary or alias address both work on Google's side). hours: Window length ending now. applications: Comma-separated Reports API application names to walk. Default: login, token, user_accounts, drive, groups, calendar, chat, meet, saml. gmail is deliberately not in the default — not every tenant has it — pass it explicitly where it exists. max_pages: Page budget per application (1000 events per page). The verdict probes use their own small budget (VERDICT_MAX_PAGES). max_events: Timeline rows returned (newest first); flags, ips and counts are computed over everything fetched regardless. domain: Configured [domain.*] section to route the lookup through. Default: resolved from the username's suffix.

ip_activityA

Every account ONE source address acted as, across applications and domains.

The follow-through after user_activity names an attacker's address: did the same source get into other accounts? The Reports API filters on actorIpAddress server-side, so this is one probe per (domain × application) over the matching events only, not a scan of the whole tenant's log — and every domain's probes run in one pool, so a slow domain does not delay another's answer. Per domain it returns the acting accounts (count, first/last seen, which applications) ordered by most recent, the posture flags the address triggered (same definition as user_activity; Google's own verdicts are included here when they carry the address, since userKey=all sees them), and per-application accounting. Failed logins from the address appear under applications.login.events as login_failure — a spray from this address shows there before it ever succeeds.

Coverage reads as in user_activity: window.start, per-application capped / error (and the union capped, a failed application included), and a per-domain error when that domain's credential is rejected (the other domains still answer). A quiet answer for a current-day window can be data lag rather than absence.

Read-only (Reports API activities().list only, the admin.reports.audit.readonly DWD scope).

Args: ip: The source address, IPv4 or IPv6 literal (no zone id). Validated before use. hours: Window length ending now. applications: Comma-separated application names; default as in user_activity. max_pages: Page budget per (domain × application). top: Accounts returned per domain, most recently seen first; accounts_total says how many there were. domain: Restrict to one configured [domain.*] section. Default: every configured domain, since an address is not tied to one.

compromise_sweepA

The Google-side follow-through after ONE account is confirmed compromised, in one call.

Four questions, each answered from its own source and reported in its own section so that a failure in one never hides the others:

  • activity: what the account did (user_activity for the window, flags first -- a 2sv_enroll from the attacker is the item that survives a password reset). A Reports failure stays in this section as error; the others are still attempted with their own grants.

  • state / grants: what the account is now (get_user: suspended, 2SV enrolled, last login) and which OAuth grants can read or act on content (grants.risky: mail / Drive / contacts / admin scopes; sign-in-only grants are listed but not called out).

  • ips: whether the same sources reached other accounts (ip_activity across every configured domain for the addresses in ips, or, when none are given, for the addresses on the account's own flags -- which include the attacker's, and also the owner's own when they authorized an app or re-authenticated -- capped at SWEEP_MAX_IPS, newest first). The swept account itself is excluded from other_accounts; a shared egress address (a campus NAT, a gateway host that impersonates accounts) legitimately shows other accounts, so read other_accounts against where the address sits.

  • gmail: the settings an attacker leaves behind to keep reading mail (auto-forwarding, forwarding addresses, filters that forward / delete / archive, delegates, send-as aliases and reply-to) plus what the mailbox sent in the window as counts and recipient domains -- never a subject or body. Its kind says whether it could be read at all: ok, partial (a settings section or the sent summary failed -- section_errors / sent.error say which; the rest stands), api_disabled (the Gmail API is off in the service account's own Cloud project), no_grant (no gmail.readonly DWD grant) or error. A failure is reported as such, never as "nothing found".

highlights lists the facts a responder acts on, one line each, and checklist what remains to do or verify by hand (second-factor removal, session sign-out, recovery contacts, message content). Nothing here changes the account; the identity provider's side of the incident (and any other SaaS) stays with the caller.

Read-only: Reports API activities().list, Directory users().get and tokens().list, and Gmail settings.* reads plus messages().list / messages().get(format="metadata") with the To / Cc / Bcc / Date headers only.

Args: username: Exact user email of the compromised account. hours: Window length ending now, for activity, the IP sweep and sent mail. ips: Comma-separated attacker addresses to sweep, at most SWEEP_MAX_IPS (more is refused, not truncated). Default: the addresses on the account's own flags, newest first, same cap. max_pages: Page budget per application for the account's activity (the IP sweep uses its own small budget). max_sent: Sent messages whose envelope is fetched, concurrently (the count covers everything listed). domain: Configured [domain.*] section for the account. Default: resolved from the username's suffix. The IP sweep always covers every configured domain.

gmail_message_traceA

Check whether a message (by RFC 822 Message-ID) reached specific users' mailboxes.

Answers "who got this email and who didn't" for a KNOWN Message-ID and a KNOWN candidate recipient list — there is no Workspace API to search across every user for one message, so the caller supplies who to check (a mailing-list roster, or simply the people who reported a problem). For each recipient this impersonates that exact user via domain-wide delegation and searches their own mailbox (including Spam and Trash) for the Message-ID.

Requires the gmail.readonly DWD scope — granted PER SERVICE ACCOUNT CLIENT ID in the Admin console (Security > API controls > Domain-wide delegation), separately from the admin.directory.* / admin.reports.* scopes the rest of this server uses, and NOT on by default. A domain missing that grant reports a per-recipient error rather than a silent "not found" — the two must never be confused, since "not found" here can also legitimately mean the message was delivered and later deleted by the user, or never delivered at all; this tool cannot tell those apart, only "a match currently exists in this mailbox" from "it doesn't".

Read-only: only messages().list and messages().get (metadata only, never the message body) are issued against each impersonated mailbox — see DomainClient.find_message_by_id.

A per-recipient result sets ambiguous: true (with match_count) when more than one message in that mailbox shares the Message-ID (e.g. a mailing-list copy plus a direct CC) — the other fields describe only the first match in that case, not a combined answer. match_count_capped is set alongside it when the mailbox has enough matches that match_count itself is a lower bound, not exact.

Args: message_id: The RFC 822 Message-ID to search for, with or without angle brackets. Must be shaped like an address (local@domain, no whitespace) — this is validated before use, since it is interpolated into a Gmail search query. recipients: Comma- and/or whitespace-separated exact recipient email addresses to check (max 50 per call — split a larger list across multiple calls rather than expecting a partial result). domain: Configured [domain.*] section to route EVERY recipient through. Default: resolved per-recipient from their own address suffix, so one call can cover a mixed staff/student list. Set this only when recipients use an alias/secondary domain with no config section of its own.

dmarc_rua_summaryA

Summarize DMARC aggregate (RUA) reports: pass/fail per domain, top reject-candidate IPs.

Impersonates the domain's configured dmarc_rua_mailbox (a real user -- domain-wide delegation cannot act as a group or alias; config default postmaster@<domain>), searches it for mail addressed to dmarc_rua_recipient (the rua=mailto: address published in DNS, e.g. the postmaster+rua@ plus-subaddress; default: same as the mailbox), reads the aggregate reports those messages carry, and answers "how much of this domain's mail volume is passing DMARC, and if we moved the policy to p=reject, what would actually get blocked?"

A record counts as PASS when either its aligned DKIM or SPF check reads "pass" (DMARC's own OR semantics) — NOT when both do. A record counts as a reject candidate when it is already quarantined or rejected (a tenant already running p=quarantine/p=reject enforces this today), or when BOTH checks fail with disposition="none" (what a stricter policy would newly start blocking); everything else is a pass. This distinction matters a lot in practice: a large share of legitimate mail (mailing-list forwards, some relays) passes DMARC via SPF alignment alone with DKIM unaligned, so counting on DKIM alone overstates the failure rate severalfold.

reject_candidate_ips (the top top per domain by volume) is where to actually look before flipping a policy to p=reject: a header_from that names a SUBDOMAIN of the audited domain is governed by that subdomain's own sp= policy, not the parent's p=, so it is not necessarily what a p=reject change on the parent would affect.

Requires the gmail.readonly DWD scope — the same one gmail_message_trace needs, granted PER SERVICE ACCOUNT CLIENT ID in the Admin console (Security > API controls > Domain-wide delegation), separately from the admin.directory.* / admin.reports.* scopes the rest of this server uses.

Read-only: only messages().list/messages().get/ attachments().get against the one configured mailbox are issued — see DomainClient.fetch_dmarc_rua_records.

Args: hours: Lookback window. Default 72 (3 days): RUA reports typically arrive roughly daily per sending source, so a single day's window risks missing infrequent senders entirely. domain: Configured [domain.*] section to report on. Default: all configured domains. mailbox: Override the configured dmarc_rua_mailbox (the user to impersonate) for every selected domain. Set this only for an ad-hoc check against a different inbox than the one in config; unless recipient is also given, that inbox's own address is searched. Also re-enables a domain configured with dmarc_rua_mailbox = none for the duration of the call. Validated as an email-shaped address (rejected otherwise). recipient: Override the address searched for (to:) for every selected domain -- the rua=mailto: value in DNS when it is not the mailbox itself. Validated as an email-shaped address (rejected otherwise) before use, since it is interpolated into a Gmail search query. max_pages: Gmail messages().list pages (100 messages each) to walk per domain. capped=true in the result means more pages existed — a capped fetch UNDER-counts real report volume, not just a lower bound on some other total, since every matching message must be walked (there is no server-side aggregate to fall back on). top: How many reject-candidate source IPs to return per domain (highest volume first). The per-domain pass/quarantined/ rejected/undisposed_fail totals themselves are never truncated.

group_delivery_policyA

Check a Google Group's own posting/delivery policy — why an external sender's mail never arrived.

A Group's access-control layer sits IN FRONT of Gmail delivery: when who_can_post is restricted (e.g. domain-members-only), an external sender's message is rejected there and never generates a per-recipient Gmail delivery event at all — gmail_message_trace (a real mailbox) and any Reports-API-based delivery trace both see nothing for that address, indistinguishable from a genuine delivery failure without this. Use this FIRST when a group address "isn't receiving" mail from an external sender, before chasing it as a transport/spam problem.

Read-only: only groups().get() is issued (Groups Settings API). Requires the apps.groups.settings DWD scope — granted PER SERVICE ACCOUNT CLIENT ID in the Admin console (Security > API controls > Domain-wide delegation), separately from every other scope this server uses, and NOT on by default.

Returns who_can_post (e.g. ALL_IN_DOMAIN_CAN_POST blocks external senders entirely; ANYONE_CAN_POST allows them), allow_external_members, is_archived, message_moderation_level, spam_moderation_level, allow_web_posting. Sets found: false (no policy fields) when group_email does not name any group in this domain — that is a normal, expected answer for a bad/typo'd address, not an error.

Args: group_email: The group's address (e.g. "team.gen@example.edu"). domain: Configured [domain.*] section to route the lookup through. Default: resolved from the address's suffix.

list_group_membersA

List a Google Group's basic metadata and member roster (Directory API).

Resolves a group's actual membership directly, independent of any specific message ever having been sent to it — unlike inferring membership from Reports API delivery-event fanout (applicationName=gmail), which only shows members who received one PARTICULAR message and requires one to already exist to trace. Pair with gmail_message_trace to deep-dive a specific member's mailbox once the roster is known, or with group_delivery_policy to see why the group as a whole may not be receiving mail at all.

Read-only: only groups().get() and members().list() are issued (Directory API), never a mutating call. Requires the admin.directory.group.readonly and admin.directory.group.member.readonly DWD scopes — granted PER SERVICE ACCOUNT CLIENT ID in the Admin console, separately from every other scope this server uses, and NOT on by default. The two calls are independent: a tenant with only one of the two scopes granted still gets that one section, with the other reported as {"error": ...} in its place rather than failing the whole call — only when BOTH fail does the tool return a single top-level error.

Sets found: false (no group/members sections) when group_email does not name any group in this domain — a normal, expected answer for a bad/typo'd address, not an error. This triggers both when BOTH calls agree with no error on either side, AND when one call CONFIRMS not-found while the other independently failed (its own error is then attached as group_lookup_error / members_lookup_error) — a confirmed non-existence from one independently-scoped call is stronger evidence than an unrelated failure on the other, and must not be buried under it.

Args: group_email: The group's address. domain: Configured [domain.*] section to route the lookup through. Default: resolved from the address's suffix. max_pages: Pagination cap for the member roster (Directory API hard limit 200 members per page). Default 20 (≤4,000 members) — raise for an unusually large group. capped: true means the roster is NOT the complete one — either more pages existed beyond this, or the member lookup failed outright (see members_error); either way it must never be read as the full membership, and an empty members list must not be mistaken for a confirmed-empty group when capped is true.

drive_external_sharingA

Report Drive ACL grants to external targets and new link/public exposure.

Counts grants whose target (target_user address, or target_domain for domain-scoped grants) is outside the configured internal domains (revocations are reported separately, not as exposure) and visibility transitions into link/public access (people_with_link / public_on_the_web, excluding a narrowing from public down to link-only; Google's shared_externally is owner-domain relative, so external-ness is judged by the target instead). untargeted_external_transitions counts transitions into shared_externally with no target address or domain (e.g. scope became "anyone with the link" — target_domain: "all" — or an unresolved target); it is a residual bucket, not a cross-check for missed named grants, since domain-scoped grants are already classified above. external_samples / exposure_samples / untargeted_samples hold examples of each. A self-grant of owner on change_user_access /change_acl_editors (a user creating their own new file — every Form/Sheet/Doc submission does this) is excluded from risky_visibility_events: it always reports a visibility transition from a missing prior state, which is indistinguishable from a genuine narrow-to-wide exposure event by visibility fields alone, but is not exposure of anything pre-existing. When no target_user is named (the change_acl_editors shape), a missing target alone is not proof of creation — an admin bulk-transferring ownership of a pre-existing, already-shared file (e.g. offboarding) looks the same — so this case is corroborated against the event's own owner parameter (self-action only if owner matches the actor); if owner itself is absent the conservative default is to count it rather than risk dropping a real ownership change. This exclusion is deliberately never applied to change_document_access_scope/change_document_visibility (see SELF_CREATION_GRANT_EVENTS) — those carry no target_user and can legitimately report new_value: "owner" for a real transition, so excluding them there would blind this tool's primary signal instead of just removing creation noise. Each audit-relevant event name is queried separately so the page budget is not consumed by view/edit noise (change_document_visibility is fetched for its acl_events count only — it duplicates change_document_access_scope on this API and does not drive classification, so it cannot compensate if that probe's own fetch fails). capped_events lists event names that exceeded max_pages*1000 events; capped is also set when any probe's fetch errored outright (see event_errors) — either way, treat that domain's counts as lower bounds. Narrow hours or raise max_pages for full coverage (term-time weekdays see >10k change_user_access events/day).

Shared-drive caveat: a file created INSIDE a shared drive emits change_user_access events for each existing drive member (ACL propagation), so an external member merely uploading files looks like bulk external sharing here. When a finding's documents share one owner that is a drive NAME rather than a user address, triage with drive_doc_activity (per-document history: the "grants" coincide with create/upload by the same actor) and shared_drive_membership_changes (who added the members, and when).

drive_doc_activityA

Full audit history of one Drive document: owner, ACL changes, lifecycle.

The triage companion to drive_external_sharing: a sharing finding names a doc_id, and judging it requires who OWNS the document (an individual user vs. a shared drive completely changes the risk read) and its grant history over time. Uses a server-side doc_id filter, so the page budget is spent on this one document only.

owner / doc_title are taken from the document's own events (a shared-drive-owned file reports the drive's name — not a user address — as owner). events lists ACL and lifecycle events newest-first (view/edit/download noise is counted in event_counts but not listed); events_truncated is set when more matched than max_events. The doc_id filter matches at the ACTIVITY level and one activity can carry sibling events for OTHER documents (a multi-file share is one activity with one event per file) — events whose own doc_id parameter does not match (or is absent) are excluded from every output field and tallied in sibling_events_skipped instead, so a bulk action cannot contaminate this document's history or misattribute its owner. An empty result means no events in the window for the queried tenant — NOT proof the document does not exist (history older than the Reports API retention, or a document living in a different tenant, looks the same).

Args: doc_id: Drive document id (from a sharing finding's doc_id field). days: How far back to scan (Reports API retains roughly 6 months). domain: Restrict to one configured domain/tenant (default: all). max_pages: Reports page cap per domain; capped=true means more existed. max_events: Cap on listed events (counts are unaffected).

shared_drive_membership_changesA

Membership add/remove/role-change history across shared drives.

Answers "who added this (external) member, and when" — the other half of triaging a shared-drive sharing finding (see drive_doc_activity). Membership changes are low-volume, so a plain window scan of the single shared_drive_membership_change event works even over months.

Each entry's drive is the shared drive's NAME as the audit log reports it (the event's owner parameter — not an id, not a user address); target_is_external classifies the affected member against the configured internal domains. The Reports API cannot filter by drive server-side, so drive_name is a client-side case-insensitive substring match on that name — it narrows the listing, not the scan. An event whose drive name is absent can neither match nor be ruled out; with drive_name set such events are excluded from total/entries but tallied in missing_drive_name so the drop is never silent (without drive_name they are listed normally with drive: null).

Args: days: How far back to scan (Reports API retains roughly 6 months). domain: Restrict to one configured domain/tenant (default: all). drive_name: Only list entries whose drive name contains this substring. max_pages: Reports page cap per domain; capped=true means more existed. max_events: Cap on listed entries (total counts all matches).

admin_actionsA

What administrators did in the Admin console: to one account or group, or tenant-wide.

Answers "did someone (IT staff, a sync job) suspend this account, reset its password, move it to another org unit, or take it out of a group?" from the Admin console audit log (Reports API applicationName=admin). Each row names the actor (the administrator's address, or a profile id for a service account or sync client), actor_ip, the event_type (USER_SETTINGS, GROUP_SETTINGS, ...), the event (SUSPEND_USER, UNSUSPEND_USER, CHANGE_PASSWORD, CHANGE_USER_ORGANIZATION, ADD_GROUP_MEMBER, REMOVE_GROUP_MEMBER, CREATE_USER, DELETE_USER, CHANGE_APPLICATION_SETTING, ...) and its parameters as the API names them (USER_EMAIL, GROUP_EMAIL, ORG_UNIT_NAME, SETTING_NAME, OLD_VALUE, NEW_VALUE, ...; long values cut at 300 characters). What the account did itself (its own password change, a second factor it enrolled) is not here: use user_activity.

With target (a user or group address), the domain is taken from its suffix and two server-side filters run, USER_EMAIL==<target> and GROUP_EMAIL==<target>; an event of a bulk activity that names some other address is skipped and counted in sibling_events_skipped, as is a sibling of another name when event is given. Without it, every admin action of the window is read (each configured domain, or domain). event narrows to one event name.

Per domain: events newest first (cut at max_events, events_truncated), by_event and by_actor counts over every matching event, capped (more pages existed) and errors (a probe that failed; a domain whose credentials failed is {"error": ...}). capped at the top is set when any domain was capped or failed. Reports data can lag by hours, so an empty window right after the fact is "not visible yet", not "nothing happened". Read-only, the same admin.reports.audit.readonly DWD scope as login_audit.

Args: target: A user or group email address (name@domain), or omit for every admin action. hours: Window length ending now (at most 4320, the ~6 months the Reports API keeps). event: One admin event name in upper case, e.g. SUSPEND_USER. domain: Configured domain to read (default: the target's suffix, or every configured domain). max_pages: Reports page budget per probe (1000 events per page; 1..20). max_events: Rows listed per domain (1..1000); the counts cover everything fetched.

workspace_statusA

Is a Google Workspace service down? Open and recent incidents from Google's status dashboard.

Reads the public Google Workspace Status Dashboard feed (no credential, no DWD scope): the incidents Google has declared for Gmail, Drive, Meet, Calendar, Chat and the other Workspace products. open lists the incidents not yet resolved; recent those that ended within the last hours. Each row: service, affected_products, severity, status_impact, begin / end (UTC), summary (the latest update's summary line), last_update and the incident's url. The dashboard is global: an empty answer means Google has declared no incident, not that this tenant cannot be affected, and a local problem (a network path, one account) never appears here.

Args: hours: How far back recent reaches (at most 2160, 90 days). service: Keep incidents whose service or affected product name contains this text (case-insensitive), e.g. Gmail.

daily_briefA

One-call security summary across all configured domains.

Aggregates login_audit (account locks, suspicious logins), account-protection changes (account_changes: 2-Step Verification enrolled/disabled, passkeys added/removed, Advanced Protection turned off, password and recovery address/phone edits, mail forwarding out of the domain; each row says whether it came from outside the GWSADM_SITES_INI table and from which country (GWSADM_IP2ASN), and is flagged with flag_reasons -- foreign (outside GWSADM_HOME_COUNTRIES), unknown_origin, after_suspicious_login -- flagged rows first, at most 50 listed, total counts all; suspicious_login_coverage says whether the last reason is complete) and drive_external_sharing (external grants, new link exposure, and untargeted_external_transitions — see that tool's docstring). max_pages / samples are passed through to the drive scan; max_pages defaults to the same page budget as the standalone tool, so both report the same counters for the same window (samples defaults lower here and only trims the example lists). Per-domain capped in the summary means at least one underlying scan was partial — treat that domain's counts as lower bounds (see capped_events in the drive section for which probes were cut short).

Synchronous: on a large tenant this can exceed a client's ~60s tool-call timeout. If it does, use daily_brief_start + daily_brief_result (same result, run in the background) or lower max_pages.

daily_brief_startA

Start a daily_brief in the background; returns immediately with a job_id.

Use this instead of daily_brief when the synchronous call risks the client's ~60s tool-call timeout (large tenants). Args mirror daily_brief. You MUST then poll daily_brief_result(job_id) every few seconds until status is done (the full daily_brief payload is under result) or error. On a config error returns {"error": ...}; if too many jobs are already active returns {"status": "rejected", ...}.

daily_brief_resultA

Fetch a daily_brief_start job by id.

status is running (keep polling), done (result holds the full daily_brief payload), error (error holds the exception type name — the message is omitted to avoid leaking internal detail), or unknown (bad/expired id).

Prompts

Interactive templates invoked by user choice

NameDescription

No prompts

Resources

Contextual data attached and managed by the client

NameDescription

No resources

TDQS

A4.1/5.0

Scored across 21 tools

Disambiguation4/5

Most tools map to clearly distinct resources/actions, and the descriptions explicitly disambiguate confusing pairs (get_user vs suspended_accounts, login_audit vs suspended_accounts vs user_activity, group_delivery_policy vs gmail_message_trace). The only real overlap is between the composite tools (compromise_sweep, daily_brief) and the primitive tools they bundle, plus the daily_brief / daily_brief_start / daily_brief_result trio, where an agent must choose between a sync call and an async job.

Naming Consistency4/5

All names are snake_case and readable, with a consistent resource-first/report-name style (user_activity, ip_activity, login_audit, drive_external_sharing). Minor deviation: a couple use verb_noun (get_user, list_group_members) while most are noun phrases, but the style never becomes confusing.

Tool Count4/5

21 tools is at the upper edge but justified by the genuinely broad domain: directory, Reports API audits, Drive sharing, Gmail trace/usage, groups, DMARC, status dashboard, plus a sync/async pair. Each tool earns its place; only the daily_brief trio feels like a small inflation.

Completeness4/5

Covers read-side lifecycle well: user state, suspended accounts, tokens, activity, IP pivots, admin audit, Drive sharing and doc history, group membership and posting policy, DMARC, and Gmail delivery trace/usage. Gaps are deliberate (read-only, no remediation) but notable: Gmail forwarding/filter settings are only reachable through compromise_sweep, and there is no standalone per-user Gmail usage or group settings beyond posting policy.

Maintenance

ActivityActive
ResponsivenessNo recent community issues