Skip to main content
Glama
Nuo-chen-287

xiaohei-companion

by Nuo-chen-287

About MCP

写在前面的话

本文由浅入深的介绍了 MCP 的相关内容,不同于技术栈或者工程化知识点的层层递进,本文只是简单介绍,具体应用环节还需要在实战过程中慢慢体会。

本文的代码地址:

Related MCP server: Amadeus-QQ-MCP

时间线

我们先来对齐一下时间线:

时间

事件

说明

2022 年 10 月

ReAct 现世

论文ReAct:Synergizing Reasoning and Acting 发表,确立了推理 + 行动交替的 Agent 范式

2022 年末 ~ 2023 年中

Tool Use 现世

这一阶段的工具调用靠的是文本的正则解析:Langchain 的 ReAct 式 Agent 把 Action 或者 Action Input 拿出来,随后逐步向 「让 LLM 输出 JSON 迈进」

2023 年 6 月 13 日

OpenAI 协议的 Tool Use

OpenAI 正式发布 Function Calling,把工具参数标准化为 JSON Schema

2024 年 11 月 25日

MCP 现世

Anthropic 发布 Model Context Protocol ,支持两种传输方式:Stdio & SSE + HTTP

2025 年 3 月 26 日

Streamable HTTP 协议 MCP

使用 Streamable 取代 SSE + HTTP

2025 年 10 月 16 日

Skill 现世

Anthropic 发布 Agent Skills

V1:单纯的Function Call

在 V1 中,我们先不涉及到任何 MCP,我们先看看 Function Call 的一个致命痛点。

我们先要定义一个工具,也就是写一个工具函数:

def get_xiaohei_status(detail: str = "all") -> dict:
    """模拟一个数据源:返回小黑此刻的状态。真实项目里这里会查数据库 / 调 API。"""
    full = {
        "name": "小黑",
        "mood": "有点想你",        # 心情
        "hunger": 72,             # 饥饿度 0-100,越高越饿
        "last_played_hours": 6,   # 距离上次陪它玩过去了几小时
    }
    if detail == "mood":
        return {"name": full["name"], "mood": full["mood"]}
    return full

那么,如果要让LLM 能够调用这个函数,需要给这个工具写好描述,也就是:

TOOLS_SCHEMA = [
    {
        "type": "function",
        "function": {
            "name": "get_xiaohei_status",
            "description": "查询电子宠物小黑此刻的状态,包括心情、饥饿度、距上次陪玩的时间。",
            "parameters": {
                "type": "object",
                "properties": {
                    "detail": {
                        "type": "string",
                        "enum": ["all", "mood"],
                        "description": "all=返回全部状态;mood=只返回心情",
                    }
                },
                "required": [],
            },
        },
    }
]

也就是以规定的格式来描述一下我们的 Tool,然后,把注册好的工具给到 LLM,让 LLM 知道工具的作用以及使用方法:

  response = client.chat.completions.create(
      model=MODEL,
      messages=messages,
      tools=TOOLS_SCHEMA,   # 关键:把 schema 交给 LLM
  )

最后用户Prompt进来之后,LLM判断要调用此工具,那么就在调用之后将工具返回的结果和用户Prompt一起给到LLM,得到最终回复。

这是一个成功完成Tool Use 也就是Function Call的过程,这其中有一个很致命的点:

如果别人要用这个工具,该怎么办?

我们工具函数和工具说明书都是自己来定的,注册方式也写到了代码中,完全无法拆开,这也就是真正的痛点,我们将在V2中解决它。

V2:Tool = MCP Sever

还是一样的工具函数,这里的一个关键点是我们在工具函数的前面添加了mcp的装饰器 @mcp.tool()

from mcp.server.fastmcp import FastMCP

# 给这个 Server 起个名字。Client 连上来 initialize 时会看到它。
mcp = FastMCP("xiaohei-companion")


@mcp.tool()
def get_xiaohei_status(detail: str = "all") -> dict:
    """查询电子宠物小黑此刻的状态,包括心情、饥饿度、距上次陪玩的时间。

    Args:
        detail: all=返回全部状态;mood=只返回心情。
    """
    # 注意:这段函数体和 v1 几乎一字不差——真正搬家的只是"它住在哪/怎么被找到"。
    # schema 不用手写了,FastMCP 会读上面的类型注解(detail: str)和 docstring 自动生成。
    full = {
        "name": "小黑",
        "mood": "有点想你",
        "hunger": 72,
        "last_played_hours": 6,
    }
    if detail == "mood":
        return {"name": full["name"], "mood": full["mood"]}
    return full

这个装饰器的作用有两个:

  1. 把函数的备注抄成description

  2. 添加 MCP 的类型判断

先说第一个,我们把这个函数的注释写在了函数头的下面,已经封装好的 MCP 会直接把这一段注释当成这个Tool 的 Description

至于第二个,被封装在了 FastMCP 内部,FastMCP 会自己来校验工具函数入参的类型,比如这里只支持字符串,那么如果在传入参数时,不是字符串,就会被拦在外面,不会进入到工具函数内容不。

我们可以通过下面的代码来获取到工具的相关信息:

session.initialize()

tools = await session.list_tools()
print("📋 Server 自报的工具列表(schema 是 Server 自动生成的):")
for t in tools.tools:
    print(f"  - {t.name}: {t.description}")
    print(f"    inputSchema: {t.inputSchema}")

获取到的信息如下所示:

📋 Server 自报的工具列表(schema 是 Server 自动生成的):
  - get_xiaohei_status: 查询电子宠物小黑此刻的状态,包括心情、饥饿度、距上次陪玩的时间。

Args:
    detail: all=返回全部状态;mood=只返回心情。

    inputSchema: {'properties': {'detail': {'default': 'all', 'title': 'Detail', 'type': 'string'}}, 'title': 'get_xiaohei_statusArguments', 'type': 'object'}

到这里,我们就解决了 V1 的痛点,工具函数 当然还是要自己来写,但是描述和格式校验可以有统一的标准了,我们要在 V3 中来详细聊聊调用的过程了。

V3:MCP Client + LLM

这里就进入到 MCP 的使用环节了,核心伪代码如下:

连接(Server):                          # ① 建立会话
    mcp_tools   = Server.list_tools()              # ② 问 Server 有哪些工具
    llm_tools   = [翻译(t) for t in mcp_tools]      # ③ MCP格式 → function calling 格式

    messages = [system, user问题]

    答复 = LLM(messages, tools=llm_tools)           # ④ 第一次问 LLM

    while 答复.要调工具:                             # ⑤ LLM 说"我要调工具"
        for 调用 in 答复.tool_calls:
            结果 = Server.call_tool(调用.名字, 调用.参数)   # ⑥ Client 替 LLM 去 Server 执行
            messages.append(结果)                         # ⑦ 把结果塞回对话
        答复 = LLM(messages, tools=llm_tools)        # ⑧ 再问一次 LLM

    return 答复.文字                                 # ⑨ 没有要调工具了 → 自然语言回复

这个过程无数人讲烂了已经,我就不再赘述了,我想讲几个点,首先是协议的问题

首先,MCP协议只有一种那就是 JavaScipt Object Notation - Remote Procedure Call ,简称为 JSON-RPC协议

它的核心很简单,就是想调用本地的函数一样,去调用另一台机器上的函数,人话 = 调用接口。

其次,MCP讨论比较高的是传输协议,我们在时间线表格里面梳理过了,MCP一共有三种协议,分别是:Stdio传输,SSE + HTTP 传输,Streamable + HTTP 传输。由于SSE + HTTP已经被废弃了,我们就不在多说,我们聊聊剩下两个。再开始之前,我们先规定好传输协议在哪一层,不然越聊越懵:

 --------------------------------------
| MCP 协议层:JSON-RPC 消息 						 | 
| initialize / list_tools / call_tool  | -> 永远不变
----------------------------------------
| 传输层,负责吧上面的 Json 从 A 搬到 B    | -> Stdio / Streamable + HTTP / ...
|--------------------------------------

Stdio 传输(Standard Input/Output )就像他的名字一样,传输方式是 Cline 当场讲他作为子进程拉起,也就是本地 MCP 的常见调用方式,既不需要端口,也不需要配置,不暴露到外侧。

Streamable + HTTP 的传输方式是,Server 端和 Client 端独立存在,Server 端可以被多个 Client 调用,虽然多了一层 HTTP 的开销,但是他可以解决一些场景下的难题,比如:

  • 一个工具要跑三十秒,Server 想中途推进度 -> 一直链接着,进度随时更新

  • Server 想反过来回调一下 Clinet -> 支持回调

F
license - not found
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Nuo-chen-287/MCP-Basis'

If you have feedback or need assistance with the MCP directory API, please join our Discord server