Google Cloud CLI Remote MCP Server (Part 3): Human-in-the-Loop (HITL) Cloud Remediation on Cloud Run
In Part 1 of this series, we connected Google Cloud’s stateless gcloud and bq Remote MCP Server to local IDEs, a single-agent ADK service, and Gemini Enterprise Managed Agents.
Getting Started with the Google Cloud CLI Remote MCP Server
In Part 2, we replaced single-prompt agent routing with a deterministic 4-node ADK Workflow graph (FunctionNode + LlmAgent + Event routing) that audits Compute Engine VMs and automatically remediates non-compliant instances.
Google Cloud CLI Remote MCP Server (Part 2): Building a Multi-Node ADK Workflow for Cloud Audit &…
If you have worked through Part 2 of the series and if you’ve touched any production system, you would be wary of letting an agent automatically start or stop a VM or even do any other resource updates, without a review and approval from your side (i.e. a human). This is exactly what is meant by Human-in-the-Loop (HITL) pattern where a human gives a sign-off and then the rest of the workflow can proceed.
But how do we does Human-in-the-Loop (HITL) approval actually work when the ADK agent is deployed on a serverless, scale-to-zero platform like Cloud Run?
In this Part 3 guide, we upgrade our 4-node remediation graph into a 6-node Resumable HITL ADK Workflow deployed on Cloud Run and backed by Vertex AI Agent Engine Session Service (VertexAiSessionService).
Companion Code on GitHub: All complete, runnable files for this guide are available in examples/06-adk-workflow-hitl-cloud-run in the rominirani/gcloud-remote-mcp-guide repository.
The overall infographic for what we are going to do is shown below. You can notice in Node 4B , that if a needs_remediation path is taken, then it will pause for human input / approval and depending on approval or rejected , the specific path and eventual action will be taken.

Let’s get going.
Persisting HITL State on Cloud Run
When you run an ADK workflow locally with InMemorySessionService, pausing for human input looks simple because the Python process stays alive in RAM.
On Cloud Run, three things happen in production:
- You cannot hold an HTTP connection open for human approval. An on-call SRE might take 30 seconds or 30 minutes to click Approve or Reject in Slack, PagerDuty, or a CLI tool. HTTP Request 1 (POST /run) must finish cleanly (200 OK) as soon as the workflow pauses.
- While waiting for the human operator’s decision, Cloud Run may scale the container instance down to zero (min-instances=0). Any session stored in InMemorySessionService is permanently lost.
- Even if containers stay warm, HTTP Request 2 (POST /run carrying the human's approval or rejection) can land on a completely different Cloud Run container instance than HTTP Request 1.
Go through the diagram here slowly.
The sequence diagram below traces the complete lifecycle across two separate HTTP requests (HTTP Request 1 to start the audit and pause, and HTTP Request 2 to resume after the human operator responds):

To make HITL work reliably on serverless containers, we wire our Cloud Run FastAPI server to VertexAiSessionService (agentengine://{agent_engine_id}). Whenever the workflow runs, ADK externally checkpoints every Session.state delta and Event log entry in Vertex AI Agent Engine.
The 6-Node Workflow Graph
Our key goal here is going to be about inserting Node 4B (request_remediation_approval) as a resumable approval gate in front of the remediation actions.
The flowchart below shows how the 6 nodes connect and where the workflow pauses and resumes:

Here is the role of each node in the graph:
- Node 1 (init_audit_context — FunctionNode): Seeds target_project, default_region, default_zone (us-central1-a), and audit_request into the workflow state.
- Node 2 (vm_discovery_agent — LlmAgent + Remote MCP): Calls run_gcloud_command (gcloud compute instances list –zones=us-central1-a –format=json) on Google Cloud CLI MCP Server saves the discovered VM inventory to state["raw_vm_telemetry"].
- Node 3 (evaluate_and_route — FunctionNode): Inspects raw_vm_telemetry in Python. If all VMs are RUNNING, it emits route="healthy". If any VM is TERMINATED, it emits route="needs_remediation".
- Node 4A (healthy_compliance_agent — LlmAgent): Runs when route="healthy", labels the running VM and its boot disk as compliant, and completes in a single HTTP request without pausing.
- Node 4B (request_remediation_approval — @node(rerun_on_resume=True)): Runs when route="needs_remediation". On Pass 1 (HTTP Request 1), it yields a RequestInput interrupt so ADK checkpoints the session to VertexAiSessionService and returns 200 OK. On Pass 2 (HTTP Request 2), it reads the human's decision and emits route="approved" or route="rejected".
- Node 5A (remediation_executor_agent) & Node 5B (remediation_rejected_node): If "approved", Node 5A labels the VM and boot disk and starts the stopped VM (gcloud compute instances start –async). If "rejected", Node 5B records the rejection reason in the session state and leaves the VM TERMINATED.
The observant among you will notice in Step 6 above, that we are using –async while starting the compute instance. More on that later, since this is a Long Running Transaction.
Code Walkthrough
Let’s walk through the key implementation details in examples/06-adk-workflow-hitl-cloud-run so you can see how each piece works.
Step 1: Provisioning the Session Store (setup_session_engine.py)
VertexAiSessionService stores ADK sessions and HITL event checkpoints as child resources under a Vertex AI ReasoningEngine:
projects/{project}/locations/{region}/reasoningEngines/{agent_engine_id}/sessions/{session_id}
Even when your agent container runs on Cloud Run, Vertex AI Agent Engine lets you create a lightweight, containerless ReasoningEngine resource ({"displayName": "gcloud-mcp-hitl-session-store"}) purely to serve as a managed cloud session store.
Running setup_session_engine.py checks if gcloud-mcp-hitl-session-store already exists in your project and provisions it if not:
export GOOGLE_CLOUD_PROJECT="your-project-id"
uv run --project examples/06-adk-workflow-hitl-cloud-run \
examples/06-adk-workflow-hitl-cloud-run/setup_session_engine.py \
--project="${GOOGLE_CLOUD_PROJECT}" \
--region=us-central1
This produces the following output:
========================================================================
VertexAiSessionService Store Ready!
ReasoningEngine Resource : projects/YOUR_PROJECT_NUMBER/locations/us-central1/reasoningEngines/4625340028699017216
GOOGLE_CLOUD_AGENT_ENGINE_ID : 4625340028699017216
ADK Session Service URI : agentengine://4625340028699017216
========================================================================
Step 2: Building the Resumable HITL Gate (app/agent.py)
In app/agent.py, we configure our LlmAgent nodes with gemini-3.8-flash using ADK's Gemini model wrapper:
from google.adk.models import Gemini
def _get_gemini_model() -> Gemini:
return Gemini(
model=os.environ.get("ADK_MODEL", "gemini-3.8-flash"),
client_kwargs={"location": "global"},
)
Next, let’s look at how Node 4B (request_remediation_approval) implements the pause-and-resume handshake using @node(rerun_on_resume=True) and RequestInput:
from google.adk.events.request_input import RequestInput
from google.adk.workflow import Workflow, node
@node(rerun_on_resume=True)
async def request_remediation_approval(
ctx: Context, node_input: dict[str, Any]
) -> AsyncGenerator[Event | RequestInput, None]:
"""Pauses the workflow on Pass 1 via RequestInput; routes on Pass 2 based on human reply."""
resume_data = ctx.resume_inputs.get(HITL_INTERRUPT_ID)
# Pass 1 (HTTP Request 1): Emit RequestInput interrupt and checkpoint to VertexAiSessionService
if resume_data is None:
vm_name = ctx.state.get("target_vm_name", "unknown-vm")
vm_zone = ctx.state.get("default_zone", DEFAULT_ZONE)
project_id = ctx.state.get("target_project", DEFAULT_PROJECT_ID)
reason = ctx.state.get("remediation_reason", "")
proposed_actions = [
f"Label VM '{vm_name}' with audit-status=remediated,risk-level=low",
f"Label persistent boot disk '{vm_name}' with audit-status=remediated,risk-level=low",
f"Start stopped VM '{vm_name}' in zone '{vm_zone}' (`gcloud compute instances start --async`)",
]
yield RequestInput(
interrupt_id=HITL_INTERRUPT_ID,
message=(
f"HITL Approval Required: {reason} "
f"Approve live remediation on '{vm_name}' in project '{project_id}'?"
),
payload={
"interrupt_id": HITL_INTERRUPT_ID,
"target_project": project_id,
"target_zone": vm_zone,
"target_vm_name": vm_name,
"reason": reason,
"proposed_actions": proposed_actions,
},
)
return
# Pass 2 (HTTP Request 2): Evaluate human operator's decision from ctx.resume_inputs
if isinstance(resume_data, str):
resume_data = json.loads(resume_data)
approved = bool(resume_data.get("approved", False))
approver = str(resume_data.get("approver", "sre-operator"))
comment = str(resume_data.get("reason", ""))
route = "approved" if approved else "rejected"
decision_state = {
"approval_status": route,
"approved_by": approver,
"approval_comment": comment,
"audit_status": "remediated" if approved else "remediation-rejected",
"risk_level": "low" if approved else "high",
}
yield Event(
author="request_remediation_approval",
content=types.Content(role="model", parts=[types.Part.from_text(text=f"route='{route}'")]),
output=decision_state,
route=route,
state=decision_state,
)
What is happening here?
- Why @node(rerun_on_resume=True)? By default in an ADK Workflow, once a node executes during a run, its output is cached in the session event log so it will not re-run when the session resumes. Decorating request_remediation_approval with @node(rerun_on_resume=True) tells ADK: "Keep the cached outputs of Nodes 1, 2, and 3 so we don't re-run vm_discovery_agent, but re-enter Node 4B when the human's response arrives."
- Pass 1 (During HTTP Request 1): When request_remediation_approval runs the first time, ctx.resume_inputs.get("approve_vm_remediation") is None. The node yields RequestInput(interrupt_id="approve_vm_remediation", …) and returns. ADK translates that RequestInput into a synthetic tool call event (functionCall: adk_request_input(id="approve_vm_remediation")), flushes all events and state to VertexAiSessionService, and finishes HTTP Request 1 (200 OK).
- Pass 2 (During HTTP Request 2): When the client sends a functionResponse with id="approve_vm_remediation" on the same sessionId, ADK reloads the session from VertexAiSessionService, replays Nodes 1–3 from cache, and re-enters request_remediation_approval. This time, ctx.resume_inputs.get("approve_vm_remediation") contains the operator's JSON payload. The node parses approved (True or False) and yields Event(route="approved") or Event(route="rejected").
Finally, we wire the 6 nodes together in Workflow and wrap it in an App with ResumabilityConfig(is_resumable=True):
root_agent = Workflow(
name="cloud_audit_hitl_remediation_workflow",
edges=[
("START", init_audit_context, vm_discovery_agent, evaluate_and_route),
(
evaluate_and_route,
{
"healthy": healthy_compliance_agent,
"needs_remediation": request_remediation_approval,
},
),
(
request_remediation_approval,
{
"approved": remediation_executor_agent,
"rejected": remediation_rejected_node,
},
),
],
)
app = App(
name="app",
root_agent=root_agent,
resumability_config=ResumabilityConfig(is_resumable=True),
)
Step 3: Wiring FastAPI to VertexAiSessionService (server.py)
In server.py, we pass session_service_uri=f"agentengine://{agent_engine_id}" to ADK's get_fast_api_app(…):
os.environ.setdefault("GOOGLE_CLOUD_LOCATION", "us-central1")
os.environ.setdefault("GOOGLE_GENAI_USE_VERTEXAI", "TRUE")
agent_engine_id = os.environ.get("GOOGLE_CLOUD_AGENT_ENGINE_ID", "").strip()
session_service_uri = (
f"agentengine://{agent_engine_id}"
if agent_engine_id
else os.environ.get("SESSION_SERVICE_URI", "memory://")
)
app: FastAPI = get_fast_api_app(
agents_dir=str(BASE_DIR),
session_service_uri=session_service_uri,
web=True,
)
When session_service_uri uses the agentengine:// scheme, ADK's service registry automatically initializes VertexAiSessionService in GOOGLE_CLOUD_LOCATION (us-central1), persisting all session state and HITL checkpoints in Vertex AI Agent Engine.
Step 4: The 2-Request HTTP Client (run_hitl_client.py)
run_hitl_client.py drives the 2-request HITL lifecycle against either a local server or a deployed Cloud Run URL:
- Create a Session: POST /apps/app/users/sre-operator/sessions creates a new session in VertexAiSessionService.
- HTTP Request 1 (POST /run): Sends the initial user prompt ("Audit Compute Engine VMs in us-central1-a…") and inspects the returned events for functionCall.name == "adk_request_input".
- HTTP Request 2 (POST /run with functionResponse): When an interrupt is found, it captures the operator's decision (–decision=approve, –decision=reject, or interactive prompt) and posts a functionResponse matching id="approve_vm_remediation" on the exact same sessionId:
req2_payload = {
"appName": args.app_name,
"userId": args.user_id,
"sessionId": session_id,
"newMessage": {
"role": "user",
"parts": [
{
"functionResponse": {
"id": int_id, # "approve_vm_remediation"
"name": "adk_request_input",
"response": {
"result": json.dumps({
"approved": approved,
"approver": args.approver,
"reason": reason,
})
},
}
}
],
},
}
run2_resp = client.post("/run", json=req2_payload)
Testing All 3 Branches
All three execution paths were tested live against VertexAiSessionService (agentengine://4625340028699017216) and the remote gcloud MCP server (https://cloudcli.googleapis.com/mcp).
Branch 1: Pause & Reject (–decision=reject)
When demo-web-server is TERMINATED and the on-call SRE rejects remediation (for example, during a planned maintenance window):
uv run --project examples/06-adk-workflow-hitl-cloud-run \
examples/06-adk-workflow-hitl-cloud-run/run_hitl_client.py \
--spawn-local-server \
--project="${GOOGLE_CLOUD_PROJECT}" \
--agent-engine-id="${GOOGLE_CLOUD_AGENT_ENGINE_ID}" \
--decision=reject
Output is shown below:
==============================================================================
Part 3 HITL Client : Cloud Run + VertexAiSessionService + Remote MCP
Target Endpoint : http://127.0.0.1:8080
Session Backend : VertexAiSessionService
Session URI : agentengine://4625340028699017216
==============================================================================
[Session Created in VertexAiSessionService] sessionId=5449817790678564864
=== HTTP Request 1: POST /run (Initiate VM Discovery & Evaluation) ===
--- HTTP Request 1: Received 15 Event(s) ---
[Event 2] author='cloud_audit_hitl_remediation_workflow'
state_delta : keys=['target_project', 'default_region', 'default_zone', 'audit_request']
output : {"message": "Initialized HITL audit context for project 'your-project-id' in zone 'us-central1-a'.", ...}
[Event 5] author='vm_discovery_agent'
functionCall: run_gcloud_command(id='call_3533433', args={"project": "projects/your-project-id", "command": "gcloud compute instances list --zones=us-central1-a --project=your-project-id --format=json"})
[Event 7] author='vm_discovery_agent'
state_delta : keys=['raw_vm_telemetry']
text : [
{
"name": "demo-web-server",
"status": "TERMINATED",
"zone": "us-central1-a",
"machineType": "e2-medium"
}
]
[Event 11] author='cloud_audit_hitl_remediation_workflow'
route : 'needs_remediation'
state_delta : keys=['target_vm_name', 'target_vm_status', 'default_zone', 'audit_status', 'risk_level', 'remediation_reason', 'terminated_vms']
[Event 14] author='cloud_audit_hitl_remediation_workflow'
functionCall: adk_request_input(id='approve_vm_remediation', args={"interruptId": "approve_vm_remediation", ...})
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
⏸️ WORKFLOW PAUSED FOR HUMAN-IN-THE-LOOP (HITL) APPROVAL
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
Interrupt ID : approve_vm_remediation
Message : HITL Approval Required: VM 'demo-web-server' in zone 'us-central1-a' is in 'TERMINATED' state (expected RUNNING). Human approval required before starting VM. Approve live remediation on 'demo-web-server' in project 'your-project-id'?
Payload : {
"interrupt_id": "approve_vm_remediation",
"target_project": "your-project-id",
"target_zone": "us-central1-a",
"target_vm_name": "demo-web-server",
"reason": "VM 'demo-web-server' in zone 'us-central1-a' is in 'TERMINATED' state (expected RUNNING). Human approval required before starting VM.",
"proposed_actions": [
"Label VM 'demo-web-server' with audit-status=remediated,risk-level=low",
"Label persistent boot disk 'demo-web-server' with audit-status=remediated,risk-level=low",
"Start stopped VM 'demo-web-server' in zone 'us-central1-a' (`gcloud compute instances start --async`)"
]
}
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
=== HTTP Request 2: POST /run (Resume Session 5449817790678564864 with Decision) ===
Submitting functionResponse: {"approved": false, "approver": "romin-sre-lead", "reason": "Rejected by SRE on-call; VM intentionally stopped for maintenance."}
--- HTTP Request 2 (Resumed): Received 10 Event(s) ---
[Event 5] author='cloud_audit_hitl_remediation_workflow'
route : 'rejected'
state_delta : keys=['approval_status', 'approved_by', 'approval_comment', 'audit_status', 'risk_level']
output : {"approval_status": "rejected", "approved_by": "romin-sre-lead", "approval_comment": "Rejected by SRE on-call; VM intentionally stopped for maintenance.", "audit_status": "remediation-rejected", "risk_level": "high"}
text : [Node 4B: request_remediation_approval] Operator 'romin-sre-lead' REJECTED remediation for VM 'demo-web-server' (comment: 'Rejected by SRE on-call; VM intentionally stopped for maintenance.') -> route='rejected'
[Event 8] author='cloud_audit_hitl_remediation_workflow'
state_delta : keys=['workflow_execution_result']
text : Remediation cancelled by operator 'romin-sre-lead' (reason: 'Rejected by SRE on-call; VM intentionally stopped for maintenance.'). VM 'demo-web-server' in zone 'us-central1-a' was left in TERMINATED state.
==============================================================================
Final Persisted State in VertexAiSessionService (sessionId=5449817790678564864):
==============================================================================
target_project : your-project-id
default_zone : us-central1-a
target_vm_name : demo-web-server
target_vm_status : TERMINATED
approval_status : rejected
approved_by : romin-sre-lead
approval_comment : Rejected by SRE on-call; VM intentionally stopped for maintenance.
audit_status : remediation-rejected
risk_level : high
workflow_execution_result : Remediation cancelled by operator 'romin-sre-lead' (reason: 'Rejected by SRE on-call; VM intentionally stopped for maintenance.'). VM 'demo-web-server' in zone 'us-central1-a' was left in TERMINATED state.
==============================================================================
Notice that in HTTP Request 2, vm_discovery_agent was not re-executed: ADK replayed the cached outputs of Nodes 1–3 from VertexAiSessionService, resumed directly at Node 4B (request_remediation_approval), routed to remediation_rejected_node, and left demo-web-server untouched in TERMINATED state.
Branch 2: Pause & Approve (–decision=approve)
Next, we run the client with –decision=approve. Because Branch 1 left demo-web-server in TERMINATED state, HTTP Request 1 pauses at approve_vm_remediation, and HTTP Request 2 resumes into Node 5A (remediation_executor_agent), which labels the VM and boot disk and starts demo-web-server over the Remote gcloud MCP Server:
uv run --project examples/06-adk-workflow-hitl-cloud-run \
examples/06-adk-workflow-hitl-cloud-run/run_hitl_client.py \
--spawn-local-server \
--project="${GOOGLE_CLOUD_PROJECT}" \
--agent-engine-id="${GOOGLE_CLOUD_AGENT_ENGINE_ID}" \
--decision=approve
Output:
[Session Created in VertexAiSessionService] sessionId=1443865922132508672
=== HTTP Request 1: POST /run (Initiate VM Discovery & Evaluation) ===
...
[Event 14] author='cloud_audit_hitl_remediation_workflow'
functionCall: adk_request_input(id='approve_vm_remediation', args={"interruptId": "approve_vm_remediation", ...})
=== HTTP Request 2: POST /run (Resume Session 1443865922132508672 with Decision) ===
Submitting functionResponse: {"approved": true, "approver": "romin-sre-lead", "reason": "Approved by SRE on-call to restart VM and apply compliance labels."}
--- HTTP Request 2 (Resumed): Received 17 Event(s) ---
[Event 5] author='cloud_audit_hitl_remediation_workflow'
route : 'approved'
state_delta : keys=['approval_status', 'approved_by', 'approval_comment', 'audit_status', 'risk_level']
text : [Node 4B: request_remediation_approval] Operator 'romin-sre-lead' APPROVED remediation for VM 'demo-web-server' (comment: 'Approved by SRE on-call to restart VM and apply compliance labels.') -> route='approved'
[Event 8] author='remediation_executor_agent'
functionCall: run_gcloud_command(id='call_2826105', args={"command": "gcloud compute instances add-labels demo-web-server --zone=us-central1-a --labels=audit-status=remediated,risk-level=low --project=your-project-id --quiet", "project": "projects/your-project-id"})
[Event 10] author='remediation_executor_agent'
functionCall: run_gcloud_command(id='call_101303', args={"project": "projects/your-project-id", "command": "gcloud compute disks add-labels demo-web-server --zone=us-central1-a --labels=audit-status=remediated,risk-level=low --project=your-project-id --quiet"})
[Event 12] author='remediation_executor_agent'
functionCall: run_gcloud_command(id='call_11056980', args={"command": "gcloud compute instances start demo-web-server --zone=us-central1-a --project=your-project-id --async --format=json --quiet", "project": "projects/your-project-id"})
[Event 14] author='remediation_executor_agent'
state_delta : keys=['workflow_execution_result']
text : Remediation actions for `demo-web-server` have been successfully executed:
1. **Compliance Labels Applied**: The VM instance and its persistent boot disk were tagged with `audit-status=remediated` and `risk-level=low`.
2. **VM Restarted**: The instance start command was initiated asynchronously.
**Operation ID**: `operation-1792903706164-65ea2ec85bbd5-b1dafbb7-8398a10d`
==============================================================================
Final Persisted State in VertexAiSessionService (sessionId=1443865922132508672):
==============================================================================
target_project : your-project-id
default_zone : us-central1-a
target_vm_name : demo-web-server
target_vm_status : TERMINATED
approval_status : approved
approved_by : romin-sre-lead
approval_comment : Approved by SRE on-call to restart VM and apply compliance labels.
audit_status : remediated
risk_level : low
==============================================================================
Branch 3: Healthy VM (route == "healthy")
Now that demo-web-server is RUNNING, running the client a third time routes directly from evaluate_and_route to Node 4A (healthy_compliance_agent) and completes in a single HTTP request without emitting an adk_request_input interrupt:
[Session Created in VertexAiSessionService] sessionId=7831896538115735552
=== HTTP Request 1: POST /run (Initiate VM Discovery & Evaluation) ===
--- HTTP Request 1: Received 21 Event(s) ---
[Event 7] author='vm_discovery_agent'
state_delta : keys=['raw_vm_telemetry']
text : [
{
"name": "demo-web-server",
"status": "RUNNING",
"zone": "us-central1-a",
"machineType": "e2-medium"
}
]
[Event 11] author='cloud_audit_hitl_remediation_workflow'
route : 'healthy'
state_delta : keys=['target_vm_name', 'target_vm_status', 'default_zone', 'audit_status', 'risk_level', 'remediation_reason', 'terminated_vms']
[Event 14] author='healthy_compliance_agent'
functionCall: run_gcloud_command(id='call_6266923', args={"command": "gcloud compute instances add-labels demo-web-server --zone=us-central1-a --labels=audit-status=compliant,risk-level=low --project=your-project-id --quiet", "project": "projects/your-project-id"})
[Event 16] author='healthy_compliance_agent'
functionCall: run_gcloud_command(id='call_4588866', args={"command": "gcloud compute disks add-labels demo-web-server --zone=us-central1-a --labels=audit-status=compliant,risk-level=low --project=your-project-id --quiet", "project": "projects/your-project-id"})
[Single-Request Completion] All VMs are RUNNING (healthy branch). No HITL interrupt was emitted!
==============================================================================
Final Persisted State in VertexAiSessionService (sessionId=7831896538115735552):
==============================================================================
target_project : your-project-id
default_zone : us-central1-a
target_vm_name : demo-web-server
target_vm_status : RUNNING
audit_status : compliant
risk_level : low
==============================================================================
Deploying to Cloud Run
examples/06-adk-workflow-hitl-cloud-run/deploy.sh provisions a dedicated least-privilege Service Account (gcloud-mcp-hitl-sa), grants only the required IAM roles (roles/aiplatform.user, roles/mcp.toolUser, and roles/compute.instanceAdmin.v1), and deploys the lean container (Dockerfile based on python:3.12-slim + uv, zero local Cloud SDK installation) to Cloud Run:
export GOOGLE_CLOUD_PROJECT="your-project-id"
export GOOGLE_CLOUD_AGENT_ENGINE_ID="4625340028699017216"
bash examples/06-adk-workflow-hitl-cloud-run/deploy.sh
Once deployed, pass your Cloud Run service URL to run_hitl_client.py:
uv run --project examples/06-adk-workflow-hitl-cloud-run \
examples/06-adk-workflow-hitl-cloud-run/run_hitl_client.py \
--url="https://gcloud-mcp-hitl-workflow-xxxxxxxxxx-uc.a.run.app" \
--decision=approve
What’s Next in Part 4?
Notice how Node 5A (remediation_executor_agent) in Part 3 used –async when starting the stopped VM (gcloud compute instances start … –async) so the https://cloudcli.googleapis.com/mcp HTTP request wouldn't time out, returning operation-1792903706164-65ea2ec85bbd5-b1dafbb7-8398a10d (STATUS: RUNNING).
But what if the underlying Compute Engine or GKE operation fails 20 seconds later due to a quota limit or zone stockout?
In Part 4: Handling & Verifying Long-Running gcloud Commands (–async) (examples/07-long-running-commands), we build the 3-Phase Async Submit → Poll → Verify pattern for both ADK Cyclic Workflows and Gemini Enterprise Managed Agents.
Google Cloud CLI Remote MCP Server (Part 3): Human-in-the-Loop (HITL) Cloud Remediation on Cloud… 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-3-human-in-the-loop-hitl-cloud-remediation-on-cloud-cb4b7bcee23f?source=rss—-e52cf94d98af—4
