Chapter 20
Week 5.5: Microsoft Agent Framework
Welcome to Week 5 - Agent Frameworks
Day 3: Microsoft Agent Framework
Same week, same five steps, a new framework. The whole idea this week is that once you know one agent framework, you basically know them all, so every day we build the same agent the same five steps and watch the idioms rhyme:
- Create the agent - give it a model and a system prompt.
- Run it - send a message, get a reply.
- Add tools - plain typed functions the agent can call.
- Add MCP - connect a tool server someone else wrote, wired the same way every time.
- Put it in a loop with a goal - hand it an objective and let it work, step after step, until the job is done.
Steps 1 and 2 are still just an LLM call. Tools and MCP give the agent things it can do. Step 5 is where it becomes an agent: the framework runs the loop on its own, choosing a tool, reading the result, choosing again, until the goal is met.
The running project is the same SQLite todo board from Day 1. A worker picks one goal off the board, plans its own steps, does the work with its agent loop, and ticks each step off. The board code (board.py) is the same file, byte for byte; only the framework around it changes.
Today is the Microsoft Agent Framework, Microsoft's official agent SDK. It has a backstory worth knowing: Microsoft ran two separate agent efforts for years, AutoGen for multi-agent research and Semantic Kernel for production plumbing, and this framework is the two converging into one. You get a single Agent that looks almost the same in Python and C#, and underneath it carries a graph-based workflow engine for durable, long-running processes. We use the plain agent here and look at the workflow engine at the end.
Setup
Two things this day needs, both already in place from earlier weeks:
- Node, for
npx(the filesystem MCP server runs over it). Check withnode --version. OPENAI_API_KEYin the repo-root.env. Today's frameworks use OpenAI'sgpt-5.4-mini, and that key has been in your.envsince Week 1.
The Microsoft Agent Framework is part of the repo's environment, so a normal uv sync from the repo root installs everything. Open this notebook in Cursor and pick the repo's default Python 3.12.12 kernel, the same one you use every week, then run the cells top to bottom.
Warm the filesystem MCP server once now so the first run is fast, then stop it with Ctrl-C as soon as it prints that it is running:
npx -y @modelcontextprotocol/server-filesystem .# The framework prints a couple of "experimental" notices on import; quiet them.
import warnings
warnings.filterwarnings("ignore", message=r".*experimental.*")
import subprocess
from pathlib import Path
from dotenv import load_dotenv
from agent_framework import Agent, MCPStdioTool
from agent_framework.openai import OpenAIChatClient
load_dotenv(override=True)Step 1: Create the agent
In the Microsoft Agent Framework an agent is an Agent: a client for the model plus instructions, which is its system prompt. We build the client once as an OpenAIChatClient pointed at gpt-5.4-mini, reading OPENAI_API_KEY from the environment, and reuse it everywhere. The framework is async-first, so we always await the agent.
MODEL = "gpt-5.4-mini"
client = OpenAIChatClient(model=MODEL)
agent = Agent(
client=client,
instructions="You are a concise, friendly assistant. Reply in a single short sentence.",
)Step 2: Run it
Send a message, await the reply, and print the result's .text. With no tools yet there is nothing to loop over, so the agent just answers. This is still only an LLM call.
result = await agent.run("Say hello in Spanish.")
print(result.text)Our project this week: a SQLite todo board
The worker coordinates through the same tiny SQLite board as Day 1, the same board.py file: one file, one table, no server to run. A worker is handed one goal; to reach it, it writes its own step todos under that goal, ticks each one off as it goes, and closes the goal at the end. Under the hood the board is just a list of dicts (a goal has parent_id of None, a step points at its goal):
import board
board.reset_board()
board.add_goal("Read notes.txt, translate its contents into natural Spanish, and write the Spanish to spanish.txt.")
board.list_todos()show_board() prints that same data nicely, in the rich style from Week 1: each goal with its steps indented beneath, done tasks struck through in green and the one in progress in yellow. There are no steps yet; the worker writes its own when it plans.
board.show_board()Step 3: Add tools
A tool in the Microsoft Agent Framework is a plain typed Python function. The framework reads the type hints and the docstring and builds the JSON schema for you, so there is nothing else to declare; the @tool decorator exists only when you want to rename or constrain something. You hand the functions to the agent in its tools=[...] list.
We write three small board tools: show_todos to read the board, plan_steps to break a goal into steps, and complete_task to mark a todo done. Here we give a quick agent just two of them and ask what is on the board; watch it decide, on its own, to call show_todos before it answers. That decide, call, read, answer is the agent loop starting to turn. All three tools come together in step 5.
def show_todos() -> list[dict]:
"""List every todo on the board. A goal has parent_id None; a step has parent_id set to its goal's id."""
return board.list_todos()
def plan_steps(goal_id: int, steps: list[str]) -> dict:
"""Break a goal into an ordered checklist of steps on the board. Pass the goal's id and a short list of step descriptions."""
return {"goal_id": goal_id, "step_ids": [board.add_step(goal_id, step) for step in steps]}
def complete_task(task_id: int, result: str) -> dict:
"""Mark a todo (a step or the goal) with this id as done and record a short result summary."""
board.complete_todo(task_id, result)
return {"task_id": task_id, "status": "done"}board_agent = Agent(
client=client,
instructions="You help manage a shared todo board.",
tools=[show_todos, complete_task],
)result = await board_agent.run("What is on the board right now, and what is its status?")
print(result.text)Step 4: Add MCP
MCP is just more tools: ones you did not write, connected over a small protocol. We give the agent the filesystem reference server, the same Node server every framework uses this week, scoped to a single workspace folder so it can only touch files in there. In the Microsoft Agent Framework an MCP server is an MCPStdioTool: you open it with async with and pass it to the agent in the same tools=[...] list.
The framework's MCPStdioTool does not expose the server's stderr, so we subclass it to set two things on the underlying stdio client. errlog=subprocess.DEVNULL quiets the server's startup banner and lets it run from a Jupyter kernel on Windows, where the kernel's stderr has no real file descriptor. cwd starts the server in the workspace, so the agent's file names resolve there. On Mac and Linux the errlog simply keeps the output clean.
workspace = Path("workspace").resolve() # the only folder the agent may touch
class FilesystemMCP(MCPStdioTool):
"""The filesystem server with its stderr sent to DEVNULL and its working
directory set to the workspace, so file names resolve there and it runs
cleanly from a Jupyter kernel on Windows."""
def get_mcp_client(self):
from mcp.client.stdio import StdioServerParameters, stdio_client
params = StdioServerParameters(command=self.command, args=self.args, env=self.env, cwd=str(workspace))
return stdio_client(server=params, errlog=subprocess.DEVNULL)
filesystem = FilesystemMCP(
name="filesystem",
command="npx",
args=["-y", "@modelcontextprotocol/server-filesystem", str(workspace)],
)file_agent = Agent(
client=client,
instructions="You can read and write files in your workspace. Use your tools to do what is asked.",
tools=[filesystem],
)async with filesystem:
result = await file_agent.run("Read notes.txt and summarize it in one short sentence.")
print(result.text)Step 5: Put it in a loop with a goal
Now the payoff. Give one agent all three board tools and the filesystem server, hand it the goal, and let it run. It plans its own steps on the board, works them with its file tools, ticks each one off, and closes the goal when the work is done. That is the agent loop running on its own: read, plan, act, check off, repeat. Watch the board fill with steps and then strike them through.
INSTRUCTIONS = """
You are a careful worker with a shared todo board and a set of file tools.
Take the pending goal and see it through. Begin by laying out a short plan: the handful of concrete steps the work itself breaks down into, added to the board under the goal. Then carry them out with your file tools, marking each step done as you finish it. Once the steps are all done, close the goal. Your files live in the single folder your tools are allowed to use.
"""
worker = Agent(
client=client,
instructions=INSTRUCTIONS,
tools=[show_todos, plan_steps, complete_task, filesystem],
)
board.reset_board()
goal_id = board.add_goal("Read notes.txt, translate its contents into natural Spanish, and write the Spanish to spanish.txt.")
board.claim_todo(goal_id)
async with filesystem:
await worker.run("Please work the pending goal on the board.")
board.show_board()Run the same worker from the terminal
Everything you just watched in step 5 is also packaged as a small script, maf_worker.py, sitting next to this notebook. It seeds the same goal, builds the same agent with the same three board tools and the filesystem MCP server, and runs the same loop, only from the command line instead of the kernel. Open a terminal in this folder and run it:
uv run maf_worker.pyYou will see the agent plan its steps, work the goal and tick each one off, then the finished board and the Spanish it wrote. This is the shape every worker takes on Day 5, where a Google ADK orchestrator launches one of these per framework as a parallel subprocess against a single shared board.
The one cool thing: durable workflows, and one API in two languages
The plain Agent is the easy surface. The reason enterprises take this framework seriously is the layer underneath: a graph-based workflow engine. You wire agents and plain functions together as typed nodes and edges, and the framework runs them as a durable, restartable process with checkpointing, streaming and human-in-the-loop approval. It is the concrete payoff of the AutoGen and Semantic Kernel merge, and it is what you reach for when a job runs long enough that it has to survive a restart. Roughly the shape, not something we run here:
from agent_framework import WorkflowBuilder
workflow = (
WorkflowBuilder()
.add_edge(researcher, writer) # each node is an agent or a plain function
.add_edge(writer, reviewer)
.build()
)The same Agent and OpenAIChatClient shapes exist in C#, so a team can move a design between Python and .NET almost line for line. And the same one-line spirit as the rest of the week: OpenAIChatClient takes a base_url, so to run on any OpenAI-compatible endpoint you point it there and change nothing else:
client = OpenAIChatClient(base_url="https://openrouter.ai/api/v1", api_key=OPENROUTER_KEY, model="openai/gpt-5.4-mini")