tyer-mcp-server
by qbsoft
README.md
# tyer MCP Server
把 PigX 系统(翌核 CRM、SRM…)的业务接口包装成 MCP 工具,供 WeKnora 智能体调用。
**核心是按人鉴权**:每个 WeKnora 用户各自 OAuth 授权,本服务原样转发其本人的
上游令牌,权限判定完全交给上游系统。**服务侧不写任何权限代码**——所以永远不会
和上游的权限模型漂移。
## 目录结构
```
tyer_mcp_core/ 共享层:PigX 鉴权 + 响应处理 + 雪花 ID,接新系统时一行不用改
mcp-tyer-crm/ 翌核 CRM :8765 .env + 业务工具
mcp-tyer-srm/ SRM 骨架 :8766 .env + 业务工具(待接入)
tests/ 核心层测试
start-crm.bat 双击启动 CRM
start-srm.bat 双击启动 SRM
start-all.bat 两个一起
```
## 启动
双击 `start-crm.bat`。关闭窗口即停止。
**依赖上游先起**:MCP 服务启动时不连上游,但首次调用/刷新令牌时需要。
上游没起就授权,会失败。
## 为什么是两个进程而不是一个
**鉴权边界是每个 MCP server 一个**:一份 `.well-known` 元数据、一个上游 IdP、
一个 OAuthProxy。合成一个进程后,第二个系统就没法按人鉴权了。
运维上也更稳:改 SRM 的工具重启进程,不会连累 CRM 的用户重新授权
(实测过——上游重启撞上令牌刷新会清空授权)。
## 接一个新的 PigX 系统
1. **在该系统的 PigX 里注册 OAuth2 客户端**(照抄 CRM 的 `weknora-mcp`,改回调端口):
```
authorizedGrantTypes authorization_code,refresh_token
web_server_redirect_uri http://127.0.0.1:<本服务端口>/auth/callback
scope server
autoapprove true
additional_information {"enc_flag":"1","captcha_flag":"0"}
```
2. 复制 `mcp-tyer-srm/` 改名,改 `.env`(端口、前缀、端点)
3. 写业务工具——**只需要写这部分**
4. 在 WeKnora 里加一个 MCP 服务,认证方式选 OAuth 2.0,用户各自授权
## 踩过的坑(都已固化进代码,接新系统不用再踩)
| 坑 | 现象 | 解法 |
|---|---|---|
| PigX 不支持 RFC 7591 DCR | WeKnora 无法完成 OAuth | `OAuthProxy` 桥接 |
| `check_token` 不合规 RFC 7662 | 授权成功但调用 401 | 改用 `/user/info` 验令牌 |
| OAuth 端点与登录页跨源 | 登录后跳到 `{"code":1,"msg":"No static resource ."}` | 端点必须与登录页同源 |
| 前端 vite `changeOrigin: true` | 同上(Host 被改写) | 改成 `false` |
| 雪花 ID 经 JSON 数字传递 | 报「不存在或无权访问」,**看起来像权限问题** | ID 全程 `str`,用 `as_id()` |
| 业务异常是 HTTP 500 + code=1 | 业务提示被压成一句 "HTTP 500" | 按 `code` 判成败 |
| 「不存在或无权访问」 | — | 防 ID 探测的含糊语义,按空结果返回不抛异常 |
| 上游重启撞上令牌刷新 | 全部用户授权被清空 | `ResilientOAuthProxy` 区分连接错误与授权失效 |
**雪花 ID 那条用演示数据(id=1)测不出来**,必须拿真实的 19 位 ID 验证。
## 加了工具之后
改完 `server.py` 重启服务**还不够**:WeKnora 缓存着 MCP 连接,`ListTools` 返回旧列表。
必须去 **WeKnora → 设置 → MCP 服务 → 点「测试连接」** 强制重连。
不做这一步的表现是「模型调用一个不存在的工具」,很容易误判成模型的问题。
## 越权复验
CRM 里留了只读测试账号 `test_agent_ro`(迁移脚本 `2026-08-05-agent-readonly-testuser.sql`)。
**每次新增工具接口后用它复验一遍**:同一个 Agent、同样的问题,低权限账号应当
查不到高权限才能看的数据。
切换账号前要先登出上游会话,否则 `/oauth2/authorize` 看到已登录会直接发码、不弹登录页:
```
http://127.0.0.1:9999/admin/oauth2/logout
```
已验证(2026-08-06):`sales01` 能查自己名下的 5 个客户、查不到 admin 的 6 个;
未授权接口返回 403。MCP 侧零权限代码。
## 扩展点:将来要同时支持 API Key 调用
当前鉴权是**互斥**的:`build_auth()` 里 OAuth 优先,配齐了就完全不看 `MCP_API_KEY`。
这是故意的——两者若能同时生效,OAuth 一出问题就会悄悄退回共用服务账号,
不报错、没人发现,按人鉴权名存实亡。
**什么时候需要改**:出现非 WeKnora 的调用方,且它不支持 OAuth。
典型是定时任务、别的后端系统直接调这些工具。**在那之前不要动**。
**要改两处,都是单点**:
1. `auth.py` 的 `build_auth()` 换成 `MultiAuth(server=ResilientOAuthProxy(...),
verifiers=[StaticTokenVerifier(...)])`。FastMCP 的文档场景就是
"OAuth proxy for interactive clients + verifier for machine-to-machine"。
2. `client.py` 的 `_bearer()` **必须加分流**,否则会出错:
OAuth 模式下它拿 `get_access_token().token` 转发给上游,而 API Key 进来的
调用方,那个 token 就是 `MCP_API_KEY` 本身——它不是 PigX 令牌,转发过去
只会得到 424。要靠 `client_id` 或 scope 区分来路:API Key 那条走
`cfg.fallback_token`(服务账号),OAuth 那条走用户本人令牌。
**别只改第一处**。只加 MultiAuth 不改 `_bearer()`,机器调用会以一种很难懂的
方式失败(授权明明通过了,取数却报凭证过期)。
## 安全说明
- 服务默认监听 `127.0.0.1`,无 HTTPS。要绑 `0.0.0.0` 或放内网前,先加 TLS。
- `.env` 已被 gitignore,不要提交真实凭据。
- WeKnora 侧需要 `SSRF_WHITELIST_EXTRA=127.0.0.1,...`,否则保存 MCP 服务时会被
安全校验拦下。生产环境应只放行具体主机,不要放行回环地址。
- 将来接入写操作接口时,务必在 WeKnora 的 MCP 服务里打开对应工具的
**「需人工审核」**开关。
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessSyncing