gwsadm-mcp
gwsadm-mcp is a read-only Google Workspace security-audit MCP server, surfacing account risks, suspicious logins, and external file sharing across configured domains via the Admin SDK Reports and Directory APIs.
health_check— Verify server status, version, config path, and per-domain authentication/API reachability.login_audit— Audit login events including Google-auto-disabled accounts (leaked passwords, hijacked/spamming), suspicious logins, and login failure top-N rankings over a configurable time window.suspended_accounts— Snapshot all currently suspended Google Workspace accounts per domain; useful for cross-referencing against downstream IdPs to find suspended-but-still-enabled accounts.user_oauth_tokens— List third-party OAuth app grants for a specific user, identifying compromise vectors invisible to login audits (previously-granted tokens require no fresh login event).drive_external_sharing— Report Drive ACL grants to external users/domains and visibility transitions to link/public exposure.drive_doc_activity— Full audit history (owner, ACL changes, lifecycle events) for a single Drive document by ID; useful as a triage companion todrive_external_sharing.shared_drive_membership_changes— History of who added, removed, or changed roles of shared drive members, with external classification and optional drive-name filtering.daily_brief— Synchronous one-call security summary aggregating login audit and Drive external sharing data across all configured domains.daily_brief_start/daily_brief_result— Asynchronous version ofdaily_brieffor large tenants:daily_brief_startreturns ajob_idimmediately, anddaily_brief_resultpolls for completion.
Provides read-only security auditing for Google Workspace, including account locks, suspicious logins, and external file sharing monitoring via the Admin SDK Reports API.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@gwsadm-mcprun daily security brief"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
gwsadm-mcp
English | 日本語
Google Workspace security-audit MCP (Model Context Protocol) server — read-only visibility into account locks, suspicious logins, and external file sharing, built on the Admin SDK Reports API (audit activities).
Named after the admin-console viewpoint (gwsadm = Google Workspace admin),
sibling of boxadm-mcp. This is
not a general-purpose Workspace MCP: it surfaces risk, it never mutates
anything.
Features
Tool | Description |
| Server version, config path, and per-domain auth probe — call at session start or after a timeout |
| Reports API |
| Directory API — current snapshot of suspended accounts ( |
| Directory API |
| Directory API |
| Reports API |
| Reports API |
| Reports API |
| Gmail API — did a known Message-ID reach specific mailboxes, and where (inbox/spam/trash/archived)? For each recipient it impersonates that user via DWD and searches their own mailbox. Requires the separate |
| Groups Settings API — a Google Group's own posting/delivery policy ( |
| Directory API — a Google Group's basic metadata and member roster, resolved directly rather than inferred from who happened to receive one particular message. Requires the separate |
| One-call summary across all configured domains |
| Same as |
Planned: dlp_events (Reports rules; requires a Workspace edition with DLP),
token_events, admin_events.
Related MCP server: google-workspace-mcp-server
Auth model
Service account with domain-wide delegation (DWD) impersonating an audit-capable admin. Fully non-interactive — no browser, no token refresh rotation — so the server runs unattended (cron, MCP gateway, CI).
Grant all of the following DWD scopes on the same service-account client ID up front, in one setup pass. Adding them one at a time as each tool gets built is how a scope goes missing until the one tool that needed it starts degrading — one place, one pass, avoids the trap:
Scope | Needed by | Missing it |
|
| those tools degrade to a per-domain error |
|
| those two tools degrade to an error (per-domain for |
|
| that tool degrades to a per-domain error; everything else keeps working |
health_check needs no scope at all to respond: it is the tool to call when
a grant might be missing — it probes each domain and reports the failing
auth in a structured per-domain result instead of failing itself.
gmail_message_trace needs one more scope, granted as a separate step —
it is intentionally not bundled into the pass above:
Scope | Needed by | Missing it |
|
| that tool reports a per-recipient error; everything else keeps working |
This is a materially broader grant than the three above: it allows reading
message content for any user the service account impersonates, not just
metadata. The tool code itself only ever requests format="metadata" — it
never reads a message body — but the grant itself does not enforce that; the
narrower gmail.metadata scope was considered and rejected because it does
not support the q= search parameter the rfc822msgid: lookup needs. Grant
it on the same service-account client ID as the other scopes (Admin
console → Security → API controls → Domain-wide delegation → find the
existing client ID → add this scope to its list), and weigh that broader
exposure against how much you actually need message-trace before turning it
on for a given domain.
group_delivery_policy and list_group_members each need their own
separate scope too — three more grants beyond the base pass, none bundled
with each other or with gmail.readonly above:
Scope | Needed by | Missing it |
|
| that tool degrades to an error; everything else keeps working |
|
| that half reports its own error; the member roster half still works independently if its own scope below is granted |
|
| same, independent of the metadata half above — the two calls never gate each other |
The Groups Settings API is a distinct product from the Directory API, hence
the separate scope; it has no readonly-only variant, but this server only
ever calls groups().get(), never a mutating method.
suspended_accounts, get_user and user_oauth_tokens all operate per
configured domain (Directory domain=/userKey=), unlike the customer-wide
Reports tools — so every domain you want covered (e.g. a separate student
domain) needs its own [domain.*] config section. Note the failure modes
differ: suspended_accounts silently omits an unconfigured domain from
its result, while get_user and user_oauth_tokens fail loudly with an
unknown-domain error (both take a domain override for an alias/secondary
address whose suffix has no section of its own).
Setup
# uv
uv pip install gwsadm-mcp
# pip
pip install gwsadm-mcpOr from source:
git clone https://github.com/shigechika/gwsadm-mcp.git
cd gwsadm-mcp
# uv
uv sync
# pip
pip install -e .Configuration
Point GWSADM_CONFIG at an INI file (default ~/.config/gwsadm-mcp/config.ini,
keep it 0600):
[gwsadm]
# optional; defaults to all [domain.*] section names
internal_domains = example.edu, mail.example.edu
[domain.example.edu]
service_account_file = /path/to/service-account.json
subject = audit-admin@example.edu
customer_id = C0xxxxxxxOne [domain.*] section per audited Workspace domain. internal_domains is
the allowlist used to classify sharing targets as internal vs external.
Usage
Claude Code (plugin)
This repository doubles as a single-plugin marketplace, so Claude Code can install the server for you:
/plugin marketplace add shigechika/gwsadm-mcp
/plugin install gwsadm-mcp@gwsadm-mcpThe plugin launches uvx gwsadm-mcp and reads GWSADM_CONFIG (falls back to
~/.config/gwsadm-mcp/config.ini), the same variable described in
Configuration. /plugin install only wires up the server
process — it cannot create the config INI or the Google Cloud service-account
JSON key(s) it points at; both must already exist on the machine running the
plugin before any tool call will succeed.
uvx must be on the PATH of the process that runs Claude Code — a login
shell usually has it, but a GUI-launched app may not; install
uv system-wide if the plugin fails to start.
Claude Code (manual)
Add to .mcp.json (no env needed when the config lives at the default path;
add "env": { "GWSADM_CONFIG": "..." } only for a non-default location):
{
"mcpServers": {
"gwsadm-mcp": {
"type": "stdio",
"command": "gwsadm-mcp"
}
}
}Claude Desktop
Add the same entry to claude_desktop_config.json.
Direct Execution
gwsadm-mcpCLI Options
gwsadm-mcp --version # Print version and exit
gwsadm-mcp --check # Config + auth + API smoke for every domain, then exit
gwsadm-mcp # Start MCP server (STDIO, default)--check exit codes: 0 success, non-zero on config or auth failure.
Notes
Every result section reports
capped: truewhen a window exceeded the page budget, or when a probe's fetch errored outright (seeevent_errors) — partial coverage is never presented as "no findings". The drive scan also reportscapped_events(which eventNames were cut short). Narrowhoursor raisemax_pagesfor full coverage — on a large tenant, term-time weekdays can produce thousands ofchange_user_accessevents/day.Google's
visibility=shared_externallyis relative to the file owner's domain, so with multipleinternal_domainsa cross-internal-domain grant (e.g. student domain → staff domain) carries it too. External-ness is therefore judged againstinternal_domainsusing the grant's target:target_userfor named grants,target_domainfor domain-scoped grants (e.g. "anyone at partner.edu"; the literal domain"all"means "anyone with the link" and is judged by visibility instead).risky_visibility_eventscounts only transitions intopeople_with_link/public_on_the_web(excluding a narrowing from public down to link-only).untargeted_external_transitionsis a residual bucket for transitions intoshared_externallywith no target address or domain to classify — it is not a cross-check for grants missed elsewhere, since domain-scoped grants are already counted above.external_samples/exposure_samples/untargeted_sampleshold examples of each.Drive events are queried one audit-relevant eventName at a time, so the page budget is not consumed by view/edit noise; an event name rejected by the API degrades into
event_errorsinstead of failing the tool.change_document_visibilityandchange_document_access_scopereport the same transition as simultaneous sibling events on this API — only the latter drives classification (the former is fetched for itsacl_eventscount only), so a domain-scoped grant or a link/public exposure is never double-counted across the two. This also means the former can no longer compensate if the latter's own fetch fails: achange_document_access_scopeentry inevent_errorssetscapped: truefor that domain, and its classification counts for the window are a lower bound even thoughchange_document_visibility(and thusacl_events) may show data.A failure in one domain degrades only that domain's section (
{"error": ...}).gmail_message_tracesetsambiguous: true(withmatch_count) on a recipient whose mailbox has more than one message under the same Message-ID (mailing-list copy plus a direct CC, a quarantine-release duplicate, …) — the rest of that recipient's fields describe only the first match, not a combined answer.match_count_cappedis set alongside it when the mailbox has enough matches thatmatch_countis a lower bound rather than exact (the search does not paginate).get_userdistinguishes "this address names no account" from "the lookup failed": a plain HTTP 404 answersfound: falsewith no state fields, which is a diagnostic result — a typo'd or deleted address — and never anerror. A missing DWD scope or a transient failure answers{"error": ...}with nofoundkey instead, so the two can never be confused in either direction. Fields Google omits staynullrather than being coerced: a missingsuspendedmust not read as "the account is fine".group_delivery_policynormalizes the Groups Settings API's"true"/"false"string fields (a quirk of that API, not JSON booleans) into real booleans in its output; a field absent from Google's response staysnull, never coerced tofalse.list_group_membersruns its group-metadata and member-roster lookups independently — a tenant with only one of the two DWD scopes still gets that one section, the other reported as{"error": ...}in its place. It reportscapped: trueboth when the member roster exceeded its page budget (default 20 pages × 200/page) and when the member lookup failed outright (seemembers_error) — either way the roster is not the full one, and an emptymemberslist must never be read as a confirmed-empty group whencappedis true. Both group tools distinguish "this address is not a group" (a plain HTTP 404, verified against production for all three underlying API calls) from a real failure:group_delivery_policysetsfound: false;list_group_memberssets it too, when either both independent lookups agree with no error on either side, OR one CONFIRMS not-found while the other independently failed (that failure is then attached asgroup_lookup_error/members_lookup_errorrather than hidden) — a confirmed non-existence outweighs an unrelated error on the other scope. Only a genuine mixed state (one side not-found, the other actually finding data) falls through to the normal per-section shape instead.Read-only by design:
activities().list(Reports API),users().list/users().get/tokens().list/groups().get/members().list(Directory API),groups().get(Groups Settings API), andmessages().list/messages().get(Gmail API, metadata only) are the only API calls issued anywhere in this package.Output contains account addresses (that is the point of an audit tool): restrict access to authorized security staff.
gmail_message_tracealso returns a message snippet and headers (From/To/Cc/Subject/Date) for a matched message — treat its output with the same care as the mailbox content it is drawn from.
Development
git clone https://github.com/shigechika/gwsadm-mcp.git
cd gwsadm-mcp
# uv
uv sync --dev
uv run pytest -v
uv run ruff check .
# pip
python3 -m venv .venv
.venv/bin/pip install -e . && .venv/bin/pip install pytest ruff
.venv/bin/pytest -v
.venv/bin/ruff check .Live smoke test
The unit suite never talks to Google, which is what makes it fast — and also
what makes it blind to a tool that has stopped returning real data.
scripts/smoke_test.py runs every registered tool against the configured
tenant and fails on empty, malformed or error answers:
# uses the same config file as the server (GWSADM_CONFIG)
uv run python scripts/smoke_test.py
uv run python scripts/smoke_test.py --only oauth --tracebackRead-only. Every tool here reads an audit log or a directory snapshot; nothing in Workspace is changed.
daily_brief_startcreates a job inside the process, which expires on its own.No payloads in the report. Tool names, statuses and row counts only; server-authored error text is redacted too, since these tools deal in account addresses and document titles throughout.
Bounded. Every bounding parameter a tool offers is passed explicitly — the defaults (5 pages, 180 days, 200 events) are sized for a human asking once, and are enforced by a test that finds them from the source.
Nothing tenant-specific in the specs. The account and the document the per-user and per-document tools need are discovered at run time, and skipped when the tenant has none to offer. Two tests keep it that way: one refuses those parameters as literals, the other bans anything address-shaped anywhere in the file, because this repository is public.
An empty answer passes: no external sharing and no locked accounts is the desired state. What is asserted instead is the envelope — and, where the answer is keyed by domain, that the domain map is not empty, since a config resolving to zero domains would otherwise report every tool as working while auditing nothing.
CI enforces the cheap half: a tool registered without a probe spec fails the build (
tests/test_smoke_probes.py), so adding a tool forces the question "how would we know it works?".scripts/smoke_harness.pyis the engine and holds no Workspace knowledge: it is kept identical across the servers that share it, so fix engine bugs once and sync the file rather than patching this copy.
Releasing
Releases are automated with release-please.
Merging Conventional Commits (feat:, fix:, …)
to main keeps a release PR open with the next version and changelog. Merging
that PR tags vX.Y.Z and publishes a GitHub Release, whose release: published
event triggers the release workflow to build and publish to PyPI and the MCP
Registry. release-please owns the version in gwsadm_mcp/__init__.py and
server.json (do not bump them by hand).
The release-please workflow should be given a repository secretRELEASE_PLEASE_TOKEN (a PAT with contents: write + pull-requests: write).
The default GITHUB_TOKEN cannot create the Release that triggers the
downstream release workflow (GitHub blocks workflow runs triggered by
GITHUB_TOKEN), so without the PAT nothing gets published. The workflow falls
back to GITHUB_TOKEN when the secret is unset so PR CI keeps working on forks.
License
MIT
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- AlicenseBqualityAmaintenanceGoogle Workspace MCP Server1223,037PythonMIT
- AlicenseAqualityDmaintenanceMCP server for Google Workspace APIs - Docs, Sheets, Drive, Gmail, and Calendar. Enables reading, creating, and editing Google Docs and Sheets, managing comments, reading emails, and viewing calendar events.344517MIT
- AlicenseBqualityCmaintenanceProduction-ready MCP server for Google Workspace providing broad coverage across Gmail, Drive, Calendar, Docs, Sheets, and more, with safe-by-default write operations and markdown-to-Google-Docs support.100MIT
- FlicenseBqualityDmaintenanceMCP server providing full access to Google Workspace services (Gmail, Drive, Calendar, Docs, Sheets, Slides, Forms, Tasks, Contacts) using OAuth authentication.1001
Related MCP Connectors
Paid remote MCP for AI Studio Workspace approval gate MCP, structured receipts, audit logs, and revi
A paid remote MCP for hosted MCP server, built to return verdicts, receipts, usage logs, and audit-r
A paid remote MCP for CLI tool MCP, built to return verdicts, receipts, usage logs, and audit-ready
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/shigechika/gwsadm-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server