@reinsjs / tools-mcp
插头一个 MCP 将服务器作为一个REINS代理 Socket. 每个工具服务器
清单变成了缰绳 Tool;循环,大脑模块(泄漏,批准,预算,压缩……)和时间线治疗
它们就像进程内工具一样。服务器永远不会知道缰绳的存在。
import { httpTransport, mcpTools } from "@reinsjs/tools-mcp"
import { approval, spill } from "@reinsjs/brain"
import { createAgent } from "@reinsjs/agent"
const github = mcpTools({
transport: httpTransport({
url: "https://mcp.example.com/mcp",
headers: { Authorization: `Bearer ${process.env.MCP_TOKEN}` },
}),
})
export const agent = createAgent({
model,
store,
tools: [myInProcessTool],
sockets: [github, spill(), approval()],
})
仅节点的工作室服务器(通过stdin/stdout说JSON-RPC的子进程)位于 /node 条目:
import { stdioTransport } from "@reinsjs/tools-mcp/node"
const files = mcpTools({
transport: stdioTransport({ command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem", "/data"] }),
prefix: "fs_",
})
主入口只使用web标准api (fetch, URL, Headers),并在Cloudflare Workers上运行
nodejs_compat (在2023兼容性日期验证,请参阅 spikes/edge-runtime-check),在Bun上是Deno
(--allow-net 就够了),而Vercel的呢 edge-runtime (spikes/runtime-matrix - - - - - - tools/list + tools/call
针对每个服务器中的本地服务器)。只有 /node 导入 node:*.
在运行时发生了什么
每次运行都绑定工具。 当运行开始时,套接字调用 tools/list 一次,把结果变成
运行工具表。表不会改变,直到运行结束- a notifications/tools/list_changed 从
服务器只影响 下一个 运行。这是经过深思熟虑的:稳定的工具表可以使提示缓存保持温暖,并且它可以使缓存保持温暖
这就是缰绳“不动态注册”立场的来源。循环将表记录在a中 core.tools_bound 事件
(对模型不可见),并且当它与之前运行的不同时,附加一个可见的 system_note 命名的
添加和删除工具,使模型知道它的手的变化。
电话接通 tools/call. 结果的 content 块成为缰绳内容部分(文本和图像按原样;
音频、资源链接和二进制资源变成了一行描述(有损,并如此说)。 isError 通过
通过。抛出的错误(未知工具、断开连接、超时)变成 tool_result 与 isError: true;
运行继续,模型决定做什么。
注释设置默认值,而不是权限。 readOnlyHint → risk: "low"; destructiveHint → risk: "high" 和
needsApproval: true;其他任何内容(包括没有注释)→ risk: "medium". MCP规范说注释是
提示和不受信任的服务器的注释不能驱动授权-因此缰绳只使用它们来 默认的
这些领域 @reinsjs/brain的 approval() 政策阅读,以及 override 允许主机更改其中任何一个:
mcpTools({
transport,
override: (tool, info) => {
if (info.name === "delete_repo") return false // never expose this one
if (info.annotations?.openWorldHint) return { ...tool, needsApproval: true }
return undefined // keep the default translation
},
})
注意规范自己的默认for destructiveHint 是 true 当缺席。缰绳不将“未注释”视为
“破坏性的”——这将使每个休闲服务器的每个工具都处于审批提示之后。如果你想要的话
严格,加进去 override 或运行 approval({ unmatched: "ask" }).
连接的生命周期
mcpTools() 不连接。需要工具表的第一次运行打开连接;以后运行重用它。
如果传输关闭(服务器重新启动,网络断开),则需要它的下一个调用- a tools/call 在
相同的运行或 tools/list 在下次运行开始时-根据传输配方重新构建连接一次。有
调用内部没有重试策略:失败的调用作为错误结果报告给模型,失败的重新连接在
Run start在向日志写入任何内容之前抛出(失败关闭)。通过 optional: true 来代替
运行没有服务器的工具,并警告一次;然后,模型看到它们被列为已删除。
呼叫 close() 当您使用套接字时(进程关闭,或者主机替换了配置)。
为每个请求构建代理的主机应该 按配置设置缓存套接字,而不是每次都重建它们。
重建对每个请求强制进行一次新的MCP握手。 examples/mcp/agent.ts 控件键控的小缓存
服务器的配置JSON,关闭配置丢失的套接字。
不重启的热重新加载
没有重载API,因为不需要重载API。 createAgent() 只组装对象,工具表是
在运行开始时解决。将每个用户的MCP服务器存储在数据库中的平台读取该配置
当请求到达时,构建(或从缓存中获取)匹配 mcpTools() 套接字和运行。一个服务器
用户添加或删除的内容在其下一条消息中生效;这个循环告诉模型发生了什么变化。
需要了解的一个结果是:暂停审批的运行使用它所拥有的工具表进行签名。如果
暂停时配置发生更改,恢复被拒绝为配置漂移(RunStateError
config_mismatch)。主机有三个诚实的选择:等待会话结束后再应用更改;
接受暂停的运行被放弃并开始一个新的运行,或者通过 allowConfigDrift: true 无论如何要继续——
这份简历还会附上工具变更说明。什么图书馆不会做的是默默地恢复与不同
工具表比审批人看到的要多。
证书到期
一个固定的令牌可以驻留 headers. 过期的令牌不能: headers 在运输工具建成时是固定的,
虽然连接是惰性创建的,但可以跨运行重用,并在删除后从相同的recipe重新构建—所以只有一次
令牌过期,即使重新构建也会携带过期的令牌。通过 auth 而不是:
httpTransport({
url: "https://business-api.tiktok.com/open_mcp/tt-ads-mcp-layer",
auth: {
token: () => db.accessToken(userId), // called before every request
onUnauthorized: () => db.refresh(userId), // called on 401, then the request is retried once
},
})
auth 和一个 Authorization 头是互斥的——在构造时传递两个抛出,而不是
让一个人默默获胜。
McpAuth 就是这两个方法,没有别的,也没有引用任何MCP SDK类型。事实并非如此
SDK的 OAuthClientProvider该界面的重要部分是驱动浏览器授权提示,
这是服务器端代理无法做到的。获取并存储令牌在您的web应用程序;循环只读取和
刷新。完整的OAuth流仍然没有包装-调用SDK refreshAuthorization (或者你自己的代码)
从内 token / onUnauthorized.
Gateway-style服务器
一些服务器将数百个操作抛在脑后 一个 调度工具。TikTok for Business是当前的趋势
示例:它的分层端点注册了41个普通工具 tool_list / tool_get / tool_execute,和
其他约330个操作仅存在于服务器端注册表中,通过 tool_execute({ tool_name, params }).
这不会对循环造成任何损失——工具表在整个运行过程中是固定的,因此提示缓存前缀和配置散列是固定的
不受影响。 但是它无声地挫败了针对工具名称编写的批准策略。 创建一个活动,
修改预算和删除资产组都被称为 tool_execute;实际发生的是
参数。
// WRONG — this never matches. Every operation is named tool_execute.
approval({ deny: ["*_delete"], allow: ["*"] })
// RIGHT — decide on the operation name inside the arguments.
const operationOf = (args: unknown): string =>
typeof args === "object" && args !== null && "tool_name" in args
? String((args as { tool_name?: unknown }).tool_name ?? "")
: ""
approval({
ask: [{
id: "gateway.writes",
match: (call) =>
call.name === "tool_execute" && /_(create|update|delete|upload)$/.test(operationOf(call.args)),
summary: (call) => `TikTok ${operationOf(call.args)}`, // the approver sees the real operation
}],
allow: [
{ id: "gateway.reads", match: (call) =>
call.name === "tool_execute" && /_(get|list|search)$/.test(operationOf(call.args)) },
"tool_list",
"tool_get",
],
})
此配方的可执行版本为 packages/brain/src/gateway-tool.recipe.test.ts,包括负面的
证明基于名称的规则允许删除的案例。
在没有配置规则的情况下,默认值是安全的,但很生硬:这样的服务器不声明注释,因此可以翻译
工具不携带 risk, unmatched: "byRisk" 发送 每一个 呼叫-包括读取-到一个人。这是
故障关闭设计;使用上面的recipe打开读路径,而不是允许dispatcher批发。
关于这种形状的服务器,还有两件事值得了解:
- 注册表列表可能很大。 TikTok的
tool_list返回~57 KB。下spill这就变成了预览 加上一个fetch_blobHandle——无用,因为选择工具正是完整列表的目的。提高 这一工具的预算:override: (tool, info) => info.name === "tool_list" ? { ...tool, resultPolicy: { maxTokens: 100_000, overflow: "spill" } } : tool. - 上游业务错误可能不会设置
isError. TikTok回报{"code": 40001, "message": "..."}作为一个 成功的 MCP的结果;只有mcp级故障(未知工具、传输错误)isError. 任何键控 从isError-重试,你自己的政策,onEvent观察者——除非你翻译它们,否则不会看到它们; 这个withBusinessErrors下一节中的包装器就是这样做的。
大型平面工作台
相反的形状:数百个普通的工具,每个都诚实地注释。TikTok的平面端点列出了其中的377个,
大约200次读取和130次写入,每个都有 readOnlyHint. 通常需要做两件事:保留写的内容
完全超出了模型的范围(它们通过您自己的工具,使用您自己的授权和审计跟踪),
不要再为每个请求发送200个工具定义了。两者是一体的 override 加上
@reinsjs/brain的 lazyTools():
import { lazyTools } from "@reinsjs/brain"
import { normalizeToolOutput, type Tool, type ToolResult } from "@reinsjs/core"
/** TikTok puts business errors in a successful result: text = {"code": <non-zero>, "message": ...} */
const businessErrorOf = (result: ToolResult): string | undefined => {
if (result.isError) return undefined
for (const part of result.content) {
if (part.type !== "text") continue
try {
const body = JSON.parse(part.text) as { code?: unknown; message?: unknown }
if (typeof body.code === "number" && body.code !== 0)
return `Upstream error ${body.code}: ${String(body.message ?? "")}`
} catch {
// not JSON, not an error envelope
}
}
return undefined
}
/** Keep the original text for the model, but mark it as an error so retries, policies and observers see it */
const withBusinessErrors = (tool: Tool): Tool => ({
...tool,
async execute(input, ctx) {
const result = normalizeToolOutput(await tool.execute?.(input, ctx))
const error = businessErrorOf(result)
return error ? { content: [...result.content, { type: "text", text: error }], isError: true } : result
},
})
/** reads: on the menu, loaded on demand; everything else: not a tool the model has */
const readOnlyLazy = (tool: Tool, info: McpToolInfo) =>
info.annotations?.readOnlyHint === true ? withBusinessErrors({ ...tool, lazy: true }) : false
const tiktok = mcpTools({ transport, override: readOnlyLazy })
createAgent({ sockets: [tiktok, lazyTools()] }) // order matters — see below
lazyTools() 从工具绑定中生成它的菜单 之前 它:主机的,加上注册的每个套接字的
早些时候。所以它在 mcpTools(). 另一个订单不是错误,只是无用:MCP工具已经发送进来了
每个转弯都满了,而且 lazyTools() 警告一次,命名他们。有一个较低的层,它有一个原生的位置
延期工具(@reinsjs/lowering-fetch 在Anthropic上)加载工具甚至不会重新加热提示缓存;看到
大脑README的数字。
加载的定义像工具块本身一样受信任(tool_find 结果是 trust: "system") - the
同样的描述已经在模型上展开了。一个200行的菜单大约需要1万个系统代币
提示;它位于缓存的前缀中,并且在运行中不会更改 lazyTools({ summarize }) 可以缩短它。
此配方的可执行版本为 packages/tools-mcp/src/readonly-lazy.recipe.test.ts,包括
次序错了。
已验证的上游行为
请求的历史记录包含 tool_use / tool_result 对于不再在工具表中的工具
被DeepSeek的anthropic协议端点和OpenAI响应协议接受(两者都使用另一个工具)
有一个空的工具表);该模型回答并正确列出了它当前的工具。所以
删除工具不需要在这些上游上创建新的会话。人类自己的终点,到达了
Cloudflare AI Gateway的直通路径,也接受这两种变体(2026-09-15); spikes/l1-deferred-tools P8);
聚合器的Claude端点返回了一个截断的流,这是该代理的已知怪癖,而不是协议
被拒绝。脚本: spikes/mcp-removed-tool-history.
选项
| 选项 | 默认的 | 意义 |
|---|---|---|
transport | - - - - - - | httpTransport({ url, headers?, auth?, fetch?, requestInit? }) 或者 stdioTransport({ command, args?, env?, cwd?, stderr? }) |
prefix | 无 | 加在显示给模型的工具名称前;服务器仍然看到原始名称 |
callTimeoutMs | 60 000 | 每 tools/call 超时;过期成为一个错误结果 |
optional | false | 在 tools/list 运行开始时失败:抛出(默认)或不提供工具并警告一次 |
override(tool, info) | - - - - - - | 每个工具都重写;返回 false 放下工具 |
clientInfo | { name: "reins", version } | 发送MCP握手 |
warn | console.warn | 哪里报告了退化 |
模型api拒绝的工具名称(^[A-Za-z0-9_-]{1,64}$)被重写(files.read → files_read)用…
一次性警告;电线上使用的是原始名称。
不支持:采样、启发、资源、提示、MCP应用程序和驱动OAuth授权
提示(在您的应用程序中获取令牌并将它们交给您) auth)。
许可证
MIT©2026 newate Limited