# 从零开始理解 MCP 作业 这份文档不是“直接给你一坨代码”,而是带你从最小概念开始,一层一层把 MCP 作业搭起来。 你现在先记住一句话: > MCP 就是把你写好的 Python 函数,包装成一个标准服务,让 Agent 可以自动发现、自动调用。 ## 0. 我们最终要做什么 老师的作业是: 用 LangGraph 做一个多角色旅行规划 Agent,通过 MCP 调用工具,生成旅行方案。 听起来很大,其实拆开只有 5 步: ```text 1. 先写普通 Python 函数 2. 把普通函数注册成 MCP Tool 3. 启动 MCP Server 4. MCP Client 获取工具列表 5. 把工具交给 Agent / LangGraph 使用 ``` ## 1. 先不谈 MCP:什么是“工具”? 假设我们有一个普通 Python 函数: ```python def calculate_bmi(weight_kg: float, height_cm: float) -> str: height_m = height_cm / 100 bmi = weight_kg / (height_m ** 2) return f"BMI = {bmi:.1f}" ``` 这个函数本质上就是一个“工具”: - 输入:体重、身高 - 处理:计算 BMI - 输出:计算结果 如果你自己调用它,就是: ```python print(calculate_bmi(100, 192)) ``` 但 Agent 不会直接知道这个函数存在。你得告诉 Agent: “我这里有个工具,名字叫 calculate_bmi,它需要 weight_kg 和 height_cm 两个参数。” 以前我们会用 LangChain 的 `@tool` 手动注册。 MCP 的做法是:把这些工具放进一个 MCP Server 里,让客户端自动发现。 ## 2. MCP Server 是什么? 你可以把 MCP Server 理解成“工具盒子”。 这个工具盒子里可以放很多工具: - 查交通 - 查酒店 - 推荐景点 - 估算预算 在代码里,创建 MCP Server 的方式是: ```python from fastmcp import FastMCP mcp = FastMCP(name="travel-tools") ``` 这句话的意思是: “我要创建一个 MCP 工具服务,名字叫 travel-tools。” ## 3. 怎么把普通函数封装成 MCP 工具? 关键只有一个装饰器: ```python @mcp.tool() ``` 例如: ```python @mcp.tool() def search_hotels(city: str, nights: int = 2, budget_per_night: int = 400) -> str: """ 查询目的地住宿区域建议。 Args: city: 目的地城市。 nights: 入住晚数。 budget_per_night: 每晚住宿预算,单位元。 """ return f"{city} 住 {nights} 晚,建议预算 {budget_per_night} 元/晚。" ``` 这里发生了三件事: 1. `@mcp.tool()` 把函数注册成 MCP 工具。 2. 函数名 `search_hotels` 会变成工具名。 3. 参数类型和注释会变成工具说明,方便模型理解什么时候该调用它。 所以你写 MCP 工具时,要特别注意两点: - 参数类型要写清楚,比如 `city: str` - docstring 要写清楚,告诉模型这个工具干什么 ## 4. 为什么一定要写参数类型? 因为 MCP Client 要把你的工具转换成模型能理解的工具 schema。 比如这个函数: ```python def search_hotels(city: str, nights: int = 2) -> str: ``` MCP 大概能理解成: ```text 工具名:search_hotels 参数: - city:字符串 - nights:整数,默认 2 返回:字符串 ``` 如果你不写类型,模型就更容易传错参数。 ## 5. Tool 和 Resource 有什么区别? 刚开始你只需要这样记: ```text Tool:让模型自动调用,用来“做事” Resource:让程序手动读取,用来“提供资料” ``` 例子: ```text 查酒店、查交通、算预算 → Tool 旅行规划原则、公司制度、固定说明文档 → Resource ``` 我们的作业里: - `search_transport` 是 Tool - `search_hotels` 是 Tool - `recommend_attractions` 是 Tool - `estimate_trip_budget` 是 Tool - `travel_policy://planning_rules` 是 Resource ## 6. stdio 模式是什么意思? 我们的 MCP Server 最后有一句: ```python mcp.run(transport="stdio") ``` `stdio` 可以理解成“本地进程通信”。 也就是说: ```text Agent 程序启动 → 自动启动 travel_mcp_server.py → 通过标准输入/输出传 JSON-RPC 消息 → 调用工具 → 拿回结果 ``` stdio 的好处: - 不需要你手动开网页服务 - 不需要端口 - 适合本地开发和课程作业 ## 7. MCP Client 是什么? Server 是工具盒子。 Client 是去连接工具盒子的人。 代码里是: ```python client = MultiServerMCPClient( { "travel-tools": { "transport": "stdio", "command": sys.executable, "args": [str(server_path)], } } ) ``` 这段意思是: “我要连接一个叫 travel-tools 的 MCP Server。它是 stdio 模式。启动命令是当前 Python 解释器,启动文件是 travel_mcp_server.py。” 然后: ```python tools = await client.get_tools() ``` 意思是: “去 MCP Server 问一下:你有哪些工具?” 这一步就是 MCP 的动态发现。 ## 8. Function Calling 在哪里? MCP Client 拿到工具以后,会把工具交给大模型 Agent: ```python agent = create_agent( model=model, tools=tools, system_prompt="你是交通规划师..." ) ``` 当用户问: ```text 我想从成都去重庆玩 3 天,帮我规划交通。 ``` 模型会判断: ```text 我需要调用 search_transport 参数 origin = 成都 参数 destination = 重庆 ``` 这一步就是 Function Calling。 然后真正执行工具的是 MCP。 所以再记一次: ```text Function Calling:模型决定调哪个工具 MCP:负责找到工具、执行工具、返回结果 ``` ## 9. LangGraph 多角色是怎么来的? LangGraph 可以把一个大任务拆成多个节点。 我们的作业拆成: ```text transport_planner 交通规划师 hotel_planner 住宿规划师 attraction_planner 景点规划师 budget_planner 预算规划师 final_planner 总规划师 ``` 每个节点做一件事。 例如交通节点: ```python async def transport_node(state): content = await ask_role( model, tools, "你是交通规划师。必须优先调用 MCP 交通工具...", state["user_request"], ) return {"transport_plan": content} ``` 这个节点的意思是: “让交通规划师 Agent 读取用户需求,调用 MCP 工具,输出交通方案。” 最后总规划师节点把前面几个角色的结果汇总成完整方案。 ## 10. 你应该按什么顺序学习代码? 不要一上来就看 `travel_agent_langgraph.py`,会晕。 建议顺序: 1. 先看 `travel_mcp_server.py` 里的 `search_hotels` 2. 再看 `search_transport` 3. 再看最下面的 `mcp.run(transport="stdio")` 4. 然后看 `travel_agent_langgraph.py` 里的 `build_mcp_client` 5. 再看 `tools = await client.get_tools()` 6. 最后再看 LangGraph 那几个 node ## 11. 你真正需要掌握的最小代码模板 以后你自己封装 MCP 工具,最小模板就是: ```python from fastmcp import FastMCP mcp = FastMCP(name="my-tools") @mcp.tool() def my_tool(name: str) -> str: """ 说明这个工具是干什么的。 Args: name: 参数说明。 """ return f"你好,{name}" if __name__ == "__main__": mcp.run(transport="stdio") ``` 你可以把它想象成: ```text 普通函数 + @mcp.tool() + mcp.run() ``` 这就是最小 MCP Server。 ## 12. 本作业你怎么讲给老师听? 你可以这样说: ```text 我先用 FastMCP 封装了一个本地旅行工具服务,里面有交通查询、酒店查询、景点推荐和预算估算四个 Tool。 然后在 LangGraph 主程序里用 MultiServerMCPClient 连接这个 MCP Server,并通过 get_tools 动态获取工具列表。 接着我把这些工具交给不同角色的 Agent,包括交通规划师、住宿规划师、景点规划师和预算规划师。 每个角色根据用户需求调用 MCP 工具,最后由总规划师节点汇总成完整旅行方案。 在这个流程里,Function Calling 负责让模型判断要调用哪个工具和传什么参数; MCP 负责工具发现、工具调用和结果返回。 ```