关于 businessmcp
businessmcp 的 mcp-gateway 是一组 Node.js 工具,供开发者构建连接第三方 MCP 服务的网关。它提供 URL 安全检查、上游错误分类、避免工具命名冲突和传输回退,但不包含网关或策略层。
主要功能
- 01检查公开 HTTP URL 和 IP 安全性
- 02分类上游故障
- 03生成不冲突的工具名称
- 04从 Streamable HTTP 回退到 SSE
- 05生成面向用户的故障说明
适用场景
- 01验证用户提供的 MCP 服务 URL
- 02说明上游连接故障
- 03聚合工具时避免名称冲突
- 04连接使用不同传输方式的服务
MCP 相关
这些工具帮助开发者构建连接上游 MCP 服务的网关。传输辅助模块使用 @modelcontextprotocol/client;该软件包本身不提供网关聚合或策略管理。
配置
远程服务无需本地安装。选择你使用的客户端,复制配置或使用一键安装。
https://businessmcp.com/api/mcpclaude mcp add --transport http com-businessmcp-businessmcp https://businessmcp.com/api/mcp{"mcpServers":{"com-businessmcp-businessmcp":{"type":"http","url":"https://businessmcp.com/api/mcp"}}}在 Cursor 中打开{"servers":{"com-businessmcp-businessmcp":{"type":"http","url":"https://businessmcp.com/api/mcp"}}}在 VS Code 中打开{"mcpServers":{"com-businessmcp-businessmcp":{"type":"http","url":"https://businessmcp.com/api/mcp"}}}{"mcpServers":{"com-businessmcp-businessmcp":{"serverUrl":"https://businessmcp.com/api/mcp"}}}命令在终端中运行;JSON 粘贴到对应客户端的 MCP 配置后重启。需要授权的服务会在首次连接时提示登录。
工具
详细介绍
mcp-gateway
用于加固 MCP 网关的实用工具——一种代理他人 MCP 服务的服务端。
这些工具摘自 BusinessMCP 的生产网关。该网关允许工作区连接第三方 MCP 服务,并通过一个受治理的端点重新对外提供这些服务。代理他人的服务意味着要在自己的基础设施上获取用户提供的 URL,而这可能以少数几种方式出大问题。我们运行的就是这套工具。
MIT。两个核心模块没有依赖——只使用两个 Node 内置模块,除此之外别无其他。
npm install @modelcontextprotocol/client # only if you use the transport helper
1. SSRF 防护,以及绕过测试套件的漏洞
网关会接收用户提供的 URL 并发起请求。没有防护的话,这就成了访问 http://169.254.169.254/ 和获取云凭据的直接途径。
最直观的实现是将主机与保留范围列表进行匹配。我们当时就是这么做的,包括 IPv4 映射的 IPv6 地址:
const mapped = v.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/) // ← the bug
这个正则表达式在生产环境中从未触发。WHATWG URL 解析器——也就是 new URL(),所有调用方实际使用的解析器——会将 IPv6 主机规范化为其标准形式,而 IPv4 映射地址的标准形式是十六进制:
new URL('http://[::ffff:169.254.169.254]/').hostname
// '::ffff:a9fe:a9fe'
正则表达式匹配的点分形式是调用方无法生成的形式。单元测试之所以通过,是因为它直接使用点分字符串调用了防护函数。测试通过了,但毫无意义:防护函数只通过真实代码路径无法生成的输入进行过测试。
以下地址都能通过检查并发起请求:
| URL | URL.hostname | 实际指向 |
|---|---|---|
http://[::ffff:169.254.169.254]/ | ::ffff:a9fe:a9fe | 云元数据 |
http://[::ffff:127.0.0.1]/ | ::ffff:7f00:1 | 回环地址 |
http://[::127.0.0.1]/ | ::7f00:1 | 回环地址(IPv4 兼容地址) |
http://[64:ff9b::169.254.169.254]/ | 64:ff9b::a9fe:a9fe | 云元数据(NAT64) |
http://[2002:a9fe:a9fe::]/ | 2002:a9fe:a9fe:: | 云元数据(6to4) |
在列表中添加更多前缀并不能解决问题。 IPv6 有多种嵌入 IPv4 地址的机制,匹配文本只会让你不断追着一个不受你控制的规范化器补漏洞。这项防护会将地址解码为八个 16 位分组,并进行数值检查;它会解开已知的所有嵌入形式(::ffff: 映射、:: 兼容、64:ff9b:: NAT64、2002:: 6to4),并递归检查其中的 IPv4 地址。无法解码的输入会被默认拒绝——无法解析的地址也就无法保证安全。
import { isPublicHttpUrl, isBlockedIp } from '@businessmcp/mcp-gateway'
await isPublicHttpUrl('http://[::ffff:169.254.169.254]/') // false
await isPublicHttpUrl('https://api.example.com/mcp') // true, after resolving every A/AAAA
isBlockedIp('::ffff:a9fe:a9fe') // true
isPublicHttpUrl 会解析主机名,并要求返回的每个地址都是公网地址,以尽力防范 DNS 重绑定。这并非万全之策:从检查到套接字连接之间发生重绑定,确实存在 TOCTOU 窗口;要彻底消除此问题,就必须将已解析的 IP 固定到连接上。我们接受这一剩余风险,并认为你应该了解它,而不是等到发现问题时才知道。
还有第二个陷阱需要注意:验证一次主机后,再调用 fetch(url, { redirect: 'follow' }) 就会绕过防护——服务器可以返回 302,将请求重定向到 http://127.0.0.1/,而这个后续请求不会经过检查。应手动跟随重定向,并重新验证每一跳。
2. 对上游故障分类——绝不要将其吞掉
这段代码最初发布的版本会用 catch {} 包裹每次上游调用,并将对应服务从工具列表中移除。身份验证失败、不支持的传输方式、超时、被拦截的主机,以及“服务正常运行,但未返回任何内容”都无法区分——也就是说,这些情况完全不可见。用户只会看到工具悄无声息地消失,产品中没有任何说明。
import { classifyUpstreamError, describeFailure } from '@businessmcp/mcp-gateway'
const failure = classifyUpstreamError(err)
// { reason: 'auth_required' | 'transport_unsupported' | 'timeout'
// | 'blocked_host' | 'unreachable', detail: string }
describeFailure(failure) // a sentence you can show a user
这也是诚实判断是否重试的依据:只有类似握手失败的情况才值得尝试其他传输方式。身份验证被拒绝时,换一种传输方式结果也一样。
3. 为工具名称添加命名空间,避免冲突
聚合 N 个服务意味着要为它们的工具添加前缀。MCP 将名称长度限制为 64 个字符,因此添加前缀再截断,可能会让两个不同的上游工具映射到同一个字符串——随后按名称查找时,调用就会被分发到错误的服务。
import { namespacedToolName } from '@businessmcp/mcp-gateway'
namespacedToolName('stripe', 'create_payment_intent')
// 'ext_stripe_create_payment_intent'
需要截断时,会在完整名称后附加一个短哈希值,从而确保两个长名称仍然各不相同。
4. 传输回退
MCP 的 2026-07-28 修订版移除了针对服务的旧版 HTTP+SSE,但你连接的许多服务仍然只支持这种协议。withUpstream 会尝试使用可流式 HTTP,失败后回退到 SSE,并告知你哪种方式生效,以便你保存该方式,下次跳过探测。
import { withUpstream } from '@businessmcp/mcp-gateway'
const res = await withUpstream(
{ url, headers, transport: knownTransport },
(client) => client.listTools(),
{ onTransportResolved: (t) => save(t) },
)
if (!res.ok) log(describeFailure(res.failure))
需要将 @modelcontextprotocol/client 作为对等依赖项。其他模块则不需要。
坦诚说明局限
- 这是实用工具,而非框架。 这里没有网关——没有聚合、没有身份验证,也没有策略层。这些功能与连接的存储方式有实质性的耦合,因此,为它们构建一个虚假的抽象还不如不做。
- 未经外部审计。 这是我们在生产环境中运行的代码。这是应该认真对待它的理由,而不是免于审查的替代品。
- 特意没有纳入聚合层和检查层,部分原因是它们与我们的数据库耦合,部分原因是它们没有测试覆盖。这里发布的所有内容都来自经过测试的部分。
- DNS 重绑定 TOCTOU 问题尚未解决,如上所述。
开发
此软件包是从 BusinessMCP 单体仓库中生成的,这些模块目前正在其中投入生产使用。修复应当在该仓库中进行,然后重新生成——只在这里应用的补丁会被覆盖;而手动维护两份安全防护代码,正是导致第 1 节所述绕过问题的原因。
无论如何,欢迎在这里提交问题和报告。
npm install
npx vitest run
npx tsc --noEmit
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。