gwsadm-mcp
English | 日本語
Google Workspace security-audit MCP (Model Context Protocol) server —read-only visibility into account locks, suspicious logins, and external filesharing, built on the Admin SDK Reports API (audit activities).
Named after the admin-console viewpoint (gwsadm = Google Workspace admin),sibling of boxadm-mcp. This isnot a general-purpose Workspace MCP: it surfaces risk, it never mutatesanything.
Features
| Tool | Description |
|---|---|
health_check |
Server version, config path, and per-domain auth probe — call at session start or after a timeout |
login_audit |
Reports API login — accounts auto-disabled by Google (account_disabled_*: leaked password, hijacked, spamming), suspicious logins, failure top-N |
drive_external_sharing |
Reports API drive — ACL grants to external addresses or domains (revocations reported separately) and visibility transitions into link/public exposure |
daily_brief |
One-call summary across all configured domains |
Planned: dlp_events (Reports rules; requires a Workspace edition with DLP),suspended_accounts (Directory API snapshot; requires theadmin.directory.user.readonly DWD scope), token_events, admin_events.
Auth model
Service account with domain-wide delegation (DWD) impersonating anaudit-capable admin. Fully non-interactive — no browser, no token refreshrotation — so the server runs unattended (cron, MCP gateway, CI).
Required DWD scope per service-account client ID:
https://www.googleapis.com/auth/admin.reports.audit.readonly
Setup
# uv
uv pip install gwsadm-mcp
# pip
pip install gwsadm-mcp
Or 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 = [email protected]
customer_id = C0xxxxxxx
One [domain.*] section per audited Workspace domain. internal_domains isthe allowlist used to classify sharing targets as internal vs external.
Usage
Claude Code
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-mcp
CLI 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 pagebudget, or when a probe's fetch errored outright (seeevent_errors) —partial coverage is never presented as "no findings". The drive scan alsoreportscapped_events(which eventNames were cut short). Narrowhoursor raisemax_pagesfor full coverage — on a large tenant, term-timeweekdays can produce thousands ofchange_user_accessevents/day. - Google's
visibility=shared_externallyis relative to the file owner'sdomain, so with multipleinternal_domainsa cross-internal-domain grant(e.g. student domain → staff domain) carries it too. External-ness istherefore 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 "anyonewith 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 isnot a cross-check for grants missed elsewhere, since domain-scoped grantsare already counted above.external_samples/exposure_samples/untargeted_sampleshold examples of each. - Drive events are queried one audit-relevant eventName at a time, so thepage budget is not consumed by view/edit noise; an event name rejected by theAPI degrades into
event_errorsinstead of failing the tool.change_document_visibilityandchange_document_access_scopereport thesame transition as simultaneous sibling events on this API — only thelatter drives classification (the former is fetched for itsacl_eventscount only), so a domain-scoped grant or a link/public exposure is neverdouble-counted across the two. This also means the former can no longercompensate if the latter's own fetch fails: achange_document_access_scopeentry inevent_errorssetscapped: truefor that domain, and itsclassification 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": ...}). - Read-only by design; the only API call issued is
activities().list. - Output contains account addresses (that is the point of an audit tool):restrict access to authorized security staff.
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 .
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. Mergingthat PR tags vX.Y.Z and publishes a GitHub Release, whose release: publishedevent triggers the release workflow to build and publish to PyPI and the MCPRegistry. release-please owns the version in gwsadm_mcp/__init__.py andserver.json (do not bump them by hand).
[!IMPORTANT]The release-please workflow should be given a repository secret
RELEASE_PLEASE_TOKEN(a PAT withcontents: write+pull-requests: write).The defaultGITHUB_TOKENcannot create the Release that triggers thedownstreamreleaseworkflow (GitHub blocks workflow runs triggered byGITHUB_TOKEN), so without the PAT nothing gets published. The workflow fallsback toGITHUB_TOKENwhen the secret is unset so PR CI keeps working on forks.
License
MIT