连接 MCP 工具#
使用McpExtension连接由 Model Context Protocol 服务器提供的工具。模型看到的是普通的工具定义;当模型请求某个工具时,扩展会调用该服务器并返回结果。如果你已经有一套 MCP 集成,可以选择它,而不必把这些工具改写成本地 Python 函数。
连接 Streamable HTTP 服务器#
import os
from zett_agent.client import create_agent
from zett_agent.extensions.mcp import McpExtension, McpHttpServer
from zett_agent.extensions.memory import InMemoryMessageAccumulator
from zett_agent.extensions.tool_guidelines import ToolGuidelinesExtension
mcp = McpExtension(
servers=[
McpHttpServer(
name="catalog",
url=os.environ["MCP_URL"],
headers={"Authorization": f"Bearer {os.environ['MCP_TOKEN']}"},
),
],
)
client = await create_agent(
model,
extensions=[InMemoryMessageAccumulator(), ToolGuidelinesExtension(), mcp],
)
reply = await client.run("Find the product matching this description.")
这是一个需要主动启用的网络示例:用连接模型提供商配置 model,并为受信任的服务器设置 URL 和令牌。如果服务器不要求 headers,可以省略它。请求头会在传输的整个生命周期中发送,但不会出现在服务器对象的repr中;你的应用仍然必须避免凭据出现在日志和版本控制里。
启动本地 stdio 服务器#
import sys
from pathlib import Path
from zett_agent.extensions.mcp import McpStdioServer
mcp = McpExtension(
servers=[
McpStdioServer(
name="local",
command=sys.executable,
args=("server.py",),
cwd=Path("trusted-server").resolve(),
),
],
)
请把路径和参数替换成你已经安装的服务器。命令和参数是分开传递的,不会被拼接进 shell 字符串。进程仍然以你应用的权限运行。只启动受信任的服务器,并在进程或容器边界上限制它们的文件系统、环境和网络访问。
使用配置文件#
{
"mcpServers": {
"catalog": {
"type": "streamable-http",
"url": "https://your-server.example/mcp"
},
"local": {
"command": "python",
"args": ["/absolute/path/to/server.py"]
}
}
}
可以用McpExtension(config_path="mcp.json")加载它。servers和mcpServers都是可接受的根键,但一个文件只能使用其中一个。HTTP 条目可以包含headers;stdio 条目可以包含env和cwd。文件中的路径和命令由你负责;不要假定它们相对于配置文件解析。密钥不会有自动的${VAR}替换;当取值来自环境变量时,请在 Python 中构造服务器定义。
McpExtension()会查找~/.zett/mcp.json。传入显式的servers列表可以避免这种隐式查找。自定义的config_path可以和服务器列表一起使用;重复的服务器名称会被拒绝。配置文件不存在时不会加载任何服务器,而配置格式错误时会在构造扩展时抛出错误。
默认情况下(strict_config=True),只要有一个无效的服务器条目,整个文件就会失效,因此拼写错误绝不会被静默忽略。如果希望改为跳过无效条目,可以传入McpExtension(strict_config=False)或McpConfiguration.from_file(path, strict=False):每个被跳过的条目都会写入日志并列入McpConfiguration.skipped,有效的服务器仍然会加载。如果文件不是有效的 JSON 或者没有服务器映射,仍然会抛出错误。
每个请求开始时都会检查该文件,只有在修改时间或大小发生变化时才会重新读取,因此修改会在下一个请求中生效,无需重启应用。如果文件在应用运行期间变得无效,会写入日志,并继续使用最后一次有效的服务器列表。
工具名称和结果#
名为catalog的服务器中有一个名为search的工具,默认会以catalog__search的形式暴露。使用命名空间可以避免与本地工具以及其他服务器冲突。namespace_tools=False会保留原始名称,但此时你必须确保每个已注册的名称都唯一。
服务器提供的输入模式描述了这些工具。远程校验和你应用的策略仍然很重要;MCP 工具不会经过@tool函数校验器。如果结果带有structured_content,就会以该结构化值返回;否则内容块会被序列化成 JSON 数据。图像块不会被自动转成带类型的视觉消息。远程的is_error结果会变成模型可以看到的普通工具失败。
连接与失败#
每个请求开始时,都会并发发现所有服务器的工具列表,因此总延迟取决于最慢的服务器,而不是各服务器之和。工具列表会按服务器配置缓存tools_cache_ttl秒(默认 300;设为0表示禁用缓存)。缓存仍然有效时,请求不会启动任何进程,也不会打开任何套接字:服务器只有在模型第一次调用它的工具时才延迟连接,而且只有那个服务器会连接。请求无论是成功、失败、取消,还是执行compact(),都会关闭该请求打开的所有连接。
如果某个服务器无法在connect_timeout秒(默认 30)内连接或列出工具,它就会被跳过。模型会被告知哪个服务器不可用,其他服务器则继续正常工作。超过call_timeout秒(默认 300)的工具调用会失败,并抛出模型可以看到的TimeoutError;把任一超时设为None即可禁用它。工具调用过程中的普通失败会产生TOOL_FAILED,并允许模型继续。你可以选择哪些远程错误文本可以被显示。该扩展不会给远程服务器授予额外权限。
发送工具模式时不会包含title和$schema注解;服务器生成器会给每个属性都附上这些注解,而它们只是重复属性名。每个工具的 guidelines 也会被省略:系统消息已经说明<server>__<tool>形式的名称属于对应的服务器,因此为每个工具都重复一遍只会消耗提示词 token。属性的名称、类型、描述和约束都保持不变。
暴露的工具名称只能包含字母、数字、_和-,并且最多 64 个字符,这是各模型提供商接受范围的交集;更长的名称会用稳定哈希缩短。如果某个工具的暴露名称已经被注册,它会被跳过并给出警告,而不会让请求失败。如果服务器重复返回同一个分页游标,或者返回超过 100 页,就会被视为不可用。
离线验证集成#
async def main() -> None:
local = LocalMcpClient()
@asynccontextmanager
async def connect(server: McpServer) -> AsyncGenerator[McpClient]:
assert server.name == "demo"
try:
yield local
finally:
local.closed = True
extension = McpExtension(
servers=[McpHttpServer(name="demo", url="https://unused.example/mcp")],
client_factory=connect,
)
client = await create_agent(McpModel(), extensions=[extension])
reply = await client.run("Get the answer.")
assert reply.content == "MCP tool result: 42"
assert local.closed
print(reply.content)
print("MCP connection closed.")
在源码检出目录中运行uv run python docs/_examples/mcp_tools.py:
MCP tool result: 42
MCP connection closed.
完整示例通过client_factory提供了一个内存中的 MCP 客户端。它无需启动服务器,就能验证注册、分发、结构化结果和传输清理。
下一步:定义并注册工具介绍本地工具的行为,或者看注册服务商托管的工具,了解由模型提供商而非 MCP 服务器执行的工具。