Deploy a Pokémon TCG Agent with ADK

Build and deploy a Pokémon TCG AI agent on Cloud Run with CI/CD.

Introduction

30 Second Summary

Imagine you're a Pokémon card collector trying to find the perfect Fire-type EX card across thousands of options. Instead of scrolling through endless lists, you could just ask an AI assistant in plain English and get an instant answer.

In this project, you will build an AI agent that searches the Pokémon Trading Card Game database using natural language, deploy it to the cloud as a live service, and set up a CI/CD pipeline so every code change automatically ships a new version.

What You'll Build

A TypeScript AI agent powered by Google's Agent Development Kit (ADK) and Gemini that answers Pokémon TCG questions by calling a live card database API.

By the end of this project, you'll have:

  • A working ADK agent with two custom tools that search and retrieve Pokémon card data through natural language.
  • A containerized deployment on Cloud Run accessible via a public URL.
  • A CI/CD pipeline with Cloud Build that automatically redeploys your agent on every push to GitHub.
  • Secret Mission: A third tool that lets users browse and search Pokémon TCG sets by name or series.

Are there any prerequisites?

This project assumes you have Node.js 24.13+ and npm 11.8+ installed, a GCP project with billing enabled, the gcloud CLI installed and authenticated, and a GitHub account.

Set Up Your ADK Project

Every AI agent needs a foundation: a runtime, a framework, and credentials to call the model.

Google's Agent Development Kit (ADK) provides a TypeScript framework for building agents, while Vertex AI provides the Gemini model that powers reasoning.

In this step, you'll scaffold a Node.js project, install the ADK libraries, and configure your local environment to talk to Vertex AI.

In this step, get ready to:
  • Initialize a Node.js project and install ADK dependencies.
  • Enable the Vertex AI API and authenticate locally.
  • Create a .env file to configure Vertex AI settings.
Initialize your project and install dependencies

ADK for TypeScript requires Node.js 24.13+ and npm 11.8+. You'll start by creating a dedicated project folder and initializing it as an ES module.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir pokemon-tcg-agent
cd pokemon-tcg-agent
  • Confirm the folder exists by running:
ls ~/Desktop

You should see pokemon-tcg-agent listed in the output.

  • Initialize the project as an ES module by running these commands:
npm init --yes
npm pkg set type="module"
npm pkg set main="agent.ts"

What do these commands do?

  • npm init --yes creates a package.json with default settings.
  • type: "module" tells Node.js to use ES module imports (import/export) instead of CommonJS (require).
  • main: "agent.ts" tells ADK which file contains your agent definition.
  • Install the ADK framework, its CLI tools, and dotenv by running:
npm install @google/adk @google/adk-devtools dotenv

What are these packages?

  • @google/adk is the core ADK library with agent and tool classes.
  • @google/adk-devtools provides the ADK CLI for running and serving agents locally and in production.
  • dotenv loads environment variables from a .env file so you can configure Vertex AI settings locally.
  • Verify the install succeeded by checking that a node_modules folder was created:
ls node_modules/@google/adk

You should see a list of files and folders inside the ADK package.

Seeing permission errors or install failures?

  • Check your Node.js version with node --version. ADK requires 24.13.0 or higher.
  • If you see EACCES errors, avoid using sudo. Instead, fix npm permissions by following the npm docs.
  • Make sure you're inside the pokemon-tcg-agent folder before running npm install.

Help me troubleshoot my npm install for ADK

Enable the Vertex AI API and authenticate

Your agent will use Gemini through Vertex AI, which means your GCP project needs the Vertex AI API enabled and your local machine needs credentials to call it.

  • Enable the Vertex AI API by running:
gcloud services enable aiplatform.googleapis.com

You should see a confirmation message like Operation finished successfully. This may take a few seconds.

Seeing a permission denied error?

Make sure your gcloud CLI is authenticated to a project where you have Editor or Owner permissions. Run gcloud config get-value project to confirm you're targeting the right project.

Help me fix my gcloud permissions for enabling APIs

  • Set up application-default credentials by running:
gcloud auth application-default login

What happens next?

This command opens your browser and asks you to sign in with your Google account. After you approve, gcloud saves a credential file locally that the ADK library uses to call Vertex AI.

  • Sign in with the Google account that owns your GCP project.
  • Click Allow when prompted to grant access.

You should see Credentials saved to file in your terminal once authentication completes.

Create your environment configuration file

The ADK library reads environment variables to know where to send requests. A .env file keeps these settings out of your code and makes them easy to change.

  • Create a file called .env in your pokemon-tcg-agent folder and add the following content:
GOOGLE_GENAI_USE_VERTEXAI=TRUE
GOOGLE_CLOUD_PROJECT=[[GCP_PROJECT_ID="your-gcp-project-id"]]
GOOGLE_CLOUD_LOCATION=us-central1

What do these variables do?

  • GOOGLE_GENAI_USE_VERTEXAI=TRUE tells ADK to route requests through Vertex AI instead of the consumer Gemini API.
  • GOOGLE_CLOUD_PROJECT identifies which GCP project to bill usage against.
  • GOOGLE_CLOUD_LOCATION=us-central1 sets the region where Vertex AI processes your requests.

Find your GCP project ID

Not sure what your project ID is? Run gcloud config get-value project in your terminal. The output is the value you should paste into the GOOGLE_CLOUD_PROJECT line.

  • Save the .env file.
  • Confirm it exists by running:
cat .env

You should see your three environment variables printed to the terminal, with your real project ID on the second line.

Don't see the file or see an error?

  • Files starting with a dot are hidden by default. Run ls -a to see hidden files in your folder.
  • Make sure you're in the pokemon-tcg-agent folder, not your Desktop.

Help me find or create my .env file

✔️ Awesome, I've got everything!

Great. Make sure you've saved your .env file before moving on.

ⓧ I'd like to double check the full code

Here's what your .env file should look like:

GOOGLE_GENAI_USE_VERTEXAI=TRUE
GOOGLE_CLOUD_PROJECT=[[GCP_PROJECT_ID="your-gcp-project-id"]]
GOOGLE_CLOUD_LOCATION=us-central1

Your environment is ready. Next up, you'll build the Pokémon TCG agent itself with custom tools that call a live API.

Build the Pokémon TCG Agent

Your environment is configured and ready to go. Now it's time to build the agent itself.

An ADK agent needs two things: tools that give it capabilities, and an LlmAgent that orchestrates those tools using a language model. You'll create two FunctionTools that call the Pokémon TCG API, then wire them into a Gemini-powered agent.

In this step, get ready to:
  • Create a search_cards tool that queries the Pokémon TCG API for cards matching filters.
  • Create a get_card_details tool that fetches full information for a specific card by ID.
  • Define and export the rootAgent with Gemini 2.0 Flash and both tools attached.
Create the search and detail tools

FunctionTools in ADK wrap a function with a name, description, and parameter schema defined using Zod. ADK passes these definitions to Gemini so it knows what tools are available and how to call them.

What is a FunctionTool?

A FunctionTool has four parts: a name that Gemini references when choosing a tool, a description that tells the model when to use it, parameters defining the expected inputs, and an execute function that runs when Gemini decides to call it.

  • Create a new file called agent.ts in your pokemon-tcg-agent folder, then paste the following code:
import 'dotenv/config';
import { FunctionTool, LlmAgent } from '@google/adk';
import { z } from 'zod';

// First tool: search for cards by name, type, set, rarity, etc.
const searchCards = new FunctionTool({
  name: 'search_cards',
  description:
    'Search for Pokémon TCG cards by name, type, set, rarity, or other attributes. Returns a list of matching cards with basic info.',
  parameters: z.object({
    query: z
      .string()
      .describe(
        'A Pokémon TCG API query string. Examples: "name:charizard", "types:fire", "set.name:generations", "rarity:rare holo", "name:pikachu types:lightning". Multiple filters are combined with spaces (AND logic).'
      ),
    pageSize: z
      .number()
      .optional()
      .describe('Number of results to return. Defaults to 10. Max 250.'),
  }),
  execute: async ({ query, pageSize }) => {
    // Build the API URL with encoded query and page size
    const size = pageSize || 10;
    const url = `https://api.pokemontcg.io/v2/cards?q=${encodeURIComponent(query)}&pageSize=${size}`;
    try {
      const response = await fetch(url);
      if (!response.ok) {
        return {
          status: 'error',
          error_message: `API returned status ${response.status}`,
        };
      }
      const data = await response.json();
      // Extract only the fields the agent needs for a summary
      const cards = data.data.map((card: any) => ({
        id: card.id,
        name: card.name,
        supertype: card.supertype,
        subtypes: card.subtypes,
        hp: card.hp,
        types: card.types,
        set: card.set?.name,
        rarity: card.rarity,
        artist: card.artist,
      }));
      return { status: 'success', totalCount: data.totalCount, cards };
    } catch (error: any) {
      return { status: 'error', error_message: error.message };
    }
  },
});

What does this code do?

  • import 'dotenv/config' loads your .env variables (Vertex AI config) into the process automatically.
  • The parameters use Zod schemas to define what inputs Gemini can pass to the tool. The .describe() calls teach Gemini how to construct valid queries.
  • The execute function calls the Pokémon TCG API at https://api.pokemontcg.io/v2/cards and maps the response down to just the fields the agent needs for a summary.
  • Save agent.ts.
  • Confirm the file exists by running:
ls
  • You should see agent.ts listed alongside package.json and node_modules.

Don't see agent.ts?

  • Make sure you created the file inside your pokemon-tcg-agent folder, not a parent or child directory.
  • Check the file extension is .ts (not .js or .txt).

Help me find where my agent.ts file ended up.

Now add the second tool. The get_card_details tool fetches the full card record by ID, returning attacks, abilities, weaknesses, prices, and images.

  • In agent.ts, add the following code below the closing }); of searchCards:
const getCardDetails = new FunctionTool({
  name: 'get_card_details',
  description:
    'Get full details for a specific Pokémon TCG card by its ID. Returns attacks, abilities, weaknesses, resistances, prices, and images.',
  parameters: z.object({
    cardId: z
      .string()
      .describe('The unique card ID (e.g., "xy1-1", "base1-4", "g1-1").'),
  }),
  execute: async ({ cardId }) => {
    // Fetch a single card by its unique ID
    const url = `https://api.pokemontcg.io/v2/cards/${encodeURIComponent(cardId)}`;
    try {
      const response = await fetch(url);
      if (!response.ok) {
        return {
          status: 'error',
          error_message: `API returned status ${response.status}`,
        };
      }
      const data = await response.json();
      const card = data.data;
      // Return the full card details including gameplay and market info
      return {
        status: 'success',
        card: {
          id: card.id,
          name: card.name,
          supertype: card.supertype,
          subtypes: card.subtypes,
          hp: card.hp,
          types: card.types,
          evolvesFrom: card.evolvesFrom,
          evolvesTo: card.evolvesTo,
          abilities: card.abilities,
          attacks: card.attacks,
          weaknesses: card.weaknesses,
          resistances: card.resistances,
          retreatCost: card.retreatCost,
          set: {
            name: card.set?.name,
            series: card.set?.series,
            releaseDate: card.set?.releaseDate,
          },
          number: card.number,
          artist: card.artist,
          rarity: card.rarity,
          flavorText: card.flavorText,
          legalities: card.legalities,
          images: card.images,
          tcgplayer: card.tcgplayer,
        },
      };
    } catch (error: any) {
      return { status: 'error', error_message: error.message };
    }
  },
});

What does this code do?

  • This tool takes a single cardId parameter (like base1-4 for the original Charizard) and fetches the full card record from the API.
  • The response includes gameplay data (attacks, abilities, weaknesses) and market data (TCGPlayer prices). The agent uses this when a user wants to drill into a specific card.
  • Both tools return a status field so the agent knows whether the call succeeded or failed, and can report errors to the user gracefully.
  • Save agent.ts.

You won't see output yet. The next section defines the agent that uses both tools.

Define and export the root agent

The rootAgent is the entry point ADK looks for when it runs your project. It ties together the model, instructions for how to behave, and the tools it can call.

  • At the bottom of agent.ts, add the agent definition:
// Export the root agent that ADK will discover and run
export const rootAgent = new LlmAgent({
  name: 'pokemon_tcg_agent',
  model: 'gemini-2.0-flash',
  description:
    'An agent that helps users find and learn about Pokémon Trading Card Game cards.',
  instruction: `You are a knowledgeable Pokémon TCG assistant. You help users find cards, compare cards, and learn about Pokémon trading cards.

When a user asks about cards, use the search_cards tool with appropriate query syntax:
- Search by name: "name:charizard"
- Search by type: "types:fire"
- Search by set: "set.name:generations"
- Search by rarity: "rarity:rare holo"
- Search by HP range: "hp:[100 TO 200]"
- Combine filters: "name:charizard types:fire"
- Use wildcards: "name:char*"

When a user wants detailed information about a specific card, use get_card_details with the card ID.

Present card information in a clear, organized format. Include relevant details like attacks, abilities, and prices when available.`,
  tools: [searchCards, getCardDetails],
});

What does this code do?

  • model: 'gemini-2.0-flash' tells ADK to use Gemini 2.0 Flash via Vertex AI (configured by your .env variables).
  • The instruction is a system prompt that teaches Gemini the Pokémon TCG API query syntax. This lets Gemini construct valid queries like name:charizard types:fire from natural language requests.
  • The tools array registers both FunctionTools so Gemini can choose between searching for cards or fetching card details based on what the user asks.
  • The export keyword is essential. ADK discovers the agent by looking for a named export called rootAgent in your entry file.
  • Save agent.ts.

Your agent code is complete. You'll test it in the next step using the ADK web interface.

Seeing red squiggles in your editor?

  • TypeScript errors about zod imports are expected if your editor doesn't recognize the bundled types. The code will still run correctly because zod is included as a dependency of @google/adk.
  • If you see an error on the import lines, confirm your package.json has "type": "module" set from Step 1.

Help me fix import errors in my agent.ts file.

✔️ Awesome, I've got everything!

Double check you've saved agent.ts before moving on.

ⓧ I'd like to double check the full code

Here is the complete agent.ts file. Make sure yours matches this exactly.

import 'dotenv/config';
import { FunctionTool, LlmAgent } from '@google/adk';
import { z } from 'zod';

const searchCards = new FunctionTool({
  name: 'search_cards',
  description:
    'Search for Pokémon TCG cards by name, type, set, rarity, or other attributes. Returns a list of matching cards with basic info.',
  parameters: z.object({
    query: z
      .string()
      .describe(
        'A Pokémon TCG API query string. Examples: "name:charizard", "types:fire", "set.name:generations", "rarity:rare holo", "name:pikachu types:lightning". Multiple filters are combined with spaces (AND logic).'
      ),
    pageSize: z
      .number()
      .optional()
      .describe('Number of results to return. Defaults to 10. Max 250.'),
  }),
  execute: async ({ query, pageSize }) => {
    const size = pageSize || 10;
    const url = `https://api.pokemontcg.io/v2/cards?q=${encodeURIComponent(query)}&pageSize=${size}`;
    try {
      const response = await fetch(url);
      if (!response.ok) {
        return {
          status: 'error',
          error_message: `API returned status ${response.status}`,
        };
      }
      const data = await response.json();
      const cards = data.data.map((card: any) => ({
        id: card.id,
        name: card.name,
        supertype: card.supertype,
        subtypes: card.subtypes,
        hp: card.hp,
        types: card.types,
        set: card.set?.name,
        rarity: card.rarity,
        artist: card.artist,
      }));
      return { status: 'success', totalCount: data.totalCount, cards };
    } catch (error: any) {
      return { status: 'error', error_message: error.message };
    }
  },
});

const getCardDetails = new FunctionTool({
  name: 'get_card_details',
  description:
    'Get full details for a specific Pokémon TCG card by its ID. Returns attacks, abilities, weaknesses, resistances, prices, and images.',
  parameters: z.object({
    cardId: z
      .string()
      .describe('The unique card ID (e.g., "xy1-1", "base1-4", "g1-1").'),
  }),
  execute: async ({ cardId }) => {
    const url = `https://api.pokemontcg.io/v2/cards/${encodeURIComponent(cardId)}`;
    try {
      const response = await fetch(url);
      if (!response.ok) {
        return {
          status: 'error',
          error_message: `API returned status ${response.status}`,
        };
      }
      const data = await response.json();
      const card = data.data;
      return {
        status: 'success',
        card: {
          id: card.id,
          name: card.name,
          supertype: card.supertype,
          subtypes: card.subtypes,
          hp: card.hp,
          types: card.types,
          evolvesFrom: card.evolvesFrom,
          evolvesTo: card.evolvesTo,
          abilities: card.abilities,
          attacks: card.attacks,
          weaknesses: card.weaknesses,
          resistances: card.resistances,
          retreatCost: card.retreatCost,
          set: {
            name: card.set?.name,
            series: card.set?.series,
            releaseDate: card.set?.releaseDate,
          },
          number: card.number,
          artist: card.artist,
          rarity: card.rarity,
          flavorText: card.flavorText,
          legalities: card.legalities,
          images: card.images,
          tcgplayer: card.tcgplayer,
        },
      };
    } catch (error: any) {
      return { status: 'error', error_message: error.message };
    }
  },
});

export const rootAgent = new LlmAgent({
  name: 'pokemon_tcg_agent',
  model: 'gemini-2.0-flash',
  description:
    'An agent that helps users find and learn about Pokémon Trading Card Game cards.',
  instruction: `You are a knowledgeable Pokémon TCG assistant. You help users find cards, compare cards, and learn about Pokémon trading cards.

When a user asks about cards, use the search_cards tool with appropriate query syntax:
- Search by name: "name:charizard"
- Search by type: "types:fire"
- Search by set: "set.name:generations"
- Search by rarity: "rarity:rare holo"
- Search by HP range: "hp:[100 TO 200]"
- Combine filters: "name:charizard types:fire"
- Use wildcards: "name:char*"

When a user wants detailed information about a specific card, use get_card_details with the card ID.

Present card information in a clear, organized format. Include relevant details like attacks, abilities, and prices when available.`,
  tools: [searchCards, getCardDetails],
});

Your agent has its brain (Gemini) and its hands (two API tools). Next up, you'll start the ADK web interface and have a real conversation with your Pokémon TCG expert.

Test Your Agent Locally

You've built an agent with two tools and a set of instructions. Before deploying anything to the cloud, you need to confirm the agent actually works.

The ADK ships with a built-in web interface for testing agents interactively. You'll use it to send natural-language queries and watch your agent call the right tools with the right parameters.

In this step, get ready to:
  • Start the ADK web interface and verify the agent responds to card searches.
  • Test edge cases like nonexistent cards and filtered queries.
Start the ADK web interface

The ADK devtools package includes a local web UI that lets you chat with your agent in the browser. It loads your agent.ts file, connects to Vertex AI for the LLM, and displays tool calls in real time.

  • Launch the ADK web interface by running this command:
npx adk web
  • Open your browser and navigate to http://localhost:8000.

You should see the ADK web interface with a chat panel. Your agent pokemon_tcg_agent should appear as the active agent.

Not seeing the web interface?

Make sure you're running the command from inside your project folder (the one containing agent.ts).

Check that port 8000 is not already in use by another application.

If you see authentication errors, confirm your application-default credentials are set by running gcloud auth application-default login again.

Why can't my ADK agent connect to Vertex AI locally?

Now test the search_cards tool by sending a natural-language query.

  • Type Find Charizard cards into the chat input and press Enter.

The agent should call the search_cards tool with a query like name:charizard. You'll see a list of Charizard cards with names, sets, types, and rarities formatted in the response.

  • Now test the get_card_details tool by typing Get details on card base1-4 and pressing Enter.

The agent should call get_card_details with card ID base1-4 and return detailed information about Charizard from the Base Set. This includes attacks, weaknesses, retreat cost, and pricing data.

What is base1-4?

Card IDs in the Pokémon TCG API follow the format setcode-number. The ID base1-4 means card number 4 from the Base Set (set code base1), which is the original Charizard.

Test edge cases

A working happy path is great, but real users will push your agent in unexpected directions. Testing edge cases now saves debugging time after deployment.

  • Test a nonexistent card by typing Get details on card xyz-99999 and pressing Enter.

The agent should handle the error gracefully. The get_card_details tool returns an error status when the API responds with a 404, and the agent should communicate that the card was not found.

  • Test a filtered search by typing Find rare holo Fire-type cards and pressing Enter.

The agent should combine multiple filters in its query. Look for it to use something like types:fire rarity:rare holo when calling the search_cards tool.

  • Try one more query to check formatting. Type Show me Water-type EX cards from the XY series and press Enter.

Confirm the agent presents the results in a clear, organized format with card names, sets, and relevant details listed out.

What should I look for in the responses?

  • The agent calls the correct tool for each query type (search vs. details).
  • Error responses are handled gracefully with a human-readable message instead of a raw stack trace.
  • Multi-filter searches combine the filters correctly in the API query string.
  • Results include specific card details (names, types, sets) rather than generic summaries.

Agent not calling the tools?

If the agent responds with general knowledge instead of calling your tools, check that the tools array in your rootAgent definition includes both searchCards and getCardDetails.

Also verify your .env file has GOOGLE_GENAI_USE_VERTEXAI=TRUE (all uppercase TRUE). If the model connection fails, the agent cannot reason about which tools to use.

Help me debug why my ADK agent is not calling its FunctionTools

Your agent is confirmed working locally. Next up, you'll package it into a Docker container and deploy it to Cloud Run so anyone can access it over the internet.

Deploy to Cloud Run

Your agent works locally. Now it needs to be accessible from anywhere, not just your machine.

Cloud Run is Google's serverless container platform. You give it a container image, and it handles scaling, HTTPS, and infrastructure for you. In this step, you will package your agent into a Docker container and deploy it to Cloud Run as a public API.

In this step, get ready to:
  • Create a Dockerfile that packages your agent for production.
  • Deploy the container to Cloud Run with Vertex AI environment variables.
  • Test the deployed agent by sending it a request.
Create a Dockerfile

Cloud Run runs containers. A Dockerfile tells Docker how to build your container image: which base image to start from, which files to copy in, and which command to run when the container starts.

  • Create a new file called Dockerfile in your project root (the same folder as agent.ts) and paste the following code:
FROM node:24-slim

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .

EXPOSE 8080

CMD ["npx", "adk", "api_server", "--host", "0.0.0.0", "--port", "8080"]

What does this Dockerfile do?

  • FROM node:24-slim starts from a lightweight Node.js 24 image.
  • COPY package*.json and RUN npm ci install dependencies first. This layer gets cached, so rebuilds are faster when only your code changes.
  • COPY . . brings in your agent source code.
  • CMD starts the ADK API server on port 8080, bound to all interfaces (0.0.0.0) so Cloud Run can route traffic to it.
  • Save Dockerfile.
  • Confirm the file exists by running:
ls Dockerfile

You should see Dockerfile in the output.

Don't see the file?

Make sure you created the file in your project root folder, not inside a subfolder. The file must be named exactly Dockerfile with a capital D and no file extension.

Help me find where to put my Dockerfile

Enable APIs and grant permissions

Before deploying, Cloud Run and Cloud Build APIs need to be enabled in your GCP project. The deploy command uses Cloud Build behind the scenes to build your container image.

  • Enable the required APIs by running:
gcloud services enable run.googleapis.com cloudbuild.googleapis.com

This may take a minute to propagate.

Your Cloud Run service needs permission to call Vertex AI (Gemini). The default Compute Engine service account needs the Vertex AI User role.

  • Grant the Vertex AI User role to your default compute service account by running:
PROJECT_NUMBER=$(gcloud projects describe $(gcloud config get-value project) --format='value(projectNumber)')

gcloud projects add-iam-policy-binding $(gcloud config get-value project) \
  --member=serviceAccount:${PROJECT_NUMBER}-compute@developer.gserviceaccount.com \
  --role=roles/aiplatform.user

What does this command do?

The first line fetches your project number. The second line grants the roles/aiplatform.user role to the default Compute Engine service account. This allows your Cloud Run service to call Gemini through Vertex AI.

You should see the updated IAM policy printed in the terminal with the new binding listed.

Seeing a permission denied error?

Make sure your authenticated gcloud account has the Owner or IAM Admin role on the project. You can check with gcloud config get-value account to see which account is active.

Help me fix my IAM permissions

Deploy to Cloud Run and test the service

The gcloud run deploy --source . command auto-detects your Dockerfile, builds the image using Cloud Build, pushes it to Artifact Registry, and deploys it to Cloud Run.

  • Deploy your agent to Cloud Run by running:
gcloud run deploy pokemon-tcg-agent \
  --source . \
  --region us-central1 \
  --allow-unauthenticated \
  --set-env-vars=GOOGLE_GENAI_USE_VERTEXAI=TRUE,GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project),GOOGLE_CLOUD_LOCATION=us-central1

What does this command do?

  • --source . tells gcloud to build from the current directory's Dockerfile.
  • --region us-central1 deploys the service in the US Central region.
  • --allow-unauthenticated makes the service publicly accessible without authentication.
  • --set-env-vars passes the Vertex AI configuration to the container so the ADK library uses Vertex AI for Gemini calls.

This takes a few minutes

The first deploy builds the container image from scratch. You will see progress logs from Cloud Build. If prompted to create an Artifact Registry repository, enter y to confirm.

When the deploy finishes, you will see output like this:

Service [pokemon-tcg-agent] revision [...] has been deployed and is serving 100 percent of traffic.
Service URL: https://pokemon-tcg-agent-XXXXXXXX-uc.a.run.app
  • Copy the Service URL from the output. You will use it to test your agent.
  • Test your deployed agent by sending it a request with curl:
curl -X POST [[SERVICE_URL="your Cloud Run service URL from the output above"]]/run \
  -H "Content-Type: application/json" \
  -d '{
    "appName": "pokemon_tcg_agent",
    "userId": "test_user",
    "sessionId": "test_session",
    "newMessage": {
      "role": "user",
      "parts": [{"text": "Find me Charizard cards"}]
    }
  }'

You should see a JSON response containing Pokémon card data. The agent called the Pokémon TCG API and returned matching results.

Getting an error response?

  • If you see a 403 or permission error, the IAM role grant may not have propagated yet. Wait 1-2 minutes and try again.
  • If you see a 500 error, check the Cloud Run logs in the GCP console under Cloud Run then pokemon-tcg-agent then Logs.
  • If the curl command hangs, make sure you copied the full Service URL including https://.

Help me debug my Cloud Run deployment

✔️ Awesome, I've got everything!

Great. Double check you have saved your Dockerfile and confirmed the deploy succeeded with a Service URL.

ⓧ I'd like to double check the full code

Here is the complete Dockerfile:

FROM node:24-slim

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .

EXPOSE 8080

CMD ["npx", "adk", "api_server", "--host", "0.0.0.0", "--port", "8080"]

Your agent is now live on the internet. Anyone with the URL can send it Pokémon card queries. Next up, you will push your code to GitHub and prepare for automated deployments.

Push Your Code to GitHub

Your agent is deployed and working on Cloud Run, but right now the code only lives on your local machine. To set up automated deployments in the next step, your code needs to live in a GitHub repository that Cloud Build can watch for changes.

You will also create an Artifact Registry repository. This is where your CI/CD pipeline will store the Docker images it builds before deploying them to Cloud Run.

In this step, get ready to:
  • Create ignore files and initialize a Git repository with your first commit.
  • Push your code to a new GitHub repository.
  • Create an Artifact Registry Docker repository for your CI/CD pipeline.
Create ignore files and initialize your Git repository

Before committing code, you need to tell Git and Docker which files to exclude. Your .env file contains project configuration that should stay local, and node_modules/ is too large to push to a repository.

  • Create a .gitignore file in your project folder by running this command:
cat << 'EOF' > .gitignore
node_modules/
.env
EOF

What does this do?

The .gitignore file tells Git to skip tracking node_modules/ (installed packages) and .env (your environment variables). Anyone who clones the repo will run npm ci to install dependencies themselves.

  • Create a .dockerignore file by running this command:
cat << 'EOF' > .dockerignore
node_modules
.env
.git
.gitignore
EOF

Why a separate .dockerignore?

When Docker builds your image, it copies your project folder into the container. The .dockerignore file prevents unnecessary files like .git history and local node_modules from bloating your container image.

  • Confirm both files exist by running:
ls -a

You should see .gitignore and .dockerignore listed alongside your existing files.

  • Initialize a Git repository and make your first commit by running these commands:
git init
git add .
git commit -m "Initial commit: Pokemon TCG ADK agent"

What do these commands do?

  • git init creates a new Git repository in your project folder.
  • git add . stages all files (except those in .gitignore) for the commit.
  • git commit saves a snapshot of your project at this point.

You should see output listing the files committed, including agent.ts, Dockerfile, .gitignore, .dockerignore, and package.json.

Seeing 'Author identity unknown'?

Git needs your name and email for commits. Run these two commands, replacing the placeholder values with your own:

git config --global user.email "your-email@example.com"
git config --global user.name "Your Name"

Then re-run the git commit command. Help me fix my git commit error.

Create a GitHub repository and push your code
  • Open your browser and go to github.com/new.
  • Enter pokemon-tcg-agent in the **Repository name** field.
  • Leave the repository set to **Public**.
  • Do NOT check any boxes for README, .gitignore, or license (your repo already has files).
  • Click **Create repository**.

GitHub shows a quick setup page with commands for pushing an existing repository.

  • Back in your terminal, connect your local repository to GitHub and push your code by running these commands:
git remote add origin [[YOUR_REPO_URL="https://github.com/YOUR_USERNAME/pokemon-tcg-agent.git"]]
git branch -M main
git push -u origin main

What do these commands do?

  • git remote add origin tells your local repo where the remote GitHub repo lives.
  • git branch -M main renames your default branch to main.
  • git push -u origin main uploads your code to GitHub and sets main as the default remote branch.
  • Refresh your GitHub repository page in the browser.

You should see all your project files listed: agent.ts, Dockerfile, .dockerignore, .gitignore, and package.json.

Push rejected or authentication failed?

GitHub no longer accepts passwords for Git operations. If prompted for credentials, you need a Personal Access Token or SSH key. Check that you copied the repository URL exactly from the GitHub quick setup page.

Help me push my code to GitHub.

Create an Artifact Registry repository

Your CI/CD pipeline will build a Docker image every time you push code. That image needs a place to live before Cloud Run can deploy it. Artifact Registry is Google Cloud's container registry for storing Docker images.

  • Enable the Artifact Registry API by running this command:
gcloud services enable artifactregistry.googleapis.com
  • Create a Docker repository in Artifact Registry by running this command:
gcloud artifacts repositories create pokemon-tcg-agent \
  --repository-format=docker \
  --location=us-central1 \
  --description="Docker images for Pokemon TCG agent"

What does this command do?

This creates a Docker-format repository called pokemon-tcg-agent in the us-central1 region. In the next step, your Cloud Build pipeline will push container images here before deploying them to Cloud Run.

  • Verify the repository was created by running:
gcloud artifacts repositories list --location=us-central1

You should see pokemon-tcg-agent listed with format DOCKER and location us-central1.

Seeing 'Permission denied' or 'API not enabled'?

Make sure the Artifact Registry API enable command finished successfully before running the create command. If you see a permissions error, confirm you are authenticated with an account that has Owner or Editor access to your GCP project.

Help me fix my Artifact Registry permission error.

Your code is now on GitHub and your image registry is ready. Next up, you will wire everything together with a Cloud Build CI/CD pipeline that automatically deploys your agent whenever you push new code.

Set Up Cloud Build CI/CD

Your agent is live on Cloud Run and your code is on GitHub. Right now, every time you change your agent, you'd have to manually rebuild and redeploy it.

A CI/CD pipeline eliminates that manual work. In this step, you'll configure Cloud Build to automatically build, push, and deploy your agent every time you push code to GitHub.

In this step, get ready to:
  • Write a Cloud Build configuration file that builds, pushes, and deploys your agent.
  • Connect Cloud Build to your GitHub repository and create a push trigger.
  • Test the full pipeline by pushing a code change and verifying automatic deployment.
Write the Cloud Build configuration

Cloud Build needs a configuration file that tells it exactly what to do when a build is triggered. Your pipeline has three jobs: build the Docker image, push it to Artifact Registry, and deploy the new image to Cloud Run.

Why explicit build steps instead of --source?

In Step 4, you used gcloud run deploy --source . which handles everything in one command. That approach works for manual deploys, but using it inside Cloud Build would create a nested build (Cloud Build triggering another Cloud Build).

Explicit build/push/deploy steps give you full control over each stage of the pipeline. This is the standard CI/CD pattern for Cloud Run.

  • Create a file called cloudbuild.yaml in your project root (the same folder as agent.ts) and paste the following configuration:
steps:
  - name: 'gcr.io/cloud-builders/docker'
    args:
      - 'build'
      - '-t'
      - '$_REGION-docker.pkg.dev/$PROJECT_ID/pokemon-tcg-agent/agent:$COMMIT_SHA'
      - '.'

  - name: 'gcr.io/cloud-builders/docker'
    args:
      - 'push'
      - '$_REGION-docker.pkg.dev/$PROJECT_ID/pokemon-tcg-agent/agent:$COMMIT_SHA'

  - name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
    entrypoint: 'gcloud'
    args:
      - 'run'
      - 'deploy'
      - 'pokemon-tcg-agent'
      - '--image=$_REGION-docker.pkg.dev/$PROJECT_ID/pokemon-tcg-agent/agent:$COMMIT_SHA'
      - '--region=$_REGION'
      - '--allow-unauthenticated'
      - '--set-env-vars=GOOGLE_CLOUD_PROJECT=$PROJECT_ID,GOOGLE_CLOUD_LOCATION=$_REGION,GOOGLE_GENAI_USE_VERTEXAI=TRUE'

substitutions:
  _REGION: us-central1

images:
  - '$_REGION-docker.pkg.dev/$PROJECT_ID/pokemon-tcg-agent/agent:$COMMIT_SHA'

What does this configuration do?

  • Step 1 builds a Docker image and tags it with the git commit SHA. This ensures every build produces a uniquely identifiable image.
  • Step 2 pushes that image to your Artifact Registry repository.
  • Step 3 deploys the new image to Cloud Run with the same environment variables your agent needs for Vertex AI.
  • $PROJECT_ID and $COMMIT_SHA are built-in Cloud Build substitutions that resolve automatically at build time. $_REGION is a custom substitution you define in the substitutions section.
  • Save cloudbuild.yaml.

YAML indentation issues?

YAML is whitespace-sensitive. Make sure you use spaces (not tabs) and that each args list is indented consistently under its name field. If you see parsing errors later, double-check the indentation of every line.

Help me fix a YAML indentation error in my cloudbuild.yaml

Connect Cloud Build to GitHub and create a trigger

Cloud Build needs permission to deploy to Cloud Run and push images to Artifact Registry. You'll grant those permissions first, then connect your GitHub repository.

  • Get your project number (needed for the service account email) by running:
PROJECT_ID=$(gcloud config get-value project)
PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)')
echo $PROJECT_NUMBER
  • You should see a number like 123456789012 printed in your terminal. This is your project number.
  • Grant the Cloud Build service account the Cloud Run Admin role by running:
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:$PROJECT_NUMBER@cloudbuild.gserviceaccount.com" \
  --role="roles/run.admin"
  • Grant the Artifact Registry Writer role so Cloud Build can push Docker images by running:
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:$PROJECT_NUMBER@cloudbuild.gserviceaccount.com" \
  --role="roles/artifactregistry.writer"
  • Grant the Service Account User role so Cloud Build can act as the Compute Engine service account when deploying to Cloud Run:
gcloud iam service-accounts add-iam-policy-binding \
  $PROJECT_NUMBER-compute@developer.gserviceaccount.com \
  --member="serviceAccount:$PROJECT_NUMBER@cloudbuild.gserviceaccount.com" \
  --role="roles/iam.serviceAccountUser"

Why three separate roles?

Cloud Build's service account is like a robot that runs your pipeline. It needs explicit permission for each action: deploying services (Cloud Run Admin), pushing images (Artifact Registry Writer), and impersonating the compute service account that Cloud Run uses (Service Account User).

Now connect your GitHub repository to Cloud Build through the GCP Console.

  • Open the GCP Console in your browser and navigate to Cloud Build.
  • Click Repositories in the left sidebar.
  • Switch to the 1st gen tab, then click Connect repository.
  • Select GitHub (Cloud Build GitHub App) as the source code management provider.
  • Click Continue.

GitHub authorization prompts

You may be asked to authorize the Google Cloud Build app on GitHub and install it on your account. Click Authorize Google Cloud Build if prompted, then Install to add the app to your GitHub account.

  • Select your GitHub account and choose the repository you created for this project (e.g., pokemon-tcg-agent).
  • Check the consent checkbox and click Connect.
  • Click Done to finish the connection.

Now create the trigger that fires when you push to main.

  • Click Triggers in the left sidebar.
  • Click Create trigger.
  • Enter deploy-pokemon-tcg-agent in the Name field.
  • Set Region to us-central1.
  • Under Event, select Push to a branch.
  • Under Source, select your connected GitHub repository.
  • Set the Branch field to ^main$ (this regex matches only the main branch).
  • Under Configuration, select Cloud Build configuration file (yaml or json).
  • Leave the file location as /cloudbuild.yaml (the default).
  • Click Create.

You should see your new trigger listed on the Triggers page.

Don't see your repository in the source list?

Go back to Repositories and confirm the connection shows as active. If not, click Connect repository again. Make sure the Cloud Build GitHub App is installed on the correct GitHub account and has access to your repository.

Help me connect my GitHub repository to Cloud Build

Test the CI/CD pipeline

The trigger is set but it hasn't fired yet because there hasn't been a push since you created it. You'll make a small change to your agent, push it to GitHub, and watch the whole pipeline run automatically.

  • In agent.ts, find this line at the start of the agent's instruction:
instruction: `You are a knowledgeable Pokémon TCG assistant. You help users find cards, compare cards, and learn about Pokémon trading cards.
  • Add a sentence to the end of that first line so it reads:
instruction: `You are a knowledgeable Pokémon TCG assistant. You help users find cards, compare cards, and learn about Pokémon trading cards. Always greet the user by saying "Welcome, Trainer!" at the start of each conversation.
  • Save agent.ts.
  • Commit the cloudbuild.yaml and the agent change, then push to GitHub by running:
git add .
git commit -m "Add cloudbuild.yaml and update agent greeting"
git push origin main

The push to main triggers your Cloud Build pipeline automatically.

  • Go back to the GCP Console and navigate to Cloud Build, then click History in the left sidebar.
  • You should see a build in progress (or recently completed with a green checkmark). Click on it to view the build steps.

How long does the build take?

The first build takes 2-4 minutes because it needs to pull the node:24-slim base image and install all dependencies. Subsequent builds benefit from layer caching and are faster.

  • Once the build shows a green checkmark, verify the new deployment by visiting your Cloud Run service URL in a new browser tab.
  • Find your service URL by running:
gcloud run services describe pokemon-tcg-agent --region=us-central1 --format='value(status.url)'
  • Open that URL in your browser and test the agent. When you start a conversation, the agent should now greet you with "Welcome, Trainer!" thanks to your instruction change.

Build failed with a permissions error?

If you see errors about missing permissions (like "Permission denied" or "does not have run.services.update"), the IAM role grants may not have propagated yet. Wait 1-2 minutes and retry the build by clicking Retry in the build details.

If the error persists, re-run the three gcloud projects add-iam-policy-binding commands from earlier and confirm the project number is correct.

Help me fix Cloud Build permissions errors when deploying to Cloud Run

✔️ Awesome, I've got everything!

Great. Double check you've saved cloudbuild.yaml and that your pipeline completed successfully with a green checkmark in Cloud Build History.

ⓧ I'd like to double check the full code

Here is the complete cloudbuild.yaml file for comparison:

steps:
  - name: 'gcr.io/cloud-builders/docker'
    args:
      - 'build'
      - '-t'
      - '$_REGION-docker.pkg.dev/$PROJECT_ID/pokemon-tcg-agent/agent:$COMMIT_SHA'
      - '.'

  - name: 'gcr.io/cloud-builders/docker'
    args:
      - 'push'
      - '$_REGION-docker.pkg.dev/$PROJECT_ID/pokemon-tcg-agent/agent:$COMMIT_SHA'

  - name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
    entrypoint: 'gcloud'
    args:
      - 'run'
      - 'deploy'
      - 'pokemon-tcg-agent'
      - '--image=$_REGION-docker.pkg.dev/$PROJECT_ID/pokemon-tcg-agent/agent:$COMMIT_SHA'
      - '--region=$_REGION'
      - '--allow-unauthenticated'
      - '--set-env-vars=GOOGLE_CLOUD_PROJECT=$PROJECT_ID,GOOGLE_CLOUD_LOCATION=$_REGION,GOOGLE_GENAI_USE_VERTEXAI=TRUE'

substitutions:
  _REGION: us-central1

images:
  - '$_REGION-docker.pkg.dev/$PROJECT_ID/pokemon-tcg-agent/agent:$COMMIT_SHA'

Secret mission

Add a Set Browser Tool

Your agent can search cards and get card details. But what if a user wants to browse card sets? Add a third tool that queries the Pokémon TCG API sets endpoint, letting users explore available sets by name or series.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project uses GCP free tier resources. Cloud Run scales to zero when idle, Artifact Registry is free under 500MB, and Cloud Build includes 2,500 free build-minutes per month.

Resources you used:

  • Cloud Run service (pokemon-tcg-agent).
  • Artifact Registry Docker repository (pokemon-tcg-agent).
  • Cloud Build trigger connected to your GitHub repository.
  • GitHub repository with your agent code.
  • Vertex AI API enabled in your GCP project.
  • Local project folder on your Desktop.

Keep everything running

No action needed. Choose this if you want your agent to stay live and accessible via its Cloud Run URL.

  • Your Cloud Run service scales to zero when idle, so it costs nothing when no one is using it.
  • Cloud Build will continue deploying automatically when you push to GitHub.
  • All resources stay within GCP free tier limits under normal usage.

Pause - I'll come back to this later

Delete the Cloud Build trigger to stop automatic deployments while keeping your agent live on Cloud Run.

  • Open the GCP Console and navigate to Cloud Build.
  • Click Triggers in the sidebar.
  • Find your trigger, click the three-dot menu, and select Delete.
  • Your Cloud Run service will remain accessible, but new pushes to GitHub will no longer trigger a deployment.

Delete - I don't want to use this again

Remove all project resources and start fresh. Run the following commands and console actions to clean up completely.

  • Delete the Cloud Run service by running:
gcloud run services delete pokemon-tcg-agent --region=us-central1
  • Delete the Artifact Registry repository by running:
gcloud artifacts repositories delete pokemon-tcg-agent --location=us-central1
  • Delete the Cloud Build trigger from the GCP Console under Cloud Build > Triggers.
  • Optionally delete your GitHub repository if you no longer need it.
  • Delete the local project folder from your Desktop.

That's a Wrap!

That's a Wrap!

Nice work! You just built a production AI agent from scratch and shipped it with a full CI/CD pipeline.

You've learned how to:

  • Built a TypeScript ADK agent with custom FunctionTools that call the Pokémon TCG REST API, powered by Vertex AI and Gemini 2.0 Flash.
  • Deployed the agent to Cloud Run as a containerized service with a public URL anyone can access.
  • Set up a complete CI/CD pipeline with Cloud Build that auto-deploys a new version on every push to GitHub.
  • Secret Mission: Added a set browser tool and verified CI/CD deployed the update automatically.

Ready to quiz yourself?