Task 5 — Capstone: build your own MCP server
Part of the Build and extend AI agents lab. New here? Start with Getting started.
Set up (start here): This is the capstone. It needs a Foundry project and the starter code. If you haven’t already, complete Getting started to create your project, clone the code, and set
PROJECT_ENDPOINTandMODEL_DEPLOYMENT_NAMEinPython/.env. It reusesfunctions.pyfrom Task 4 — already in the starter folder, so you don’t need to have finished Task 4. Then verify:
python setup/check_env.py --task 5
Continuing from a previous task? If you just finished an earlier task in the same
Pythonfolder, your project, virtual environment, and.envare already set — go straight to Set up below to start editingserver.pyandclient.py.
Goal: Host your own tools on an MCP server, then bring the lab together into a single Tailwind Traders Assistant — one agent that both plans trips and prices gear (the function tools from Task 4) and checks live warehouse stock and sales (the tools you host here).
Concept reinforced: the MCP server/client split — a server registers tools; a client
discovers and calls them — plus how one agent can hold more than one kind of tool at
once. In respond() you route each call to the right place: local Python functions run
in-process, MCP tools run over the server session.
How this builds on Task 4: This capstone combines the trip-planner tools from Task 4 with a new MCP server. You don’t need to have finished Task 4 — those tools (
next_available_trip,calculate_rental_cost,generate_booking_report) are provided ready-made inclient.py— so you can focus on the new work: hosting your MCP server and combining both tool sets on one agent. (Already did Task 4? Even better — you’ll recognize them.)
Set up:
- In the
Labfiles/A-build-and-extend-ai-agents/Pythonfolder, activate the virtual environment (.\labenv\Scripts\Activate.ps1) and confirm your .env hasPROJECT_ENDPOINTandMODEL_DEPLOYMENT_NAME(see Getting started). You’ll edit server.py and client.py.
Try it first: Wire up server.py and client.py using the comments in each file. As you go, consider: why must diagnostic output go to
stderr(or be suppressed) rather thanstdout? (Hint: MCP speaks JSON-RPC over stdio, so anything printed to stdout is parsed as protocol messages — a stray banner corrupts the stream. That’s why the server starts withshow_banner=False.) And: once the agent has both tool sets, how does your code know whether a givenfunction_callshould run a local function or an MCP tool?
Show a solution
In server.py — create the server and expose the two provided functions as tools:
# Add references
from fastmcp import FastMCP
# Create an MCP server
mcp = FastMCP(name="Inventory")
@mcp.tool()
def get_inventory_levels() -> dict:
... # returns the sample inventory dict already in the file
@mcp.tool()
def get_weekly_sales() -> dict:
... # returns the sample sales dict already in the file
# Run the MCP server
mcp.run(show_banner=False)
In client.py — connect to the server, discover its tools, register them alongside
the trip-planner tools on one agent, then route each call in respond(). Because the chat UI
runs on an async event loop, the connection code lives in an async setup() that runs once on
the first message.
-
Add the MCP references at the top of the file:
from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_clientThe
trip_planner_toolslist and thelocal_functionsdispatch dict (the Task 4 tools) are already provided near the top of the file — you don’t need to rewrite them. -
Inside
setup(), start the server over stdio and open a session, then list the available tools and wrap each as a callable:stdio_transport = await exit_stack.enter_async_context(stdio_client(server_params)) stdio, write = stdio_transport session = await exit_stack.enter_async_context(ClientSession(stdio, write)) await session.initialize() tools = (await session.list_tools()).tools def make_tool_func(tool_name): async def tool_func(**kwargs): return await session.call_tool(tool_name, kwargs) tool_func.__name__ = tool_name return tool_func functions_dict = {tool.name: make_tool_func(tool.name) for tool in tools} mcp_function_tools = [ FunctionTool( name=tool.name, description=tool.description, parameters={"type": "object", "properties": {}, "additionalProperties": False}, strict=True, ) for tool in tools ] -
Create the agent with both tool sets — the trip planner and the warehouse tools:
agent = project_client.agents.create_version( agent_name="tailwind-assistant", definition=PromptAgentDefinition( model=model_deployment, instructions=""" You are the Tailwind Traders assistant. You help customers plan guided trips and price gear rentals, and you help warehouse staff check live stock and sales. Trip planning and rentals: - Use the trip and rental tools to find guided trips, price gear, and produce booking reports. Warehouse inventory: - Recommend restock if item inventory < 10 and weekly sales > 15 - Recommend clearance if item inventory > 20 and weekly sales < 5 """, tools=[*trip_planner_tools, *mcp_function_tools], ), ) -
In
respond(), route eachfunction_callto the right executor — local functions run directly (they return a string); MCP tools are awaited over the session:for item in response.output: if item.type == "function_call": kwargs = json.loads(item.arguments) if item.name in local_functions: output_text = local_functions[item.name](**kwargs) # Task 4 function else: result = await functions_dict[item.name](**kwargs) # your MCP tool output_text = result.content[0].text input_list.append( FunctionCallOutput( type="function_call_output", call_id=item.call_id, output=output_text, ) ) # ...send outputs back, then: return AgentReply(text=response.output_text)
Run python client.py. Your browser opens the chat window — the server is launched for you
over stdio on the first message. Now try a prompt that exercises both halves of the
assistant in one conversation:
Plan me a trip: find the next available trip in Patagonia and price 5 days of premium gear at priority service.
Now check the warehouse — are there any products we should restock?
The first prompt calls your Task 4 trip-planner functions; the second calls your MCP inventory tools — all on the same agent, in the same chat. Close the browser tab and press Ctrl+C in the terminal to stop the app.
Stretch: add a third MCP tool (for example, get_reorder_threshold) and watch the agent
discover it without any other client changes — the routing already handles any tool it
doesn’t recognize as a local function.
Compare: the same capstone with the Microsoft Agent Framework
In client.py you hand-wired the MCP client (ClientSession, stdio_client), wrapped each
discovered tool, built FunctionTool schemas, and then routed every function_call yourself
— local function or MCP tool. The Microsoft Agent Framework collapses all of that. Open
client_maf.py (provided complete) and run it with python client_maf.py — same capstone,
same two-tool-sets-on-one-agent behavior.
Your server.py is unchanged — you still author the MCP server. What disappears is the client
wiring and the routing loop. An MCPStdioTool launches the server and exposes its tools, and
you hand it to agent.run() alongside the agent’s own @tool functions:
from agent_framework import tool, Agent, MCPStdioTool
agent = Agent(
client=FoundryChatClient(...),
name="tailwind-assistant",
instructions="You are the Tailwind Traders assistant...",
tools=[next_available_trip, calculate_rental_cost, generate_booking_report],
)
async with MCPStdioTool(name="Inventory", command="python", args=["server.py"]) as mcp_tool:
# One call handles either tool set — no manual "local vs MCP" routing
result = await agent.run(user_message, tools=mcp_tool, session=session)
Notice there’s no if item.name in local_functions ... else ... branch: agent.run() invokes
whichever tool the model picks, whether it’s one of your Python functions or a tool hosted on
your MCP server. Having built the routing by hand first, you can see exactly which step the
framework is taking over.
Next (optional): Task 6 — Promote your assistant to a hosted agent, or head back to the lab overview.