Goal: wire the OctoTrip Flights MCP server into TravelBuddy so it can search real flights from a public, anonymous knowledge source — while keeping the Step 2 function tools.
- What the Model Context Protocol (MCP) is and why hosted agents can talk to MCP servers natively
- The difference between function tools (Step 2, local Python) and MCP tools (this step, a remote standardised server) — and why they coexist in the same agent
- How to register a remote MCP server in code with
client.get_mcp_tool(...)and whatapproval_modecontrols - How the manifest declares the MCP server's configuration through environment variables
- Why adding an MCP tool changes your code and config but not your deployment shape (still
resources: [])
travel_assistant/main.py,travel_assistant/tools.py,agent.yaml,agent.manifest.yaml— carried over from Step 2 (your TravelBuddy agent with the three function tools). Nothing was deleted when you advanced; your Step 2 work is preserved.- The MCP settings are already listed in
.env.example(MCP_SERVER_LABEL,MCP_SERVER_URL); this step starts using them.
In this step you make delta-only edits: add the MCP env vars to .env, add one line to main.py to register the MCP tool, append one sentence to TravelBuddy's instructions, and update the manifest metadata. You do not rewrite main.py, tools.py, or the YAML files from scratch — you add to the files you finished in Step 2.
TravelBuddy already has three function tools (weather, local time, currency) — small Python functions that run in-process inside its container. That pattern is perfect for capabilities you own and can code. But some capabilities are better consumed as a service: a large, live data source (say, real-time flight availability) you don't want to re-implement or redeploy every time it changes. That's where MCP comes in.
The Model Context Protocol (MCP) is an open standard for connecting AI applications to external tools, data, and prompts. An MCP server exposes a set of capabilities (tools, resources, prompts) over a standard protocol; an MCP client (here, your hosted agent) connects to that server, discovers what it offers, and calls it when useful. Because the protocol is standardised, the same client can talk to any compliant server — GitHub, a database gateway, a docs service — without custom glue for each one.
The key contrast with Step 2:
| Function tools (Step 2) | MCP tools (this step) | |
|---|---|---|
| Where the code runs | In-process, in your container | On a remote MCP server |
| Who owns it | You (your Python) | The server operator (here, OctoTrip) |
| How it's registered | Pass the function in tools=[...] |
client.get_mcp_tool(name=..., url=...) |
| Tool schema | Inferred from type hints + docstring | Streamed from the server at connect time |
| Evolves without redeploy | No — you edit and redeploy | Yes — the server can add/update tools |
Crucially, from the model's point of view they are all just tools. The same tool-calling loop from Step 2 applies: the model sees the available tools (local and remote), decides which to call, the framework runs the call, and the result flows back into the answer. A single hosted agent can connect to many MCP servers at once, and the model picks the right capability per question.
This step points TravelBuddy at the OctoTrip Flights MCP server, a public, anonymous endpoint that searches live flights (routes, prices, times). Weather, time, and currency stay as local function tools (they're small and app-specific); flight search becomes an MCP tool because it's a large, live data source that OctoTrip keeps current for you.
flowchart LR
User[Traveler question] --> Agent[TravelBuddy hosted agent]
Agent --> Decision{Which capability?}
Decision -->|weather / time / currency| Local[Function tools<br/>in-process Python]
Decision -->|flight search| MCP[OctoTrip Flights<br/>MCP server]
MCP --> Docs[(Live flight<br/>availability & prices)]
Docs --> MCP
Local --> Agent
MCP --> Agent
Agent --> Answer[Answer with<br/>live flight options]
client.get_mcp_tool(...) is the Agent Framework helper that turns a remote MCP server into a tool the agent can use. You give it a name (a stable label that identifies the server in logs and tool-call traces), a url (the MCP endpoint), and approval_mode. Setting approval_mode="never_require" lets the runtime call the server automatically without pausing for human approval — appropriate here because OctoTrip Flights is a read-only, public search source. For servers that mutate state (booking a flight, sending mail), you'd require approval instead.
The upstream 03-mcp sample connects to the GitHub MCP server and passes an Authorization header built from a GITHUB_PAT. This workshop uses the exact same Agent Framework pattern but points at the anonymous OctoTrip Flights endpoint, so no token or header is needed. (The Troubleshooting section shows the authenticated variant if you switch to a server that requires a token.)
Helpful references:
- What is the Model Context Protocol (MCP)? — the open standard TravelBuddy speaks to.
- OctoTrip Flights MCP server — the public, anonymous flight-search server used in this step (streamable HTTP; a single
searchtool takingorigin,destination, anddeparture_date). - Model Context Protocol tools in Microsoft Foundry Agents — how Foundry agents connect to MCP servers and what
approval_modecontrols. - Using tools with an agent — the shared tool-calling loop that function tools and MCP tools both flow through.
- What are hosted agents? — the hosted boundary your agent (and its MCP connection) runs inside.
- Upstream
03-mcphosted-agent sample — the sample this step is based on.
Open .env and add the MCP settings. They're already listed in .env.example; this step starts using them.
# .env
MCP_SERVER_LABEL=octotrip-flights
MCP_SERVER_URL=https://mcp.octotrip.app/flights/mcpMCP_SERVER_LABELis a short, stable identifier for the server. It becomes the tool group name that shows up in logs and tool-call traces, so keep it predictable and avoid spaces.MCP_SERVER_URLis the MCP endpoint the agent connects to.
Keep the Foundry values (AZURE_AI_PROJECT_ENDPOINT, AZURE_AI_MODEL_DEPLOYMENT_NAME, WORKSHOP_RESOURCE_PREFIX) in .env too. Don't add any secret for OctoTrip Flights MCP — it's public and anonymous.
Your main.py is already complete from Step 2 — don't rewrite it. There's exactly one functional addition: append a client.get_mcp_tool(...) entry to the existing tools=[...] list. Then add one sentence to the instructions so the model knows when to reach for flight search.
Keep your Step 2 imports and function tools exactly as they are. The three function tools are still registered; you're adding a fourth, remote tool alongside them:
tools = [
get_weather, # <-- kept from Step 2
get_local_time, # <-- kept from Step 2
convert_currency, # <-- kept from Step 2
client.get_mcp_tool( # <-- add this entry
name=os.environ["MCP_SERVER_LABEL"],
url=os.environ["MCP_SERVER_URL"],
approval_mode="never_require",
),
]Then keep your Step 2 instructions exactly as they are and append one MCP sentence so the model knows the flight-search capability exists:
instructions=(
# ... keep your Step 2 instructions here, unchanged ...
"Use the OctoTrip Flights MCP server when the traveler asks about "
"flights, routes, fares, or schedules; pass IATA airport codes and a "
"departure date (YYYY-MM-DD) — if the traveler doesn't give one, call "
"get_local_time and use the date part of its iso_time as today's date — "
"and summarize the options you find."
),
tools=tools, # <-- the list you just extended aboveThat's the whole code change. client.get_mcp_tool(...) reads the label and URL from the environment (the same values you just added to .env) and hands the agent a remote tool. Everything else in main.py — the FoundryChatClient setup, the three function tools, default_options={"store": False}, and ResponsesHostServer(agent).run() — is unchanged from Step 2. If you get stuck, the finished file is in .workshop/solutions/03-mcp/.
Why
os.environ[...]and not a hardcoded URL? Reading the label and URL from the environment keeps them out of source control and lets you point at a different MCP server (or the authenticated variant in Troubleshooting) by editing.env— no code change. The hosted runtime gets the same values from the manifest at deploy time.
An MCP connection is made in code and configured through environment variables, so the manifest structure barely changes — same template, same protocols, and resources stays empty ([]) because no new Azure resource is needed. This step makes two kinds of edit: metadata (update the human-facing description, add an MCP Tools tag and an MCP entry to tool_declarations) and configuration (add the two MCP environment variables so the hosted runtime receives them).
Update the description:
# travel_assistant/agent.manifest.yaml
description: >
TravelBuddy is an Agent Framework hosted agent with local Python function tools
for weather, local time, and currency, plus an OctoTrip Flights MCP connection for
live flight search.Extend metadata — add the MCP Tools tag and an MCP entry alongside the Step 2 tool_declarations (keep the three function-tool entries):
metadata:
tags:
- Agent Framework
- AI Agent Hosting
- Azure AI AgentServer
- Responses Protocol
- Travel Assistant
- Function Tools
- MCP Tools # <-- added
tool_declarations:
# ... keep the get_weather / get_local_time / convert_currency entries ...
- name: octotrip-flights # <-- added: the remote MCP tool group
description: OctoTrip Flights MCP server for live flight search.
type: mcp
url: ${MCP_SERVER_URL}Then add the two MCP variables to the existing template.environment_variables list (keep the Step 1/Step 2 entries):
template:
# ... name, kind, protocols unchanged ...
environment_variables:
# ... AZURE_AI_PROJECT_ENDPOINT, AZURE_AI_MODEL_DEPLOYMENT_NAME, WORKSHOP_RESOURCE_PREFIX ...
- name: MCP_SERVER_LABEL # <-- added
value: ${MCP_SERVER_LABEL}
- name: MCP_SERVER_URL # <-- added
value: ${MCP_SERVER_URL}
resources: [] # <-- unchanged: no new Azure resourcetool_declarations is descriptive metadata — it documents the agent's capabilities for humans and tooling that browse the manifest. The MCP tools are still connected in code via client.get_mcp_tool(...); the MCP server itself decides which concrete tools it exposes at connect time. resources stays [] because MCP adds no Azure resource — the connection is an outbound HTTPS call from the running container.
agent.yaml is the local hosted-agent runtime definition, so it carries its own environment-variable list. Add the same two MCP variables here so the local run (azd ai agent run) picks them up — the hosted agent.yaml and the manifest's template share the same environment contract, but each file declares the variables it needs:
# travel_assistant/agent.yaml
environment_variables:
# ... AZURE_AI_PROJECT_ENDPOINT, AZURE_AI_MODEL_DEPLOYMENT_NAME, WORKSHOP_RESOURCE_PREFIX ...
- name: MCP_SERVER_LABEL # <-- added
value: ${MCP_SERVER_LABEL}
- name: MCP_SERVER_URL # <-- added
value: ${MCP_SERVER_URL}Leave the name, kind, protocols, and CPU/memory blocks exactly as they were. No new Azure resource is declared, so you won't need to re-provision — but because azd ai agent init copied your code and manifest into the project folder in earlier steps, you'll re-run azd ai agent init in the next section to refresh that copy before deploying.
Do you need to re-init? Yes. In the earlier steps, azd ai agent init copied your travel_assistant/ code into the generated ${WORKSHOP_RESOURCE_PREFIX}-travel-buddy/ project folder — that copy is the snapshot azd actually builds and deploys. Your Step 3 edits live in travel_assistant/ (the main.py MCP tool line, the appended instruction, and the manifest/agent.yaml changes), so the copied snapshot is now stale. Re-run azd ai agent init to refresh it before you run or deploy; it re-copies the current travel_assistant/ code and re-reads the updated agent.manifest.yaml.
You do not need azd provision again — you added no new Azure resources (resources: is still []), so the infrastructure from earlier steps is unchanged. The re-init just refreshes the copied code + manifest, and then azd deploy ships the new container version.
Prefer not to re-init? You can instead copy your edited
travel_assistant/main.py(and the updated YAML) into the code directory inside${WORKSHOP_RESOURCE_PREFIX}-travel-buddy/and skip straight toazd deploy. Re-init is the reliable path because it also picks up the manifest changes and can't drift out of sync.
-
Re-init from the repository root. Load your
.envinto the shell first — the repo.envisn't auto-loaded, and the shell needsWORKSHOP_RESOURCE_PREFIXto expand--agent-name(and tocdinto the folder later):# bash / zsh set -a; source .env; set +a azd ai agent init -m travel_assistant/agent.manifest.yaml \ --agent-name "${WORKSHOP_RESOURCE_PREFIX}-travel-buddy"
# PowerShell Get-Content .env | Where-Object { $_ -match '^\s*[^#].*=' } | ForEach-Object { $name, $value = $_ -split '=', 2 Set-Item "Env:$($name.Trim())" $value.Trim() } azd ai agent init -m travel_assistant/agent.manifest.yaml ` --agent-name "$($env:WORKSHOP_RESOURCE_PREFIX)-travel-buddy"
This refreshes the
${WORKSHOP_RESOURCE_PREFIX}-travel-buddy/folder with your updatedmain.pyand the updated manifest metadata (including the new MCP environment variables). -
cdinto the project folder and add the new MCP values to the azd env. azd keeps its own environment store (.azure/<env-name>/.env), separate from the repo.env. The Foundry values (AZURE_AI_PROJECT_ENDPOINT,AZURE_AI_MODEL_DEPLOYMENT_NAME,WORKSHOP_RESOURCE_PREFIX) are already in the azd env from earlier steps, so you only need to set the two new MCP variables. Keep.envloaded in the shell so you can pass the values through:# bash / zsh — after: set -a; source .env; set +a cd "${WORKSHOP_RESOURCE_PREFIX}-travel-buddy" azd env set MCP_SERVER_LABEL "$MCP_SERVER_LABEL" azd env set MCP_SERVER_URL "$MCP_SERVER_URL"
# PowerShell — after loading .env into the shell cd "$($env:WORKSHOP_RESOURCE_PREFIX)-travel-buddy" azd env set MCP_SERVER_LABEL "$env:MCP_SERVER_LABEL" azd env set MCP_SERVER_URL "$env:MCP_SERVER_URL"
-
Run TravelBuddy locally in the hosted Responses runtime:
azd ai agent run
azdreadsagent.yaml, substitutes values from your azd environment, and starts the server onhttp://localhost:8088— now with your three function tools and the OctoTrip Flights MCP connection loaded. Leave this terminal running. -
Invoke the local agent from a second terminal. The
azd ai agent runprocess is still holding the first terminal, so open a new one (in the same project folder) and ask a question that forces a flight search:azd ai agent invoke --local "Find flights from Seattle (SEA) to Tokyo (NRT). List a few options with airline, price, and times."Expected: TravelBuddy calls the OctoTrip Flights MCP server and answers with real flight options — not just generic advice. The exact flights and prices change as the live server updates.
Prefer a UI? With the local agent still running, open the Agent Inspector from the Foundry Toolkit (Command Palette → Foundry Toolkit: Open Agent Inspector). It connects to
http://localhost:8088and shows each streamed MCP tool call and result. -
Deploy to Foundry:
azd deploy
This builds the container image from the refreshed project-folder snapshot — now including the MCP tool registration and env vars — pushes it to your Azure Container Registry, and rolls out a new hosted agent version. No
azd provisionis needed because the infrastructure is unchanged. -
Invoke the deployed agent:
azd ai agent invoke "Find flights from Seattle (SEA) to Tokyo (NRT)."Prefer a UI? Open the Hosted Agent Playground from the Foundry Toolkit (Developer Tools → Build → Hosted Agent Playground), pick your deployed agent and version, and watch the MCP tool calls in the session details.
Try prompts that make the tool choice obvious.
- "Find me flights from London (LHR) to New York (JFK)."
Now try a mixed prompt that should use both a local function tool and MCP in one conversation:
- "What's the weather in Reykjavik right now, and can you find flights from Reykjavik (KEF) to Copenhagen (CPH)?"
The weather portion should use the Step 2 get_weather function tool; the flight portion should use OctoTrip Flights MCP. That's the key lesson: local function tools and remote MCP tools are both just tools from the model's point of view, and it routes each part of the question to the right one.
Check the URL in .env.
# .env
MCP_SERVER_URL=https://mcp.octotrip.app/flights/mcpThe OctoTrip Flights MCP is public and anonymous, but it's rate-limited (roughly one request per second). If calls fail intermittently, space out your prompts and retry. If the endpoint is temporarily unavailable, try again later.
Confirm the two settings are in both .env and the manifest, and that main.py actually appends client.get_mcp_tool(...) to the tools list passed to Agent.
Common mistakes:
- putting
environment_variablesunderresourcesinstead oftemplate; - using
environmentVariablesinstead ofenvironment_variables; - adding
MCP_SERVER_LABEL/MCP_SERVER_URLto.envbut not to the manifest; - forgetting to restart the agent (or re-run
azd ai agent init) after editing.envor the manifest.
The final tools list should contain four entries: the three function tools from Step 2 (get_weather, get_local_time, convert_currency) plus the MCP tool from this step. If you replaced the whole list with only the MCP tool, restore the function tools and run again.
OctoTrip Flights MCP is anonymous, so an auth error usually means you changed MCP_SERVER_URL to a server that requires a token. For an authenticated MCP server, follow the upstream 03-mcp auth pattern: read a token from the environment and pass an Authorization header to client.get_mcp_tool(...).
# travel_assistant/main.py
token = os.environ["MCP_SERVER_TOKEN"]
mcp_tool = client.get_mcp_tool(
name=os.environ["MCP_SERVER_LABEL"],
url=os.environ["MCP_SERVER_URL"],
headers={"Authorization": f"Bearer {token}"},
approval_mode="never_require",
)Do not commit tokens to .env.example, agent.manifest.yaml, or source control. Use your local .env or the deployment environment's secret store.
Make the prompt explicit: give an origin, a destination (IATA codes work well), and a departure date, and ask for flight options. MCP gives the model access to live flight search, but the model still chooses whether to call it. Strong prompts make the desired tool use clear while you're testing.
azd ai agent init copied your code into the ${WORKSHOP_RESOURCE_PREFIX}-travel-buddy/ project folder, so edits in travel_assistant/ don't deploy on their own. Re-run azd ai agent init (step 1 above) to refresh that snapshot — or copy your edited files into the folder's code directory — then azd deploy again.
If you get stuck:
.workshop/solutions/03-mcp/
Based on the upstream
03-mcpsample.