MCP 与 A2A 的代码入口在哪:微软 AI Agent 入门课第 11 课
翻 ai-agents-for-beginners 第 11 课的时候,很容易被章节标题带偏。11-agentic-protocols/README.md 里 MCP、A2A、NLWeb 三节篇幅相当,看完像是三条并列的路线。但真要动手,第一个问题是很实际的:我该打开哪个文件?
答案不对称。11-agentic-protocols/code_samples/ 下面只有四个入口:11-mcp-agent-framework.ipynb、11-a2a-agent-framework.ipynb,以及 github-mcp/ 和 mcp-agents/ 两个目录。NLWeb 在正文里有完整一节,写明每个 NLWeb 部署同时也是一个 MCP server、对外暴露 ask 方法,但 code_samples/ 下没有对应的目录,README 的 Resources 只给了 NLWeb 仓库链接。所以「三个协议」在文档层面是三条,在代码层面是两条半。
下面顺着代码走一遍。
两个 notebook 都自述是模拟
先说容易踩空的一点:那两个带 agent-framework 字样的 notebook,都没有真的说协议。
11-mcp-agent-framework.ipynb 在「Simulating MCP Tool Discovery」这一节里明确写了,因为真实的 MCP server 需要一个运行中的服务进程,所以这里改用 @tool 函数来演示同一个模式。它定义的 search_accommodations 和 get_local_experiences 两个函数,函数体里就是写死的字典,注释里注明生产环境下这些工具应当从 MCP server 动态发现。装饰器写法是 @tool(approval_mode="never_require"),参数用 Annotated 标注描述,然后交给 client.as_agent(tools=[...], name=..., instructions=...)。这里的 client 是 agent_framework.foundry.FoundryChatClient,靠 AZURE_AI_PROJECT_ENDPOINT 与 AZURE_AI_MODEL_DEPLOYMENT_NAME 两个环境变量初始化,缺一个就在 cell 里直接 raise ValueError。
notebook 末尾提到,要接真实的 MCP server,可以走 hosted_mcp_tool() 或 MAF 的 MCP client 集成——但这一句只给了名字,没有示例代码,示例链接指向 Microsoft Agent Framework 自己的仓库。
11-a2a-agent-framework.ipynb 同理,它自述是把三个专职 agent(CurrencyExchangeAgent、ActivityPlannerAgent、TravelManagerAgent)串成一条工作流来模拟 A2A 式的消息传递,用的是 WorkflowBuilder(start_executor=currency_agent).add_edge(a, b).build(),然后 workflow.run(..., stream=True) 流式消费事件,靠 event.type == "output" 与 isinstance(event.data, AgentResponseUpdate) 判断是否是 agent 的增量输出。notebook 自己在「Understanding A2A in Production」一节里说得很直白:这是一个简化的进程内版本,真实部署里每个 agent 要暴露 HTTP 端点、发布 Agent Card、通过 A2A 的 JSON-RPC 通信。
所以如果你的目的是理解 A2A 协议报文长什么样,这份 notebook 帮不上忙;它教的是编排形态。协议本身的组成部分只在 README.md 的文字里:Agent Card(名称、任务描述、技能列表、当前 Endpoint URL、版本与能力)、Agent Executor、Artifact、Event Queue,一共四块。
真正跑协议的那条链在 mcp-agents/
code_samples/mcp-agents/ 是这一课里唯一一份完整的客户端加服务端实现,用的是 MCP Python SDK 的 StreamableHTTP 传输。它的 README.md 开头一句话点了题:能不能在 MCP 上做 agent 之间的通信——可以。
服务端在 server/server.py,核心是 class ResumableServer(Server),构造函数里用 @self.list_tools() 和 @self.call_tool() 两个装饰器注册处理函数。handle_list_tools 返回三个 Tool:travel_agent、research_agent、long_running_agent。注意这里的措辞——agent 是以 MCP tool 的形式暴露出去的,README 里也写明了这一点:本文把 agent 视为 MCP server 上的一个 tool。
跟一次 travel_agent 调用,看它在代码里经过哪几层:
handle_call_tool拿到ctx = self.request_context,按步骤列表循环;- 每步调
ctx.session.send_progress_notification(progress_token=ctx.request_id, progress=..., total=100, message=step, related_request_id=str(ctx.request_id))推进度; - 走到确认价格那一步时调
ctx.session.elicit(message=..., requestedSchema=PriceConfirmationSchema.model_json_schema(), related_request_id=ctx.request_id),PriceConfirmationSchema是一个 pydanticBaseModel,只有confirm: bool和notes: str两个字段; - 客户端
client/client.py里注册的elicitation_callback收到这个请求,在终端里问一句是否接受,返回types.ElicitResult(action="accept"|"decline", content={...}); - 服务端按
elicit_result.action分支:accept继续订,decline置booking_cancelled = True并推一条 100% 的「Booking cancelled by user」进度后break。
research_agent 那条分支换成了 ctx.session.create_message(messages=[SamplingMessage(...)], max_tokens=100, related_request_id=ctx.request_id),也就是 sampling——服务端反过来向客户端要模型输出。其中 100 只是仓库示例代码里写的取值,不是什么推荐配置。客户端 client/client.py 的 sampling_callback 在这份示例里返回的是写死的一段 mock 文本,套在 types.CreateMessageResult 里;mcp-agents/README.md 讲 sampling 的那段示例代码还专门留了一行注释,说真实应用里这里可以去调 LLM API。这两条分支都被 try/except 包住,日志文案是「这在测试里是正常的」,elicitation 那条的 except 后面还跟着注释 # Continue with booking anyway for fallback——就代码结构而言,异常不会终止整条流程,而是继续往下走。
断线重连是这份示例真正想演示的东西
travel_agent 只是壳,mcp-agents 的重点在 resumability。这条链涉及的文件与符号是这样接上的:
server/event_store.py里的class SimpleEventStore(EventStore),把事件存在内存 list 里,提供store_event(stream_id, message)和replay_events_after(last_event_id, send_callback);server/server.py的create_server_app(event_store)把它传给StreamableHTTPSessionManager(app=server, event_store=event_store, json_response=False, security_settings=security_settings),json_response=False那行的注释写明是要走 SSE 流;- 挂载路径是
Mount("/mcp", app=session_manager.handle_request); - 客户端
streamablehttp_client(server_url, headers=headers, terminate_on_close=False),terminate_on_close=False后面直接跟了注释# Enable resumption; - 重连时的 header 用的是从 SDK 导入的
MCP_SESSION_ID_HEADER与MCP_PROTOCOL_VERSION_HEADER; - token 落盘由
client/utils.py的class TokenManager负责,默认文件名常量DEFAULT_TOKEN_FILE = "resumption_tokens.json"(这是仓库当前代码里的默认值,随版本可能变动),save_tokens里还专门挡了一道:session_id或resumption_token为空、或者 token 以pending_/temp_开头,一律拒绝写文件; - 发请求时通过
ClientMessageMetadata携带,有旧 token 就传resumption_token=...,没有就传on_resumption_token_update=enhanced_callback等服务端下发。
README.md 的 Getting Started 给的两条命令是:
# Start the server with event store for resumption
python -m server.server --port 8006
# In another terminal, run the interactive client
python -m client.client --url http://127.0.0.1:8006/mcp
8006 与 server.py 里 argparse 的 --port 默认值一致,这是仓库当前代码里的默认值,随版本可能变动。服务端还有一个 --no-event-store 开关,加上之后 run_server 不创建 SimpleEventStore,日志会打「no resumption support」——想验证「没有 event store 会怎样」就用它做对照。
README 没有单列 Windows 说明,这两条 python -m 命令在 PowerShell 或 cmd 里就是同样写法,需要开两个终端窗口。README 的验证方式是在客户端执行过程中按 Ctrl+C 打断,再重启客户端看它是否接着上次的位置继续,Windows 终端里的打断键同样是 Ctrl+C。要留意的是 DEFAULT_TOKEN_FILE 是个不带目录的相对文件名,token 文件落在你启动客户端时所在的工作目录里,换个目录启动,上一轮的 token 就不在手边了。
还有一处安全设置容易忽略:create_server_app 里的 TransportSecuritySettings(allowed_hosts=["127.0.0.1:*", "localhost:*"], allowed_origins=["http://127.0.0.1:*", "http://localhost:*"])。这是本机白名单,你把服务挂到局域网 IP 上再从别的机器连,会撞在这里而不是撞在代码逻辑上。
github-mcp 是另一种入口:连别人的 MCP server
code_samples/github-mcp/ 演示的是相反方向——不自己写 server,而是把现成的 GitHub MCP server 接进来。宿主是 Chainlit,app.py 里 @cl.on_mcp_connect 装饰的回调拿到 session: ClientSession 之后调 await session.list_tools(),把每个 tool 拼成 name / description / input_schema 三个键的字典(第三个键的值取自 t.inputSchema),按连接名存进 cl.user_session 的 mcp_tools;真正调用在 @cl.step(type="tool") 的 call_tool,先按 tool 名反查是哪个连接,再 await mcp_session.call_tool(tool_name, tool_input)。这就是第一节 README 讲的动态发现在代码里的样子:工具清单是运行时问出来的,不是写死的。
这个 demo 的 agent 编排仍然是 WorkflowBuilder,github_agent → hackathon_agent → events_agent 三段,外加一个纯正则的 route_user_input 决定走单 agent 还是走工作流。
连接命令写在 github-mcp/README.md 里,是在 Chainlit 聊天框下方的插头图标里填的:
npx -y @modelcontextprotocol/server-github --env GITHUB_PERSONAL_ACCESS_TOKEN=[YOUR PERSONAL ACCESS TOKEN]
这里有一处仓库内部对不上的地方,值得提前知道:同目录的 MCP_SETUP.md 在「Environment Variables」一节要求 .env 里配 GITHUB_TOKEN,而 README.md 的连接命令传的是 GITHUB_PERSONAL_ACCESS_TOKEN。两个名字不一样,照哪一份配都可能落空,建议以你实际使用的那版 MCP server 的说明为准。另外 MCP_SETUP.md 的前置条件里写了要先装 Node.js 与 npm——Windows 上这一步是独立于 Python 环境的,npx 能不能直接跑取决于 Node 有没有进 PATH。
顺带一提,server/server.py 里那行 await anyio.sleep(2) 后面的注释写的是 # Fixed 0.5 second delay between steps,注释和代码里的实参对不上。这类小出入在教学仓里不算少见,读代码时以实参为准。
回到那个分工问题
把三节和四个入口摆到一起,课程给出的分工其实很清楚:MCP 管 agent 到工具与数据的连接(Hosts / Clients / Servers 三个角色,Tools / Resources / Prompts 三个原语),A2A 管 agent 到 agent 的协作(Agent Card 描述能力、Agent Executor 传上下文、Artifact 装结果、Event Queue 挡住长任务期间的连接关闭),NLWeb 管网站内容对自然语言与 agent 的暴露,且它自身也以 MCP server 的形态出现。
但落到代码,能照着跑起协议交互的只有 MCP 那一支;A2A 与 NLWeb 目前留在文字与外部链接里。知道这个落差,比把三节都读一遍更省时间——想学协议报文与会话恢复,直接进 mcp-agents/;想学工具动态发现的接法,进 github-mcp/app.py;想学多 agent 编排,两个 notebook 才是对的地方,只是它们讲的其实是 WorkflowBuilder 而不是协议。该项目持续更新,上述文件路径与接口写法以仓库最新内容为准。
以上代码片段均原样取自仓库文件,未经实测,以仓库最新代码为准。
安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。
本文依据 github.com/microsoft/ai-agents-for-beginners 仓库于 2026-08-18 的公开内容整理,
事实来自仓库内的课程正文与代码示例。我们没有跑过文中涉及的代码,
因此不涉及运行结果、耗时与 Agent 实际表现的任何描述。
该课程持续更新,文中涉及的文件路径、依赖与接口写法随版本变动,请以仓库最新内容为准。
文中涉及的云端服务调用会产生费用并可能上传数据,请自行评估密钥与数据边界。