使用 Python 入门 Model Context Protocol(MCP)——服务器的构建与测试

125 阅读14分钟

在本章中,我们将介绍构建与测试服务器的基础。我们会从一个使用 STDIO 传输方式的简单服务器开始。STDIO 传输是一种通过标准输入与标准输出流与服务器通信的简单方法。用 STDIO 构建服务器非常适合入门 模型上下文协议(MCP) 并理解其工作原理。还需要补充的是,STDIO 传输是与服务器通信的最常见方式,主要用于运行在你本机上的服务器。

作为本章的一部分,我们还将介绍测试服务器的不同方式。确保我们构建的内容按预期工作非常重要。我们会涵盖可视化工具、CLI(命令行)以及代码中的多种测试方法。

在本章中,你将学到如何:

  • 使用 STDIO 传输构建一个简单服务器
  • 描述服务器的核心概念
  • 通过不同工具测试服务器

本章涵盖以下主题:

  • 一个 STDIO 服务器
  • 概念
  • 运行时
  • 测试服务器
  • 第一个服务器

一个 STDIO 服务器

使用 STDIO 传输意味着服务器通过标准输入/输出流与客户端通信。这表示服务器可以通过标准输入(stdin)读取来自客户端的输入,并通过标准输出(stdout)把输出返回给客户端。听起来很简单,但它具体如何工作呢?

MCP 使用 JSON-RPC 2.0 作为其线路格式。因此,它需要把流式消息转换为 JSON-RPC 格式以进行传输,并将接收到的 JSON-RPC 消息再转换回流式消息。想象一下在终端中的如下示例:

>> hello world

要在 STDIO 服务器中使用,上述消息会被转换为 JSON-RPC 格式,看起来如下:

{
  "jsonrpc": "2.0",
  "method": "sendMessage",
  "params": {
    "message": "hello world"
  },
  "id": 1
}

这个例子很有意思,但对开发者而言,真正重要的是两点:

  1. 如何构建一个 STDIO 服务器。 我们很快会在后续小节中展示。
  2. 为什么重要,以及“你的服务器是 STDIO 服务器”意味着什么。 这意味着你的服务器运行在本机上,并通过标准输入/输出流与客户端通信。我们会在第 4 章和第 5 章展示如何构建能通过互联网与其他服务器通信的服务器。

概念

接下来看看服务器可以提供的核心概念(也可理解为“特性”)。

Resources(资源)

这是服务器可以提供给客户端的数据与上下文。MCP 的预期用法是:客户端在与服务器通信时带有一个 LLM。此用例中,资源作为上下文,在提示(prompt)时可添加给 LLM。设想如下场景的发生:

image.png

图 3.1——资源场景

在该场景中,这些上下文确保终端用户得到更好的结果,因为服务器的上下文与用户的提示配对,有点像简化版的 RAG(检索增强生成) 模式。也就是说,你将用户的提示与自己的数据配对,以获得更优的响应。

一个按这种思路实现的具体示例是用户像这样请求商品:

Prompt
用户:我在找一台新笔记本电脑

image.png

图 3.2——资源交互示例

在这个特定的商品查询中,我们调用资源以了解该查询应访问哪张表,然后让 LLM 识别应调用哪一个工具。由于调用了资源,我们获得了关于工具选择与参数设置的额外知识。

资源也可以在不使用 LLM的情况下被使用,但思考你服务器的方式应当是:它的存在是为了增强客户端的 LLM。这意味着你提供的工具(tools)、资源(resources)和提示(prompts)都应当是有帮助的。

既然明白了资源何时使用,我们再谈谈它们的性质。资源是静态的,可以是服务器能够访问并与客户端共享的任何内容。需要了解的是:你可以带模板或不带模板地请求某个资源。如果只有一个文件或一项应用配置,使用固定名称(如 config)就很合理:

server.resource(
  "config",
  "config://app",
  async (uri) => ({
    contents: [{
      uri: uri.href,
      text: "App configuration here"
    }]
  })
);

在这里,每次我们都返回相同类型的信息。

然而,如果设置项种类很多,比如用户设置、日历设置等等,那么创建一个模板化版本就更合理,例如 settings://{type}

@mcp.resource("settings://{type}")
def get_setting(type: str) -> str:
    """Get a specific setting"""
    return f"Setting from file {type}!"

这些设置仍然是非变更的(静态) ,但它们数量众多,因此我们选择在同一命名空间下进行归类。

在此例中,资源名为 settings,并接收 type 作为参数。例如,settings://hello 就会匹配这个模式。

Tools(工具)

工具是服务器可以执行的函数或能力。例如数据处理函数、调用某个 API 抓取数据的工具等。对于工具,我们需要通过提供**模式(schema)**来定义输入与输出。具体写法会随所用的运行时不同而有所差异,但核心思想是:应当让工具的消费者清楚输入与输出是什么。例如,如果我们有一个接收两个数字并返回其乘积的工具,可以这样定义:

# Add a multiplication tool
@mcp.tool()
def multiply(a: int, b: int) -> int:
    """Multiply two numbers"""
    return a * b

在这个例子中,我们定义了一个名为 multiply 的工具,它接收两个数字作为输入并返回它们的乘积。输入与输出通过 Python 的类型注解来定义。

Prompts(提示)

提示是模板化的消息或工作流,用来引导客户端与服务器之间的交互。一个好的例子是:在电商应用的上下文中,帮助客户端撰写商品描述或标语的模板。与资源和工具类似,提示也可以接收输入。下面是一个接收商品名称并返回商品描述的提示示例:

@mcp.prompt()
def describe_product(product: str) -> str:
    return f"Write a product description for {product}"

这里我们定义了一个名为 describe_product 的提示,它接收商品名称作为输入并返回商品描述。输入通过 Python 类型注解来定义。

现在我们知道服务器可以包含哪些内容,那么我们还需要了解些什么呢?

运行时(Runtimes)

当前 MCP 官方支持的运行时包括 TypeScript、Python、.NET、Java 与 Kotlin、Rust、Go。支持列表仍在不断增加。要获取最新的运行时清单,请参阅 MCP 文档:modelcontextprotocol.io/docs/sdk。每种运行时都有相应可用的 SDK。它们的实现思路相似,但也存在一些差异。

很兴奋吧?那就开始吧!

测试服务器(Testing the server)

用于测试服务器的工具有很多。之所以要测试,是为了确保服务器按预期工作。本章将介绍以下工具。

Inspector

这是一个可以同时提供 UICLI 界面的命令行工具;CLI 更适合脚本与自动化场景。
Inspector 工具通过 npx 运行一个 Node.js 包,因此请确保已安装 Node.js 运行时。即便你通过 Python 命令来调用 inspector,本质上也是 Python 去包装并调用底层的 Node.js 进程。

UI 主要用于手动测试与调试。下例展示如何运行 inspector 工具。运行下面的命令时,请确保所在目录与服务器文件相同。

Python SDK 安装了一个可执行程序 mcp,可用于帮助运行服务器:

mcp dev server.py

通过该工具,我们可以在可视化模式下测试服务器。下图是 inspector 工具的截图:

image.png

图 3.3——Inspector 工具

请确保在可视化工具中指定以下字段:

  • Transport Type:STDIO
  • Commandmcp
  • Argumentsrun server.py

你也可以在 CLI 模式 下运行 inspector,这对脚本与自动化很有用。命令与前面几乎相同,但需要增加 --cli 标志:

npx @modelcontextprotocol/inspector --cli mcp run server.py --method tools/list

这里我们添加了 --method 参数,并跟上 tools/list,表示要列出服务器上的所有工具。

请注意,前面 Python 侧的 mcp dev 实际是对 Node.js 工具(即 inspector)的一层封装。如果你想完全访问 inspector 的全部功能,建议直接运行它(无论是给 Node.js 还是 Python 用),也就是如下所示的命令:

npx @modelcontextprotocol/inspector

cURL

cURL 这样的标准命令行工具可用于向服务器发送请求,这通常用于测试 SSEStreamable HTTP 作为传输方式的服务器。其他能发起 Web 请求的工具也同样适用。需要注意的是,curl 只有在服务器对外提供网络访问(例如 SSE 服务器)时才可用。对于 STDIO 服务器,你需要使用 inspector 或自定义客户端来发送请求。来看一个典型的 curl 命令:

curl -X POST -H "Content-Type: application/json" -d '{"method": "tools/list", "params": {}, "id": 1}' http://localhost:3000/sse

在上述命令中,我们向服务器发送了一个 POST 请求,调用 tools/list 方法。服务器会返回可用工具的列表。这是使用 CLI 模式 inspector 的一个不错替代方案。

测试(Tests)

你也可以为服务器编写单元测试。这不仅是对使用 inspector 的有益补充,还能在 CI/CD 流水线中运行,用于确保服务器按预期工作。你可以使用任意测试框架,并向服务器添加资源、工具与提示,然后对其进行测试。下面是一个测试示例:

@pytest.mark.anyio
async def test_add_tool_decorator(self):
    mcp = FastMCP()
    @mcp.tool()
    def add(x: int, y: int) -> int:
        return x + y
    assert len(mcp._tool_manager.list_tools()) == 1

在上述示例中,我们完成了以下工作:

  • 使用 pytest 框架测试服务器
  • 创建一个新的服务器实例并添加名为 add 的工具
  • 断言该工具已被添加到服务器上,且工具数量等于 1

这是一个简单的测试,但展示了如何利用 pytest 来测试服务器。

现在,我们已经对服务器的概念与特性有了良好的把握;接下来,让我们写下构建第一个服务器的计划。

第一个服务器(First server)

要构建我们的第一个服务器,先按以下步骤搭建一个简单服务器:

  • 创建新项目:新建项目并安装所需依赖,设置必要的环境。强烈建议创建虚拟环境,以避免全局安装库。使用虚拟环境可使各项目相互隔离,避免库版本冲突。

  • 安装依赖:根据你所用的运行时不同,列出并安装本项目的依赖。

  • 添加服务器代码:在这里加入服务器功能;同时讨论该功能的输入、输出及其 schema。

  • 用 Inspector 测试服务器:Inspector 可以帮助你确认新功能工作正常。它既能以 CLI 模式 运行,也能以可视化模式(提供浏览器 UI)运行:

    • CLI 模式适合 CI/CD 场景,因为它在终端返回 JSON
    • 可视化模式更适合开发者手动确认服务器行为是否正确

让我们开始写代码吧!

第 1 步:创建新项目

在编写任何代码之前,我们需要一个项目骨架,以便顺利开展。项目中将包含编写服务器所需的一切,以及用于测试与运行服务器的测试与脚本。

创建如下目录结构:

├── src/
|---- server.py

创建虚拟环境:

python -m venv venv

激活虚拟环境:

venv\Scripts\activate

你应当能在终端提示符中看到虚拟环境的名称。这说明虚拟环境已被激活,之后安装的包都会进入该环境。

很好!现在我们已经有了虚拟环境与目录结构。接下来安装依赖。

第 2 步:安装依赖

在终端运行以下命令:

pip install "mcp[cli]"

这将安装 MCP SDK 以及 CLI 工具

第 3 步:添加服务器代码

将以下代码加入 server.py

# server.py
from mcp.server.fastmcp import FastMCP

# Create an MCP server
mcp = FastMCP("Demo")

# Add a multiply tool
@mcp.tool()
def multiply(first: int, second: int) -> int:
    """Multiply two numbers"""
    return first * second

# Add a dynamic greeting resource
@mcp.resource("greeting://{name}")
def get_greeting(name: str) -> str:
    """Get a personalized greeting"""
    return f"Hello, {name}!"

@mcp.prompt()
def review_code(code: str) -> str:
    return f"Please review this code:\n\n{code}"

上述代码完成了以下工作:

  • 创建名为 Demo 的 MCP 服务器实例
  • 添加名为 multiply 的工具,接收两个数字输入并返回它们的乘积(译注:原文一句话写成“返回它们的和”,应为乘积,以代码为准)
  • 添加名为 greeting 的资源,接收姓名并返回个性化问候
  • 创建名为 review_code 的提示(prompt),接收代码片段并返回代码评审提示语

第 4 步:使用 Inspector 测试服务器

这里我们使用 Inspector 测试服务器。Inspector 是一个 CLI 工具,既能提供 UI,也能提供 CLI 界面;CLI 适合脚本与自动化,UI 适合手动测试与调试。

在终端运行:

mcp dev server.py

这会在 端口 6274 上启动 Inspector 工具。

在浏览器访问:

http://localhost:6274

它会启动一个带可视化界面的 Web 服务,允许你测试示例。

在可视化界面中,请按如下填写字段:

  • Command"mcp"
  • Arguments"run server.py"

点击 Connect
切换到 Tools 标签并点击 List Tools,应能看到 multiply。点击 multiply,并按如下填写:

  • first2
  • second4

你应在 Tool Result 字段中看到结果 8

很好!现在我们已经有了一个可工作的服务器,并能用 Inspector 进行测试。

第 5 步:以 CLI 模式使用 Inspector 测试

在本小节中,我们直接以 CLI 模式 运行 Inspector。Inspector 是一个 Node.js 应用,而 mcp dev 只是其包装。

在撰写本文时,mcp dev 尚未支持 Inspector 的全部功能。因此我们展示如何直接以 Node.js 应用的方式运行 Inspector(这也是它的实现方式)。

下面是一些在 Inspector 中可运行的实用命令:

列出工具(List tools)

npx @modelcontextprotocol/inspector --cli mcp run server.py --method tools/list

这会列出服务器上所有可用工具,你应看到如下输出(示例):

{
  "tools": [
    {
      "name": "multiply",
      "description": "Multiply two numbers",
      "inputSchema": {
        "type": "object",
        "properties": {
          "first": { "title": "First", "type": "integer" },
          "second": { "title": "Second", "type": "integer" }
        },
        "required": ["first", "second"],
        "title": "multiplyArguments"
      },
      "outputSchema": {
        "type": "object",
        "properties": {
          "result": { "title": "Result", "type": "integer" }
        },
        "required": ["result"],
        "title": "multiplyOutput"
      }
    }
  ]
}

这里你能以 JSON 形式看到服务器上的全部工具。我们当前只有一个 multiply,可以看到其 inputSchema 里包含 firstsecond 参数。

调用工具(Call a tool)

npx @modelcontextprotocol/inspector --cli mcp run server.py --method tools/call --tool-name multiply --tool-arg first=2 --tool-arg second=4

你应看到类似以下响应:

{
  "content": [
    { "type": "text", "text": "8" }
  ],
  "structuredContent": { "result": "8" },
  "isError": false
}

列出资源(List resources)

npx @modelcontextprotocol/inspector --cli mcp run server.py --method resources/list

这会列出服务器上所有资源,你可能会看到如下输出:

{
  "resources": []
}

怎么是空的?原因在于资源模板化资源的区别。用下面命令即可列出模板化资源:

npx @modelcontextprotocol/inspector --cli mcp run server.py --method resources/templates/list

你应得到类似:

{
  "resourceTemplates": [
    {
      "uriTemplate": "greeting://{name}",
      "name": "get_greeting",
      "description": "Get a personalized greeting"
    }
  ]
}

读取模板化资源(Call a templated resource)

npx @modelcontextprotocol/inspector --cli mcp run server.py --method resources/read --uri greeting://chris

你应看到如下响应:

{
  "contents": [
    {
      "uri": "greeting://chris",
      "mimeType": "text/plain",
      "text": "Hello, chris!"
    }
  ]
}

列出提示(List prompts)

npx @modelcontextprotocol/inspector --cli mcp run server.py --method prompts/list

输出类似:

{
  "prompts": [
    {
      "name": "review_code",
      "description": "",
      "arguments": [
        { "name": "code", "required": true }
      ]
    }
  ]
}

调用提示(Call a prompt)

npx @modelcontextprotocol/inspector --cli mcp run server.py --method prompts/get --prompt-name review_code --prompt-args code="print('Hello World')"

你应看到如下响应:

{
  "messages": [
    {
      "role": "user",
      "content": {
        "type": "text",
        "text": "Please review this code:\n\nprint('Hello World')"
      }
    }
  ]
}

(可选)你还可以在 server.py 中再添加一个资源,例如:

@mcp.resource("command://ping")
def get_echo() -> str:
    """Send pong"""
    return "Pong"

然后这样读取:

npx @modelcontextprotocol/inspector --cli mcp run server.py --method resources/read --uri command://ping

你应看到如下响应:

{
  "contents": [
    {
      "uri": "command://ping",
      "mimeType": "text/plain",
      "text": "Pong"
    }
  ]
}

请注意,你在调用资源资源模板时,都是通过同一个 uri 参数来完成的。

总结

在本章中,我们讲解了如何构建你的第一个服务器。对于入门示例,我们使用 STDIO 传输方式,创建了一个运行在本机上的服务器。我们还借助名为 inspector 的工具,探索了多种测试服务器功能的方法。Inspector 提供两种模式:CLI 模式可视化模式。前者适用于 CI/CD 场景,后者便于开发者快速尝试与调试功能。

在接下来的章节中,我们将介绍 SSE 传输方式——当你希望服务器通过 URL 被访问与使用时,可以选用该方式。