Google Cloud CLI Remote MCP Server (Part 2): Building a Multi-Node ADK Workflow for Cloud Audit & Live Remediation

In Part 1, we explored how the Google Cloud CLI Remote MCP Server (https://cloudcli.googleapis.com/mcp) lets AI agents execute gcloud and bq commands inside a serverless, Google-managed sandbox.
In real-world Cloud Operations and FinOps, however, you don’t want a single monolithic LLM prompt deciding everything end-to-end.
Instead, production cloud automation pipelines follow a structured Discover → Evaluate → Route → Remediate workflow:
- One node executes gcloud or bq discovery commands via the Remote MCP Server and saves the raw cloud telemetry into workflow state.
- A deterministic Python node inspects that telemetry, sets structured state variables, and routes execution based on whether non-compliant resources were found.
- Downstream action nodes pick up those state variables and execute real remediation actions in Google Cloud.
In this Part 2 guide, we build and test a 4-node ADK Workflow graph (examples/05-adk-workflow-remediation) using the Google Agent Development Kit (ADK) (google-adk >= 2.0.0), gemini-3-flash-preview, and https://cloudcli.googleapis.com/mcp.
Real-World Scenario: Automated Stopped-VM & Orphaned Boot Disk Remediation
When a Compute Engine VM is stopped (TERMINATED), Google Cloud stops billing for vCPU and memory—but your project continues to be billed every month for its attached persistent boot disk.
Our 4-node ADK Workflow automates the audit and tagging of stopped VMs and their attached persistent disks:

Core ADK Workflow Concepts Used
Before looking at the code, let’s review how data and control flow move between nodes in an ADK Workflow (from google.adk.workflow import Workflow):

Step 1: Building the 4-Node ADK Workflow (app/agent.py)
Let’s walk through each node in examples/05-adk-workflow-remediation/app/agent.py.
Node 1: init_audit_context (FunctionNode)
When the workflow starts, START passes the user's trigger prompt into init_audit_context via node_input. This function initializes target_project, default_region, default_zone, and audit_request in the workflow state by returning an Event:
def init_audit_context(node_input: str) -> Event:
"""Initializes workflow state variables from the user prompt and environment."""
project_id = os.environ.get("GOOGLE_CLOUD_PROJECT", DEFAULT_PROJECT_ID)
region = os.environ.get("GOOGLE_CLOUD_REGION", DEFAULT_REGION)
zone = os.environ.get("GOOGLE_CLOUD_ZONE", DEFAULT_ZONE)
state_init = {
"target_project": project_id,
"default_region": region,
"default_zone": zone,
"audit_request": str(node_input).strip(),
}
return Event(
output={
"message": f"Initialized audit context for project '{project_id}' in zone '{zone}'.",
**state_init,
},
state=state_init,
)
Node 2: vm_discovery_agent (LlmAgent + Remote MCP)
Next, execution moves to vm_discovery_agent. Notice two things:
- Its instruction reads {target_project}, {default_zone}, and {audit_request} directly from the state dictionary populated by Node 1.
- It sets output_key="raw_vm_telemetry", which instructs ADK to store the agent's JSON output into session.state["raw_vm_telemetry"]:
vm_discovery_agent = LlmAgent(
name="vm_discovery_agent",
model=os.environ.get("ADK_MODEL", "gemini-3-flash-preview"),
description="Discovers Compute Engine VM instances in the target project and zone via Remote MCP.",
instruction="""You are the Cloud Discovery Node in an automated ADK Workflow.
Workflow State Variables:
- Target Project: {target_project}
- Default Zone: {default_zone}
- Audit Request: {audit_request}
Task:
1. Call `run_gcloud_command` with:
- `project`: "projects/{target_project}"
- `command`: "gcloud compute instances list --zones={default_zone} --project={target_project} --format=json"
2. Output ONLY a valid JSON array summarizing the discovered VM instances, where each object contains:
- "name" (string)
- "status" (string, e.g. "RUNNING" or "TERMINATED")
- "zone" (string, short zone name e.g. "us-central1-a")
- "machineType" (string, short machine type e.g. "e2-medium")
Do not wrap the JSON in conversational text.
""",
tools=[gcloud_mcp_tools],
output_key="raw_vm_telemetry",
)
Node 3: evaluate_and_route (FunctionNode)
Once vm_discovery_agent finishes, evaluate_and_route runs. Notice its function signature:
- node_input: str receives the direct output from vm_discovery_agent.
- target_project: str, default_zone: str, and raw_vm_telemetry: str are automatically bound by ADK from ctx.state because their parameter names match the state keys!
It parses the discovered VMs, checks if any VM is not RUNNING, and returns an Event containing both route="needs_remediation" (or "healthy") and the downstream state variables (target_vm_name, audit_status, risk_level, remediation_reason):
def evaluate_and_route(
node_input: str,
target_project: str,
default_zone: str,
raw_vm_telemetry: str,
) -> Event:
"""Evaluates discovered VM telemetry and routes to 'needs_remediation' or 'healthy'."""
telemetry_source = raw_vm_telemetry or str(node_input)
vms = _extract_vm_list(telemetry_source)
non_running_vms = [
vm for vm in vms if str(vm.get("status", "")).upper() != "RUNNING"
]
if non_running_vms:
flagged_vm = non_running_vms[0]
vm_name = str(flagged_vm.get("name", "unknown-vm"))
vm_status = str(flagged_vm.get("status", "UNKNOWN"))
vm_zone = str(flagged_vm.get("zone", default_zone)).split("/")[-1]
route = "needs_remediation"
audit_status = "needs-remediation"
risk_level = "high"
reason = (
f"VM '{vm_name}' in zone '{vm_zone}' is in '{vm_status}' state "
f"(expected RUNNING)."
)
...
state_updates = {
"target_vm_name": vm_name,
"target_vm_status": vm_status,
"default_zone": vm_zone,
"audit_status": audit_status,
"risk_level": risk_level,
"remediation_reason": reason,
}
return Event(
output={"route_selected": route, **state_updates},
route=route,
state=state_updates,
)
Node 4A & 4B: remediation_executor_agent and healthy_compliance_agent
When evaluate_and_route emits route="needs_remediation", the workflow triggers remediation_executor_agent.
Using {target_vm_name}, {default_zone}, {audit_status}, and {risk_level} from state, it executes two live run_gcloud_command operations:
- gcloud compute instances add-labels: Tags the stopped VM instance with audit-status=needs-remediation,risk-level=high.
- gcloud compute disks add-labels: Tags the VM's persistent boot disk with audit-status=needs-remediation,risk-level=high so FinOps cost reports flag the unattached/stopped storage cost.
Finally, the entire graph is wired together in root_agent = Workflow(…):
root_agent = Workflow(
name="cloud_audit_remediation_workflow",
description=(
"4-node ADK Workflow that discovers Compute Engine VMs via the "
"Google Cloud CLI Remote MCP Server, evaluates state via FunctionNode, "
"and conditionally executes live VM and boot disk compliance labeling."
),
edges=[
("START", init_audit_context, vm_discovery_agent, evaluate_and_route),
(
evaluate_and_route,
{
"needs_remediation": remediation_executor_agent,
"healthy": healthy_compliance_agent,
},
),
],
)
Step 2: Running the Workflow (run_workflow.py)
Run examples/05-adk-workflow-remediation/run_workflow.py using uv run:
export GOOGLE_CLOUD_PROJECT="your-project-id"
uv run examples/05-adk-workflow-remediation/run_workflow.py \
--project="${GOOGLE_CLOUD_PROJECT}"
Live Execution Output
Watch how each node in the graph executes in sequence, passing state variables from init_audit_context → vm_discovery_agent → evaluate_and_route → remediation_executor_agent:
==============================================================================
Starting ADK Workflow: cloud_audit_remediation_workflow
Target Project : your-project-id
Target Zone : us-central1-a
Trigger Prompt : Audit Compute Engine VMs in our default zone and remediate any non-running instances.
==============================================================================
[cloud_audit_remediation_workflow] Event Emitted:
Node Output : {
"message": "Initialized audit context for project 'your-project-id' in zone 'us-central1-a'.",
"target_project": "your-project-id",
"default_region": "us-central1",
"default_zone": "us-central1-a",
"audit_request": "Audit Compute Engine VMs in our default zone and remediate any non-running instances."
}
State Updates : {
"target_project": "your-project-id",
"default_region": "us-central1",
"default_zone": "us-central1-a",
"audit_request": "Audit Compute Engine VMs in our default zone and remediate any non-running instances."
}
[vm_discovery_agent] -> Tool Call: run_gcloud_command({"project": "projects/your-project-id", "command": "gcloud compute instances list --zones=us-central1-a --project=your-project-id --format=\"json(name,status,zone.scope(),machineType.scope())\""})
[vm_discovery_agent] Event Emitted:
State Updates : {
"raw_vm_telemetry": "[\n {\n \"name\": \"demo-web-server\",\n \"status\": \"TERMINATED\",\n \"zone\": \"us-central1-a\",\n \"machineType\": \"e2-medium\"\n }\n]"
}
[vm_discovery_agent] Final Response:
[
{
"name": "demo-web-server",
"status": "TERMINATED",
"zone": "us-central1-a",
"machineType": "e2-medium"
}
]
[cloud_audit_remediation_workflow] Event Emitted:
Route Selected : needs_remediation
Node Output : {
"route_selected": "needs_remediation",
"target_vm_name": "demo-web-server",
"target_vm_status": "TERMINATED",
"default_zone": "us-central1-a",
"audit_status": "needs-remediation",
"risk_level": "high",
"remediation_reason": "VM 'demo-web-server' in zone 'us-central1-a' is in 'TERMINATED' state (expected RUNNING)."
}
State Updates : {
"target_vm_name": "demo-web-server",
"target_vm_status": "TERMINATED",
"default_zone": "us-central1-a",
"audit_status": "needs-remediation",
"risk_level": "high",
"remediation_reason": "VM 'demo-web-server' in zone 'us-central1-a' is in 'TERMINATED' state (expected RUNNING)."
}
[remediation_executor_agent] -> Tool Call: run_gcloud_command({"command": "gcloud compute instances add-labels demo-web-server --zone=us-central1-a --labels=audit-status=needs-remediation,risk-level=high --project=your-project-id --quiet", "project": "projects/your-project-id"})
[remediation_executor_agent] -> Tool Call: run_gcloud_command({"project": "projects/your-project-id", "command": "gcloud compute disks add-labels demo-web-server --zone=us-central1-a --labels=audit-status=needs-remediation,risk-level=high --project=your-project-id --quiet"})
[remediation_executor_agent] Final Response:
The following remediation actions were successfully executed in Google Cloud for project `your-project-id`:
1. **VM Instance Tagging**: The non-compliant VM `demo-web-server` in zone `us-central1-a` was updated with the compliance labels `audit-status=needs-remediation` and `risk-level=high`.
2. **Persistent Disk Tagging**: The attached boot disk `demo-web-server` in zone `us-central1-a` was also updated with matching `audit-status=needs-remediation` and `risk-level=high` labels to ensure FinOps and compliance visibility for orphaned storage costs.
==============================================================================
Final Workflow Session State (`session.state`):
==============================================================================
{
"target_project": "your-project-id",
"default_region": "us-central1",
"default_zone": "us-central1-a",
"audit_request": "Audit Compute Engine VMs in our default zone and remediate any non-running instances.",
"raw_vm_telemetry": "[\n {\n \"name\": \"demo-web-server\",\n \"status\": \"TERMINATED\",\n \"zone\": \"us-central1-a\",\n \"machineType\": \"e2-medium\"\n }\n]",
"target_vm_name": "demo-web-server",
"target_vm_status": "TERMINATED",
"audit_status": "needs-remediation",
"risk_level": "high",
"remediation_reason": "VM 'demo-web-server' in zone 'us-central1-a' is in 'TERMINATED' state (expected RUNNING).",
"workflow_execution_result": "The following remediation actions were successfully executed in Google Cloud for project `your-project-id`:\n\n1. **VM Instance Tagging**: The non-compliant VM `demo-web-server` in zone `us-central1-a` was updated with the compliance labels `audit-status=needs-remediation` and `risk-level=high`.\n2. **Persistent Disk Tagging**: The attached boot disk `demo-web-server` in zone `us-central1-a` was also updated with matching `audit-status=needs-remediation` and `risk-level=high` labels to ensure FinOps and compliance visibility for orphaned storage costs."
}
Step 3: Verifying the Remediation in Google Cloud
We can independently verify that the workflow modified both resources in Google Cloud by inspecting the VM instance and its persistent boot disk:
1. Check the VM Instance Labels
gcloud compute instances describe demo-web-server \
--zone=us-central1-a \
--project="${GOOGLE_CLOUD_PROJECT}" \
--format="json(name,status,labels)"
Output:
{
"labels": {
"audit-status": "needs-remediation",
"risk-level": "high"
},
"name": "demo-web-server",
"status": "TERMINATED"
}
A peek in the Google Cloud console does confirm the label:

2. Check the Persistent Boot Disk Labels
gcloud compute disks describe demo-web-server \
--zone=us-central1-a \
--project="${GOOGLE_CLOUD_PROJECT}" \
--format="json(name,sizeGb,status,labels)"
Output:
{
"labels": {
"audit-status": "needs-remediation",
"risk-level": "high"
},
"name": "demo-web-server",
"sizeGb": "10",
"status": "READY"
}
Google Cloud Console screenshot for the specific disk is shown below:

Summary & Conclusion
While the Google Cloud CLI Remote MCP Server (https://cloudcli.googleapis.com/mcp) executes individual gcloud and bq commands inside a stateless sandbox, combining it with ADK's Workflow graph API gives you a deterministic, stateful control plane for real-world Cloud SRE and FinOps automation:

Key Takeaways
- Separate Discovery from Evaluation: Let an LlmAgent node connected to cloudcli.googleapis.com/mcp gather structured JSON telemetry (output_key="raw_vm_telemetry"), and let a Python FunctionNode evaluate policy rules and emit Event(route=…, state={…}).
- Pass Context Explicitly via session.state: Downstream LlmAgent nodes never have to guess project IDs, zones, or resource names—they interpolate {target_project}, {default_zone}, {target_vm_name}, and {audit_status} directly from workflow state.
- Remediate the Full Resource Lifecycle: In Cloud FinOps, stopping a VM (TERMINATED) still leaves its persistent boot disk incurring monthly storage charges—making multi-step workflow remediation (tagging both the instance and its attached disk) immediately valuable in production.
Google Cloud CLI Remote MCP Server (Part 2): Building a Multi-Node ADK Workflow for Cloud Audit &… was originally published in Google Cloud – Community on Medium, where people are continuing the conversation by highlighting and responding to this story.
Source Credit: https://medium.com/google-cloud/google-cloud-cli-remote-mcp-server-part-2-building-a-multi-node-adk-workflow-for-cloud-audit-1f921f2593ea?source=rss—-e52cf94d98af—4
