Skip to main content
Glama
anandapurva55

Multi-Cloud MCP Server

README.md
# Multi-Cloud MCP Server

A local [Model Context Protocol](https://modelcontextprotocol.io) server that gives an LLM client (Claude Desktop, VS Code, Claude Code, etc.) tools to inspect and query your own **GCP** and **AWS** infrastructure: data warehouses, CI build systems, container/serverless runtimes, and their logs. You point it at your own project(s) and account(s) through a short interactive setup wizard. Nothing is hardcoded to any specific company, project, or account.

## Table of contents

- [Quickstart](#quickstart)
- [How it works](#how-it-works)
- [File by file](#file-by-file)
- [Configuring environments](#configuring-environments)
- [GCP auth modes](#gcp-auth-modes)
- [AWS auth modes](#aws-auth-modes)
- [Tool catalog with examples](#tool-catalog-with-examples)
- [Extending with a new service](#extending-with-a-new-service)
- [Testing](#testing)
- [Troubleshooting](#troubleshooting)

## Quickstart

```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

python3 setup_wizard.py    # interactive: configure your GCP and/or AWS environments
python3 server.py          # runs the MCP server over stdio
```

Point your MCP client at `server.py` as the command to run (with the same `.venv` interpreter). For example, in a VS Code `mcp.json` or Claude Desktop config, the command is the absolute path to `.venv/bin/python3` and the argument is the absolute path to `server.py`.

## How it works

```
                     .env  (written by setup_wizard.py)
                       |
                       v
                  config.py  (parses .env into GCPEnvironment / AWSEnvironment objects)
                       |
                       v
                  server.py  (registers tools for whichever providers have at least
                       |       one configured environment, then serves over stdio)
          -------------+-------------
          |                         |
   providers/gcp/             providers/aws/
   one file per GCP service   one file per AWS service
   (bigquery, cloudbuild,     (athena, codebuild, ecs,
    cloudrun)                  lambda, s3, identity)
```

Every tool function takes an optional `env=` argument naming which configured environment to act against (for example `env="prod"`). If you omit it, the tool falls back to whichever environment the wizard set as the default for that provider. Call the always-available `list_environments` tool to see what is currently configured and which environment is the default for each provider.

## File by file

- **`server.py`**: the entry point. At import time it reads `config.CONFIG` and registers only the tool modules for providers that actually have a configured environment (a GCP-only setup never exposes AWS tools, and vice versa). It also defines one provider-agnostic tool, `list_environments`, directly. If neither provider is configured it prints a message to stderr and exits with status 1, rather than hanging waiting for input, because the stdio transport is reserved for MCP protocol traffic once an MCP client has launched it.

- **`config.py`**: the configuration model. Defines `GCPEnvironment` and `AWSEnvironment` dataclasses, an `AppConfig` registry keyed by environment name, `load_config(path)` to parse a `.env` file into an `AppConfig`, `render_env_file(cfg)` and `save_config(cfg, path)` to write one back out, and a `slug(name)` helper that turns an arbitrary environment name into the uppercase, underscore-separated form used in `.env` key names (so `"client-a-prod"` becomes the key prefix `CLIENT_A_PROD`). `AppConfig.resolve_gcp(env)` / `resolve_aws(env)` are what every tool calls to turn an `env=` argument (or `None`) into a concrete environment object, raising a `ValueError` that lists what is actually configured if the name does not exist.

- **`setup_wizard.py`**: the interactive onboarding flow. Run it any time, it loads whatever `.env` already exists, lets you add, remove, or change the default environment for either provider through a numbered menu, verifies each new environment's credentials as you add it (a live BigQuery/STS call, non-fatal for GCP, prompts to save-anyway for AWS on failure), and rewrites `.env` from scratch on save.

- **`live_check.py`**: a standalone smoke test you can run any time against your real `.env` to confirm a configured environment's credentials still work, without needing the full pytest suite or any specific cloud resources to exist. See [Testing](#testing) below.

- **`providers/errors.py`**: a single decorator, `friendly_errors`, applied to every tool function. It catches `ValueError` (an unknown environment name, a missing key file) and any other exception (an expired credential, an API failure) and turns it into a plain string starting with `"Error: ..."` or `"Error calling <tool>: ..."`, so a bad call comes back as readable text instead of a stack trace.

- **`providers/gcp/auth.py`**: resolves GCP credentials for a `GCPEnvironment`, either via Application Default Credentials (`google.auth.default()`) or a service-account JSON key file, and exposes `get_access_token()` for the one tool module that talks to a GCP REST API directly instead of through a client library.

- **`providers/gcp/bigquery_tools.py`**: `bq_list_datasets`, `bq_list_tables`, `bq_describe_table`, `bq_query`, `bq_preview_table`. Thin wrappers around the `google-cloud-bigquery` client.

- **`providers/gcp/cloudbuild_tools.py`**: `cloudbuild_list_builds`, `cloudbuild_get_logs`, `cloudbuild_describe_build`. There is no official Cloud Build Python client used here; it calls the REST API directly with a bearer token from `auth.py`, and fetches full build logs straight from the build's GCS logs bucket.

- **`providers/gcp/cloudrun_tools.py`**: `cloudrun_list_services`, `cloudrun_describe_service`, `cloudrun_list_revisions`, `cloudrun_get_logs`, `cloudrun_get_error_logs`. Uses `google-cloud-run` for service/revision metadata and `google-cloud-logging` for log retrieval, filtered by `severity>=ERROR` for the error-only variant.

- **`providers/aws/auth.py`**: resolves a `boto3.Session` for an `AWSEnvironment`, either via a named profile (or the default credential chain) or explicit static keys, plus `verify_credentials()` (a thin `sts:GetCallerIdentity` wrapper used by both the wizard and the `aws_whoami` tool).

- **`providers/aws/logs_helpers.py`**: one shared `fetch_log_events()` function used by both `ecs_tools.py` and `lambda_tools.py`, since both need the same "fetch recent CloudWatch Logs, optionally error-filtered" behavior. The error filter is a CloudWatch Logs filter pattern (`?ERROR ?Error ?CRITICAL ?FATAL ?Exception ?Traceback`), since CloudWatch has no built-in structured severity field the way GCP's logging API does.

- **`providers/aws/athena_tools.py`**: `athena_list_databases`, `athena_list_tables`, `athena_describe_table`, `athena_query`, `athena_preview_table`. This is the closest AWS equivalent to BigQuery: a serverless SQL engine over the Glue Data Catalog. Unlike BigQuery, a query is asynchronous (start, then poll for completion, then fetch results), so `athena_query` polls every second for up to 30 seconds before giving up.

- **`providers/aws/codebuild_tools.py`**: `codebuild_list_projects`, `codebuild_list_builds`, `codebuild_get_logs`, `codebuild_describe_build`. The CodeBuild equivalent of the Cloud Build tools.

- **`providers/aws/ecs_tools.py`**: `ecs_list_clusters`, `ecs_list_services`, `ecs_describe_service`, `ecs_list_tasks`, `ecs_get_logs`, `ecs_get_error_logs`. Covers the container half of what Cloud Run does in one service on GCP; the log tools resolve the right CloudWatch log group automatically from the service's task definition.

- **`providers/aws/lambda_tools.py`**: `lambda_list_functions`, `lambda_describe_function`, `lambda_list_versions`, `lambda_get_logs`, `lambda_get_error_logs`. Covers the serverless-function half of Cloud Run's niche. A function's log group is always `/aws/lambda/<function_name>`, so no extra lookup is needed before fetching logs.

- **`providers/aws/s3_tools.py`**: `s3_list_buckets`, `s3_list_objects`. Bonus coverage beyond the GCP tool set's shape.

- **`providers/aws/identity_tools.py`**: `aws_whoami`. Bonus coverage: confirms which AWS account and identity a named environment actually resolves to, useful once you have several environments configured.

## Configuring environments

An "environment" is a named GCP project or AWS account/region you want the server able to talk to. You can configure any number of them under any names you like: `dev`, `prod`, `client-a-prod`, whatever makes sense to you. Run the wizard to add one:

```
$ python3 setup_wizard.py
=== Multi-Cloud MCP Setup Wizard ===

Loaded 0 GCP environment(s): (none)
Loaded 0 AWS environment(s): (none)

Main menu:
  1) Add a cloud environment
  2) Remove an environment
  3) Set default environment for a provider
  4) Save and exit
  5) Exit without saving
Choose [1-5]: 1
Provider?
  1) gcp
  2) aws
Choose [1-2]: 1

--- Add GCP environment ---
Environment name (e.g. 'dev', 'prod', 'client-a-prod'): prod
GCP project ID: my-real-project-id
Region [us-central1]:
Auth method:
  1) Application Default Credentials (gcloud auth application-default login)
  2) Service account JSON key file
Choose [1-2]: 1
Verifying...
  Auth OK.
  BigQuery smoke test OK.
Added GCP environment 'prod'.
```

Re-run the wizard any time to add another environment, remove one, or change which one is the default. It always reloads whatever `.env` already exists first, so nothing you have already configured is lost.

## GCP auth modes

Chosen per environment, in the wizard:

- **Application Default Credentials (ADC)**: run `gcloud auth application-default login` once beforehand, or rely on the default service account if this is running inside GCP itself.
- **Service account JSON key file**: point the wizard at a downloaded key file. Useful if you do not have `gcloud` installed at all. The path is stored in `.env` as `GCP_ENV_<NAME>_CREDENTIALS_PATH`.

## AWS auth modes

Also chosen per environment, in the wizard:

- **Named profile** (recommended): set one up first with `aws configure` or `aws configure sso`. No secrets are ever written to `.env`, only the profile name.
- **Access key and secret**: pasted directly into `.env` for an environment with no AWS CLI configured at all. `.env` is git-ignored and the wizard sets its file permissions to `600`, but a profile is still the safer choice when you have one available.

## Tool catalog with examples

Every tool below also accepts an optional `env` argument; it is omitted from the example calls where the default environment is being used.

### BigQuery (GCP)

```
bq_list_datasets()
→ Project: my-real-project-id
    part_catalog
    analytics

bq_query(sql="SELECT id, name FROM `my-real-project-id.part_catalog.part_level` LIMIT 2")
→ [
    {"id": "A100", "name": "Bearing"},
    {"id": "A101", "name": "Gasket"}
  ]
```

### Cloud Build (GCP)

```
cloudbuild_list_builds(branch="main", limit=5)
→ [
    {"id": "8f2e...", "status": "SUCCESS", "branch": "main",
     "createTime": "2026-09-01T10:02:00Z", "finishTime": "2026-09-01T10:06:00Z",
     "logUrl": "https://console.cloud.google.com/cloud-build/builds/8f2e..."}
  ]
```

### Cloud Run (GCP)

```
cloudrun_get_error_logs(service_name="my-service", limit=20)
→ [
    {"timestamp": "2026-09-01T10:05:12Z", "severity": "ERROR",
     "message": "connection refused: upstream timeout"}
  ]
```

### Athena (AWS, BigQuery equivalent)

```
athena_query(sql="SELECT id, amount FROM salesdb.orders LIMIT 2")
→ [
    {"id": "1", "amount": "9.99"},
    {"id": "2", "amount": "14.50"}
  ]
```

### CodeBuild (AWS, Cloud Build equivalent)

```
codebuild_describe_build(build_id="my-project:abcd-1234")
→ {
    "id": "my-project:abcd-1234", "status": "SUCCEEDED",
    "startTime": "2026-09-01 10:00:00+00:00", "endTime": "2026-09-01 10:04:12+00:00",
    "sourceVersion": "main",
    "phases": [{"phaseType": "BUILD", "phaseStatus": "SUCCEEDED", "durationInSeconds": 42}]
  }
```

### ECS / Fargate (AWS, Cloud Run container half)

```
ecs_get_error_logs(cluster="my-cluster", service_name="my-service")
→ [
    {"timestamp": 1767283512000, "logStream": "ecs/app/abc123",
     "message": "ERROR something broke"}
  ]
```

### Lambda (AWS, Cloud Run serverless half)

```
lambda_describe_function(function_name="my-function")
→ {
    "name": "my-function", "runtime": "python3.12", "memorySize": 256, "timeout": 30,
    "handler": "lambda_function.handler", "role": "arn:aws:iam::123456789012:role/lambda-role",
    "lastModified": "2026-09-01T10:00:00.000+0000", "envVars": ["STAGE", "LOG_LEVEL"]
  }
```

### S3 (AWS, bonus)

```
s3_list_objects(bucket="my-bucket", prefix="logs/2026/09/", limit=5)
→ [
    {"key": "logs/2026/09/01.json", "size": 4021, "lastModified": "2026-09-01 00:05:00+00:00"}
  ]
```

### Identity (AWS, bonus)

```
aws_whoami()
→ Environment: prod
  Account:     123456789012
  ARN:         arn:aws:iam::123456789012:role/my-role
  Region:      us-east-1
```

### Always available

```
list_environments()
→ GCP environments:
    dev (default) - project: my-dev-project, region: us-central1
    prod - project: my-real-project-id, region: us-central1
  AWS environments:
    staging (default) - region: us-east-1
```

## Extending with a new service

Every tool module follows the same shape: a `register(mcp)` function that defines its `@mcp.tool()`-decorated functions as closures, each wrapped with `@friendly_errors`, each resolving its environment through `CONFIG.resolve_gcp(env)` or `CONFIG.resolve_aws(env)`. To add a new service:

1. Create `providers/gcp/<service>_tools.py` or `providers/aws/<service>_tools.py` following that shape.
2. Add it to the relevant import list and registration loop in `server.py`.
3. Write a test file under `tests/gcp/` or `tests/aws/` using the `gcp_config` / `aws_config` fixtures described below.

Never hardcode a project ID, account ID, or region anywhere. Everything should flow through the resolved environment object.

## Testing

The test suite mocks every cloud call, so it runs with no real GCP or AWS credentials and makes no real network calls (aside from AWS tests, which run entirely against [moto](https://github.com/getmoto/moto)'s in-memory AWS simulation, still fully offline).

```bash
pip install -r requirements-dev.txt
python3 -m pytest tests/ -v
```

`tests/conftest.py` provides two fixtures used throughout: `gcp_config(module)` and `aws_config(module)`, which each monkeypatch a tool module's `CONFIG` to a fake, single-environment `AppConfig`. AWS tests wrap their body in moto's `@mock_aws` decorator and use fake static keys, so the real `providers/aws/auth.py` code path runs end to end against moto's simulated AWS rather than mocking authentication away. GCP has no equivalent in-memory emulator, so GCP tests mock at the client-class boundary instead (`bigquery.Client`, `run_v2.ServicesClient`, and so on).

`tests/test_server.py` and `tests/test_setup_wizard.py` run the real scripts as subprocesses in an isolated temporary working directory, since both have import-time or interactive side effects that are not safe to exercise by importing them directly inside the test process.

### Live smoke test

The mocked suite proves the code is correct in isolation, but it can't catch things like a mistyped project ID, an API that isn't enabled, or an expired credential. `live_check.py` covers that gap: it reads your real `.env` and makes one real, read-only call per configured environment (an actual credential fetch plus a lightweight BigQuery call for GCP, an actual `sts:GetCallerIdentity` for AWS), so it exercises the exact same auth code path the real tools use, without needing you to have any specific resources (datasets, buckets, functions) already set up.

```bash
python3 live_check.py                # check every configured environment
python3 live_check.py --env prod     # check only one environment by name
python3 live_check.py --provider aws # check only one provider
```

This was run against a real GCP project and a real AWS account during development: BigQuery, S3, Lambda, ECS, CodeBuild, and Athena all round-tripped successfully end to end (each reporting empty results, since those particular resources had nothing in them yet), and Cloud Run / Cloud Build correctly surfaced real "API not enabled" errors as clean text through `friendly_errors` instead of a raw traceback, confirming the error-handling path works against real failures too, not just mocked ones.

## Troubleshooting

- **"No cloud providers configured"** on startup: run `python3 setup_wizard.py`.
- **"Unknown GCP/AWS environment '...'"**: the error message lists what is actually configured; use one of those names, or add a new one with the wizard.
- GCP ADC issues: `gcloud auth application-default login`.
- AWS profile issues: `aws sts get-caller-identity --profile <name>` to test outside the wizard.