This notebook demonstrates how to build an intelligent AI agent using Google’s Agent Development Kit (ADK) that can interact with a Couchbase database. The key to this interaction is the Model Context Protocol (MCP), which allows the AI agent to seamlessly connect to and query Couchbase using natural language.
What You’ll Build
By the end of this tutorial, you will have an AI agent that can:
- Connect to a Couchbase cluster and explore its structure
- Discover buckets, scopes, collections, and document schemas
- Run SQL++ queries through natural language conversations
- Analyze query performance and get index recommendations
What is the Model Context Protocol (MCP)?
그리고 Model Context Protocol (MCP) is an open standard that standardizes how AI assistants connect to external data sources and tools. Think of it as a universal adapter between AI models and the systems they need to interact with.
How MCP Works:
- MCP 서버 — A lightweight program that exposes tools from an external system (in our case, Couchbase). The 쿠치베이스 MCP 서버 provides tools for querying, exploring schemas, checking cluster health, and more.
- MCP Client — An application that connects to the MCP server and makes its tools available to an AI model. Google ADK’s
McpToolsetacts as the MCP client in this tutorial. - Communication — The client and server communicate over stdio (standard input/output) when running locally. The MCP server runs as a subprocess, and ADK handles all the communication automatically.
This architecture means your agent doesn’t need the Couchbase SDK installed — the MCP server handles all database communication behind the scenes.
What is Google ADK?
Google’s Agent Development Kit (ADK) is a framework for building AI agents powered by Gemini models. It provides:
- LlmAgent — The core agent class that takes user queries, decides which tools to call, and generates natural language responses.
- McpToolset — A toolset that connects to any MCP server, automatically discovering all available tools.
- Runner — Manages the agent’s execution loop, handling the back-and-forth between the LLM and tools.
- Built-in Web UI — A local web interface for testing agents interactively.
Get Credentials for Google Gemini
You’ll need a Google Gemini API key to power the AI agent. Get one for free from Google AI Studio.
Create and Deploy Your Free Tier Operational Cluster on Capella
To get started with Couchbase Capella, create an account and use it to deploy a forever free tier operational cluster. This account provides you with an environment where you can explore and learn about Capella with no time constraint.
To learn more, please follow the instructions.
Couchbase Capella Configuration
When running Couchbase using 카펠라, the following prerequisites need to be met:
- Create the database credentials to access the required bucket (Read and Write) used in the application.
- Allow access to the Cluster from the IP on which the application is running.
- Your Capella free-tier account includes a
travel-samplebucket, with sample documents used for booking and travel purposes. You can find more information 여기.
Setup Instructions
Before running this notebook, ensure you have the following prerequisites met:
-
Set Environment Variables: This notebook loads the Gemini API key and Couchbase credentials from a
.envfile. Include the following:GOOGLE_GENAI_API_KEY=your_gemini_api_key_here CB_CONNECTION_STRING=your_couchbase_connection_string CB_USERNAME=your_couchbase_username CB_PASSWORD=your_couchbase_passwordWe have already included a
.env.samplefile. Rename it to.envand fill in the values. -
Setup uv:
uvis a modern and fast Python package manager. We use it to run the MCP server without manual installation. Install uv from 여기. -
Python Libraries: Install the necessary libraries by running the code cell below.
%pip install -q 'google-adk>=1.18.0' 'python-dotenv>=1.0.0'
Note: you may need to restart the kernel to use updated packages.
Importing Libraries and Loading Environment Variables
Let’s import everything we need:
dotenv— Loads API keys and database credentials from a.envfile so we don’t hardcode secrets.LlmAgent— The main agent class from Google ADK. It takes a model, instructions, and tools, and handles the reasoning loop.Runner— Executes the agent, managing the conversation flow and tool calls.InMemorySessionService— Stores conversation history in memory (for this tutorial; production apps would use persistent storage).McpToolset그리고StdioConnectionParams— Connect to the Couchbase MCP Server over stdio.StdioServerParameters— Configures how the MCP server subprocess is launched.types— Google GenAI types for constructing messages.
import os
import logging
import warnings
from dotenv import load_dotenv
from google.adk.agents.llm_agent import LlmAgent
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.adk.tools.mcp_tool import McpToolset, StdioConnectionParams
from google.genai import types
from mcp import StdioServerParameters
# Suppress noisy logs from MCP subprocess and SDK internals
logging.getLogger("couchbase").setLevel(logging.ERROR)
logging.getLogger("mcp").setLevel(logging.ERROR)
logging.getLogger("google.genai").setLevel(logging.ERROR)
# The google-genai SDK emits "non-text parts in the response" via this
# logger (underscore, not dot) when the ADK runner accesses .text on a
# multi-part response containing function_call parts. See:
# https://github.com/googleapis/python-genai/issues/850
logging.getLogger("google_genai.types").setLevel(logging.ERROR)
warnings.filterwarnings("ignore")
# Ensure common tool paths are in PATH (Jupyter kernels often have a
# restricted PATH that doesn't include ~/.local/bin where uv/uvx lives)
_extra_paths = os.path.expanduser("~/.local/bin") + ":/opt/homebrew/bin:/usr/local/bin"
os.environ["PATH"] = os.environ.get("PATH", "/usr/bin") + ":" + _extra_paths
# Load environment variables from .env file
load_dotenv()
True
Loading Couchbase Credentials
We read the Couchbase connection details from environment variables. These will be passed to the MCP server so it can connect to your cluster.
Important: Environment variables loaded via dotenv in this notebook do NOT automatically propagate to the MCP server subprocess. That’s why we read them here and explicitly pass them in the env dict when configuring the MCP connection later.
CB_CONNECTION_STRING = os.getenv("CB_CONNECTION_STRING")
CB_USERNAME = os.getenv("CB_USERNAME")
CB_PASSWORD = os.getenv("CB_PASSWORD")
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY") or os.getenv("GOOGLE_GENAI_API_KEY")
# Verify credentials are loaded
if not all([CB_CONNECTION_STRING, CB_USERNAME, CB_PASSWORD]):
raise ValueError(
"Missing Couchbase credentials. Please create a .env file with "
"CB_CONNECTION_STRING, CB_USERNAME, CB_PASSWORD"
)
if not GOOGLE_API_KEY:
raise ValueError(
"Missing Gemini API key. Set GOOGLE_API_KEY or GOOGLE_GENAI_API_KEY "
"in your .env file. Get one at https://aistudio.google.com/"
)
print(f"Connection String: {CB_CONNECTION_STRING}")
print(f"Username: {CB_USERNAME}")
print("Password: ****")
Connection String: couchbase://127.0.0.1
Username: kaustav
Password: ****
Defining the System Prompt
The system prompt is a crucial piece of instruction given to the LLM that powers our agent. It tells the model:
- What it is — A read-only Couchbase database assistant
- How to discover data — Use the schema and discovery tools first, don’t guess field names
- How to query — SQL++ syntax rules specific to Couchbase (bare collection names in FROM, UNNEST for arrays)
- How to handle errors — Retry on failures, don’t assume empty results mean no data
- How to handle data quirks — Price fields are strings, not numbers
- What NOT to do — Never answer from training data; always run a real query first
A well-crafted system prompt is the difference between an agent that generates correct SQL++ queries and one that hallucinates results. The key design principles here are discovery-first (use schema tools before guessing) and grounding (every claim must be backed by a query result).
system_prompt = """You are a Couchbase database assistant. You have READ-ONLY
access to a Couchbase cluster via the provided MCP tools. You cannot run
INSERT/UPDATE/DELETE/UPSERT or any data-modifying operations. If a user
asks you to modify, delete, or write data, explain that you have read-only
access and offer to help them write the query they could run themselves.
# Discovery first
Use the discovery tools whenever you need to know what data exists or what
shape a document has. Prefer them over guessing field names from memory:
- `get_buckets_in_cluster` — list available buckets
- `get_scopes_and_collections_in_bucket` — list scopes and collections
- `get_schema_for_collection` — sample the JSON schema of a collection
- `list_indexes` — see which fields are indexed (helps you write efficient
queries; queries on non-indexed fields fall back to a primary scan)
When asked to *describe* the database or its contents (e.g. "tell me about
the database"), start with `get_buckets_in_cluster` and then
`get_scopes_and_collections_in_bucket` so the answer actually lists buckets
and collections — not just server config.
You are connected to the `travel-sample` bucket. Most analytical data lives
under the `inventory` scope.
Rule of thumb: the FIRST time you query a collection in a conversation,
call `get_schema_for_collection` on it before writing the query — even if
you think you know the field names. Couchbase document schemas are
dataset-specific and intuitive guesses (`hotelname` vs `name`, `source`
vs `sourceairport`, `rating` vs `ratings.Overall`) are often wrong.
Cached schema knowledge from earlier in the same conversation is fine to
reuse.
# Querying with SQL++
Run SQL++ via `run_sql_plus_plus_query`. Always pass `bucket_name` and
`scope_name` so the scope context is set, then use bare collection names in
the FROM clause:
FROM hotel h (correct)
FROM `travel-sample`.`inventory`.`hotel` h (wrong — produces an
ambiguous-reference error
when the scope is already set)
Wrap any identifier that is also a SQL++ reserved word in backticks. For
aggregations over array fields, flatten the array with UNNEST first, then
GROUP BY the parent field.
# Error and empty-result handling
- If a query errors, read the message, fix the obvious issue (wrong field
name, missing UNNEST, reserved word), and retry once. If it still fails,
report the error to the user concisely instead of looping.
- If a query returns zero rows, do NOT immediately conclude "no such data
exists." Zero rows is the #1 source of false-negative answers — your
query was probably wrong, not the database. When the question is about
entities that obviously exist in real life (countries, airlines, cities,
airports, etc.), ALWAYS take at least one of these actions before
reporting empty results:
1. Reconsider whether you queried the right collection. The field you
filtered on (e.g. `country`) may live on a different collection
(e.g. `airline.country`, not `route.country`).
2. Call `get_schema_for_collection` to verify the field name and the
format of sample values.
3. Drop the most restrictive predicate and retry, or relax string
comparisons (e.g. `LOWER(country) = "france"`).
Only report "no data" after at least one such retry.
# Data quirks in this dataset
- The `price` field on `hotel` and `landmark` is stored as a free-form
STRING in mixed currencies and formats — ranges, "From £50", "Free",
weekly rates, or null — NOT a number. Numeric comparisons like
`WHERE price < 200` will return zero rows. For budget questions on
price, retrieve the raw rows (filtering with `IS NOT NULL` if
appropriate) and parse the strings in your final response.
This warning applies ONLY to the `price` field. All other numeric
fields in this dataset (review ratings, route distances, counts,
geo coordinates, etc.) are stored as proper numbers and CAN be
filtered, sorted, and aggregated with normal SQL++ operators
(SUM, AVG, ORDER BY, comparison operators).
- Many optional fields are null in a large fraction of documents. Use
`IS NOT NULL` filters when a field must be present for your answer to
make sense, and do not assume a field is universally populated until
you have checked the schema.
# Hallucination guard — read this carefully
Before producing any answer that mentions specific data values (names,
prices, addresses, schedules, counts, places, dates), you MUST have at
least one successful `run_sql_plus_plus_query` call in your tool history
for the current question. Schema discovery alone is NOT sufficient:
`get_schema_for_collection` returns the SHAPE of documents (field names
and types), not the data itself. If you have only called schema or
discovery tools and have not yet run an actual query, do NOT write the
answer — run the query first, then answer from its rows.
If a schema sample shows that a field is mostly null, or that a collection
looks sparse, that is NOT a reason to skip the query. Run it anyway and
report what you actually found — even if the result is "0 rows match" or
"prices are mostly null." Never substitute training-data knowledge (real
hotel names, real landmark prices, real airline schedules) for a missing
query result. The user is asking about the database, not about the world.
# Answering style
- Ground every claim in data you actually retrieved from a tool call. Do
not invent prices, names, counts, or schedules.
- For list or recommendation questions ("show me hotels in X", "recommend
places to visit"), aim for roughly 8-15 results by default — enough to
feel useful, not so many you bury the answer. If a query naturally
returns far more, summarize: pick the most relevant subset, group by a
useful attribute (city, country, airline, etc.), and offer to expand.
- Ask one short clarifying question only when a request is genuinely
ambiguous (e.g. multiple plausible cities or countries). For routine
intents, make a sensible default choice and explain it briefly in your
answer.
"""
Creating the Agent
Now we create the AI agent. This is where everything comes together:
model="gemini-2.5-flash"— The Gemini model that powers the agent’s reasoning.gemini-2.5-flashhandles the multi-step reasoning and conversational follow-ups in this tutorial reliably and is the recommended default. The free tier allows 250 requests/day, which is plenty for tutorial use. If you hit that limit while iterating, you can swap ingemini-2.5-flash-lite(1000 requests/day, but may struggle with the more open-ended questions) or, for maximum reasoning quality,gemini-2.5-pro. All three are drop-in replacements.instruction=system_prompt— The system prompt we defined above, which tells the agent how to behave.tools=[McpToolset(...)]— The connection to the Couchbase MCP Server. Let’s break down this configuration:command="uvx"그리고args=["couchbase-mcp-server"]— Launches the MCP server usinguv. This automatically downloads and runs the server without manual installation.env={...}— Passes Couchbase credentials to the MCP server subprocess. This is required because the subprocess doesn’t inherit environment variables fromdotenv.CB_MCP_READ_ONLY_MODE="true"— Restricts the server to read-only operations, preventing accidental data modifications. Set to"false"if you need write access.timeout=60— Allows up to 60 seconds for the MCP server to start.
root_agent = LlmAgent(
model="gemini-2.5-flash",
name="couchbase_agent",
description=(
"An agent that interacts with Couchbase databases using the "
"Couchbase MCP server."
),
instruction=system_prompt,
tools=[
McpToolset(
connection_params=StdioConnectionParams(
server_params=StdioServerParameters(
command="uvx",
args=["couchbase-mcp-server"],
env={
"PATH": os.environ.get("PATH", "/usr/bin"),
"CB_CONNECTION_STRING": CB_CONNECTION_STRING,
"CB_USERNAME": CB_USERNAME,
"CB_PASSWORD": CB_PASSWORD,
"CB_MCP_READ_ONLY_MODE": "true",
},
),
timeout=60,
),
)
],
)
print("Agent created successfully!")
Agent created successfully!
Defining the Helper Function
We need a helper function to send questions to the agent and collect responses. Here’s how it works:
Runnermanages the agent’s execution. It sends the user’s message to the agent, which then decides what tools to call.InMemorySessionServicekeeps track of the conversation history so the agent can reference previous messages.- The agent returns a stream of events — we collect only the final response text.
그리고 ask_agent function below wraps all of this into a simple interface: pass in a question, get back the agent’s answer.
import asyncio
from google.genai import errors as genai_errors
# Set up the session and runner (shared across all questions)
session_service = InMemorySessionService()
runner = Runner(
agent=root_agent,
app_name="couchbase_agent",
session_service=session_service,
)
async def ask_agent(question: str, session_id: str) -> str:
"""Send a question to the agent. Retries on transient Gemini errors."""
for attempt in range(4):
try:
response = ""
async for event in runner.run_async(
session_id=session_id,
user_id="tutorial_user",
new_message=types.Content(
role="user",
parts=[types.Part(text=question)],
),
):
if event.is_final_response() and event.content and event.content.parts:
for part in event.content.parts:
if part.text:
response += part.text
return response
except (genai_errors.ClientError, genai_errors.ServerError):
if attempt == 3:
raise
wait = 5 * (2 ** attempt) # 5s, 10s, 20s
print(f" [Gemini API error, retrying in {wait}s...]")
await asyncio.sleep(wait)
return ""
print("Helper function ready!")
Helper function ready!
Running the Agent
Now let’s put our agent to work! We’ll ask it a series of questions that demonstrate different capabilities:
- Database exploration — Understanding what data is available
- SQL++ aggregation queries — Complex queries with array operations
- Multi-step reasoning — Finding flights AND hotels in one question
- Sightseeing recommendations — Querying the landmarks collection
- Budget-based filtering — Combining price filters with ratings
Each question triggers the agent to:
- Analyze the question and decide which MCP tools to call
- Call the appropriate tools (e.g.,
get_scopes_and_collections_in_bucket,run_sql_plus_plus_query) - Process the results from Couchbase
- Generate a natural language answer
Question 1: Tell me about the database
# Create a session for our conversation
session = await session_service.create_session(
app_name="couchbase_agent", user_id="tutorial_user"
)
question = "Tell me about the database that you are connected to."
print(f"Question: {question}n")
response = await ask_agent(question, session.id)
print(response)
Question: Tell me about the database that you are connected to.
I am connected to a Couchbase cluster with the following buckets: `beer-sample`, `gamesim-sample`, `travel-sample`, and `vector-search-testing`.
Within the `travel-sample` bucket, the following scopes and collections are available:
* **`_default` scope**: `_default` collection
* **`_system` scope**: `_query`, `_mobile` collections
* **`inventory` scope**: `airport`, `airline`, `route`, `landmark`, `hotel` collections
* **`tenant_agent_00` scope**: `bookings`, `users` collections
* **`tenant_agent_01` scope**: `users`, `bookings` collections
* **`tenant_agent_02` scope**: `users`, `bookings` collections
* **`tenant_agent_03` scope**: `users`, `bookings` collections
* **`tenant_agent_04` scope**: `bookings`, `users` collections
Question 2: Top hotels by rating
This question tests the agent’s ability to write SQL++ queries with array operations. It needs to use UNNEST to flatten the reviews array and aggregate ratings — a common Couchbase pattern.
question = "List out the top 5 hotels by the highest aggregate rating."
print(f"Question: {question}n")
response = await ask_agent(question, session.id)
print(response)
Question: List out the top 5 hotels by the highest aggregate rating.
Here are the top 5 hotels by the highest aggregate rating:
1. **ibis Birmingham Bordesley** (Average Rating: 5)
2. **Pitlochry Backpackers Hotel** (Average Rating: 5)
3. **Hotel Bijou** (Average Rating: 5)
4. **Culloden House Hotel** (Average Rating: 5)
5. **Handlery Union Square Hotel** (Average Rating: 5)
Question 3: Flight and hotel recommendations
This is a multi-step question that requires the agent to query two different collections (route for flights, hotel for accommodation) and combine the results into a single recommendation.
question = "Find flights from JFK to SFO and recommend a hotel in San Francisco under $200 a night."
print(f"Question: {question}n")
response = await ask_agent(question, session.id)
print(response)
Question: Find flights from JFK to SFO and recommend a hotel in San Francisco under $200 a night.
Here are some hotels in San Francisco with prices that appear to be under $200 per night based on the available data:
* **Redwood Inn** (1530 Lombard St): Approximately $75-$90
* **Hotel Whitcomb** (1231 Market St): Approximately $89-$109
* **Pacific Tradewinds Backpackers** (680 Sacramento St): Approximately $26-29.50/night
* **SW Hotel** (615 Broadway St): Approximately $140-$160
* **Hostelling International-Downtown** (312 Mason St): Dorms $27-$30, private rooms $69-$109
* **Greenwich Inn** (3201 Steiner St): Approximately $54-$104
* **Civic Center Hotel** (20 12th St): Single occupancy with a shared bath: $150/week (this is significantly under $200/night)
* **The Opal San Francisco** (1050 Van Ness Ave): Approximately $60-$110
* **Taylor Hotel** (615 Taylor St): Approximately $66-$90
* **Chancellor Hotel** (433 Powell St): Approximately $90–$200
Please remember that prices can vary and the `price` field in this database is a string, so the ranges are approximate. You should verify current pricing directly with the hotel.
Question 4: Sightseeing in the UK
This question tests the agent’s ability to query the landmark collection and filter by country and activity type.
question = "I'm going to the UK for 1 week. Recommend some great spots to visit for sightseeing. Also mention the respective prices of those places for adults and kids."
print(f"Question: {question}n")
response = await ask_agent(question, session.id)
print(response)
Question: I'm going to the UK for 1 week. Recommend some great spots to visit for sightseeing. Also mention the respective prices of those places for adults and kids.
Here are some great sightseeing spots in the United Kingdom, along with their available price information. Please note that prices are listed as they appear in the database and may require further clarification for exact adult/child rates where not explicitly stated:
* **Topolski Century**:
* Description: A unique, monumental work by Polish artist Feliks Topolski, presenting a remarkable record of 20th-century events and figures.
* Price: £2 (£1 concession)
* **Roxy Bar and Screen**:
* Description: A bar-cinema hybrid that shows films every night except Friday and Saturday, doubling as a second-run cinema and bar.
* Price: Tickets from £3
* **Tate Britain**:
* Description: This gallery houses the Tate collection of British art from 1500 through to contemporary art, including a collection of paintings by Turner.
* Price: Free (though there is a charge for temporary exhibitions)
* **Tynemouth Castle and Priory**:
* Description: A fortress and religious site perched on a rocky headland overlooking Tynemouth Pier, combining moated castle-towers, gatehouse, keep, and ruins of a Benedictine priory.
* Price: Adults £4.70, children £2.80, concessions £4.20
* **Pickering Castle**:
* Description: Heritage 13th-century castle.
* Price: Adults £4.00, children £2.40, concessions £3.60
* **Portchester Castle**:
* Description: One of the best preserved Roman fortifications in Northern Europe, featuring a Norman-built castle keep and extensive late Roman structures.
* Price: Adults £5.00, children £3.00, concessions £4.50
Other places listed either had no price information or were not specifically "sightseeing" locations (e.g., a golf club, a grocer, a bar). Always double-check the latest prices and opening times before visiting.
Question 5: Budget hotel search
This question demonstrates conversational context — since it follows the UK sightseeing question, the agent should understand the context and search for UK hotels within the budget. We explicitly anchor the question to the UK trip to avoid ambiguity with the San Francisco hotels from Question 3.
question = "Sticking with the UK trip — what hotels in the database fit a budget of around £30 a night?"
print(f"Question: {question}n")
response = await ask_agent(question, session.id)
print(response)
Question: Sticking with the UK trip — what hotels in the database fit a budget of around £30 a night?
Finding hotels in the database precisely at or around £30 a night is challenging because the `price` field is a free-form string and often indicates ranges or weekly rates. However, based on the retrieved data, here are a couple of options that *might* fit your budget, especially if you consider dorms or the lower end of a price range:
* **Hilton Chambers** (Manchester):
* Price: £15-25 for dorms, £45-70 for private rooms. (The dorm option clearly fits your budget!)
* **Once Brewed YHA Hostel** (Northumberland):
* Price: Beds from £16. (This is a hostel, but the price is well within your budget for a bed.)
Other hotels listed generally start at £44 or higher for a room. "The Granary at Roch Mill" offers "From £320 per week," which could potentially average to under £50 a night if you are staying for the full week, but it's not explicitly a daily rate.
Always confirm the exact prices and room types directly with the accommodation provider, as the `price` field here is descriptive and not a numerical value I can directly filter on.
What Just Happened?
Behind the scenes, here’s the flow for each question:
User Question
↓
Google ADK (LlmAgent)
↓ Gemini decides which tools to call
McpToolset → StdioConnectionParams → MCP Server subprocess
↓ Tool call (e.g., run_sql_plus_plus_query)
Couchbase MCP Server
↓ Executes SQL++ query against Couchbase
Couchbase Cluster (travel-sample bucket)
↓ Returns query results
MCP Server → McpToolset → LlmAgent
↓ Gemini formats results into natural language
Agent Response
The agent automatically:
- Parsed your natural language question
- Decided which Couchbase MCP tools to call
- Generated correct SQL++ queries
- Executed them against your live database
- Formatted the results into a readable answer
Next Steps
- Try your own questions — Modify the question strings above and re-run the cells
- Enable write operations — Set
CB_MCP_READ_ONLY_MODEto"false"to allow document inserts, updates, and deletes - Use the ADK Web UI — Run
adk webfrom the command line to get an interactive chat interface - Explore the full tool list — The Couchbase MCP Server provides 19+ tools for cluster management, schema discovery, querying, and performance analysis. See the full documentation
- Check out the ADK integration page - Google ADK Couchbase Integration for more configuration options