Let an agent read your cloud, not rewrite it.
platform-mcp gives Claude and other MCP clients sixteen read-only tools across Google Cloud — logs, errors, metrics, inventory, recommendations and spend — so an agent can investigate an incident or hunt for waste across staging and production in one conversation.
# no install step; uv fetches it on demand
claude mcp add platform-mcp --scope user -- uvx platform-mcp
Quickstart
Three steps, about five minutes, assuming you can create a service account.
-
Create a read-only service account
Once per GCP project you want to reach. The script is idempotent and prints the config to paste when it finishes.
git clone https://github.com/deBilla/platform-mcp && cd platform-mcp ./scripts/setup-service-account.sh \ --project my-app-staging \ --user you@example.com \ --billing-dataset my-billing-project:billing # optional -
Write the config file
One file at
~/.config/platform-mcp/config.tomlkeeps project ids out of every client config you own.default_environment = "staging" [environments.staging] project = "my-app-staging" impersonate = "platform-mcp-ro@my-app-staging.iam.gserviceaccount.com" [environments.production] project = "my-app" impersonate = "platform-mcp-ro@my-app.iam.gserviceaccount.com" billing_export_table = "my-billing-project.billing.gcp_billing_export_v1_XXXXXX" -
Register it, then check it
gcloud auth application-default login claude mcp add platform-mcp --scope user -- uvx platform-mcp platform-mcp doctordoctorproves every prerequisite for every environment before you discover a gap mid-incident.
Every tool here is read-only, so approving each call adds nothing. Allow the server once in your Claude Code settings:
{ "permissions": { "allow": ["mcp__platform-mcp__*"] } }
The sixteen tools
Every tool takes an optional environment argument and echoes back
which project answered.
| Area | Tool | What it answers |
|---|---|---|
| Config | list_environments | Which projects can I reach, and which is the default? |
| Logging | query_logs | Cloud Logging with a filter expression and a freshness window. |
get_recent_errors | Recent error-severity entries, condensed. | |
| Errors | list_error_groups | Grouped application errors with counts and affected users. |
| Monitoring | query_metric | A metric time series with an aligner and alignment period. |
list_alert_policies | Alert policies and whether they are enabled. | |
list_uptime_checks | Uptime check configurations. | |
| Recommender | list_cost_recommendations | Idle and rightsizing findings, fanned out across locations. |
list_recommendations | One named recommender at one location. | |
| Cost | get_cost_breakdown | Spend by service, SKU, project or region from the billing export. |
get_billing_info | Which billing account is linked, and is it enabled? | |
| Inventory | search_assets | Any resource, via Cloud Asset Inventory. |
list_compute_instances | Compute Engine VMs. | |
list_cloud_run_services | Cloud Run services. | |
list_gke_clusters | GKE clusters. | |
list_sql_instances | Cloud SQL instances. |
All sixteen declare readOnlyHint: true and destructiveHint: false
in their MCP annotations. Those are advisory hints for clients; the actual guarantee comes
from the identity the server runs as.
Environments
One server process reaches every project you configure. Rather than a stateful "switch environment" tool, each call names its own target — so a context compaction can never leave an agent querying production while it believes it is on staging.
Names resolve generously. prod, prd and live reach
an environment called production; stg, stage and
qa reach staging; a bare project id works too. An unrecognised
name is an error listing the valid options — never a silent fallback.
Omitting the argument uses the default, which prefers staging when one
exists, so an unqualified question does not reach production by accident.
Configuration
The config file lives at ~/.config/platform-mcp/config.toml. Environment
variables override it, so a shared committed file plus a local override works fine.
| Variable | Purpose |
|---|---|
PLATFORM_MCP_CONFIG | Use a config file somewhere else. |
PLATFORM_MCP_ENVIRONMENTS | JSON registry, overriding the file's [environments]. |
PLATFORM_MCP_DEFAULT_ENVIRONMENT | Which environment an unqualified call uses. |
PLATFORM_MCP_DEFAULT_LIMIT | Default row cap for list-style tools. |
PLATFORM_MCP_AUDIT_LOG | Audit file path, or off. |
PLATFORM_MCP_LOG_LEVEL | stderr verbosity. |
GOOGLE_APPLICATION_CREDENTIALS | Service-account key file, if you must use one. |
GCP setup
Run scripts/setup-service-account.sh once per project, or grant these by
hand. Every role is read-only, so the account cannot change anything regardless of what
the code does.
| Role | Granted on | Needed for |
|---|---|---|
roles/viewer | project | Resource inventory |
roles/logging.viewer | project | Logs and recent errors |
roles/monitoring.viewer | project | Metrics, alerts, uptime |
roles/errorreporting.viewer | project | Error groups |
roles/recommender.viewer | project | Recommendations |
roles/cloudasset.viewer | project | Asset search, location discovery |
roles/bigquery.jobUser | project | Starting a cost query — no data access |
roles/bigquery.dataViewer | billing dataset | Reading the billing export |
roles/iam.serviceAccountTokenCreator | the account itself | Each person who uses the server |
bigquery.jobUser only lets the account start a query; it grants
no access to any data. A billing export almost always lives in a different
project from the one being monitored, so the account needs a separate read grant
on that dataset:
bq add-iam-policy-binding \
--member="serviceAccount:$SA" --role=roles/bigquery.dataViewer \
YOUR_BILLING_PROJECT:billing
Miss it and get_cost_breakdown returns 403 while
every other tool works — which reads like a bug in the tool rather than a missing grant.
If you lack admin on the billing project, that one command is what to send to someone who
has it.
Security model
Read-only is enforced by IAM, not by OAuth scope. The server requests
the broad cloud-platform scope and stays read-only because it never calls a
mutating API. Do not rely on that alone — run it under a viewer-only identity so the
credential itself is incapable of writing, whatever code executes.
You authenticate as yourself with Application Default Credentials, and the server impersonates a read-only service account per environment. That means no key files on anyone's laptop, nothing to leak or rotate, and every call attributable to a named identity in Cloud Audit Logs.
Because the server never uses your credentials, your own access
tells you nothing about whether it works. You may read a table perfectly well while the
service account cannot. That gap is exactly what platform-mcp doctor exists to
close: it tests the identity the server actually uses.
Observability
Every tool call appends one line to
~/.local/state/platform-mcp/audit.jsonl:
{"ts":"2026-08-30T18:20:11+0800","tool":"query_logs","environment":"production",
"project":"my-app","duration_ms":412,"count":50,"bytes":18422,"error":null}
Free-text arguments are recorded by name only. A Cloud Logging filter can carry user ids or addresses from the logs being searched, and the audit file must not become a second copy of that.
Diagnostic logs go to stderr. In stdio transport stdout carries the
JSON-RPC stream, so a single stray byte there ends the session — nothing in this package
writes to it. Claude Code surfaces stderr with claude --debug=mcp.
For a record that does not depend on this server at all, enable Data Access audit logs in GCP for the read-only accounts. Token minting already appears in Admin Activity logs with no configuration.
Troubleshooting
Start with platform-mcp doctor. It checks credentials, impersonation, a real
API read and the billing export for every environment, and prints the command to fix
whatever fails.
platform-mcp doctor
config file: ~/.config/platform-mcp/config.toml
[ ok ] Application Default Credentials (quota project: my-app-staging)
environment: staging (default) -> my-app-staging
[ ok ] impersonate platform-mcp-ro@my-app-staging.iam.gserviceaccount.com
[ ok ] read Cloud Logging in my-app-staging
[ FAIL ] read billing export my-billing-project.billing.export_v1
Fix: the identity needs roles/bigquery.jobUser on my-app-staging
AND roles/bigquery.dataViewer on the dataset holding the export
| Symptom | Cause and fix |
|---|---|
| No credentials found | Run gcloud auth application-default login. |
403 on getAccessToken | You lack Token Creator on the service account. If your ADC is itself an impersonated account, that account needs it, not your user. |
Only get_cost_breakdown fails | The cross-project billing grant. See GCP setup. |
| Cost totals identical across environments | Upgrade — before 0.2.0 the export was queried without a project filter. |
Unexpected SERVICE_DISABLED | Set a quota project: gcloud auth application-default set-quota-project. |
| Server dies at startup | An mcp 2.x install. 0.2.0 pins mcp<2. |
Development
git clone https://github.com/deBilla/platform-mcp && cd platform-mcp
python3 -m venv .venv && ./.venv/bin/pip install -e ".[dev]"
./.venv/bin/python -m pytest
The suite runs in-process against an in-memory MCP client — no subprocess, no network, no GCP credentials — covering environment resolution, the tool contract, annotations, error translation and the audit log. CI additionally builds the package, asserts the sdist carries no local configuration, and proves a clean install serves all sixteen tools.
Releases are tag-triggered and publish to PyPI with Trusted Publishing, so no API token exists to leak.