把后台管理系统接入 MCP

将 18 个 REST 接口封装为 Model Context Protocol 工具的设计记录

融e贷是一套面向贷款业务的后台管理平台,包含产品配置、申请审批、用户档案与风控、全局设置、数据大盘等模块。前端为纯静态多页应用,后端提供 18 个 REST 接口,二者通过 JWT 鉴权与统一响应契约对接。

在接口规范定型之后,一个问题变得难以回避:既然全部业务能力都已经以结构化的形式定义完毕,管理员为何仍需通过界面逐级点击来触发它们。界面在此处承担的并非业务逻辑,而是参数收集与调用编排——而这恰好是语言模型所擅长的部分。

本文记录对这一问题的一次实现:在原有系统之外增加一层 MCP Agent,将后端接口封装为 Model Context Protocol 工具并接入 Claude,使管理操作得以通过自然语言完成。

「把设备采购贷上线」

「驳回王五的申请,理由是近期信用评分异常」

「把风险预警阈值调到 0.4」

下文讨论这一层的具体设计,重点在三个方面:鉴权的生命周期管理、读写分离与副作用标注、以及执行后的读回校验。


一、MCP 是什么,以及它解决了什么

Model Context Protocol 是一个开放协议,用来把外部能力(工具、数据源、提示词)以标准化的方式暴露给大模型客户端。你写一个 MCP Server,声明自己有哪些工具、每个工具收什么参数,Claude 客户端连上来之后就能在对话中自主调用它们。

其要点在于模型无需推测 HTTP 接口的形态。工具名称、参数 schema、副作用标注均由服务端显式声明,模型接收到的是一份结构化的能力清单,而非一段自然语言描述的 API 文档。这一区别决定了整套机制可靠性的上限。

整体架构

两条操控路径并存、互不干扰:

┌─────────────────────────────────────────────────────────────┐
│                        管理员                                │
└───────────────┬─────────────────────────┬───────────────────┘
                │ 自然语言对话             │ 浏览器点击
                ▼                          ▼
   ┌────────────────────────┐   ┌────────────────────────┐
   │   Claude / Claude Code │   │   管理前端 (静态多页)   │
   │  ┌──────────────────┐  │   │  index.html / 各业务页  │
   │  │ MCP Agent        │  │   │  ┌──────────────────┐   │
   │  │ (mcp-server/)    │  │   │  │ api-client.js    │   │
   │  │ 17 个工具         │  │   │  │ auto/remote/local│   │
   │  └────────┬─────────┘  │   │  └────────┬─────────┘   │
   └───────────┼────────────┘   └───────────┼─────────────┘
               │ Bearer JWT                 │ Bearer JWT
               ▼                            ▼
   ┌─────────────────────────────────────────────────────────┐
   │              后端 REST API   /api/v1                     │
   │        18 个接口 · JWT 鉴权 · 统一 {code,data,message}    │
   └─────────────────────────────────────────────────────────┘

Agent 经由 MCP,前端经由 api-client.js,二者最终请求同一后端、使用相同的 JWT 鉴权、遵循相同的响应契约。Agent 层对原系统零侵入:其代码完全隔离在 mcp-server/ 目录内,移除后前端功能不受任何影响。这一约束在设计之初即已确立——一个使主应用产生反向依赖的"增强层",其代价终将超过它带来的收益。


二、工具设计:17 个工具,读写分离

18 个后端接口中,「管理员登录」被内置为鉴权机制而未单独暴露——Agent 需要的不是一个可供调用的登录工具,而是任何时刻都已处于登录状态。其余 17 个封装为工具,分为三类。

2.1 只读工具(6 个,零副作用)

工具接口说明
list_productsGET /loan/products全部产品(利率/额度/期限/还款方式/状态)
get_productGET /loan/products/{id}单个产品完整配置
list_usersGET /user/list用户档案分页(含信用评分、贷款历史)
get_userGET /user/{id}单个用户详情
list_applicationsGET /loan/applications申请单列表,支持关键字/状态/产品筛选
get_settingsGET /settings全局设置(审批开关/风险阈值/默认分页)

2.2 写操作工具(6 个,有副作用)

这一组我额外标注了可逆性,这是后面安全机制的依据:

工具必填参数可逆性
set_product_statusid, status✅ 可逆
update_settings任意子集✅ 可逆
update_productid + 全量字段⚠️ 需先读原值方可回滚
submit_risk_actionid, action⚠️ 有配对操作但留日志
submit_approvalid, action(+条件字段)❌ 不可逆
create_productname, annualRate, 额度, 期限…❌ 无删除接口

submit_approval 的条件字段是一个典型的 schema 设计问题:action=已放款 须传 approvedAmountloanTerm已驳回 须传 rejectReason待补件 须传 supplementText。此类"必填性依赖于其他字段取值"的约束,在 JSON Schema 中表达代价较高。此处采取的做法是在工具描述中明确说明,同时在服务端保留一次校验:描述用于提高模型首次传参的正确率,校验用于兜住其余情况。

2.3 认证 / 用户端流程工具(5 个)

register_adminreset_passwordchange_passwordsubmit_user_data_inputsubmit_loan_application。这一组覆盖了用户端的完整流程,管理员 token 已做兼容。


三、鉴权:整个对话过程你不需要接触 token

鉴权是这一层中最容易被低估的部分。若每次对话都需人工获取 token、粘贴、并在过期后重复一次,则这层 Agent 的收益将被其自身引入的操作成本抵消殆尽——它节省的点击次数少于它新增的步骤

Agent 首次调用工具
   │
   ├─ 内存无 token / 已过 23h ──► POST /admin/login ──► 缓存 JWT
   │
   ├─ 携带 Bearer JWT 调用目标接口
   │
   └─ 若返回 401 / code 4001(token 失效)
          └─► 强制重新登录 ──► 用新 token 重试一次

其中有三处细节值得说明:


四、安全机制:annotations + 执行后读回

4.1 用 annotations 把副作用告诉客户端

MCP 的 tools/list 允许每个工具携带 annotations。这是协议层提供的表达能力,应当充分使用:

注解只读工具写工具
readOnlyHinttruefalse
destructiveHintfalsetrue
工具标题普通前缀 ⚠️ 写操作:

对应的 Agent 侧行为约定分成三档:

  1. 只读:直接执行并返回,不请求用户介入。
  2. 可逆写:直接执行,但须报告变更前后的值。
  3. 不可逆写(审批 / 新增产品 / 风控标记 / 修改凭据):先复述完整参数,待用户明确确认后再提交。

需要强调的是,该分档的依据为可逆性而非重要程度。一个影响面有限但无法撤销的操作,比一个影响面很大但可随时回滚的操作更需要确认环节。

4.2 执行后读回校验

这是整套设计中价值最高的一条:写操作不信任接口返回值,而是二次读取真实状态以确认变更已生效。

其必要性在于,「接口返回 200」与「数据库确已变更」是两个独立的事实。二者之间可能隔着一层缓存、一个静默失败的事务,或一处未报错但也未生效的字段名拼写错误。人工操作界面时,会在提交后返回列表核对一次——这本身就是一次读回校验。Agent 不会自发形成该习惯,须将其显式实现。

在此基础上,针对可逆写操作另有一个完整的往返验证脚本:读原值 → 修改 → 验证 → 还原 → 复核,执行完毕后数据库中不留痕迹。

# 可逆写操作真实往返验证(读→改→验证→还原→复核,无残留)
node validate-reversible.js

该脚本的意义不止于一次性验证。它使得在每次改动 Agent 逻辑之后,都能以零残留、零副作用的方式确认写路径依然可用;否则每次验证都需真实地上下架一次产品,若干轮之后测试数据即失去参考价值。

4.3 一次真实交互

上述两项机制在实际对话中的表现如下。指令为「把王五的申请驳回,理由为近期信用评分异常」——一个典型的不可逆写操作:

Claude 执行驳回申请操作的完整对话:先复述参数并提示业务异常、等待用户确认、提交后二次读取申请列表校验状态变更
这次交互同时触发了本节讨论的两项机制。其一是提交前的确认:Agent 没有直接调用 submit_approval,而是先完整复述了接口名、申请单号、actionrejectReason,并列出两个待确认点。值得注意的是它还识别出了一处业务异常——该申请当前状态为「已放款」,款项已经发出,将其改为「已驳回」在业务上不符合常规流程(驳回通常针对待处理/待审核的申请)。这不是预设的规则,而是模型从读到的真实数据中判断出来的。

其二是执行后的读回校验:确认之后,Agent 提交了驳回,随即再次调用 list_applications 读取真实状态,确认状态确已由「已放款」变为「已驳回」、审批记录已写入(handledAt 22:33:31)。它对这一步的说明是"不拿返回值当数"——这正是第 4.2 节所要求的行为。最后它再次提示该操作不可逆、后端不存在撤销接口。

五、一处例外:不遵循统一前缀的接口

18 个接口中有 17 个统一挂载于 /api/v1 前缀之下,唯 change_password 是例外——它位于服务器根路径 {host}/auth/change-password,不拼接前缀,且另有一条备用路径。

处理方式是在 client 层引入 hostRootUrl() 剥离前缀,并在主路径失败时自动回退至 /api/admin/change-password

此事本身影响有限,但它反映出一个更具普遍性的问题:当接口被封装供模型调用时,接口间的不一致性成本会被显著放大。人阅读文档时可以一眼识别出某个接口属于特例,模型接收到的则只是一个失败的 HTTP 响应。因此这类特例必须在封装层予以抹平,而不应寄望模型自行理解其差异的成因。


六、接入 Claude Code

配置结构如下(真实凭据走 .mcp.json,已被 .gitignore 排除,仓库里只放脱敏模板 .mcp.json.example):

{
  "mcpServers": {
    "rongedie-admin": {
      "command": "node",
      "args": ["mcp-server/src/index.js"],
      "env": {
        "RONGEDIE_BASE_URL": "http://<后端地址>/api/v1",
        "RONGEDIE_ADMIN_USER": "admin",
        "RONGEDIE_ADMIN_PASSWORD": "在此填入管理员密码"
      }
    }
  }
}

接入步骤:

  1. 项目根目录启动 claude
  2. 首次会弹出两个确认:是否信任本文件夹、是否批准项目 MCP(rongedie-admin)——均批准;
  3. 批准后 17 个工具自动加载,直接用中文对话操控后台;
  4. claude mcp list 查看连接状态,claude mcp reset-project-choices 可重置批准记忆。

还有一个自检脚本,验证 initializetools/list(应为 17 个)→ 只读工具真实调用:

node smoke-test.js
# 应输出 17 个工具、6 只读 + 11 写全绿

七、实际用起来是什么样

查询(只读,直接执行)

现在都有哪些产品
查所有待处理的贷款申请
看看用户 10002 的详细档案
3 号产品的完整配置是什么

可逆改动(直接执行 + 报告前后值)

把设备采购贷上线
下架个人消费贷
把风险预警阈值调到 0.4
默认分页改成 10

不可逆操作(Agent 先复述参数、待确认)

驳回 APL-20231026003,理由是近期信用评分异常
新增一款车贷产品,年化 3.99%,额度 1 到 50 万,期限 12 到 60 个月,等额本息
给用户 10002 标记重点观察

其中变化最显著的是组合操作。「查所有待补件的申请,列出其中申请金额超过 50 万的」——在界面上,这需要先筛选一次、再人工比对;在对话中则是一句话,Agent 自行决定先调用 list_applications(status=待补件) 再作过滤。该需求未对应任何一行新增代码。

这正是工具化相较于接口化的实质收益:所提供的是原子能力,组合方式在使用时决定,而非在开发时预设。


八、边界与安全说明

需明确本项目的定位:课程设计与自用演示,安全水位与之匹配,不接入生产数据或真实客户信息。


九、若干总结

着手实现之前,预期的难点在于"让模型理解业务"。实际结果并非如此——模型对"上架一个产品"这类任务的理解不存在障碍。真正的工作量集中在边界条件上:token 于何时刷新、哪些操作需要确认、接口报告成功后是否应予采信、不遵循统一约定的路径如何抹平。

换言之,Agent 层的价值不在于其自身的推理能力,而在于它将那些人工操作界面时会顺带完成、但一旦转为程序实现便极易遗漏的步骤,显式地固化了下来——提交后核对列表以确认变更生效、在不可逆操作前停顿一次、token 失效后重新登录。

这些步骤本就属于流程的必要组成,只是长期隐含在操作者的习惯之中,未被当作系统的一部分对待。

← 返回首页