# 从零开始理解 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 负责工具发现、工具调用和结果返回。
```

