SYSTEM OK | ACTION CONTRACTS 247 DOMAINS 35 DEMO ENVIRONMENT 无需申请 | 服务热线 18926139835
首页/洞察/技术系列/Tool Registry 与 MCP Server

Tool Registry 与 MCP Server

将系统开放为 MCP Server,意味着外部 AI 客户端可以直接调用本系统的业务能力。 由此产生一个必须先行回答的问题:外部客户端并不携带调用者的身份。 本文说明系统如何处理该问题:Tool Registry 集中注册每一项工具,getAuthorized() 按权限生成清单, MCP Server 在协议层对外暴露,并保证三条消费路径共用同一份权限口径。

作者 EAMX 产品团队 发布 2026-09-28 阅读 约 11 分钟 专题 技术系列

01一项工具由五个要素构成

系统中每一项可以被 Agent 调用的业务能力,均被定义为一个 Tool。Tool 是动作契约在 AI 侧的投影: 同一项业务能力在 Web 控制台、内嵌 AI 助手与 MCP 通道下共用同一份实现, Tool 只负责把它暴露给调用方,并声明调用它所需满足的条件。

一个 Tool 由五项要素构成:名称、说明、注解、入参模式与处理函数。接口定义如下。

interface McpTool { name: string; // "equipment:list" description: string; // 供模型判断调用时机 annotations: { casl?: { action: Actions; subject: Subjects }; // 权限要求 timeout?: number; // 超时控制,默认 15s }; inputSchema: object; // Zod schema → JSON Schema handler: (input: any, ctx: AuthorizedContext) => Promise<ToolCallResult>; }

五项要素各自承担一项职责。名称形如 equipment:list,前段为业务域,后段为动作, 在注册表中全局唯一。说明供模型判断调用时机,其写法要求可判读,需写明筛选维度与返回内容。 注解声明该工具的权限要求与超时上限。入参模式以 Zod 声明,运行时转换为 JSON Schema 交给客户端, 因此客户端的参数校验范围与服务端一致。处理函数接收入参与授权上下文,返回统一的结果结构。

权限要求由工具自身声明,这一点值得单独说明。调用方不需要传入权限信息, 注册表也不必执行任何业务逻辑,即可判断某位调用者能否使用该工具。 该性质是第 02 节所述清单过滤的实现基础。

需要与普通接口区分的是:普通接口只描述输入与输出,调用方是否可以使用由接口之外的约定决定; Tool 把这一约定写进自身,使可见性成为一项可计算的事实。其直接结果是工具的可用范围不需要逐项人工维护, 也不会因新增一条通道而失效。命名采用业务域加动作的形式,按域分组后可直接对应到业务模块, 便于在核验时逐域核对。

依据:tools/<模块>/<名称>.tool.ts 的接口定义与 ai/tool-registry.ts 的工具类型声明。

02注册表的两项职责

Tool Registry 的实现共 86 行,职责只有两项:存放已注册的工具,以及按权限生成可见清单。

class ToolRegistry { private tools = new Map<string, McpTool>(); registerAll(tools: McpTool[]): void { for (const t of tools) { if (this.tools.has(t.name)) throw new Error(`Tool "${t.name}" already registered`); this.tools.set(t.name, t); } } get(name: string): McpTool | undefined { return this.tools.get(name); } // 关键:按 CASL 权限生成清单 getAuthorized(ability: AppAbility): McpTool[] { return [...this.tools.values()].filter(t => { const casl = t.annotations?.casl; if (!casl) return true; // 未声明权限要求 = 通用工具 return ability.can(casl.action, casl.subject); }); } getAll(): McpTool[] { return [...this.tools.values()]; } }

registerAll 在启动时一次性写入全部工具;若两次注册使用同一名称,直接抛出错误, 避免两处实现覆盖同一项能力。get 按名称取回单条工具,供调用执行阶段使用。 getAll 返回全部工具,供启动日志与运维核对使用。

其中承担权限职责的是 getAuthorized。它接收一个 CASL 能力对象, 逐条判断工具注解中声明的权限要求是否成立;未声明权限要求的工具被视为通用工具,对所有调用者开放。 因此注册表既不持有身份信息,也不参与身份认证——身份由各条通道在进入注册表之前确定, 注册表只负责依据身份裁剪清单。

该实现的边界同样明确。注册表不做身份认证,不访问数据库,也不判断业务状态; 它只回答一个问题:在某位调用者的权限下,哪些工具应当出现在清单里。 把职责限制在这一层,使注册表可以在任意通道中被复用,也使其行为可以在单元测试中直接断言, 不必为验证权限过滤而准备数据库与环境依赖。

本文的核心实现

权限过滤发生在清单生成阶段,而非调用执行阶段。 调用方收到的是「该身份可以使用的工具集合」,而不是「全部工具加上逐条拒绝」。 两者的差别在于前者的边界在协议层即已确定:客户端看不到未授权工具的名称、说明与入参模式,因而无法构造调用。

依据:ai/tool-registry.ts(86 行)与 tools/register.ts 的实现记录;权限注解取值为 CASL 的 action 与 subject。

03新增一项工具需要改动的位置

注册入口集中在 tools/register.ts,启动时调用一次。

// tools/register.ts —— 启动时一次性注册 export function registerAllTools(): void { toolRegistry.registerAll([ // 设备 listEquipmentsTool, // "equipment:list" → read Equipment getEquipmentTool, // "equipment:get" → read Equipment // 故障 createFaultReportTool, // "fault:create-report" → create FaultReport listFaultReportsTool, // "fault:list" → read FaultReport // 通用 entitySearchTool, // "system:entity-search" → 无权限要求 ]); console.log(`[Tools] Registered ${toolRegistry.getAll().length} AI tools`); }

新增一项工具需要两步:在 tools/<模块>/<名称>.tool.ts 创建实现, 在 register.ts 增加一行 import 与一行注册。注册表本身与 MCP 适配层均不需要修改。

该结构的实际意义在于扩展成本固定。新增业务能力不会触动已注册的其他工具,改动范围与工具总数无关。 这一性质在工具数量较多的系统中会持续产生收益:注册表是唯一需要维护的清单, 新增动作时不必在多个位置同步信息,也就不会出现某处已更新、另一处仍指向旧实现的情形。

注册完成后输出一行日志,报出当前注册的工具总数。该数值可在启动阶段与预期值核对, 用于发现模块未加载或注册遗漏两类问题。

重名直接抛出错误而不作告警处理,原因在于同名工具意味着同一项业务能力存在两份实现。 若允许后者覆盖前者,故障形态为某条通道的行为与其他通道不一致,且难以在启动阶段被察觉。 启动即失败可以避免该类问题进入运行期。

依据:tools/register.ts(30 行)的注册入口设计与 ai/tool-registry.ts 的启动日志记录。

04一项工具的实现:设备列表

以设备列表查询为例,说明一项具体工具的写法。

// tools/equipment/list.tool.ts export const listEquipmentsTool: McpTool = { name: "equipment:list", description: "搜索设备列表。支持按名称、资产编码、型号、位置、运行状态等条件筛选", annotations: { casl: { action: "read", subject: "Equipment" }, timeout: 10000, }, inputSchema: z.object({ keyword: z.string().optional(), operationalStatus: z.enum(["running","idle","fault","repairing","scrapped"]).optional(), department: z.string().optional(), page: z.number().default(1), pageSize: z.number().default(20), }), handler: async (input, ctx) => { const conditions = buildConditions(input, ctx.ability); const rows = await db.select().from(equipmentsTable).where(conditions); return { success: true, data: { rows, total: rows.length }, cardType: "equipment_list" }; }, };

该实现有四处需要说明。

  • 入参以 Zod 声明。运行状态采用枚举,页码与每页条数带默认值。同一份声明既用于服务端校验,也转换为 JSON Schema 供客户端使用,两端的校验范围因此一致。
  • 权限注解为 read Equipment。该工具能否进入清单,取决于调用者是否具备读取设备资源的权限。
  • 数据范围在处理函数内构造。通过 ctx.ability 生成筛选条件,同一条工具在集团角色与子公司角色下返回的数据范围不同,差异由权限裁决产生,工具自身不写死范围。
  • 返回结构统一为三项。success、data 与 cardType。其中 cardType 供内嵌 AI 助手选择呈现方式,MCP 通道只取其中的 data。

超时上限单独声明,用于区分响应时间差别较大的工具。查询类工具的返回行数随筛选条件变化, 其超时上限通常大于单条记录的读写。该取值按工具设定,注册表不在逐项设置之外另设统一值。

表 1 · 设备列表工具的构成
要素取值作用
名称equipment:list业务域与动作的组合,在注册表中全局唯一
说明搜索设备列表,支持按名称、资产编码、型号、位置、运行状态筛选供模型判断调用时机
权限注解read / Equipment注册表据此判断该工具是否进入清单
超时10 000 毫秒覆盖默认的 15 秒,适用于返回行数可能较多的查询
入参模式关键字、运行状态、部门、页码、每页条数客户端参数校验与服务端保持一致
返回结构success / data / cardType内嵌助手取 cardType 呈现,MCP 通道取 data

该写法同样适用于清单、工单与报表类工具。区别只在权限注解的取值与返回结构中 data 的字段, 工具与注册表之间的接口保持不变。

依据:tools/equipment/list.tool.ts 的实现;入参校验与数据范围条件取自 CASL 权限裁决结果。

05MCP Server:清单生成与调用执行

MCP 适配层 mcp/server.ts 共 66 行,向协议层提供两个入口:生成工具清单,以及执行工具调用。 两者均以已授权的调用上下文为输入。

// mcp/server.ts —— MCP 协议适配层 export function generateMcpToolList(ctx: AuthorizedContext) { const tools = toolRegistry.getAuthorized(ctx.ability); // 按权限生成清单 return { tools: tools.map(t => ({ name: t.name, description: t.description, inputSchema: { type: "object", properties: {} }, })), }; } export async function executeMcpToolCall( name: string, args: Record<string, unknown>, ctx: AuthorizedContext, ) { const tool = toolRegistry.get(name); if (!tool) return { content: [{ type: "text", text: "Tool not found" }], isError: true }; const result = await tool.handler(args, ctx); if (result.success) { return { content: [{ type: "text", text: JSON.stringify(result.data) }], isError: false }; } return { content: [{ type: "text", text: result.error?.message }], isError: true }; }

generateMcpToolList 先调用 getAuthorized 生成该身份的可见清单, 再转换为 MCP 协议要求的格式。清单在服务端生成,与客户端种类无关—— 同一凭证据在两个不同客户端中列出的工具应当一致。未列入清单的工具, 客户端不会取得其名称、说明与入参模式,因而无法构造合法调用。

executeMcpToolCall 按名称取回工具并执行处理函数。名称不存在时返回错误标记; 处理函数返回失败时同样返回错误标记,并携带错误信息。两种情形均由客户端原样呈现,服务端不做改写。

清单生成本身不足以构成权限控制,因此执行阶段仍需按名称取回工具。两者的分工是: 清单决定客户端能看到什么,执行入口决定服务端如何处理一次具体请求。 客户端可以自行构造请求,但无法凭清单之外的名称取回工具。

表 2 · 协议层两个入口的职责
入口输入输出的权限形态
generateMcpToolList 已授权的调用上下文 仅包含该身份可见的工具;未列入者客户端不可见,无法取得入参定义
executeMcpToolCall 工具名称与入参 按名称取回工具并执行;名称不存在时返回错误标记,不执行任何业务逻辑
该设计可直接核验

权限边界由清单决定。以演示账号接入后,由客户端列出可用工具,核对返回的条目数量与业务域分组。 若清单中不含写入类动作,说明这些动作未被列入,而非在调用时被逐条拦截。 查看核验步骤 →

依据:mcp/server.ts(66 行)的协议适配实现;该账号可见工具数以端点实时返回为准。

06三条消费路径共用同一份权限口径

同一项 Tool 可以被三类消费者调用。三者的入口不同,权限检查发生的位置也不同。

表 3 · 三条消费路径的入口与权限检查位置
消费方入口权限检查位置
内部 AI
EAMX 内嵌助手
Tool Factory → mcpToolToAiSdkTool() Tool Factory 的 execute 内
外部 AI
Claude Desktop / Cursor 等
MCP Server → generateMcpToolList() getAuthorized() 生成清单时按权限过滤
HTTP API Express Router → 中间件 API 中间件

三个入口的检查位置不同,裁决依据相同:均取自同一套 CASL 规则。 工具自身不承担权限判断,只在注解中声明其要求。因此权限口径不存在第二套写法, 也不会出现某一通道可以执行、另一通道不可执行的情形。

三条路径共用同一批工具,另一层含义在于:为界面开发的业务能力不需要为 Agent 单独重写一遍。 新增一项动作时,三条通道同时获得该能力,改动仍集中在注册入口一处。

将这一层放回系统整体,其上游关系如下。动作契约共 247 条,覆盖 35 个业务域,是工具注册的源清单。 权限裁决来自 CASL 同构权限设计,前后端共用同一套规则。写入类动作统一经过 executeAction 管道, 在管道内依次完成参数校验、权限裁决、预演、人工确认、幂等键、执行与审计七道环节, 与调用方是 Web 控制台还是外部 Agent 无关。七道环节的顺序固定,任一条通道的调用都不会跳过其中的任何一环, 因此权限与审计的口径不因接入方式而改变。

演示账号的工具清单由端点实时返回。该数值是权限裁剪的结果:写入类动作不在只读角色的清单内。 Agent 不因接入而取得更高权限,亦不存在可以绕过的路径。

依据:动作契约清单(247 条 / 35 个业务域)、executeAction 管道的七道环节与 CASL 同构权限设计;该账号可见工具数以端点实时返回为准。

07边界与不承诺

讨论开放接口时,能力清单通常被放在最前面。对信息中心与技术评估而言,下述三类信息比能力清单更早被追问。

表 4 · 应当在能力之前说明的三件事
事项说明
演示账号不可见什么 全部写入类动作不在演示账号的工具清单内。以权限设计划定边界,而非在调用时逐条拦截
哪些动作不得自主执行 改账、报废、权限变更等高危动作在动作定义上标注 confirm,未携带确认一律拒绝执行
不承诺什么 不承诺识别准确率 100%,不承诺完全无人值守

工具数量并不固定。该数值随版本迭代变化,站内不写固定值,核验时以端点实时返回为准; 核验时以端点实时返回的清单为准。演示凭证由使用者在端点自行领取,页面不留固定值—— 固定值无法轮换,也无法按使用者分别限流与审计,因此不写入页面。

上述三项均属可在接入前确认的事项。工具清单与业务域覆盖可由客户端当场报出; 高危动作的确认要求写在动作定义中,与调用方无关;不承诺项则在能力说明中列明, 不随演示环境与正式环境而改变。

演示环境(无需申请)
端点地址   https://app.eamx.com.cn/mcp
传输协议   Streamable HTTP
认证形式   Authorization: Bearer <演示账号>
凭证来源   页内公开的演示账号,用户名密码登录
可见工具    由端点实时返回(按账号角色)

端点地址、传输协议与认证形式在「AI 原生与 Agent 接入」页与本系列其余各篇保持同一口径, 本文不另行列示其他参数。

依据:该账号可见工具数以端点实时返回为准;演示环境的账号为页内公开的演示账号,用户名密码登录。

08作者与依据

作者
EAMX 产品团队 · 产品与解决方案
依据
动作契约与工具注册的实现记录:ai/tool-registry.ts(86 行)、mcp/server.ts(66 行)、tools/register.ts(30 行)、ai/tool-factory.ts(93 行)
数据来源
动作契约清单(可实时导出,247 条 / 35 个业务域)、演示环境(无需申请公开,该账号可见工具数由端点实时返回工具)
引用标准
ISO 55000 系列仅为方法论依据,不构成认证;认证对象是组织,而非软件
更新日期
2026-09-28
01相关产品能力

本文所述的实现,对应哪些产品能力

02相关文章

同专题与跨专题的延伸

本文所述的实现可以逐项核验

247 条动作契约、35 个业务域、该账号可见工具数由端点实时返回、写入类动作统一经过一条执行管道—— 上述各项均无需采信本文陈述,可由演示环境自行核实。

信息中心 / 技术评估
需要接入参数与协议细节
业务部门 / 执行层
关注 Agent 可为业务提供的操作支持