前五篇把 Mastra 单 Agent 的模型接入、Zod 工具、结构化输出与记忆线程陆续跑通,Agent 已经能在自己进程里干活。但真实世界的 Agent 迟早要连外部系统:查维基百科、读本地文件、查生产数据库。MCP(Model Context Protocol)就是为这件事生的——一套开放协议把工具提供方与使用方解耦成 Server 与 Client 两端,任何一端换实现都不影响另一端。
MCP 的协议原理、Server 开发与 Python 侧接入,在《AI Agent 开发》系列第 19~21 篇已经讲透,本篇不重复,只回答一个问题:TypeScript 工程师怎么把 MCP Server 接进 Mastra Agent。全文按"全景对照 → MCPClient 上手 → stdio 与 HTTP 两种连接 → 工具命名与过滤 → 生命周期管理 → 双服务器实战"组织,结尾留一个跨栈练习。
1、TS 侧接入全景
先看分层,两层结构如下:
解读:底层是 MCP 官方 TypeScript SDK,Client 类负责 JSON-RPC 协议语义,StdioClientTransport 管子进程通信,StreamableHTTPClientTransport 管远程 HTTP 通信;上层是 Mastra 的 MCPClient,包名 @mastra/mcp,负责自动选择 transport、把 MCP 工具转换成 Mastra 工具、管理连接生命周期。日常接入只写 MCPClient,写自定义客户端或自研 Server 时才需要下探裸 SDK——这个分工先记住。
版本事实先对齐:截至 2026 年 10 月,@mastra/mcp 在 1.x 线上活跃发版,随 @mastra/core 的 1.x 稳定线一同演进。有一个历史包袱要当心——v0 时代的类名叫 MastraMCPClient,v1 起已废弃换成 MCPClient,网上大量老教程还在用旧名,照抄会吃到废弃告警,官方有迁移指南可查。
《AI Agent 开发》第 21 篇里,Python 侧的接入层是 langchain-mcp-adapters 的 MultiServerMCPClient;本篇是同一件事的 TypeScript 版。关键差异一张表看清:
| 维度 | Python 版 | TypeScript 版 |
|---|---|---|
| 客户端类 | MultiServerMCPClient |
MCPClient |
| 传输指定 | 配置里显式写 transport 字段 |
有 command 走 stdio、有 url 走 HTTP |
| 取工具 | await client.get_tools() |
await client.listTools() |
| 挂到 Agent | create_agent(tools=...) 或 bind_tools |
构造期 tools 或运行期 toolsets |
| HTTP 认证 | 连接配置里带 headers |
requestInit.headers,标准 fetch 参数 |
三点观察。其一,TS 版把"选哪种传输"从配置里拿掉了,凭 command 还是 url 自动判断,少写一个字段也少一个写错的字段。其二,两边的工具获取都是异步的,因为都要先连 Server 拉清单。其三,挂载方式 TS 版多出"运行期 toolsets"一条路,专门服务按请求换凭证的场景,第 6 节展开。
2、MCPClient 快速上手
三步:装包、建客户端、把工具挂给 Agent。先装包:
pnpm add @mastra/mcp@latest
迭代活跃的包建议带 @latest 显式拉新版,npm 与 yarn 同理。接着建客户端,servers 里每个键下注册一个 MCP Server:
import { MCPClient } from '@mastra/mcp'
export const mcpClient = new MCPClient({
id: 'main-client',
servers: {
wikipedia: {
command: 'npx',
args: ['-y', 'wikipedia-mcp'],
},
},
})
三个细节。id 是客户端实例的标识,会出现在日志里;servers 的键名(这里的 wikipedia)不只是个名字,它就是工具的命名空间前缀,第 5 节细说;command 加 args 声明一个 stdio 子进程。最后一步,把工具挂给 Agent:
import { Agent } from '@mastra/core/agent'
import { mcpClient } from './mcp/index.ts'
export const wikiAgent = new Agent({
id: 'wiki-agent',
name: '维基助手',
instructions: '回答事实类问题前,先用 MCP 工具查证。',
model: 'anthropic/claude-sonnet-5-5',
tools: await mcpClient.listTools(),
})
listTools() 是关键一环:它会真正连接所有注册的 Server,拉取工具清单,把 MCP 的 JSON Schema 转换成 Mastra 工具,返回一个"工具名到工具"的对象——形状与第 3 篇手写的 tools 完全一致。对 Agent 来说,MCP 工具和本地 Zod 工具没有任何区别,模型照常调用,Studio 的 trace 里照常可见。工具的 description 也一并从 Server 带过来——模型判断何时调用,靠的就是 Server 端写的工具说明,客户端不需要再补文档。顶层的 await 依赖 ESM 的 top-level await,模块加载时连接就建好了。
注意:
MCPClient没有显式的connect()方法,连接是惰性建立的——第一次需要工具清单或第一次调用工具时才握手。顶层await listTools()等于在模块加载期把连接"焐热",代价是冷启动变慢,Serverless 场景要权衡。
3、stdio 连接本地 Server
stdio 是本地 Server 的标准形态:Mastra 起一个子进程,JSON-RPC 消息走标准输入输出。以《AI Agent 开发》第 20 篇写的 SQLite 查询 Server(FastMCP 工程,uv 管理)为演示对象,配置如下:
sqlite: {
command: 'uv',
args: ['run', 'server.py'],
env: { DB_PATH: './data/blog.db' },
inheritDefaultEnv: false,
timeout: 15000,
},
五个字段这样读:command 是可执行文件,args 是参数;env 传子进程专用的环境变量;inheritDefaultEnv 设为 false 后子进程只拿到 env 里列出的变量,默认 true 时继承的也是 SDK 维护的白名单(PATH、HOME 等),不是整个父进程环境——这个默认值比想象中安全;timeout 给这个 Server 单独设毫秒级超时,不设则回落到客户端级默认 60 秒。
底层发生的事情:command 字段一出现,MCPClient 就自动选用 SDK 的 StdioClientTransport,spawn 子进程、建立 stdio 通道、完成 MCP 握手,全部封装在内,不需要 import 任何 transport 类。子进程的生死完全跟随客户端:MCPClient 断开或宿主进程退出,stdio Server 一起终止,不存在孤儿服务占端口的问题——HTTP Server 则是独立部署独立运维,两边运维模型不同。Python 写的 Server 与 TS 写的客户端在 stdio 上完全互通——协议就是干这个的,第 8 节的练习会让你亲手验证。
4、Streamable HTTP 连接远程
远程 Server 走 Streamable HTTP:单个 url 端点承载工具调用与响应。带认证的配置如下:
weather: {
url: new URL('https://weather.example.com/mcp'),
requestInit: {
headers: {
Authorization: `Bearer ${process.env.WEATHER_TOKEN}`,
},
},
},
url 一出现,MCPClient 自动换用 StreamableHTTPClientTransport。认证走 requestInit——它就是标准 fetch 的 RequestInit,这里的 headers 会在每次请求上携带;Bearer token 从环境变量读,与第 2 篇的密钥纪律一致:凭证不进代码、不进 git。
两种连接的取舍很直观:stdio 免部署、延迟低(进程间管道),适合本地工具与受信环境;HTTP 天然可远程、可扩展、可挂负载均衡,适合团队共享的 Server。要点是两种形态可以混用——同一个 MCPClient 的 servers 里同时写 command 与 url 两类条目,第 7 节就这么干。另外,OAuth 保护的 Server 不用手拼 token,MCPClient 提供 authenticate() 方法走浏览器授权流程,细节见官方文档。
5、工具命名与过滤
多 Server 接入后第一个问题:两个 Server 都有叫 search 的工具怎么办?MCPClient 的答案是强制命名空间——工具名一律加 Server 键名前缀,且前缀不可自定义:
| 方法 | 工具名格式 | 挂载时机 |
|---|---|---|
listTools() |
serverName_toolName |
构造期,写进 tools |
listToolsets() |
serverName.toolName |
运行期,写进 toolsets |
于是 wiki_search 与 db_search 天然消歧,重名冲突在 TS 侧不是问题;代价是起 servers 键名时要有点品味——键名叫 a,就会收获 a_search 这种没有信息量的工具名。
过滤是另一回事。MCPClient 的构造参数里没有工具过滤选项(MCPClient 参考文档里不存在 tools 白名单这类字段),正确姿势是在拿到工具对象后自己筛:
const all = await mcpClient.listTools()
const picked = Object.fromEntries(
Object.entries(all).filter(([name]) => name.startsWith('sqlite_')),
)
listTools() 返回普通对象,Object.entries 一过滤、fromEntries 一组装,白名单就生效了。被滤掉的工具不会出现在 Agent 的 tools 里,模型完全看不到——过滤发生在"转换成 Mastra 工具之后、交给模型之前",不需要断开 Server。排查连接问题另有专用工具:listToolsWithErrors() 会把每个 Server 的连接错误与详情一并返回,比看一坨异常栈好读。
追求类型安全的还可以开 typegen:构造参数里写 typegen: { outFile: 'src/mcp/mcp-types.generated.ts' },再执行 npx @mastra/mcp generate ./src/mcp/client.ts,框架把所有 Server 的工具清单生成为带类型的定义文件,工具名按 serverName_toolName 静态推导——Server 端改了工具,重新生成一次,调用处的类型检查器立刻告诉你哪里断了。
6、连接生命周期管理
第 2 节的静态挂载(模块级 MCPClient 加顶层 await)适合工具集固定的场景:连接建一次、进程内复用,长驻的 Hono 或 Express 服务直接用。真正要小心的是按请求换凭证的场景——每个用户的 token 不同,模块级客户端装不下,必须每请求建一个。Hono 里的正确姿势如下:
import { Hono } from 'hono'
import { MCPClient } from '@mastra/mcp'
const app = new Hono()
app.post('/ask', async (c) => {
const client = new MCPClient({
id: 'per-request',
servers: {
docs: {
url: new URL('https://docs.example.com/mcp'),
requestInit: {
headers: { Authorization: c.req.header('Authorization')! },
},
},
},
})
try {
const res = await agent.generate(await c.req.text(), {
toolsets: await client.listToolsets(),
})
return c.text(res.text)
} finally {
await client.disconnect()
}
})
三个要点。其一,用户自己的 Authorization 头透传进 requestInit,连的是同一个远程 Server,权限各归各。其二,工具不走构造期 tools,而是运行期传 toolsets: await client.listToolsets()——工具集跟着这次请求走。其三,finally 里的 disconnect() 不能省:它会关闭所有连接并清理资源,stdio 场景下意味着杀掉子进程。Next.js 的 Route Handler 里是同一个模式:模块顶层放静态 MCPClient 处理固定工具集,handler 函数内走 toolsets 处理用户凭证,代码几乎不用改。再提醒一次惰性连接:Serverless 上模块级顶层 await 会把建连算进冷启动,Server 多、握手慢时明显,必要时改用运行期 toolsets,把建连推迟到首个请求。
这个坑与《AI Agent 开发》第 21 篇 Python 版是同一个:每请求建客户端、用完不断开,stdio 子进程就会一个个堆积,直到耗尽资源。TS 侧的表现更隐蔽——Node 不会替你收尸 spawn 出来的子进程,disconnect() 必须显式调用。反过来,静态客户端无需手动断开,进程退出自然回收。
7、双服务器实战
把前面全部拼起来:一个 Agent 同时挂本地 stdio 文件系统 Server 与远程 HTTP 文档 Server。客户端定义如下:
import { MCPClient } from '@mastra/mcp'
export const mcp = new MCPClient({
id: 'mixed-client',
timeout: 20000,
servers: {
fs: {
command: 'npx',
args: ['-y', '@modelcontextprotocol/server-filesystem', './data'],
},
docs: {
url: new URL('https://mcp.context7.com/mcp'),
},
},
})
fs 走 stdio 挂官方文件系统 Server(限定在 ./data 目录内),docs 走 HTTP 挂 Context7 文档 Server,客户端级 timeout 20 秒统一兜底。Agent 与运行脚本如下:
import { Agent } from '@mastra/core/agent'
import { mcp } from './mcp/index.ts'
const helperAgent = new Agent({
id: 'helper-agent',
name: '文档助手',
instructions: '先查本地 data 目录,文档类知识再查远程库。',
model: 'anthropic/claude-sonnet-5-5',
tools: await mcp.listTools(),
})
const result = await helperAgent.generate(
'data 目录下有哪些文件?再查一下 Hono 框架怎么注册路由。',
)
console.log(result.text)
await mcp.disconnect()
跑起来后,模型会先用 fs_ 前缀的目录工具看本地文件,再用 docs_ 前缀的文档工具查 Hono 路由——两种 transport、两个命名空间、一轮对话里交叉调用,Agent 层完全无感。收尾的 disconnect() 是脚本的好习惯;把 Agent 注册进 Mastra 实例后,Studio 里能看到每个 MCP 工具的调用轨迹,与本地 Zod 工具长得一模一样。
8、总结
回顾全文:TS 侧接 MCP 是两层结构——官方 TypeScript SDK 提供协议与 transport,@mastra/mcp 的 MCPClient 在其上封装出"配置即工具";command 与 url 两个字段决定 stdio 还是 Streamable HTTP,传输选择全自动;工具名强制 serverName_toolName 前缀,重名免焦虑,过滤在拿到工具对象后自行完成;生命周期上,静态挂载用模块级客户端一劳永逸,按请求换凭证用 toolsets 加 try/finally 里的 disconnect()——坑与 Python 版同源,解法也同源。
留一个跨栈练习:把《AI Agent 开发》第 20 篇的 SQLite 查询 Server 接进本文的 Agent——command 用 uv run 启动它,env 传数据库路径,挂载后用 listToolsWithErrors() 确认连接成功,再到 Studio 里问一个要查库的问题、看 trace 里的工具调用。跑通这一步,你就有了一条 Python Server 供 TS Agent 调用的完整跨栈链路,MCP"一次实现、处处接入"的价值也就摸到了。对于正在做 TS Agent 的工程师,建议今天就挑一个现成的 MCP Server——文件系统、搜索、数据库任选——挂进自己的 Agent,比读十篇协议文档都有效。

