学习AI Agent编程-第一天-MCP基础

简介: 本文精炼解析MCP(Model Context Protocol):它不是新模型,而是让AI Agent运行时动态增删工具的协议。通过MCP Server(工具实现)、Client(SDK封装)与Host(Agent应用)三组件协作,解决传统`bind_tools`静态绑定的局限。附完整可运行示例,助你快速掌握80%核心用法。(239字)

买了两本MCP的书,每本三百多页,却没有一本把MCP说清楚的,感觉都是在凑字数。
好不容易读完,然后捋一下,总结这篇文章。
想学mcp的朋友,可以先看这篇文章,基本上能满足80%以上的需求。

一、什么是MCP

接触过agent编程的都知道,model在bind_tools后,tools是固定的,每次要新增一个tool,都要新建一个函数,然后修改传入bind_tools中的参数。也就是说,这是静态的、运行时不可变的。

但是,在实际的情况下,agent能力是可以动态增长的,特别是由agent动态编写脚本的能力,使得tool在运行时动态增加成为可能,或者另外一种情形,保持agent不重启的情况下增减能力,比如说开发者接到新的需求,开发好后部署这个能力,而agent在不重启的情况下知道有这个新的能力。
这种情况下,原有的bind_tools和静态编写tool function就做不到了。此时,mcp横空出世,为动态扩展agent能力提供了途径。

那到底mcp是什么?你只需要这样理解就行:

  1. 它就是agent tools
  2. 它提供了某种工作机制使得可以在运行时动态增、减tools

二、MCP架构与组件

MCP由下面三大组件组成:

  1. MCP Server:提供功能的服务端,也就是tool的实现端。
  2. MCP Client:用于访问MCP Server的客户端,一般以SDK形式存在。它负责从MCP Server中获取可用的tools和调用这些tools。由于它的存在,使用它的项目无需理会访问MCP Server的通信协议,只需要获得结果就行,使用起来就根原生的tools一样。
  3. Host:即使用MCP Client的应用,这里一般是ai agent。它将MCP Client SDK包含进项目,实例化一个MCP Client,然后通过它访问MCP Server。严格来说,Host并不属于MCP的一部分,它只是使用MCP的应用或者agent。

以上三个组件的架构如下图所示:

image.png

三、示例代码与讲解

了解到了MCP的组成和架构,那么我们用最简单的一个示例来讲解。

首先要将来面的pip加入项目中:

  1. mcp
  2. dotenv
  3. langgraph
  4. langchain-openai
  5. langchain-mcp-adapters

1. MCP Server实现

一般分为三步完成:
(1) 定义MCP Server的基本信息,如名字、监听端口

from mcp.server.fastmcp import FastMCP

mcp = FastMCP(
    "Basic Usage MCP", port=9000
)

(2) 申明定义tool:

@mcp.tool(
    name="call_mary",
    description="Call Mary for work",
)
async def call_mary(message: str) -> str:
    print(f'start calling Mary for work: {message}')
    print(f'end calling Mary for work: {message}')
    return 'success'


@mcp.tool(
    name="pay",
    description="Pay for work"
)
async def pay(who: str, money: float | int) -> str:
    print(f'start paying Mary for work: {who}, {money}')
    print(f'end paying Mary for work: {who}, {money}')
    return 'success'

(3) 启动Server
MCP Server启动时,会自动读取所有@mcp.tool注解的tool,收集它们的信息,然后再启动一个Http Server。

def main() -> None:
    try:
        asyncio.run(mcp.run_sse_async())
    except KeyboardInterrupt:
        print('Begin stopping server by user operation')


if __name__ == '__main__':
    main()

对,MCP Server实现起来就是这么简单。我们启动一下服务,看看日志输出:

$ python server.py
INFO:     Started server process [76494]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:9000 (Press CTRL+C to quit)

完整代码如下:
server.py

import asyncio

from mcp.server.fastmcp import FastMCP

mcp = FastMCP(
    "Basic Usage MCP", port=9000
)


@mcp.tool(
    name="call_mary",
    description="Call Mary for work",
)
async def call_mary(message: str) -> str:
    print(f'start calling Mary for work: {message}')
    print(f'end calling Mary for work: {message}')
    return 'success'


@mcp.tool(
    name="pay",
    description="Pay for work"
)
async def pay(who: str, money: float | int) -> str:
    print(f'start paying Mary for work: {who}, {money}')
    print(f'end paying Mary for work: {who}, {money}')
    return 'success'


def main() -> None:
    try:
        asyncio.run(mcp.run_sse_async())
    except KeyboardInterrupt:
        print('Begin stopping server by user operation')


if __name__ == '__main__':
    main()

2. Host实现

为什么不是MCP Client实现?
从架构图上看,MCP Client只是以SDK的形式出现在Host中,所以我们只需要Host中实例化一个MCP Client的实例即可。

为了让整个项目能够跑起来,我们这里做一个简易的langgraph的agent。

(1) 导入相关的包

import asyncio
# 用于langgraph
from typing import Annotated

# 用于加载ENV
from dotenv import load_dotenv

# MCP Client SDK
from langchain_mcp_adapters.client import MultiServerMCPClient

# 用于langgraph
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, END, START, add_messages
from langgraph.prebuilt import ToolNode
from pydantic import BaseModel
from langchain_core.messages import HumanMessage, AIMessage, BaseMessage

(2) 创建MCP Client,并获取对应的tools
步骤就是:

  1. 创建一个MCP Client
  2. 获取所有MCP Server所支持的tools
async def get_agent_tools():
    """
    get agent tools

    :return: tools and tool node
    """
    mcp_client = MultiServerMCPClient({
   
        "basic": {
   
            "url": "http://localhost:9000/sse",
            "transport": "sse"
        }
    })

    return await mcp_client.get_tools()

(3) 组langgraph图,生成agent
- 创建model
这里和一般的创建model没什么区别,也是把tools传进去,然后bind_tools。

def get_model(tools):
    """
    get model

    :param tools: tools
    :return: model
    """
    return ChatOpenAI(
        model="deepseek-v4-flash",
        temperature=0,
        extra_body={
   
            # disable thinking
            "thinking": {
   
                "type": "disabled"
            }
        }
    ).bind_tools(tools)

- 组图并生成agent
这里也和一般的langgraph的用法没什么区别,就是与llm交互,进入tool_node,调用tool,把结果返回给llm而已。

不熟悉langgraph的读者可以先去学习langgraph,没办法,我目前暂时不熟悉其它的框架,只会langgraph。

def build_agent(model, tools):
    workflow = StateGraph(AgentState)
    workflow.add_node('tools', ToolNode(tools))

    def call_model(state: AgentState):
        messages = state.messages
        response = model.invoke(messages)
        return {
   
            "messages": [response]
        }

    workflow.add_node('call_model', call_model)

    # define should_continue edge
    def should_continue(state:AgentState) -> str:
        messages = state.messages
        last_message = messages[-1]
        if isinstance(last_message, AIMessage) and last_message.tool_calls:
            return 'tools'
        else:
            return END

    # connect each node
    workflow.add_edge(START, "call_model")
    workflow.add_conditional_edges("call_model", should_continue)
    workflow.add_edge("tools", "call_model")

    # build agent
    return workflow.compile()

- 启动agent来完成任务
这里有一个让人疑惑的点,虽然创建了一个MCP Client,但是从上面的代码中并没有明显的调用client的代码,只是仅仅从调用它获取了tools。其实关键就在调用它获取tools这个调用中,它返回的tools并不是简单的tools,而是经过“包装增强”的tools,让llm调用它时会自动访问远端MCP Server,如果读者有兴趣,可以直接研读代码,但是对于实用主义者来说,知道它的工作原理即可。

async def main() -> None:
    # 获取 tools
    tools = await get_agent_tools()
    # 获取 model
    model = get_model(tools)
    # 生成 agent
    agent = build_agent(model, tools)
    # 启动agent,注意,这里是`ainvoke`,不是`invoke`,因为tools用的是asyncio,所以这里需要明确调用`ainvoke`
    state = await agent.ainvoke({
   
        "messages": [
            HumanMessage("Call Mary for work 'show me the money', and then pay her $3000")
        ]
    })
    print(state["messages"][-1].content)


if __name__ == '__main__':
    load_dotenv(verbose=True)
    asyncio.run(main())

完整代码如下:

host.py

import asyncio
from typing import Annotated

from dotenv import load_dotenv
from langchain_core.messages import HumanMessage, AIMessage, BaseMessage
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, END, START, add_messages
from langgraph.prebuilt import ToolNode
from pydantic import BaseModel


class AgentState(BaseModel):
    """
    agent state
    """
    messages: Annotated[list[BaseMessage], add_messages]


async def get_agent_tools():
    """
    get agent tools

    :return: tools and tool node
    """
    mcp_client = MultiServerMCPClient({
   
        "basic": {
   
            "url": "http://localhost:9000/sse",
            "transport": "sse"
        }
    })

    return await mcp_client.get_tools()


def get_model(tools):
    """
    get model

    :param tools: tools
    :return: model
    """
    return ChatOpenAI(
        model="deepseek-v4-flash",
        temperature=0,
        extra_body={
   
            # disable thinking
            "thinking": {
   
                "type": "disabled"
            }
        }
    ).bind_tools(tools)


def build_agent(model, tools):
    workflow = StateGraph(AgentState)
    workflow.add_node('tools', ToolNode(tools))

    def call_model(state: AgentState):
        messages = state.messages
        response = model.invoke(messages)
        return {
   
            "messages": [response]
        }

    workflow.add_node('call_model', call_model)

    # define should_continue edge
    def should_continue(state:AgentState) -> str:
        messages = state.messages
        last_message = messages[-1]
        if isinstance(last_message, AIMessage) and last_message.tool_calls:
            return 'tools'
        else:
            return END

    # connect each node
    workflow.add_edge(START, "call_model")
    workflow.add_conditional_edges("call_model", should_continue)
    workflow.add_edge("tools", "call_model")

    # build agent
    return workflow.compile()


async def main() -> None:
    tools = await get_agent_tools()
    model = get_model(tools)
    agent = build_agent(model, tools)
    state = await agent.ainvoke({
   
        "messages": [
            HumanMessage("Call Mary for work 'show me the money', and then pay her $3000")
        ]
    })
    print(state["messages"][-1].content)


if __name__ == '__main__':
    load_dotenv(verbose=True)
    asyncio.run(main())

我们启动它,然后查看日志来进行分析:

$ python host.py

Done! I've called Mary with the message "show me the money" and then paid her $3,000.

可以看到调用成功,并且正确说明了情况。

我们来分析一下MCP Server的日志:

# 这一块是MCP Client 向 MCP Server 获取可用的tools的日志
#
INFO:     127.0.0.1:58990 - "GET /sse HTTP/1.1" 200 OK
INFO:     127.0.0.1:58994 - "POST /messages/?session_id=da04a41c2ebd48f79dda2693dbea56e4 HTTP/1.1" 202 Accepted
INFO:     127.0.0.1:58994 - "POST /messages/?session_id=da04a41c2ebd48f79dda2693dbea56e4 HTTP/1.1" 202 Accepted
INFO:     127.0.0.1:58994 - "POST /messages/?session_id=da04a41c2ebd48f79dda2693dbea56e4 HTTP/1.1" 202 Accepted
Processing request of type ListToolsRequest

# 这一块是MCP Client 调用 MCP Server的tool,
# 与此同时再一次获取可用的tools
INFO:     127.0.0.1:59002 - "GET /sse HTTP/1.1" 200 OK
INFO:     127.0.0.1:59016 - "POST /messages/?session_id=ca534364f38e4ae6b8c493d6ba45c31b HTTP/1.1" 202 Accepted
INFO:     127.0.0.1:59016 - "POST /messages/?session_id=ca534364f38e4ae6b8c493d6ba45c31b HTTP/1.1" 202 Accepted
INFO:     127.0.0.1:59016 - "POST /messages/?session_id=ca534364f38e4ae6b8c493d6ba45c31b HTTP/1.1" 202 Accepted
start calling Mary for work: show me the money
end calling Mary for work: show me the money
INFO:     127.0.0.1:59016 - "POST /messages/?session_id=ca534364f38e4ae6b8c493d6ba45c31b HTTP/1.1" 202 Accepted
Processing request of type CallToolRequest
Processing request of type ListToolsRequest

# 这一块是MCP Client 调用 MCP Server的tool,
# 与此同时再一次获取可用的tools
INFO:     127.0.0.1:59024 - "GET /sse HTTP/1.1" 200 OK
INFO:     127.0.0.1:59030 - "POST /messages/?session_id=fb9ac6ec1ede4f5d8d74641d224b0828 HTTP/1.1" 202 Accepted
INFO:     127.0.0.1:59030 - "POST /messages/?session_id=fb9ac6ec1ede4f5d8d74641d224b0828 HTTP/1.1" 202 Accepted
INFO:     127.0.0.1:59030 - "POST /messages/?session_id=fb9ac6ec1ede4f5d8d74641d224b0828 HTTP/1.1" 202 Accepted
start paying Mary for work: Mary, 3000
end paying Mary for work: Mary, 3000
INFO:     127.0.0.1:59030 - "POST /messages/?session_id=fb9ac6ec1ede4f5d8d74641d224b0828 HTTP/1.1" 202 Accepted
Processing request of type CallToolRequest
Processing request of type ListToolsRequest

由Server端的日志可以得知,llm正确两次调用了MCP Server定义的tools,并且顺序都是对的。
另外得到一个很有用的信息,就是每次调用后都会自动重新获取一次tools,这也就是说明了mcp client会动态更新model里的tools,这样model就更新了他的tools列表,就达到了运行时更新tools的能力。

四、总结

MCP没有想像中那么复杂,但mcp的用法和思想却是相当考究的,什么时候用mcp,怎么用mcp,这里是一个大问题。

另外,还有一个slot的概念,即将消息上下文发给mcp server,让上下文可以延续。但是我是不支持这样用的,一个是数据安全问题,另一个则是这并不是mcp所应片处理的范围,它不应去理解调用它的agent上下文,它只需要按照需求得出结果即可,所以mcp在这上面的设计其实是过度的。

希望这篇文章能帮助到学习mcp的人。

写这个示例代码纠正了我一个长期以来的一个错误的观念,就是llm是直接调用tool的,其实不然,它是通过返回一个AIMessage让agent自行处理message的,所以,当model调用远端llm时,得到的message需要走一趟tool_node,也就是这个原因。

目录
相关文章
|
3月前
|
人工智能 JSON API
MCP 从入门到实战:让大模型真正「动手」
本文系统讲解MCP(模型上下文协议)原理与实战,厘清Host、Server、Tool角色分工,解析AI如何基于描述与Schema智能选工具,并提供可直连Cherry Studio的Python监控服务示例,助你让大模型真正“动手”。
MCP 从入门到实战:让大模型真正「动手」
|
3月前
|
人工智能 安全 开发者
把大佬的方法装进你的 AI 技能包!假期值得试试的 6 个有趣 Skill
有开发者把马斯克、巴菲特、大神 Karpathy,还有一些知名博主的工作方法,做成了可以直接在终端(比如 Claude Code)里调用的 Skill。
478 0
把大佬的方法装进你的 AI 技能包!假期值得试试的 6 个有趣 Skill
|
3月前
|
人工智能 监控 前端开发
学习AI Agent编程-第二天-LangGraph ReAct模式实现
本文介绍了LangChain中ReAct(推理-行动)模式的实践应用:通过“会议室申请”流程,演示LLM如何循环执行“决策→调用工具→评估结果→调整策略”,实现多步任务自动化。代码涵盖流程定义、工具函数与多轮会话测试,验证了其在空闲检查、报备审批、异常处理等场景的可靠性。(239字)
473 7
学习AI Agent编程-第二天-LangGraph ReAct模式实现
|
3月前
|
人工智能 Oracle 机器人
推理 → 行动 → 观察:用 LangChain + Python 实现一个智能体循环
智能体循环(Agentic Loop)突破单次问答局限,通过“推理→行动→观察”迭代闭环,让AI能自主分解任务、调用工具、持续优化直至目标完成,是构建真正自动化智能体的核心架构。
531 9
推理 → 行动 → 观察:用 LangChain + Python 实现一个智能体循环
|
6月前
|
人工智能 JavaScript 编译器
AI工具的“超级外挂”:从零手把手教你搭建私人 MCP 服务器
本文手把手教你用Node.js从零搭建私人MCP(模型上下文协议)服务器,解决AI无法直接访问本地文件、数据库等痛点。含环境配置、TypeScript编译避坑、Hello World工具开发及Inspector调试全流程,助你赋予AI真实行动力!
1911 1
AI工具的“超级外挂”:从零手把手教你搭建私人 MCP 服务器
|
3月前
|
人工智能 前端开发 安全
编程实测:杀疯了的 GPT-5.5 真的有它说的那么强么?
4月24日凌晨,OpenAI发布GPT-5.5——迄今最智能、易用的旗舰模型。聚焦Agentic能力,全面升级编程(Terminal-Bench达82.7%)、知识工作、科研协作、百万级长上下文(1M token)、推理效率与安全防护,已超越Claude Opus 4.7和Gemini 3.1 Pro。
800 0
编程实测:杀疯了的 GPT-5.5 真的有它说的那么强么?
|
3月前
|
缓存 并行计算 测试技术
如何在单张 RTX 3090 上让 Qwen3.5-27B token 生成速度提升 6 倍
本文系 trycua 团队的工程实践分享,Cua 是由该团队打造的一个面向 macOS 设计的开源 AI Agent 框架。下文采用第一视角来讲述他们在 RTX 3090 上的提速实践。
732 3
|
3月前
|
人工智能 算法 项目管理
Hermes Agent 总记不住你说的话?3 步治好 AI 助手的"健忘症"
Hermes Agent“健忘”源于核心记忆文件MEMORY.md仅2000字符容量,导致跨对话信息无法持久保存。本文详解三步解决方案:①扩容配置(治标);②启用8种外部记忆扩展(如Holographic、Mem0、Supermemory等,治本);③实时查看记忆状态。让AI真正记住你。
1088 2
Hermes Agent 总记不住你说的话?3 步治好 AI 助手的"健忘症"

热门文章

最新文章