从零开始理解MCP作业.md 7.9 KB

从零开始理解 MCP 作业

这份文档不是“直接给你一坨代码”,而是带你从最小概念开始,一层一层把 MCP 作业搭起来。

你现在先记住一句话:

MCP 就是把你写好的 Python 函数,包装成一个标准服务,让 Agent 可以自动发现、自动调用。

0. 我们最终要做什么

老师的作业是:

用 LangGraph 做一个多角色旅行规划 Agent,通过 MCP 调用工具,生成旅行方案。

听起来很大,其实拆开只有 5 步:

1. 先写普通 Python 函数
2. 把普通函数注册成 MCP Tool
3. 启动 MCP Server
4. MCP Client 获取工具列表
5. 把工具交给 Agent / LangGraph 使用

1. 先不谈 MCP:什么是“工具”?

假设我们有一个普通 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
  • 输出:计算结果

如果你自己调用它,就是:

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 的方式是:

from fastmcp import FastMCP

mcp = FastMCP(name="travel-tools")

这句话的意思是:

“我要创建一个 MCP 工具服务,名字叫 travel-tools。”

3. 怎么把普通函数封装成 MCP 工具?

关键只有一个装饰器:

@mcp.tool()

例如:

@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。

比如这个函数:

def search_hotels(city: str, nights: int = 2) -> str:

MCP 大概能理解成:

工具名:search_hotels
参数:
- city:字符串
- nights:整数,默认 2
返回:字符串

如果你不写类型,模型就更容易传错参数。

5. Tool 和 Resource 有什么区别?

刚开始你只需要这样记:

Tool:让模型自动调用,用来“做事”
Resource:让程序手动读取,用来“提供资料”

例子:

查酒店、查交通、算预算 → Tool
旅行规划原则、公司制度、固定说明文档 → Resource

我们的作业里:

  • search_transport 是 Tool
  • search_hotels 是 Tool
  • recommend_attractions 是 Tool
  • estimate_trip_budget 是 Tool
  • travel_policy://planning_rules 是 Resource

6. stdio 模式是什么意思?

我们的 MCP Server 最后有一句:

mcp.run(transport="stdio")

stdio 可以理解成“本地进程通信”。

也就是说:

Agent 程序启动
→ 自动启动 travel_mcp_server.py
→ 通过标准输入/输出传 JSON-RPC 消息
→ 调用工具
→ 拿回结果

stdio 的好处:

  • 不需要你手动开网页服务
  • 不需要端口
  • 适合本地开发和课程作业

7. MCP Client 是什么?

Server 是工具盒子。

Client 是去连接工具盒子的人。

代码里是:

client = MultiServerMCPClient(
    {
        "travel-tools": {
            "transport": "stdio",
            "command": sys.executable,
            "args": [str(server_path)],
        }
    }
)

这段意思是:

“我要连接一个叫 travel-tools 的 MCP Server。它是 stdio 模式。启动命令是当前 Python 解释器,启动文件是 travel_mcp_server.py。”

然后:

tools = await client.get_tools()

意思是:

“去 MCP Server 问一下:你有哪些工具?”

这一步就是 MCP 的动态发现。

8. Function Calling 在哪里?

MCP Client 拿到工具以后,会把工具交给大模型 Agent:

agent = create_agent(
    model=model,
    tools=tools,
    system_prompt="你是交通规划师..."
)

当用户问:

我想从成都去重庆玩 3 天,帮我规划交通。

模型会判断:

我需要调用 search_transport
参数 origin = 成都
参数 destination = 重庆

这一步就是 Function Calling。

然后真正执行工具的是 MCP。

所以再记一次:

Function Calling:模型决定调哪个工具
MCP:负责找到工具、执行工具、返回结果

9. LangGraph 多角色是怎么来的?

LangGraph 可以把一个大任务拆成多个节点。

我们的作业拆成:

transport_planner   交通规划师
hotel_planner       住宿规划师
attraction_planner  景点规划师
budget_planner      预算规划师
final_planner       总规划师

每个节点做一件事。

例如交通节点:

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 工具,最小模板就是:

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")

你可以把它想象成:

普通函数 + @mcp.tool() + mcp.run()

这就是最小 MCP Server。

12. 本作业你怎么讲给老师听?

你可以这样说:

我先用 FastMCP 封装了一个本地旅行工具服务,里面有交通查询、酒店查询、景点推荐和预算估算四个 Tool。
然后在 LangGraph 主程序里用 MultiServerMCPClient 连接这个 MCP Server,并通过 get_tools 动态获取工具列表。
接着我把这些工具交给不同角色的 Agent,包括交通规划师、住宿规划师、景点规划师和预算规划师。
每个角色根据用户需求调用 MCP 工具,最后由总规划师节点汇总成完整旅行方案。

在这个流程里,Function Calling 负责让模型判断要调用哪个工具和传什么参数;
MCP 负责工具发现、工具调用和结果返回。