5.3 MCP 服务器接入
本节你会学到
- 什么场景适合用 MCP,什么场景应当写自定义 Tool
- Agentao 支持的三种 transport(stdio、Streamable HTTP、SSE)
- 多租户模式:会话级
extra_mcp_servers、环境变量展开、trust:的边界
MCP(Model Context Protocol) 是"工具互操作的事实标准"。Agentao 作为 MCP Client,可以接入任何 MCP 兼容服务器——GitHub、Filesystem、Postgres、Slack、Jira、你自己写的……所有这些工具都会自动以 mcp_{server}_{tool} 的形式出现在 Agent 可用工具列表里。
MCP 能做什么
| 场景 | 推荐的 MCP Server |
|---|---|
| 读写文件 / 代码仓库 | @modelcontextprotocol/server-filesystem |
| GitHub issues/PR | @modelcontextprotocol/server-github |
| 数据库查询 | @modelcontextprotocol/server-postgres |
| Slack / Linear / Jira | 官方或社区 MCP |
| 内部工具 | 自建 MCP Server(见末尾) |
优势:不用自己写 Tool 子类——社区已经写好并维护。
配置的两种方式
方式 A · JSON 配置文件
文件位置(同名冲突时用户级胜;项目级仅可新增名字):
~/.agentao/mcp.json ← 用户级(所有项目共享;任何在该文件声明的 server name 都以此为准)
<working_dir>/.agentao/mcp.json ← 项目级(仅可声明新 name,不能覆盖用户级同名条目)为何仅可新增?
项目级 mcp.json 一旦 checked-in 进 git,就可能被恶意(或意外)地把已知 server name(如 github)静默重定向到不同 transport 或 endpoint。冲突时打 warning + 跳过项目项。要解决,请改名项目项或移除用户级同名条目。
格式:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me/code"],
"env": {},
"trust": false,
"timeout": 60
},
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}"
}
},
"analytics-sse": {
"url": "https://mcp.your-company.com/sse",
"type": "sse",
"headers": {
"Authorization": "Bearer ${ANALYTICS_TOKEN}"
},
"timeout": 30
}
}
}方式 B · 程序式(嵌入首选)
构造 Agent 时通过 extra_mcp_servers 参数注入——完全跳过 JSON 文件,按会话/租户动态生成:
from agentao import Agentao
agent = Agentao(
working_directory=Path(f"/tmp/tenant-{tenant.id}"),
extra_mcp_servers={
"github-per-tenant": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {"GITHUB_PERSONAL_ACCESS_TOKEN": tenant.github_token},
},
},
)合并规则:同名覆盖 .agentao/mcp.json 的同名条目。
配置字段详解
stdio 传输(子进程)
| 字段 | 必填 | 说明 |
|---|---|---|
command | ✅ | 可执行文件(npx, python, 绝对路径) |
args | ❌ | 命令行参数列表 |
env | ❌ | 额外环境变量;支持 $VAR / ${VAR} 从进程环境展开 |
cwd | ❌ | 子进程工作目录 |
timeout | ❌ | 写数字:连接和握手的时限(秒),默认 60,不限制工具调用。要同时限制每次调用,写 {"startup": 60, "request": 30};request 没有默认值(配置参考) |
trust | ❌ | 为 true 时跳过工具确认 |
Streamable HTTP 传输(远程服务,裸 url 的默认)
| 字段 | 必填 | 说明 |
|---|---|---|
url | ✅ | Streamable HTTP endpoint URL |
type | ❌ | "http"(别名:"streamable-http" / "streamable_http");给裸 url 时这是默认值 |
headers | ❌ | HTTP 头;支持 ${VAR} 展开 |
timeout | ❌ | 同 stdio |
trust | ❌ | 同上 |
SSE 传输(远程服务,旧版)
| 字段 | 必填 | 说明 |
|---|---|---|
url | ✅ | SSE endpoint URL |
type | ✅ | 必须为 "sse"——SSE 是旧版传输(在 MCP 规范 2025-03-26 中已弃用)。裸 url 现在默认走 Streamable HTTP,所以 SSE 必须显式选择。 |
headers | ❌ | HTTP 头;支持 ${VAR} 展开 |
timeout | ❌ | 同 stdio |
trust | ❌ | 同上 |
三种 transport 还都接受两个键,对应下文的两个功能:
| 字段 | 默认值 | 作用 |
|---|---|---|
resources | true | 设为其他任何值,该 server 都会从资源工具和 /mcp resources 中隐去,按名字指定它也会被拒绝 |
skills | false | 只有 true 才为该 server 开启 MCP Skills 扩展 |
Streamable HTTP 已支持
Agentao 的 MCP 客户端导入了 stdio_client、sse_client,以及 streamable_http_client(外加 create_mcp_http_client),三种 transport 都能接入。type 选择 transport("stdio" / "http" / "sse");省略时自动推断——command → stdio,裸 url → Streamable HTTP。旧版 SSE 需要显式写 "type": "sse"。未知的 type 或缺少必填键会 fail closed(McpTransportConfigError)。ACP 握手会通告 mcpCapabilities.http: true。
环境变量展开
"env": {
"TOKEN": "${MY_TOKEN}", // ${...} 形式
"REGION": "$AWS_REGION" // $... 形式
}展开时机:加载配置时——也就是 Agent 构造时。展开后的字面量值进入子进程 env。
未定义的变量展开成空字符串(不抛错)。
MCP 工具的命名
一个 MCP Server 发现的每个工具都被包装为 Agentao Tool,名字加前缀:
Server: "github"
MCP 工具: "create_issue"
Agentao 里的名字: "mcp_github_create_issue"名字里的非 [a-zA-Z0-9_] 字符会被替换成 _。
这意味着:
- 你写自己的 Tool 时,不要以
mcp_打头(避免看起来像 MCP 工具) - 权限规则可以按前缀匹配:
{"tool": "mcp_github_*", ...}
MCP 资源(resources)
声明了 resources capability 的 server 还会暴露可供模型读取的数据。只要有一个已连接的 server 声明了它,就会注册三个只读工具(名字和形状沿用 codex 与 pi,模型已经认识):
| 工具 | 参数 | 返回 |
|---|---|---|
list_mcp_resources | server?、cursor? | JSON {server?, resources: [{server, uri, name, …}], nextCursor?, errors?} |
list_mcp_resource_templates | server?、cursor? | JSON {server?, resourceTemplates: [{server, uriTemplate, name, …}], nextCursor?, errors?} |
read_mcp_resource | server、uri(均必填) | 资源内容 |
- 不带
server时遍历所有 server;单个 server 的失败放进errors。cursor必须配server。 - 文本按文本返回。二进制资源保存到
<working_directory>/.agentao/tool-outputs/(0600)并返回路径;超过 10 MiB 的 blob 只报告、不解码。 - 三者都是只读的:只读模式和 plan 模式下都允许,从不弹确认。点名它们的权限规则仍然生效,
disable_tools/enabled_tools也接受这三个名字。 - server 配置
"resources": false会把它从三个工具中隐去(点名也会被拒)。 - 工具结果里的
resource_link会保留 URI,并在模型能跟进时指明用read_mcp_resource及对应 server 读取。
同样的操作在 manager 上是公开的,宿主可以据此做自己的选择器:
page = agent.mcp_manager.list_resources("docs") # ResourcePage
templates = agent.mcp_manager.list_resource_templates("docs")
read = agent.mcp_manager.read_resource("docs", "report://q3") # ResourceRead(文本或 base64 blob)失败时抛出 agentao.mcp.resources.McpResourceError(带 kind),不会返回残缺数据。CLI 里 /mcp resources [server] 可直接列出,不消耗模型轮次。
MCP skills
server 还可以通过 MCP Skills 扩展(io.modelcontextprotocol/skills)发布 Agent Skills。它默认关闭:在 mcp.json 里给该 server 加上 "skills": true。需要 mcp 2.x,且 server 的协议版本不低于 2026-07-28。
- 目录。 连接时列出该 server 的 skill(不取任何文件)。每个 skill 的名字是
mcp:<server>:<SKILL.md URI>,即身份本身,所以两个同名 skill 不会冲突;它们在系统提示词里单独成节,并标明由该 server 编写。 - 激活。
activate_skill("mcp:docs:skill://pdf-processing/SKILL.md")每个会话问用户一次,然后取回SKILL.md,按 server 的条目核对大小、摘要和 frontmatter。正文放在<mcp-skill server="…" uri="…">里交给模型。列表里没有的 skill 也能用这个名字加载。 - 文件。
read_skill_file(skill, path)从该 skill 自己的 server 读取已激活 MCP skill 的文件并校验,不在清单里的路径一律拒绝。在已加载 skill 目录内的read_mcp_resource也按同样方式校验。 - 闸门。 只要有 MCP skill 已加载(直到
/clear,停用不算),run_shell_command、派生能执行 shell 的子代理、以及对其他 server 的read_mcp_resource都会询问,任何权限模式下都是如此。它们只会收紧:deny规则和只读模式仍然拒绝。 - 子代理共享会话里已加载的 skill,所以闸门在子代理里同样生效。
- 没有人可以询问的宿主。 激活和每个闸门确认都需要人来回答。
NullTransport、没有confirm_tool的SdkTransport、没有confirmation_callback的build_compat_transport会一律拒绝。会记住"总是允许"的宿主必须读取agentao.transport.gate_note()(见 4.5)。 - 会话恢复。
restore_agent_skills从不重新激活 MCP skill,会在恢复的消息里隐去 MCP skill 的内容,并重新打开闸门(见 2.4)。 - 限制。 每个 skill 最多 512 个文件、16 MiB。
SKILL.md超过 100,000 字节的 skill 会被列为不可用,因为已激活 skill 的SKILL.md每次请求都会发送。read_skill_file每次最多返回 30,000 个字符,用offset和limit分页读取。 - 内容保存在哪里。 skill 内容从不写入
.agentao/tool-outputs/。replay 文件和agentao.log原样保留工具结果,包括 MCP skill 内容;之后用read_file读取它们不受闸门限制。
宿主可以从 manager 读到同样的数据:
agent.mcp_manager.skill_servers() # 通过闸门的 server
entries, unavailable, problem = agent.mcp_manager.skill_listing("docs")
entry = agent.mcp_manager.get_skill("docs", "skill://pdf-processing/SKILL.md") # skills/get/skills 按 server 列出 MCP skill,并说明哪些无法加载以及原因;/mcp list 显示每个开启了 skills 的 server 有多少个 skill,或者为什么一个都没有。
调试 MCP 接入
# 查看所有发现的工具
for t in agent.tools.list_tools():
if t.name.startswith("mcp_"):
print(t.name, "—", t.description[:60])
# 查看 MCP manager 状态
if agent.mcp_manager:
print(f"{len(agent.mcp_manager.clients)} server(s) connected")日志文件 agentao.log 会记录:
- MCP Server 启动成功/失败
- 每个工具发现
- 工具调用参数和结果
自己写一个 MCP Server(3 分钟上手)
最小的 MCP Server 用 Python 写:
# my_mcp_server.py
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("my-internal-tools")
@mcp.tool()
def get_user_info(user_id: str) -> str:
"""Query internal user info by ID."""
return my_backend.get_user(user_id).to_json()
@mcp.tool()
def send_notification(user_id: str, message: str) -> str:
"""Send an in-app notification to a user."""
my_backend.notify(user_id, message)
return "ok"
if __name__ == "__main__":
mcp.run() # 默认 stdio然后在 .agentao/mcp.json 里:
{
"mcpServers": {
"internal": {
"command": "python",
"args": ["/path/to/my_mcp_server.py"]
}
}
}Agent 重启后自动发现 mcp_internal_get_user_info 和 mcp_internal_send_notification。
多租户策略
生产 SaaS 里,典型 MCP 使用模式:
| Server | 谁写 | 范围 |
|---|---|---|
| 官方/开源(github、filesystem、postgres) | 用户/运维配 | 全局或项目级 JSON 文件 |
| 你自己的业务 MCP | 你写 Python/Node | 每租户一个实例,经 extra_mcp_servers= 按会话启 |
| 租户自带的 MCP | 租户配置(SaaS 控制台) | 存 DB,构造 Agent 时翻译成 extra_mcp_servers= |
安全要点:
- 租户 token/密钥永远不要写进 JSON 文件——通过环境变量或
extra_mcp_servers的env动态注入 - MCP 子进程继承父进程环境变量——确保没有泄漏其他租户的凭据
- 每会话独立子进程,避免跨租户状态污染
与权限引擎的配合
MCP 工具默认也需要确认(等同 requires_confirmation=True),除非配置 trust: true:
{
"mcpServers": {
"trusted-internal": {
"command": "...",
"trust": true ← 这些工具直接允许执行,不走 confirm_tool
}
}
}或者用权限规则细粒度控制:
{
"rules": [
{"tool": "mcp_github_get_*", "action": "allow"},
{"tool": "mcp_github_delete_*", "action": "deny"},
{"tool": "mcp_github_create_*", "action": "ask"}
]
}权限详见 5.4。
MCP 工具注解:readOnlyHint / destructiveHint
如果 server 声明了标准 MCP ToolAnnotations,Agentao 会读取 —— 但仅当 trust: true 时(按 MCP 规范:客户端不应基于来自不受信任 server 的注解作出工具决策)。
trust | 注解 | 效果 |
|---|---|---|
false(默认) | 任何 | 注解被忽略 —— server 可能撒谎;总是要求确认 |
true | readOnlyHint: true | is_read_only=True(read-only 模式可放行该调用),不要确认 |
true | destructiveHint: true | 要求确认 —— 覆盖 trust 默认 |
true | 无 / 都没有 | 当前 trusted 行为 —— 不要求确认 |
这是安全方向正向的接线:注解可以增加摩擦(一个 trusted server 把某调用标为 destructive 时触发确认),但永远不会在不受信任路径上降低摩擦。Host 可以通过 McpTool.mcp_annotations 读到原始 dict 用于更丰富的投影。
⚠️ 常见陷阱
上线前先确认这几条
- ❌ Server 启动失败但 Agent 静默继续 ——
agent.chat()不会暴露 MCP init 失败 - ❌ 工具名超长 —— Agentao 会截断并加哈希,权限规则要写注册后的名字
- ❌
trust: true用得太宽 —— 绕过所有安全确认
下面每一条都附完整修法。
❌ Server 启动失败但 Agent 静默继续
Agentao 的 MCP 初始化是容错的——单个 Server 失败只会 log warning,不会阻塞 Agent 构造。检查 agentao.log:
MCP: failed to start 'github': ...
MCP: 12 tools from 2 server(s) ← 有些 Server 没起来部署前务必确认期望数量的 Server 都在。
❌ 工具名超长
有些 MCP Server 工具名很长。拼上前缀后可能超过 OpenAI function calling 的名字长度限制(64 字符)。Agentao 会截断这样的名字,并加上一个由原始服务器名和工具名算出的 8 位哈希(每次连接都相同),工具照常可用——但权限规则或子代理的 tools: 列表要写注册后的名字,agentao.log 里有记录(Registered MCP tool: …)。
❌ trust: true 用得太宽
写过/删过东西的 Server 不要轻易设 trust: true——等于绕过所有安全确认。只给纯读或已经有自己权限层的 Server 用。
TL;DR
- MCP 用来消费第三方工具生态(GitHub / 文件系统 / Postgres / Slack …);自定义 Tool 用来封装你自己的业务逻辑。
- 三种 transport:stdio 子进程、Streamable HTTP URL(裸
url的默认)、或旧版 SSE URL("type":"sse")。 - 多租户 token:构造时传
extra_mcp_servers({name: {command, args, env}}),同名会覆盖.agentao/mcp.json。项目级.agentao/mcp.json是仅可新增的 —— 不能覆盖用户级同名条目。 - 工具命名:
mcp_{server}_{tool}——自动加前缀避免不同服务器的同名冲突。含非 ASCII 字符的服务器名或工具名,也会像超长名字一样加上 8 位哈希后缀(否则查询和搜索都会变成__),所以升级后注册名会变。仍然映射到同一名字的两个工具(my-srv与my_srv)不会都注册:先列出的保留这个名字,另一个不注册,并在agentao.log记一条错误。 - 写操作能力的 Server 绝对不要设
trust: true,会绕过所有确认。 - server 的 resources 会变成三个只读工具(
"resources": false可隐去)。它的 skills 默认关闭,要设"skills": true;加载了 MCP skill 期间,shell 和跨 server 读取在任何模式下都会询问。
→ 下一节:5.4 权限引擎