Skip to content

feat(gcp-mcp-finops-agent): add FinOps recipe on the Google Cloud BigQuery MCP server - #2659

Open
vishal-bulbule wants to merge 2 commits into
google:mainfrom
vishal-bulbule:feat/gcp-mcp-finops-agent
Open

vishal-bulbule wants to merge 2 commits into
google:mainfrom
vishal-bulbule:feat/gcp-mcp-finops-agent

Conversation

@vishal-bulbule

Copy link
Copy Markdown

Adds contrib/python/gcp-mcp-finops-agent, proposed in #2658.

What it is

A single-agent recipe that answers Google Cloud cost questions from the Cloud Billing export in BigQuery. It reaches BigQuery through the Google-hosted BigQuery MCP server, so there is no MCP server to install or run.

I could not find an existing recipe that connects to a Google Cloud hosted MCP server. This one is meant to be the runnable example for that integration.

What it teaches

  • Connecting McpToolset to a Google-hosted MCP server over Streamable HTTP, with the IAM roles it needs.
  • Refreshing the access token on every request with header_provider, so the agent keeps working after the first hour.
  • A read-only tool_filter allowlist. The server also offers execute_sql and cancel_job; the model never sees them.
  • Loading credentials on first use, so the agent imports and the tests run without credentials.
  • An instruction built around how the billing export behaves: query in the project that holds the export and filter by the project asked about, check the date range of the data first, and report only numbers that came from a query.

If BILLING_TABLE is not set, the agent still loads and tells the user what to configure.

Testing

  • uv run validate contrib/python/gcp-mcp-finops-agent: manifest, structure, README and placement pass.
  • ruff format and ruff check: clean.
  • uv run pytest: 11 tests pass on Python 3.11 and 3.13 with .env copied from .env.example, no credentials and no network.
  • check_env_vars.py, check_recipe_pyproject.py, check_lockfile_hashes.py and check_house_rules.py: pass, no house-rule findings.
  • uv run pytest tests/integration: 2 tests pass against the live BigQuery MCP server and a real billing export. One checks that the server offers exactly the allowlisted tools through the filter, the other that the agent answers from an execute_sql_readonly call.
  • Manual runs in adk web: data window, month over month net cost by service, a delete request (refused), and the unconfigured path.

Notes

…Query MCP server

A single-agent Python recipe that answers cloud cost questions from the Cloud Billing export through the Google-hosted BigQuery MCP server.

It shows per-request token refresh with header_provider, a read-only tool_filter allowlist, credentials loaded on first use, and an instruction built around how the billing export behaves.

Proposal: google#2658

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Security review — 1 finding(s).


Round 1 · 0 of this PR's 25 automated comments used · this round is capped at 20 across all reviewers.

- "Cloud Billing export"

ownership:
team: "TechTrapture"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MAJOR] Can you please provide the specific maintaining team name here instead of the company?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 331e7fd. I maintain this recipe on my own, so ownership.team now names me and poc stays my GitHub handle. The README also has a Maintainer section with a contact address.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Maintainability review — 2 finding(s).


Round 1 · 0 of this PR's 25 automated comments used · this round is capped at 20 across all reviewers.

name="root_agent",
model=Gemini(
model=os.getenv("MODEL_NAME"),
retry_options=types.HttpRetryOptions(attempts=3),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MINOR] Perhaps we should extract the retry attempt count to a named constant? This makes it easier to configure and reuse in other model definitions.



def gcp_auth_headers(
_context: ReadonlyContext | None = None,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MINOR] ignored parameter

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Correctness review — 1 finding(s).


Round 1 · 0 of this PR's 25 automated comments used · this round is capped at 20 across all reviewers.

async for event in runner.run_async(
user_id="test_user", session_id=session.id, new_message=message
):
for part in event.content.parts if event.content else []:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MAJOR] If event.content.parts is None in some stream events (which can happen with metadata-only chunks), this loop will raise a TypeError. Can we use event.content.parts or [] to safely handle None values?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, fixed in 331e7fd. The loop now iterates over parts or [].

Handle events with no parts in the integration test, name the maintainer in the manifest and README.

@happyhuman happyhuman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please submit a recipe proposal here before submitting a PR.

@vishal-bulbule

Copy link
Copy Markdown
Author

Thanks. The proposal is #2658, filed the day before this PR. I opened it from the CLI, so it did not pick up the template's default assignees and may not be in the reviewers' queue. Could you assign it, or would you prefer that I refile it through the template?

I'll convert this PR to a draft until the proposal is approved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants