跳转至

第一步

机器翻译

本页由英文文档自动翻译而来,以英文页面为准。如果有读起来不对的地方,翻译页面说明了如何反馈。

首页 节奏很快:写一个服务器,运行它,调用一个工具。

这一页慢慢来:服务器能暴露的三样东西全都讲到,沿途遇到的每个概念也都给出名字。

宿主、客户端和服务器

从这里开始,每一页都会见到这三个词:

  • 宿主 是 LLM 应用:Claude、IDE、智能体运行时。用户与之对话的就是它。
  • 客户端 位于宿主内部,讲 MCP。宿主每连接一个服务器,就运行一个客户端。
  • 服务器 是你用这个 SDK 构建的东西。它向客户端暴露内容,从不直接和模型对话。

你写的是服务器。宿主是别人的产品。SDK 还提供了一个 Client,你会用它来测试自己的服务器,本页后面就会用到。

三种原语

服务器暴露的东西恰好有三种。区分它们的标准是 谁来决定使用它们

原语 由谁控制 是什么 示例
工具 模型 模型为执行操作而调用的函数 一次 API 调用、一次数据库写入
资源 应用 宿主加载进模型上下文的数据 文件内容、API 响应
提示词 用户 用户按名称调用的可复用消息模板 斜杠命令、菜单项

“由谁控制”正是这样划分的全部意义。工具会运行,是因为 模型 决定调用它。资源会被附加进来,是因为 应用 认为模型需要它。提示词会运行,是因为 用户 选了它。

Info

如果你做过 Web API,大部分直觉其实已经有了:资源 相当于 GET(加载数据,什么都不改),工具 相当于 POST(干活,可能有副作用)。提示词 在 HTTP 里没有对应物,它更接近一个用户按名称运行的已保存查询。

一个服务器,三样俱全

server.py
from mcp.server import MCPServer

mcp = MCPServer("Demo")


@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two numbers."""
    return a + b


@mcp.resource("greeting://{name}")
def greeting(name: str) -> str:
    """Greet someone by name."""
    return f"Hello, {name}!"


@mcp.prompt()
def summarize(text: str) -> str:
    """Summarize a piece of text in one sentence."""
    return f"Summarize the following text in one sentence:\n\n{text}"

三个普通函数,三个装饰器。每个装饰器就是注册的全部:

  • @mcp.tool()add 变成 工具
  • @mcp.resource("greeting://{name}")greeting 变成 资源模板:URI 里的 {name} 就是函数的参数。
  • @mcp.prompt()summarize 变成 提示词。它返回的字符串会成为一条用户消息。

其余的一切(名称、描述、参数模式),SDK 都从函数本身读取:函数名、文档字符串、类型注解。这些你都没有单独声明过。

Tip

SDK 的两半各有一条导入路径:from mcp import Clientfrom mcp.server import MCPServer。不存在 from mcp import MCPServer 这种写法。

试一试

用 MCP Inspector 运行它:

uv run mcp dev server.py

打开它打印出来的 URL。Inspector 为每种原语各设一个标签页,按顺序逐个看一遍。

工具。 只有一项:add,描述是“Add two numbers.”。表单里有一个必填的整数字段 a,另一个是 b。填好后调用,结果是 3。这张表单是 Inspector 根据 a: int, b: int 生成的。其他所有客户端也都这样做。

资源。 “Resources”列表是空的。greetingResource Templates 下面,因为 greeting://{name} 带有参数:在有人给出 name 之前,没有哪个具体的资源可以列出。填入 World 并读取:

Hello, World!

提示词。 只有一项:summarize,带一个必填参数 text。传一段文本去获取它,会收到一条 role: user 的消息,内容就是你渲染出的字符串。提示词就是这么回事:一个构建消息的函数。

Inspector 是通过 stdio 运行你的服务器的,这是 MCP 服务器可用的传输方式之一。现在还不用选;运行服务器 专门讲这个。

能力

你在 Inspector 里看到了三个标签页。它怎么知道有三个?

客户端连接时,服务器会声明自己的 能力:它会响应哪几类请求。客户端根据这份声明来决定该请求什么。这份声明你从没写过;是 MCPServer 替你声明的。

自己看一下。SDK 的 Client 可以直接接受服务器对象,并在 内存中 与之连接(没有子进程,没有端口):

import asyncio

from mcp import Client

from server import mcp


async def main() -> None:
    async with Client(mcp) as client:
        print(client.server_capabilities.model_dump(exclude_none=True))


asyncio.run(main())
{'prompts': {'list_changed': True}, 'resources': {'subscribe': True, 'list_changed': True}, 'tools': {'list_changed': True}}

这个字典就是你的服务器所声明的 能力。每个连接上来的客户端最先得知的就是它:

能力 客户端现在可以调用
tools tools/list, tools/call
resources resources/list, resources/templates/list, resources/read
prompts prompts/list, prompts/get

MCPServer 三种原语都提供,所以这三项始终都会声明。

注意这里缺了什么。completions(资源模板和提示词的参数自动补全)需要一个由你编写的处理函数,而这个服务器没有,所以这项能力不会出现,行为规范的客户端也就不会去问。所有可选项都遵循这条规则:注册了对应的东西,能力就出现;补全 会证明这一点。

Info

Client(mcp) 正是这些文档里每个示例测试时所用的那个内存客户端,你测试自己的服务器也会用它。它有整整一页:测试

你没有写的东西

回头看看这一页。你写了三个小小的 Python 函数。你 没有 写:

  • JSON Schema。a: int, b: int 就是 add 的模式。
  • 请求处理函数。tools/listresources/readprompts/get:全都替你处理好了。
  • 能力声明。MCPServer 替你生成了。
  • 一行协议代码。版本协商、JSON-RPC 分帧、能力交换:全都发生在 mcp devClient(mcp) 内部,你一眼都没见到。

这个比例,正是这个 SDK 的意义所在。

回顾

  • 宿主 是 LLM 应用,客户端 是它讲 MCP 的那一半,服务器 是你构建的东西。
  • 工具由 模型 控制,资源由 应用 控制,提示词由 用户 控制。
  • 每种原语一个装饰器:@mcp.tool()@mcp.resource(uri)@mcp.prompt()。名称、描述和模式都来自函数本身。
  • {param} 的 URI 生成的是资源 模板,与具体资源分开列出。
  • 服务器的 能力 会替你声明好,而客户端只会请求服务器声明过的内容。
  • Client(mcp) 在内存中连接服务器对象:从第一天起,它就是你的测试工具。

接下来是 连接到真实宿主:把这个服务器真正放进 Claude Desktop 或 IDE 里。然后是 测试:一页内容,一个内存客户端,从此不用再猜它到底能不能用。再之后,每种原语各有自己的一页,从模型驱动的那一种开始:工具