A2A + MCP (Google & Asana) + Vertex AI + BigQuery + Asana
Live demo for Google Cloud Next
Author: Kevin Blanco | Senior Developer Advocate, Asana | Google Developer Expert (Cloud & AI/ML)
Enterprise have years of historical project data and PMOs are not taking advatage of it when evaluating a new project request. This PMO Intelligence Engine closes that gap.
When a PMO analyst submits a project request through an Asana intake form, a multi-agent AI pipeline activates within seconds:
- A webhook fires the moment the task is created in Asana
- An ADK orchestrator on Cloud Run dispatches to four specialist agents via A2A (Agent-to-Agent protocol)
- Two agents query BigQuery through Google's managed MCP server for historical patterns and resource data
- One agent queries the live Asana Work Graph through Asana's MCP server
- A risk scorer synthesizes everything using Gemini 2.0 Flash on Vertex AI
- The orchestrator writes enriched intelligence back to the Asana task via REST API
The PMO reviewer opens the task and has everything they need to make a data-driven decision — no spreadsheets, no BigQuery queries, no Slack threads.
| Technology | Role |
|---|---|
| Google ADK | Agent runtime for all 5 Cloud Run services |
| A2A (Agent-to-Agent) | Protocol for orchestrator ↔ specialist agent communication |
| MCP — Google-managed | BigQuery data access for BQ Analyst + Resource Advisor |
| MCP — Asana-managed | Live Work Graph access for Asana Context agent |
| Vertex AI / Gemini 2.0 Flash | LLM for all agent reasoning |
| BigQuery | Historical project outcomes, resource capacity, company OKRs |
| Cloud Run | Hosts all 5 services (serverless containers) |
| Asana Python SDK | All Asana REST API calls (task fetch, field updates, comments, webhook registration) |
| Asana | Human interface (intake form → task) + live data source (MCP) |
| Secret Manager | Credentials at runtime — no secrets in source |
- Google Cloud project with billing enabled
gcloudCLI authenticated (gcloud auth application-default login)- Python 3.12+
- Asana account with admin access to a workspace
Optional:
- Docker (for local builds)
brew install graphviz(macOS) — for diagram generation
git clone https://github.com/kevinblanco/pmo-intelligence-engine
cd pmo-intelligence-engine
cp .env.example .envLeave .env open — you'll fill in values as you collect them in the steps below.
You need three things from Asana before provisioning any GCP infrastructure.
- Go to app.asana.com → click your profile photo → My Settings
- Open the Apps tab → View Developer Console
- Click + Create New Token in the Personal Access Tokens section, give it a name, copy the token
- Add to
.env:ASANA_PAT=your-token-here
- Open your New Project Requests project in Asana (create it first if it doesn't exist — see
asana/setup_guide.mdfor steps on how to configure your project for best results) - Look at the URL:
https://app.asana.com/0/<PROJECT_GID>/... - Copy the numeric ID and add to
.env:ASANA_PROJECT_GID=1234567890123456
The Asana Context agent connects to the live Asana Work Graph via MCP. This requires a separate OAuth app — it is not the same as your PAT.
- Go to app.asana.com/0/my-apps → + Create new app and select MCP App as the App type
- Under OAuth, add
http://localhost:8888/callbackto the redirect URL allowlist - Under Manage Distribution, add your workspace (or set to Any workspace) — without this the token exchange fails with "This app is not available to your Asana workspace or organization"
- Copy the Client ID and Client Secret, then add to
.env(we will securely store these in GCP's Secret Manager in the next step):ASANA_MCP_CLIENT_ID=your-client-id ASANA_MCP_CLIENT_SECRET=your-client-secret
export GCP_PROJECT_ID=your-project-id
export GCP_REGION=us-central1
bash infra/setup.shThis enables all required APIs, creates the Artifact Registry repo, service account, IAM bindings, and Secret Manager placeholder secrets.
Also add GCP_PROJECT_ID and GCP_REGION to your .env file.
infra/setup.sh creates secrets with placeholder values. Replace them with your real credentials now:
# Asana PAT (from Step 2a)
echo -n "your-asana-pat" | gcloud secrets versions add asana-pat --data-file=-
# Asana MCP OAuth credentials (from Step 2c)
echo -n "your-mcp-client-id" | gcloud secrets versions add asana-mcp-client-id --data-file=-
echo -n "your-mcp-client-secret" | gcloud secrets versions add asana-mcp-client-secret --data-file=-Verify a secret was stored correctly:
gcloud secrets versions access latest --secret=asana-pat
asana-webhook-secretis written automatically during the webhook handshake (webhook_register.py) — do not set it manually.
asana-mcp-access-token,asana-mcp-refresh-token,asana-mcp-token-expiryare written automatically byasana/mcp_auth_setup.py— do not set them manually.
pip install google-cloud-bigquery asana google-cloud-secret-manager httpx python-dotenv
python3 bigquery/seed_data.pyCreates the pmo_intelligence dataset with 150 historical projects, 40 resource allocation records, and 12 company OKRs. If you already have workloads running in BigQuery and want this tool to use that instead, this step is not needed, this is for test/demo porpuse only
This one-time OAuth flow stores the MCP access token in Secret Manager so the Asana Context agent can connect at runtime:
python3 asana/mcp_auth_setup.pyA browser window opens for you to authorize the app. After approval it writes the token to Secret Manager automatically.
bash deploy.shDeploys all 5 services in the correct order, captures service URLs, and injects them as environment variables.
python3 asana/webhook_register.pyRegisters the webhook against the "New Project Requests" project. The script blocks while Asana completes the handshake with the webhook receiver (up to ~30 seconds), then prints the webhook GID on success.
Submit the Asana intake form with:
- Project Type: Infrastructure
- Budget Range: $500K–$2M
- Requestor Team: Engineering
Watch Cloud Logging stream real-time agent activity. The task enriches within ~30 seconds with a Risk Score: 7 and Recommendation: Flag for Review.
The architecture diagram is version-controlled as Python code:
python3 -m pip install -r diagrams/requirements.txt
python3 diagrams/architecture.py
# Output: pmo_intelligence_engine.pngpmo-intelligence-engine/
├── diagrams/ # Architecture-as-code (diagrams library)
├── infra/ # GCP provisioning script
├── bigquery/ # Schema DDL + synthetic seed data
├── asana/ # Setup guide, webhook registration, MCP OAuth
├── agents/
│ ├── orchestrator/ # ADK orchestrator + A2A client + Asana REST write-back
│ ├── bigquery_analyst/ # ADK + BigQuery MCP (historical patterns)
│ ├── risk_scorer/ # ADK + Gemini (risk 1–10, recommendation)
│ ├── resource_advisor/ # ADK + BigQuery MCP (capacity + OKR alignment)
│ └── asana_context/ # ADK + Asana MCP (live Work Graph)
├── webhook_receiver/ # FastAPI: handshake + HMAC + heartbeat + dispatch
└── deploy.sh # Cloud Run deployment (all 5 services, ordered)
The webhook-receiver can reach Asana but the PAT is rejected. Two causes:
1. Secret Manager still has the placeholder value.
infra/setup.sh creates secrets with the value "placeholder". You must update them before deploying (see Step 2b above). To check:
gcloud secrets versions access latest --secret=asana-patIf it prints placeholder, update it:
echo -n "your-real-asana-pat" | gcloud secrets versions add asana-pat --data-file=-2. The running container didn't pick up the new secret.
Cloud Run injects --set-secrets as environment variables at container start time, not on every request. If you updated the secret after the container started, it still has the old value in memory. Force a new revision:
gcloud run services update webhook-receiver \
--region us-central1 \
--update-env-vars "GCP_PROJECT_ID=$(gcloud config get-value project)"
⚠️ Use--update-env-vars(not--set-env-vars) —--set-env-varsreplaces all environment variables, which will wipeORCHESTRATOR_URLand break dispatch to the orchestrator.
The webhook-receiver is reaching the orchestrator but the request is rejected because it has no authentication token. Cloud Run services deployed with --no-allow-unauthenticated require every caller to present a Google-signed OIDC identity token.
This is handled automatically in code — the webhook-receiver fetches an ID token from the GCE metadata server before each orchestrator call. If you see this error it usually means you're running an older container image that predates this fix. Redeploy the webhook-receiver:
gcloud run deploy webhook-receiver \
--source ./webhook_receiver \
--region us-central1 \
--quietIf ORCHESTRATOR_URL gets wiped during the redeploy, re-inject it without touching other vars:
ORCHESTRATOR_URL=$(gcloud run services describe orchestrator \
--region us-central1 --format "value(status.url)")
gcloud run services update webhook-receiver \
--region us-central1 \
--update-env-vars "ORCHESTRATOR_URL=${ORCHESTRATOR_URL}"Two known causes:
- Redirect URI mismatch:
http://localhost:8888/callbackmust be in your app's OAuth allowlist. Go to app.asana.com/0/my-apps → your app → OAuth → add the redirect URL. - Workspace not authorized: Under Manage Distribution, add your workspace or set to "Any workspace." Without this, Asana returns "This app is not available to your Asana workspace or organization."
macOS system Python 3.9 has a namespace package bug with google.* libraries when the script directory is on sys.path. Fix: use a virtual environment.
python3 -m venv .venv
source .venv/bin/activate
pip install google-cloud-bigquery asana google-cloud-secret-manager httpx python-dotenv
python3 bigquery/seed_data.pyApplication Default Credentials are not configured. Run:
gcloud auth application-default login- Verify the webhook-receiver is running:
curl $WEBHOOK_RECEIVER_URL/health - The health response must show
"webhook_secret_configured": false(expected before first registration). If it shows"status": "unreachable", the service isn't deployed. - Re-run:
python3 asana/webhook_register.py
Apache 2.0 — see LICENSE
