MCP Python SDK 工具(Tools)开发指南:从 `@mcp.tool()` 到 JSON Schema 的完整实践
2026/9/20 22:24:50 网站建设 项目流程
  • 人工智能
  • MCP 服务
  • MCP Clients

【免费下载链接】python-sdk

The official Python SDK for Model Context Protocol servers and clients

项目地址:https://gitcode.com/gh_mirrors/pythonsd/python-sdk
点击查看免费下载

本篇指南以 Model Context Protocol(MCP)官方 Python SDK 的@mcp.tool()装饰器为起点,系统讲解如何在 MCP Server 中声明、约束、校验和暴露工具(Tool)函数。你将掌握:如何仅凭类型注解生成客户端可见的 JSON Schema、如何用 PydanticFieldLiteral构建富约束参数、如何将参数建模为结构化对象、如何正确处理同步/异步函数,以及如何通过ToolAnnotations向客户端传达工具行为语义。全部示例与原理均可在当前仓库的docs_src/tools/src/mcp/server/mcpserver/下找到对应实现。

工具的本质:一个可被模型调用的普通函数

在 MCP 协议中,工具(Tool)是模型可以调用的函数——它把模型的能力边界从"只能对话"扩展到"可以执行真实操作",例如查询数据库、调用 API 或读写文件。

在本 SDK 中声明一个工具极其简单:在一个普通 Python 函数上加上@mcp.tool()装饰器即可,这就是完整的 API 入口。以 docs_src/tools/tutorial001.py 为例:

from mcp.server import MCPServer mcp = MCPServer("Bookshop") @mcp.tool() def search_books(query: str, limit: int) -> str: """Search the catalog by title or author.""" return f"Found 3 books matching {query!r} (showing up to {limit})."

这段代码里没有 schema、没有 JSON、没有协议细节,只有一个普通函数。SDK 只从函数中读取三样东西:

来源含义示例
函数名工具的名称(name)search_books
函数 docstring模型看到的工具描述(description)Search the catalog by title or author.
参数类型注解模型被允许传入的参数(arguments)query: strlimit: int

从源码结构看,这一推断过程发生在 Tool.from_function:函数名通过fn.__name__取得,docstring 通过fn.__doc__取得,而参数 schema 则由func_metadata(...)model_json_schema(by_alias=True)生成。此外这里还会做两件额外的事:调用validate_and_warn_tool_name校验工具命名合法性(lambda 函数必须显式命名),并自动检测函数中是否存在请求Context对象的参数。

输入 Schema:类型注解即契约

基于上述类型注解,SDK 会生成一份 JSON Schema,并在tools/list握手阶段发送给客户端:

{ "type": "object", "properties": { "query": {"title": "Query", "type": "string"}, "limit": {"title": "Limit", "type": "integer"} }, "required": ["query", "limit"], "title": "search_booksArguments" }

注意两个细节:

  • querylimit都出现在required中,因为二者都没有默认值(下文会说明如何使其可选)。
  • title键是 Pydantic 生成的产物;真正构成"契约"的是properties、字段类型和required数组。
  • schema 中没有$schema键:MCP 将不带该键的 schema 视为JSON Schema 2020-12,而 Pydantic 生成的就是这一方言,因此无需显式声明;只有在底层 low-level Server 上手工编写 schema 时才需要选择方言。

这里有一个重要的心智模型转变:类型注解在这里不是文档,而是契约。如果客户端发送"limit": "ten",SDK 会在你的函数运行之前就拒绝该调用。参数校验发生在Tool.run内部(base.py 的validate_arguments),校验失败会以ToolError的形式返回,错误信息可以直接被模型读取并用于自我纠正。

模型拿回的返回值

{"query": "dune", "limit": 5}调用该工具,结果包含两部分:

result.content # [TextContent(text="Found 3 books matching 'dune' (showing up to 5).")] result.structured_content # {'result': "Found 3 books matching 'dune' (showing up to 5)."}
  • content:模型阅读的文本内容。
  • structured_content:供客户端应用消费的类型化数据。它之所以存在,是因为你声明了返回类型-> str

现阶段不必深究structured_content——只要你从工具中返回真实的 Python 对象,SDK 就会自动处理好一切;完整机制见 Structured Output 专题文档。

用 MCP Inspector 立刻试起来

仓库提供了交互式调试工具 MCP Inspector,运行:

uv run mcp dev server.py

打开命令打印出的 URL,进入Tools标签页调用search_books。Inspector 会基于你的类型注解渲染出一个表单:必填的query文本输入框和必填的limit数字输入框。其他所有 MCP 客户端的行为与此一致——表单结构完全由服务端类型注解驱动。

可选参数:给参数一个默认值

把参数改为带默认值,它就不再是必填项。不需要任何额外的装饰器或协议配置:

@mcp.tool() def search_books(query: str, limit: int = 10) -> str: """Search the catalog by title or author.""" return f"Found 3 books matching {query!r} (showing up to {limit})."

对应生成的 schema:

{ "type": "object", "properties": { "query": {"title": "Query", "type": "string"}, "limit": {"default": 10, "title": "Limit", "type": "integer"} }, "required": ["query"], "title": "search_booksArguments" }

limitrequired中移出,同时获得"default": 10。客户端省略该参数时,函数收到10,与 Python 语义完全一致。这体现了本 SDK 的设计哲学:声明式 Type Hint + 纯 Python 语义,无需学习任何 MCP 专属语法

Field构建富约束 Schema

类型注解能覆盖大部分场景,但有时你需要对参数做更细的描述或约束。将类型包进Annotated,并追加 PydanticField即可(完整代码见 docs_src/tools/tutorial003.py):

from typing import Annotated, Literal from pydantic import Field from mcp.server import MCPServer mcp = MCPServer("Bookshop") @mcp.tool() def search_books( query: Annotated[str, Field(description="Title or author to search for.")], limit: Annotated[int, Field(ge=1, le=50, description="Maximum number of results.")] = 10, genre: Literal["fiction", "non-fiction", "poetry"] | None = None, ) -> str: """Search the catalog by title or author.""" where = f" in {genre}" if genre else "" return f"Found 3 books matching {query!r}{where} (showing up to {limit})."

这里引入了三样新东西,全部作用于参数:

  1. Field(description=...):逐参数描述,模型阅读时与 docstring 配合使用。
  2. Field(ge=1, le=50):数值边界,最终落入 schema 成为"minimum": 1, "maximum": 50
  3. Literal["fiction", "non-fiction", "poetry"]:枚举,模型只能从中选择一个值。

一个关键点是:约束不是装饰。以limit=999调用该工具,SDK 会在你的函数运行之前就返回工具错误:

Input should be less than or equal to 50

该错误会作为工具结果回传给模型,模型读取后自动用合法值重试。你只写了一次le=50,就免费获得了一个能自我纠错的 Agent 行为。genre同时演示了"可选枚举":Literal[...] | None = None意味着模型可以不传,也可以从三个类别中选一个。

如果你熟悉 FastAPI 或 Pydantic,这里的FieldAnnotated与校验逻辑完全一致,没有任何 MCP 专属的新概念需要学习。

用 Pydantic 模型承载结构化参数

当工具参数超过两三个时,应当把它们归组为一个 Pydantic 模型(完整代码见 docs_src/tools/tutorial004.py):

from pydantic import BaseModel, Field from mcp.server import MCPServer mcp = MCPServer("Bookshop") class Book(BaseModel): title: str author: str year: int = Field(ge=1450, description="Year of first publication.") @mcp.tool() def add_book(book: Book) -> str: """Add a book to the catalog.""" return f"Added {book.title!r} by {book.author} ({book.year})."

这里Book的 schema 会以$defs引用的方式嵌套进工具的整体输入 schema 中,客户端侧模型会把它当做一个 JSON 对象来填充,而你的函数收到的是一个已经完成校验的真实Book实例,可以直接访问.title.author.year属性,无需任何手动解析。

而且这种建模方式可以自由混搭:普通参数与模型参数并列、嵌套模型、模型列表都支持——"Pydantic 一路到底"。

同步与异步:async def的正确姿势

如果工具涉及 I/O(调用 API、读文件、查数据库),请把它声明为async def并在内部await,SDK 会直接等待它:

@mcp.tool() async def fetch_book(isbn: str) -> str: data = await client.get(f"/books/{isbn}") return data

普通的def工具同样可用:SDK 会将其放入线程池运行,从而保证不阻塞整个服务器的事件循环。从 Tool.from_function 的is_async_callable(fn)检测可以看到,SDK 在注册时即识别函数的异步属性,并据此选择调用策略。

除此之外没有其他需要配置的东西——选哪种形式,完全取决于你的函数是否做 I/O。

名称、标题与行为注解(ToolAnnotations)

凡是 SDK 自动推断的内容,都可以在装饰器中显式覆盖(完整代码见 docs_src/tools/tutorial005.py):

from mcp.server import MCPServer from mcp.types import ToolAnnotations mcp = MCPServer("Bookshop") @mcp.tool( title="Search the catalog", annotations=ToolAnnotations(read_only_hint=True, open_world_hint=False), ) def search_books(query: str) -> str: """Search the catalog by title or author.""" return f"Found 3 books matching {query!r}."
  • title:面向 UI 的人类可读名称。客户端会显示"Search the catalog"而不是search_books
  • annotations:面向客户端的行为提示(hint),类型定义见 src/mcp-types/mcp_types/_types.py 的ToolAnnotations,共四个布尔字段:
    • read_only_hint=True:该工具不会修改任何状态。
    • open_world_hint=False:它作用于封闭对象集合(例如本地目录),而非开放网络——比如网络搜索工具的 world 是开放的,而记忆工具的 world 是封闭的。
    • destructive_hintidempotent_hint:描述写类工具的行为——它是否可能删除某些东西,以及重复调用是否等价于单次调用。规范规定这两者只对非只读工具(read_only_hint == false)有意义,因此它们对search_books不产生任何语义。

行为良好的客户端会依据这些提示做诸如"运行前是否需要询问用户确认"之类的决策。但请务必记住:它们是提示,不是安全机制,绝不能依赖客户端一定会遵守它们。

需要特别说明的是,@mcp.tool()装饰器还支持name=description=参数(当你不希望从函数名和 docstring 派生时),以及iconsmetastructured_output等进阶参数。完整的装饰器签名与文档字符串可在 server.py 的 tool 方法 中查看;底层注册、去重与移除逻辑则由 tool_manager.py 承担——同名工具重复注册会触发告警并保留首次注册,remove_tool用于动态摘除工具。

回顾与最佳实践

  • @mcp.tool()加在函数上即声明工具:名称来自函数名,描述来自 docstring。
  • 类型注解就是输入 schema:带默认值即参数可选。
  • Annotated[..., Field(...)]添加描述与约束,Literal添加枚举。
  • Pydantic 模型参数用于接收结构化的"请求体"。
  • 非法参数会在函数执行前被拦截,并返回模型可读、可恢复的错误信息。
  • 做 I/O 用async def,其余用普通def
  • 工具返回值经过转换后,会拆分为模型可读的content与客户端可用的structured_content两部分——return一个值之后发生了什么,请继续阅读 Structured Output。

仓库中完整的可运行示例位于 docs_src/tools/(tutorial001 至 tutorial005),对应的自动化测试见 tests/docs_src/test_tools.py;若希望看到工具在完整 Server 中的调用链路,可参考 examples/mcpserver/ 下的各个示例服务器。

  • 人工智能
  • MCP 服务
  • MCP Clients

【免费下载链接】python-sdk

The official Python SDK for Model Context Protocol servers and clients

项目地址:https://gitcode.com/gh_mirrors/pythonsd/python-sdk
点击查看免费下载
上一篇:Spago损失函数完全指南:交叉熵、MSE等常用损失函数的实现与应用
下一篇:如何5分钟上手3dtiles:OSGB到3D-Tiles转换实战教程

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询