融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_products | GET /loan/products | 全部产品(利率/额度/期限/还款方式/状态) |
get_product | GET /loan/products/{id} | 单个产品完整配置 |
list_users | GET /user/list | 用户档案分页(含信用评分、贷款历史) |
get_user | GET /user/{id} | 单个用户详情 |
list_applications | GET /loan/applications | 申请单列表,支持关键字/状态/产品筛选 |
get_settings | GET /settings | 全局设置(审批开关/风险阈值/默认分页) |
2.2 写操作工具(6 个,有副作用)
这一组我额外标注了可逆性,这是后面安全机制的依据:
| 工具 | 必填参数 | 可逆性 |
|---|---|---|
set_product_status | id, status | ✅ 可逆 |
update_settings | 任意子集 | ✅ 可逆 |
update_product | id + 全量字段 | ⚠️ 需先读原值方可回滚 |
submit_risk_action | id, action | ⚠️ 有配对操作但留日志 |
submit_approval | id, action(+条件字段) | ❌ 不可逆 |
create_product | name, annualRate, 额度, 期限… | ❌ 无删除接口 |
submit_approval 的条件字段是一个典型的 schema 设计问题:action=已放款 须传 approvedAmount 与 loanTerm;已驳回 须传 rejectReason;待补件 须传 supplementText。此类"必填性依赖于其他字段取值"的约束,在 JSON Schema 中表达代价较高。此处采取的做法是在工具描述中明确说明,同时在服务端保留一次校验:描述用于提高模型首次传参的正确率,校验用于兜住其余情况。
2.3 认证 / 用户端流程工具(5 个)
register_admin、reset_password、change_password、submit_user_data_input、submit_loan_application。这一组覆盖了用户端的完整流程,管理员 token 已做兼容。
三、鉴权:整个对话过程你不需要接触 token
鉴权是这一层中最容易被低估的部分。若每次对话都需人工获取 token、粘贴、并在过期后重复一次,则这层 Agent 的收益将被其自身引入的操作成本抵消殆尽——它节省的点击次数少于它新增的步骤。
Agent 首次调用工具
│
├─ 内存无 token / 已过 23h ──► POST /admin/login ──► 缓存 JWT
│
├─ 携带 Bearer JWT 调用目标接口
│
└─ 若返回 401 / code 4001(token 失效)
└─► 强制重新登录 ──► 用新 token 重试一次
其中有三处细节值得说明:
- 提前刷新:后端 JWT 有效期为 24h,Agent 在 23h 时主动刷新。预留一小时余量,是为规避"请求发出时尚未过期、抵达服务端时已经过期"的临界情形。
- 401 自愈:即便已有提前刷新,仍保留遇 401 强制重新登录并重试一次的路径。该路径在正常情况下不会触发,但其存在保证了token 相关的失败不会上浮至对话层。
- 凭据来自环境变量:
RONGEDIE_ADMIN_USER与RONGEDIE_ADMIN_PASSWORD,代码中不作硬编码。Token 仅存在于 MCP Server 进程内存,不落盘。
四、安全机制:annotations + 执行后读回
4.1 用 annotations 把副作用告诉客户端
MCP 的 tools/list 允许每个工具携带 annotations。这是协议层提供的表达能力,应当充分使用:
| 注解 | 只读工具 | 写工具 |
|---|---|---|
readOnlyHint | true | false |
destructiveHint | false | true |
| 工具标题 | 普通 | 前缀 ⚠️ 写操作: |
对应的 Agent 侧行为约定分成三档:
- 只读:直接执行并返回,不请求用户介入。
- 可逆写:直接执行,但须报告变更前后的值。
- 不可逆写(审批 / 新增产品 / 风控标记 / 修改凭据):先复述完整参数,待用户明确确认后再提交。
需要强调的是,该分档的依据为可逆性而非重要程度。一个影响面有限但无法撤销的操作,比一个影响面很大但可随时回滚的操作更需要确认环节。
4.2 执行后读回校验
这是整套设计中价值最高的一条:写操作不信任接口返回值,而是二次读取真实状态以确认变更已生效。
其必要性在于,「接口返回 200」与「数据库确已变更」是两个独立的事实。二者之间可能隔着一层缓存、一个静默失败的事务,或一处未报错但也未生效的字段名拼写错误。人工操作界面时,会在提交后返回列表核对一次——这本身就是一次读回校验。Agent 不会自发形成该习惯,须将其显式实现。
在此基础上,针对可逆写操作另有一个完整的往返验证脚本:读原值 → 修改 → 验证 → 还原 → 复核,执行完毕后数据库中不留痕迹。
# 可逆写操作真实往返验证(读→改→验证→还原→复核,无残留)
node validate-reversible.js
该脚本的意义不止于一次性验证。它使得在每次改动 Agent 逻辑之后,都能以零残留、零副作用的方式确认写路径依然可用;否则每次验证都需真实地上下架一次产品,若干轮之后测试数据即失去参考价值。
4.3 一次真实交互
上述两项机制在实际对话中的表现如下。指令为「把王五的申请驳回,理由为近期信用评分异常」——一个典型的不可逆写操作:
submit_approval,而是先完整复述了接口名、申请单号、action 与 rejectReason,并列出两个待确认点。值得注意的是它还识别出了一处业务异常——该申请当前状态为「已放款」,款项已经发出,将其改为「已驳回」在业务上不符合常规流程(驳回通常针对待处理/待审核的申请)。这不是预设的规则,而是模型从读到的真实数据中判断出来的。其二是执行后的读回校验:确认之后,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": "在此填入管理员密码"
}
}
}
}
接入步骤:
- 在项目根目录启动
claude; - 首次会弹出两个确认:是否信任本文件夹、是否批准项目 MCP(
rongedie-admin)——均批准; - 批准后 17 个工具自动加载,直接用中文对话操控后台;
claude mcp list查看连接状态,claude mcp reset-project-choices可重置批准记忆。
还有一个自检脚本,验证 initialize → tools/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=待补件) 再作过滤。该需求未对应任何一行新增代码。
这正是工具化相较于接口化的实质收益:所提供的是原子能力,组合方式在使用时决定,而非在开发时预设。
八、边界与安全说明
需明确本项目的定位:课程设计与自用演示,安全水位与之匹配,不接入生产数据或真实客户信息。
- 明文 HTTP:后端为
http://,传输不加密。若要投入实际使用,这是首先需要变更的一项。 - 演示凭据:仓库中的管理员账号同时是真实可用的后端账号,故仓库设为 Private。
- 密码不入库:含真实密码的
.mcp.json已由.gitignore排除,Agent 凭据从环境变量读取。 - 写操作风险:审批、新增产品、征信录入等均为不可逆写操作,后端未提供对应的撤销接口。这也正是确认机制必须实现在 Agent 侧的原因——其下游不存在任何兜底。
九、若干总结
着手实现之前,预期的难点在于"让模型理解业务"。实际结果并非如此——模型对"上架一个产品"这类任务的理解不存在障碍。真正的工作量集中在边界条件上:token 于何时刷新、哪些操作需要确认、接口报告成功后是否应予采信、不遵循统一约定的路径如何抹平。
换言之,Agent 层的价值不在于其自身的推理能力,而在于它将那些人工操作界面时会顺带完成、但一旦转为程序实现便极易遗漏的步骤,显式地固化了下来——提交后核对列表以确认变更生效、在不可逆操作前停顿一次、token 失效后重新登录。
这些步骤本就属于流程的必要组成,只是长期隐含在操作者的习惯之中,未被当作系统的一部分对待。