UCLA Extension - Agentic AI Course
| Part | Focus | Points |
|---|---|---|
| Part 1 | Build a Google Workspace agent with ADK | 70 |
| Part 2 | Connect to GitHub via MCP Server | 30 |
workspace_assistant/
├── agent.py # STUDENT: Main agent definition
├── main.py # Given: CLI entry point
├── tools/
│ ├── auth.py # Given: Google OAuth helper
│ ├── calendar_tools.py # STUDENT (Option A): Calendar tools
│ ├── tasks_tools.py # STUDENT (Option B): Tasks tools
│ └── mcp_tools.py # STUDENT: GitHub MCP integration
├── config/
│ ├── settings.py # Given: Configuration from .env
│ ├── mcp_servers.json # Given: MCP server config (optional)
│ └── credentials/ # Your OAuth files (gitignored)
├── tests/
│ ├── test_tools.py # Part 1 verification
│ └── test_mcp.py # Part 2 verification
└── docs/
├── google_auth_setup.md # Google OAuth setup guide
├── github_mcp_setup.md # GitHub token setup guide
└── reflection_template.md
| File | What to implement |
|---|---|
tools/calendar_tools.py |
Option A: 3+ calendar tools |
tools/tasks_tools.py |
Option B: 3+ tasks tools |
agent.py |
System instruction + LlmAgent with your tools |
tools/mcp_tools.py |
Part 2: GitHub McpToolset configuration |
| File | What it provides |
|---|---|
tools/auth.py |
get_calendar_service() and get_tasks_service() — authenticated Google API clients |
config/settings.py |
Settings class — loads model name, credentials path, and options from .env |
main.py |
CLI with --interactive mode using rich |
tests/test_tools.py |
Checks tool count, docstrings, type hints, naming, error handling |
tests/test_mcp.py |
Checks GitHub token and McpToolset presence in agent |
Build an AI agent using Agent Development Kit (ADK).
Choose ONE option:
| Option | Focus |
|---|---|
| A | Calendar Assistant - Schedule meetings, find free time, manage conflicts |
| B | Tasks Manager - Create tasks, track to-dos, manage deadlines |
- Minimum 3 tools with clear names and descriptions
- Error handling with user-friendly messages
- System instruction that guides the agent's behavior
The tools/auth.py file is already implemented and gives you authenticated Google API clients:
from tools.auth import get_calendar_service # Option A
from tools.auth import get_tasks_service # Option B
# Use inside your tools:
service = get_calendar_service() # Returns authorized Calendar API client
service = get_tasks_service() # Returns authorized Tasks API clientFirst-time use triggers an OAuth browser flow. See docs/google_auth_setup.md for setup.
Define each tool as a plain Python function with a clear docstring and type hints — ADK reads these to build the tool schema and auto-wraps the function when you pass it to the agent. Each tool should call the Google API and return a dict:
from tools.auth import get_calendar_service
def list_upcoming_events(max_results: int = 10) -> dict:
"""List upcoming calendar events.
Args:
max_results: Maximum number of events to return.
Returns:
dict with 'status' and 'events' keys.
"""
try:
service = get_calendar_service()
events_result = service.events().list(
calendarId='primary', maxResults=max_results, ...
).execute()
return {"status": "success", "events": events_result.get("items", [])}
except Exception as e:
return {"status": "error", "message": str(e)}Add your completed tools to the list at the bottom of the file:
calendar_tools = [list_upcoming_events, find_available_slots, create_event]Implement in tools/calendar_tools.py using get_calendar_service():
| Tool | Purpose |
|---|---|
list_upcoming_events |
Get events for a date range |
find_available_slots |
Find free time between participants |
create_event |
Schedule a new meeting |
check_conflicts |
Detect scheduling conflicts |
reschedule_event |
Move an existing event |
Implement in tools/tasks_tools.py using get_tasks_service():
| Tool | Purpose |
|---|---|
list_tasks |
Get tasks from a list |
create_task |
Add a new task |
complete_task |
Mark a task as done |
update_task |
Modify task details |
delete_task |
Remove a task |
In agent.py, wire your tools into an LlmAgent:
from google.adk.agents import LlmAgent
from config.settings import Settings
from tools.calendar_tools import calendar_tools # or tasks_tools
def create_agent() -> LlmAgent:
settings = Settings()
instruction = """You are a Google Workspace assistant that helps users
manage their calendar. You can list events, find free time, create
meetings, check for conflicts, and reschedule events. Always confirm
before making changes..."""
return LlmAgent(
name="workspace_assistant",
model=settings.model_name,
instruction=instruction,
tools=calendar_tools,
)Extend your agent to interact with GitHub using McpToolset.
- Create a GitHub Personal Access Token with
reposcope - Configure
McpToolsetto connect to the GitHub MCP server - Update your system instruction to include GitHub capabilities
from google.adk.agents import LlmAgent
from google.adk.tools.mcp_tool import McpToolset
from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams
from mcp import StdioServerParametersserver_params = StdioServerParameters(
command="npx",
args=["-y", "@modelcontextprotocol/server-github"],
env={"GITHUB_PERSONAL_ACCESS_TOKEN": token}
)
toolset = McpToolset(
connection_params=StdioConnectionParams(server_params=server_params)
)Alternatively, load from config/mcp_servers.json:
{
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}"
}
}
}
}Use the provided load_mcp_config() helper to load and parse this config.
Your agent should handle:
- "List my GitHub repositories"
- "Show open issues in [repo]"
- "Create an issue in [repo] about [topic]"
| Component | Points |
|---|---|
| Tool Design | 18 |
| Agent Instructions | 14 |
| Error Handling | 14 |
| Functionality | 14 |
| Code Quality | 10 |
| Component | Points |
|---|---|
| McpToolset Configured | 10 |
| GitHub Queries Work | 15 |
| Error Handling | 5 |
Bonus (+25): Tool search: load GitHub tools on demand (B1 to B5 below)
What: Don't declare every GitHub MCP tool to the model up front (@modelcontextprotocol/server-github has 26). Start with a search tool plus at most 2 always-loaded GitHub tools. The agent searches for the tool it needs, then uses it.
Why: Every declared tool's name, description and input schema is sent on every model call. Fewer declared tools means a smaller prompt. How much smaller is something you measure (B5).
Correction (Oct 11): An earlier version of this section said to configure McpToolset with defer_loading=True. Google ADK has no such parameter. Passing it raises TypeError: McpToolset.__init__() got an unexpected keyword argument 'defer_loading' (checked on google-adk 1.18.0, the minimum in requirements.txt, and 2.11.0, the latest release on Oct 11). defer_loading is a field in Anthropic's Messages API, not in ADK. Build the pattern with the ADK pieces below. No points depend on the name defer_loading.
ADK building blocks:
- Deferred toolset:
McpToolset(..., tool_filter=fn).tool_filtercan be a functionfn(tool, readonly_context=None) -> bool. ADK calls it whenever it lists tools for the model, andreadonly_context.stateis the session state. Return True for your always-loaded tools and for the names your search stored in state. Also return True whenreadonly_contextis None: thenawait toolset.get_tools()with no argument returns every tool, which is the catalog your search needs. - Search tool:
search_github_tools(query: str, tool_context: ToolContext) -> dict. A tool function can take atool_context: ToolContextparameter: ADK fills it in and hides it from the model. Rank the catalog against the query, store the names you found intool_context.state, and return each match's name, description and input schema. - Input schema:
tool.raw_mcp_tool.model_dump(by_alias=True)["inputSchema"]gives an MCP tool's input schema on both mcp 1.x and 2.x. - Same-message use: ADK 2.x (what
pip install -r requirements.txtinstalls today) lists each toolset's tools once per user message, so a tool your filter unlocks mid-message is only declared from the user's next message. ADK 1.x re-lists before every model call. To search for a tool and use it within one user message on 2.x, do one of these:- add a proxy tool
call_github_tool(tool_name: str, arguments: dict, tool_context: ToolContext)that looks the tool up inawait toolset.get_tools()and runs it withawait tool.run_async(args=arguments, tool_context=tool_context), or - set
toolset._use_invocation_cache = Falseso ADK re-lists before every model call. It is a private ADK attribute that can change between releases, so add a comment, or - pin
google-adkto 1.x inrequirements.txt.
- add a proxy tool
Points (+25):
| # | What is checked | Points |
|---|---|---|
| B1 | Small upfront tool set: the first model request of create_agent_with_tool_search() declares only your search helpers (search_github_tools, plus call_github_tool if you add one) and at most 2 always-loaded GitHub tools. Part 1 tools don't count. |
5 |
| B2 | search_github_tools: searches the tool list the MCP server returns (get_tools()), not a hand-written list (3). Returns at most 5 matches, best first: "list issues" and "create issue" give list_issues and create_issue first (2). |
5 |
| B3 | Found tools are usable: a tool returned by search can be run (4), the model has its real input schema when it calls it (2), and search then use completes within one user message on the ADK version your requirements.txt installs (2). |
8 |
| B4 | create_agent_with_tool_search() in agent.py: builds without error (1) and keeps your Part 1 Calendar/Tasks tools (1). |
2 |
| B5 | Measured token comparison in your reflection: both agents measured on the same query, with ADK version and model (3), % reduction computed from your numbers (1), one limit you noticed (1). | 5 |
See tools/mcp_tools.py for scaffolding.
Run create_agent() and create_agent_with_tool_search() on the same query and record the prompt tokens Gemini reports. Use either:
event.usage_metadata.prompt_token_counton the events fromrunner.run_async(...), as in the script below, or- Gemini's
countTokensAPI on the same request.
Estimates, such as characters / 4, don't count. There is no expected value: the numbers depend on your ADK version, model, instruction and query. Earlier versions of this README showed example token numbers that were never measured, so don't reuse them.
# measure_tokens.py: run from workspace_assistant/ with your real .env
import asyncio
import google.adk
from dotenv import load_dotenv
from google.adk.runners import InMemoryRunner
from google.genai import types
from agent import create_agent, create_agent_with_tool_search
QUERY = "Show open issues in octocat/Hello-World" # same query for both agents
async def prompt_tokens(agent) -> list[int]:
"""Prompt tokens of each model call in one user message."""
runner = InMemoryRunner(agent=agent, app_name="measure")
session = await runner.session_service.create_session(app_name="measure", user_id="u")
message = types.Content(role="user", parts=[types.Part(text=QUERY)])
counts = []
async for event in runner.run_async(user_id="u", session_id=session.id, new_message=message):
if event.usage_metadata and event.usage_metadata.prompt_token_count:
counts.append(event.usage_metadata.prompt_token_count)
await runner.close()
return counts
async def main():
load_dotenv()
print(f"google-adk {google.adk.__version__}, query: {QUERY!r}")
for build in (create_agent, create_agent_with_tool_search):
agent = build()
counts = await prompt_tokens(agent)
print(f"{build.__name__} ({agent.model}): first call {counts[0]}, "
f"whole turn {sum(counts)} over {len(counts)} model calls")
asyncio.run(main())Put the results in the "Bonus: Token Comparison" table in docs/reflection_template.md: ADK version, model, query, and first-call and whole-turn prompt tokens for both agents, then the % reduction and one limit you noticed. For example, the search adds a model call, so whole-turn savings are smaller than first-call savings.
conda create -n workspace-agent python=3.11 -y
conda activate workspace-agent
pip install -r requirements.txt
cp .env.example .envFollow docs/google_auth_setup.md to:
- Create a Google Cloud project with Calendar/Tasks API enabled
- Download OAuth credentials to
config/credentials/credentials.json - The first tool call will open a browser for authorization
- Implement 3+ tools in
tools/calendar_tools.pyortools/tasks_tools.py - Create your agent in
agent.pywith system instruction + tools - Verify:
cd workspace_assistant
python -m tests.test_tools- Test interactively:
adk web # ADK Web UI
# or
python main.py --interactive # CLI mode- Get a GitHub Personal Access Token (see docs/github_mcp_setup.md)
- Add
GITHUB_PERSONAL_ACCESS_TOKEN=ghp_...to.env - Implement
get_github_mcp_toolset()intools/mcp_tools.py - Add the toolset to your agent in
agent.pyand update the system instruction - Verify:
python -m tests.test_mcpADK provides a built-in web interface for testing your agent interactively.
cd workspace_assistant
adk webThis launches a local web server (typically at http://localhost:8000).
- Select your agent from the dropdown (should show your
workspace_assistant) - Type queries in the chat interface to test your tools
- View tool calls - the UI shows which tools are invoked and their responses
- Debug errors - see full error messages and stack traces
Part 1 - Calendar (Option A):
- "What meetings do I have tomorrow?"
- "Find a free slot for a 1-hour meeting next week"
- "Schedule a team sync for Monday at 2pm"
Part 1 - Tasks (Option B):
- "Show my tasks"
- "Add a task: Review PR by Friday"
- "Mark the first task as complete"
Part 2 - GitHub MCP:
- "List my GitHub repositories"
- "Show open issues in octocat/Hello-World"
- "Create an issue in my-repo about fixing the login bug"
| Issue | Solution |
|---|---|
| Agent not appearing | Ensure agent.py has a valid create_agent() function |
| Tools not showing | Check that tools are added to the agent's tools list |
| MCP connection failed | Verify GITHUB_PERSONAL_ACCESS_TOKEN in .env |
| Import errors | Run pip install -r requirements.txt |
This assignment uses GitHub Classroom. Your work is submitted by pushing to your repository's main branch.
- Test your agent with
adk weband verify all functionality works - Run the verification tests:
cd workspace_assistant python -m tests.test_tools python -m tests.test_mcp - Complete
docs/reflection_template.md - Commit and push your changes:
git add -A git commit -m "Submit assignment 2" git push origin main - Verify your submission on GitHub — go to your repository page and confirm all files are present
- Your submission is graded from the code pushed to
mainin your repository - You can push multiple times before the deadline; the latest state on
mainat the deadline is what's graded - Make sure your final code is committed and pushed before the deadline
- Do NOT commit sensitive files —
credentials/,token.json, and.envare already in.gitignore - Make sure your code passes the local tests before pushing