MCP

MCP: 模型上下文协议

无mcp时常见痛点:

  • 每个 AI 应用都要重复接一遍外部系统
    例如同样是接 GitHub、Slack、数据库、文件系统,Cursor 要写一套,Claude Desktop 要写一套,自研 Agent 平台又要写一套。

  • 每个框架和宿主各有自己的接法
    即使大家都支持 Tool / Function Calling,真正落地时仍然要处理:服务如何发现、参数 schema 怎么描述、鉴权怎么传、进程怎么启动、结果怎么返回。

  • 工具很难复用成“生态能力”
    没有统一协议时,一个工具即使写得很好,也往往只能服务于某个特定应用,迁移和复用成本很高。

MCP 出现的背景,不是“以前没人会写工具”,而是:

大家都在写工具,但缺少一套跨应用、跨框架、跨宿主都能复用的统一连接标准

一句话:让“外部工具、资源、提示词模板”等能力,能够按统一协议被不同 AI 应用发现和使用。

定义: 是一套开放的标准协议,用于规范 AI 应用 / Agent / IDE / 聊天客户端 如何与 外部工具、资源和上下文提供方 交互

区别:

概念 解决什么问题 典型关注点
Tool / Function Calling 模型如何调用一个具体工具 工具 schema、参数、调用结果
RAG 模型如何拿到外部知识上下文 文档加载、切块、检索、上下文拼接
MCP 外部能力如何被标准化暴露与接入 Host / Client / Server、协议、传输、发现
Agent 谁来规划、决策、调用这些能力 推理、编排、记忆、执行闭环
  • Tool 解决“能不能调用”
  • RAG 解决“能不能拿到知识”
  • MCP 解决“怎么统一接入”
  • Agent 解决“谁来决定何时调用”

MCP 不是在替代 Tool,而是在 Tool 之上再向上抽象了一层“协议层”。

MCP的核心能力

类型 作用 控制方式
Tools 可执行动作,例如查天气、查数据库、发请求 模型可触发
Resources 可读取内容,例如文件、配置、数据库 schema、API 响应 应用 / 宿主决定如何使用
Prompts 可复用的提示词模板 / 工作流模板 用户显式选择更常见

其他能力

类型 作用
Sampling 服务器通过客户端向宿主侧的 LLM 请求一次生成(把“算力在哪一侧”也纳入协议协作)
Elicitation 服务器通过客户端向用户请求补充信息
Logging 服务器向客户端发送结构化日志
Progress / Notifications 长任务过程中的进度和通知

 

MCP架构知识

角色 含义
MCP Host(MCP 主机) 用户真正交互的应用,例如 IDE、桌面客户端、聊天应用、自研 AI 平台
MCP Client(MCP 客户端) Host 内部负责和某个 MCP Server 建立协议连接的组件
MCP Server(MCP 服务器) 对外暴露 Tools / Resources / Prompts 等能力的服务
  • Host 是你在用的应用
  • Client 是 Host 内部的协议连接组件

 

主线:

  1. Host / Client 发起连接
  2. Client 和 Server 做初始化
  3. 双方声明各自支持的 capabilities
  4. 客户端发现服务器提供的 tools / resources / prompts
  5. 按需调用或读取
  6. 结果返回给 Host,再由模型 / UI 使用

一次完整的MCP调用:

  1. 握手与能力发现(Handshake & Discovery)
    Host 启动后,会根据配置连接 MCP Server,并完成初始化。此时客户端会知道:这台服务器提供了哪些 Tools、Resources、Prompts,以及它支持哪些 capabilities。

  2. 用户提问与上下文注入(Context Injection)
    用户提出问题后,Host 会把“用户问题 + 已发现的工具说明 / 资源信息 / 提示词信息”一并提供给模型或应用逻辑。

  3. 模型或应用做决策(Reasoning / Decision)
    模型决定是否需要调用某个 Tool,或者应用决定是否读取某个 Resource、选用某个 Prompt。

  4. 路由与执行(Routing & Execution)
    Host / Client 按协议把请求发给 MCP Server。Server 在自己的进程或远端服务中执行真正的逻辑,例如查天气、读文件、查数据库。

  5. 结果回传与继续生成(Result Feedback)
    Server 把结果返回给 Client,Client 再把结果交回 Host,由 Host 继续让模型生成最终回答,或者直接展示给用户。

两类常见传输

  1. stdio
  2. Streamable HTTP

更准确的理解应该是:

  • STDIO:本地子进程通信
  • Streamable HTTP:独立服务进程,通过 HTTP 通信,必要时可配合 SSE 流式返回

安全边界

  • 写操作要有人确认:删除、外呼、支付、批量修改这类 Tool,不要默认让模型静默执行。
  • HTTP 服务要做鉴权和来源校验:至少要考虑 token、会话身份、Origin / 来源校验,而不是“能连上就算接入成功”。
  • 本地服务尽量收口暴露范围:能只监听本机就不要默认对公网开放,避免把调试用 MCP Server 直接变成外网入口。
  • 日志与业务数据要分级处理:进度、错误、调试日志很有用,但不要把敏感配置、私有数据原样暴露给模型或不可信客户端。

了解FASTAPI用法:

# 创建服务实例
from mcp.serve.fastmcp import FastMCP
mcp = FastMCP("Demo")

# 注册Tool
@mcp.tool()
def add(a: int, b: int)-> int:
    return a + b

# 注册Resource
@mcp.resource("greeting://default")
def get_greeting() -> str:
    return "Hello from static resource!"

# 注册Prompt
@mcp.prompt()
def greet_user(name: str, style:str = "friendly") -> str:
    return f"为{name}生成问候语"

# 启动服务
mcp.run(transport="stdio")
# 或者
mcp.run(transport="streamable-http")
# 历史写法
# mcp.run(transport="sse", host="127.0.0.1", port=8000)

从底层SDK视角看客户端

 以官方 SDK 思路来看,一个 MCP 客户端的典型动作通常是:

  1. 建立传输连接
  2. 创建 ClientSession
  3. 调用 initialize()
  4. list_tools() / list_resources() / list_prompts()
  5. call_tool() / read_resource() / get_prompt()

STDIO

import asyncio
from mcp import ClientSession,StdioServerParameters
from mcp.client.stdio import stdio_client

async def main():
    server_params = StdioServerParameters(
        command="python",
        args= ["mcp_server_stdio.py"]
      )
     async with stdio_client(server_params) as (read,write):
         async with ClientSession(read, write) as session:
            await session.initialize()
                tools = await session.list_tools()
                result = await session.call_tool("add", {"a": 1, "b": 2})
                print(tools)
                print(result)
asyncio.run(main())

Streamable HTTP 差异主要在“连接方式”变成了远程 URL

import asyncio
from mcp import ClientSession
from mcp.client.streamable_http import streamable_http_client

async def main():
    async with streamable_http_client("http://127.0.0.1:8000/mcp") as (read, write, _):
        async with ClientSession(read, write) as session:
            await session.initialize()
            tools = await session.list_tools()
            print(tools)

asyncio.run(main())

完整调用过程理解

把协议层想象成一条完整链路,会更容易理解:

  1. 用户在 Host 中发起请求
  2. Host 内部某个 MCP Client 与对应 Server 建立会话
  3. Client 发现 Server 暴露的能力
  4. 模型或应用决定是否调用某个 Tool / 读取某个 Resource / 获取某个 Prompt
  5. Server 执行或返回结果
  6. Host 把结果展示给用户,或继续交给模型推理

流程再落到 LangChain Agent 里,就会变成:

  1. MultiServerMCPClient 连接 MCP 服务
  2. get_tools() 获取工具
  3. Agent 拿到工具列表
  4. 用户提问
  5. Agent 选择工具
  6. 工具返回结果
  7. 模型基于结果继续生成最终回答

获取天气案例

服务端:

import json
import os

# pip install mcp httpx python-dotenv
from dotenv import load_dotenv
from mcp.server.fastmcp import FastMCP
import httpx

load_dotenv()

# 构造函数只接受「服务名」;网络绑定信息在 run() 时再指定
mcp = FastMCP(
    "WeatherServerSSE"
)  # "WeatherServerSSE" 就是你自己起的名,可改成 "MyWeather" 等


@mcp.tool()
def get_weather(city: str) -> str:
    """查询指定城市的即时天气信息。city 为城市英文名,如 Beijing、Shanghai。"""
    url = "https://api.openweathermap.org/data/2.5/weather"
    params = {
        "q": city,
        "appid": os.getenv("OPENWEATHER_API_KEY"),
        "units": "metric",
        "lang": "zh_cn",
    }
    resp = httpx.get(url, params=params, timeout=10)
    data = resp.json()
    return json.dumps(data, ensure_ascii=False)


if __name__ == "__main__":
    # host、port 在 run() 时传入,不是构造函数。
    # 这里启动后,mcp.json 中的 weather 服务就可以按约定地址连到它。
    mcp.run(transport="sse", host="127.0.0.1", port=8000)

客户端:

- """从同目录的 mcp.json 加载 MCP 服务配置,使用 langchain_mcp_adapters 的 MultiServerMCPClient 连接多台
  MCP 服务器并获取工具列表,再交给 LangChain 的 create_tool_calling_agent + AgentExecutor,形成
  「LLM + MCP 工具」的对话 Agent。这也是第 21 章里“外部工具接入 Agent”的代表案例。
- mcp.json 是“客户端侧的连接配置约定”,不是 MCP 协议本身。它描述的是“有哪些服务、分别怎么连”,
  例如本仓库里既有网络方式的 weather 服务,也有 stdio 方式的 fetch 服务。
- 流程:加载 mcp.json → 初始化 MultiServerMCPClient → 异步获取 MCP Tools → 创建 DeepSeek 模型与
  提示模板 → 组装 Agent 与 AgentExecutor → 启动命令行聊天循环(输入 quit 退出)。
- 本案例重点展示“把 MCP Tools 交给 LangChain Agent”;Resources 和 Prompts 虽然也是 MCP 能力,
  但这里没有作为主线展开。
- 这个文件延续了仓库里更容易教学的 classic Agent 路线;如果改走更偏 1.x 的直接路线,也常见
  `await client.get_tools()` 之后把工具交给 `create_agent`,再配合 `ainvoke()` / `astream()` 使用。
- 依赖:pip install langchain-mcp-adapters langchain-openai langchain-classic loguru;部分适配器要求 Python 3.12 及以下。需配置环境变量 deepseek-api(或改用其他兼容 OpenAI 的 api_key/base_url)。
"""

import asyncio
import json
import os
from pathlib import Path

from loguru import logger

# 默认 mcp.json 路径(与本文件同目录)
_MCP_JSON_PATH = Path(__file__).resolve().parent / "mcp.json"


def load_servers(file_path: str | Path | None = None) -> dict:
    """
    加载 MCP 服务器配置。
    :param file_path: 配置文件路径,默认使用同目录下的 mcp.json
    :return: 完整配置字典,如 {"mcpServers": {"weather": {...}, "fetch": {...}}}

    这里读取的是“客户端如何连接服务”的约定配置,而不是协议本体。
    """
    path = Path(file_path) if file_path else _MCP_JSON_PATH
    if not path.exists():
        logger.warning(f"未找到 mcp 配置文件: {path}")
        return {"mcpServers": {}}
    with open(path, "r", encoding="utf-8") as f:
        config = json.load(f)
    logger.info(
        f"已加载 mcp 配置: {path},共 {len(config.get('mcpServers', {}))} 个服务"
    )
    return config


async def run_chat_loop(config_path: str | Path | None = None) -> None:
    """
    启动并运行一个基于 MCP 工具的聊天 Agent 循环。
    该函数会:1)加载 MCP 服务器配置;2)初始化 MCP 客户端并获取工具;
    3)创建基于 DeepSeek 的语言模型和 Agent;4)启动命令行聊天循环;5)退出时清理资源。
    """
    try:
        from langchain_mcp_adapters.client import MultiServerMCPClient
    except ImportError as e:
        logger.error(
            "请先安装 langchain-mcp-adapters: pip install langchain-mcp-adapters(部分环境需 Python 3.12 及以下)"
        )
        raise e

    from langchain_openai import ChatOpenAI
    from langchain_classic.agents import AgentExecutor, create_tool_calling_agent
    from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

    config = load_servers(config_path)
    servers = config.get("mcpServers", {})
    if not servers:
        logger.warning("mcp.json 中未配置任何服务,无法获取 MCP 工具")
        return

    # 初始化 MCP 客户端:connections 就是 mcp.json 中的 mcpServers 字典
    # 每个条目描述一台 MCP 服务该如何连接,例如 stdio 子进程或 HTTP/SSE 地址
    client = MultiServerMCPClient(connections=servers)

    # 按官方默认用法,MultiServerMCPClient 是无状态的;获取工具时使用异步接口即可
    tools = await client.get_tools()
    if not tools:
        logger.warning(
            "未从 MCP 服务获取到任何工具,请确认服务已启动且 mcp.json 配置正确"
        )
        return

    logger.info(f"已获取 {len(tools)} 个 MCP 工具: {[t.name for t in tools]}")

    # 语言模型(DeepSeek,与截图一致;可改为其他 OpenAI 兼容接口)
    llm = ChatOpenAI(
        model="deepseek-chat",
        api_key=os.getenv("deepseek-api"),
        base_url="https://api.deepseek.com",
    )

    # 对话提示:系统提示要求使用工具完成用户请求,agent_scratchpad 供 Executor 填入中间步骤
    prompt = ChatPromptTemplate.from_messages(
        [
            ("system", "你是一个有用的助手,需要使用提供的工具来完成用户请求。"),
            ("human", "{input}"),
            MessagesPlaceholder(variable_name="agent_scratchpad"),
        ]
    )

    agent = create_tool_calling_agent(llm, tools, prompt)
    agent_executor = AgentExecutor(
        agent=agent,
        tools=tools,
        verbose=True,
        handle_parsing_errors="解析用户请求失败,请重新输入清晰的指令",
    )

    logger.info("\n MCP Agent 已启动,请先输入一个提问给(LLM+MCP),输入 'quit' 退出")

    while True:
        try:
            user_input = input("\n您: ").strip()
            if not user_input:
                continue
            if user_input.lower() == "quit":
                logger.info("已退出")
                break
            result = agent_executor.invoke({"input": user_input})
            output = result.get("output", result)
            print(f"\nAgent: {output}")
        except KeyboardInterrupt:
            logger.info("已退出")
            break


def main() -> None:
    asyncio.run(run_chat_loop())


if __name__ == "__main__":
    main()

这个文件最值得学习的地方有两个:

第一,它说明 MCP 和 Agent 是怎么接起来的。
MCP 不负责帮你规划,也不负责帮你推理;它负责把工具接进来。真正决定“什么时候调用天气工具”的,是 Agent。

第二,它说明 LangChain 对 MCP 的支持已经不只停留在工具发现。
根据 LangChain 官方文档,除了 get_tools(),现在还可以:

  • get_resources()
  • get_prompt()
  • 使用 stateful session
  • 处理 logging / elicitation / structured content

mcp.json:

某些 MCP Host / Client / 适配器常用的客户端连接配置文件。

也就是说,它解决的是:

  • 要连接哪几台服务器
  • 每台服务器用什么 transport
  • URL / command / args 是什么

这里的 mcp.json,可以这样理解:

  • weather
    • 走 sse
    • 指向本地天气服务
  • fetch
    • 走 stdio
    • 通过命令启动一个本地 MCP Server
{
  "mcpServers": {
    "weather": {
      "url": "http://127.0.0.1:8000/sse",
      "transport": "sse"
    },
    "fetch": {
      "command": "uvx",
      "args": ["mcp-server-fetch"],
      "transport": "stdio"
    }
  }
}

 

posted @ 2026-05-08 14:33  幻影之舞  阅读(60)  评论(0)    收藏  举报