platform-mcp
Model Context Protocol server

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.

Read-only by construction PyPI · platform-mcp Python 3.11+ MIT
# no install step; uv fetches it on demand
claude mcp add platform-mcp --scope user -- uvx platform-mcp
Getting started

Quickstart

Three steps, about five minutes, assuming you can create a service account.

  1. 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
  2. Write the config file

    One file at ~/.config/platform-mcp/config.toml keeps 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"
  3. Register it, then check it

    gcloud auth application-default login
    claude mcp add platform-mcp --scope user -- uvx platform-mcp
    platform-mcp doctor

    doctor proves every prerequisite for every environment before you discover a gap mid-incident.

Skip the approval prompts

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__*"] } }
Reference

The sixteen tools

Every tool takes an optional environment argument and echoes back which project answered.

AreaToolWhat it answers
Configlist_environmentsWhich projects can I reach, and which is the default?
Loggingquery_logsCloud Logging with a filter expression and a freshness window.
get_recent_errorsRecent error-severity entries, condensed.
Errorslist_error_groupsGrouped application errors with counts and affected users.
Monitoringquery_metricA metric time series with an aligner and alignment period.
list_alert_policiesAlert policies and whether they are enabled.
list_uptime_checksUptime check configurations.
Recommenderlist_cost_recommendationsIdle and rightsizing findings, fanned out across locations.
list_recommendationsOne named recommender at one location.
Costget_cost_breakdownSpend by service, SKU, project or region from the billing export.
get_billing_infoWhich billing account is linked, and is it enabled?
Inventorysearch_assetsAny resource, via Cloud Asset Inventory.
list_compute_instancesCompute Engine VMs.
list_cloud_run_servicesCloud Run services.
list_gke_clustersGKE clusters.
list_sql_instancesCloud 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.

Concepts

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.

Reference

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.

VariablePurpose
PLATFORM_MCP_CONFIGUse a config file somewhere else.
PLATFORM_MCP_ENVIRONMENTSJSON registry, overriding the file's [environments].
PLATFORM_MCP_DEFAULT_ENVIRONMENTWhich environment an unqualified call uses.
PLATFORM_MCP_DEFAULT_LIMITDefault row cap for list-style tools.
PLATFORM_MCP_AUDIT_LOGAudit file path, or off.
PLATFORM_MCP_LOG_LEVELstderr verbosity.
GOOGLE_APPLICATION_CREDENTIALSService-account key file, if you must use one.
Setup

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.

RoleGranted onNeeded for
roles/viewerprojectResource inventory
roles/logging.viewerprojectLogs and recent errors
roles/monitoring.viewerprojectMetrics, alerts, uptime
roles/errorreporting.viewerprojectError groups
roles/recommender.viewerprojectRecommendations
roles/cloudasset.viewerprojectAsset search, location discovery
roles/bigquery.jobUserprojectStarting a cost query — no data access
roles/bigquery.dataViewerbilling datasetReading the billing export
roles/iam.serviceAccountTokenCreatorthe account itselfEach person who uses the server
The grant everyone forgets

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.

Design

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.

A consequence worth knowing

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.

Operations

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.

Operations

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
SymptomCause and fix
No credentials foundRun gcloud auth application-default login.
403 on getAccessTokenYou 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 failsThe cross-project billing grant. See GCP setup.
Cost totals identical across environmentsUpgrade — before 0.2.0 the export was queried without a project filter.
Unexpected SERVICE_DISABLEDSet a quota project: gcloud auth application-default set-quota-project.
Server dies at startupAn mcp 2.x install. 0.2.0 pins mcp<2.
Contributing

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.