# Python MCP Gateway 这是物流系统给 Workbuddy 使用的轻量 MCP Gateway。它负责接收 MCP 请求、管理员工授权会话,并把工具调用转发到现有 ThinkPHP 项目的 MCP 接口。 Gateway 保持“薄网关”边界: - 不直接连接业务数据库。 - 不复制 ThinkPHP 的业务权限逻辑。 - 不在 Python 内判断订单、费用、轨迹等业务权限。 - ThinkPHP 继续负责 `mcp_token`、员工状态、公司、工具白名单和业务数据权限的最终校验。 ## 当前能力 - 支持 `.env` 优先的配置加载。 - 支持本地 stdio MCP 模式:`serve-stdio`。 - 支持公网 HTTP JSON-RPC 模式:`serve-public`。 - 支持 Redis token/session 存储。 - 支持文件 token store 作为开发排障兜底。 - 支持 `bind_auth_code` 授权绑定工具。 - 支持 `query_order` 订单查询工具。 - 支持 `query_track` 轨迹查询工具。 - 支持 MCP `initialize`、`tools/list`、`tools/call`。 - 公网模式支持 `gateway_session_id` 请求级隔离、Redis Gateway session、审计日志和基础限流。 ## 两种运行模式 ### 本地 stdio 模式 适合员工本机 Workbuddy 通过命令方式拉起 Gateway。 ```powershell python \\192.168.1.241\chenjiacheng\mcp\app.py serve-stdio ``` Workbuddy 配置示例: ```json { "mcpServers": { "fms": { "command": "python", "args": [ "\\\\192.168.1.241\\chenjiacheng\\mcp\\app.py", "serve-stdio" ] } } } ``` 不要把 `Y:\mcp\app.py` 写进员工 Workbuddy 配置;`Y:` 只是开发机映射盘视角,员工机器未必存在。 本地模式下,员工流程为: 1. 员工在后台获取 Workbuddy 授权码。 2. Workbuddy 自动拉起 `serve-stdio`。 3. 员工在 Workbuddy 中调用 `bind_auth_code`,输入授权码完成绑定。 4. Gateway 调用 `/mcp/auth/exchange`,并带上本机生成的 `session_key`。 5. token 存入 Redis 当前员工对应 key。 6. 后续直接使用 `query_order`、`query_track` 等工具。 ### 公网 HTTP 模式 适合把 Gateway 部署成公网共享服务,由多个员工的 Workbuddy 远程访问同一个 Gateway。 启动命令: ```powershell python app.py serve-public --host 0.0.0.0 --port 8765 ``` 生产部署必须满足: - 必须放在 HTTPS 反向代理后面。 - Redis 必须在内网并开启密码。 - 不使用 `.mcp_token.json` 保存公网用户 token。 - 不使用固定 `FMS_SESSION_KEY` 表示公网用户。 - 不使用进程级全局 token 表示当前员工。 - Workbuddy 每次请求必须稳定传递 `gateway_session_id`。 公网模式当前支持三种 session 传递方式,优先级如下: 1. Header:`X-Gateway-Session: GWS_xxx` 2. Bearer:`Authorization: Bearer GWS_xxx` 3. Cookie:`gateway_session_id=GWS_xxx` 如果 Workbuddy 远程 MCP 无法稳定传递 header、Bearer 或 cookie 中任意一种会话身份,公网共享 Gateway 不能生产放量。 ## 公网 Gateway 会话生命周期 ### 1. 生成 gateway_session_id 客户端首次使用时生成高熵会话 ID: ```python import secrets gateway_session_id = 'GWS_' + secrets.token_urlsafe(32) ``` 客户端需要保存这个 ID,并在后续每次请求中复用。 ### 2. 绑定授权码 员工从后台复制授权码后,Workbuddy 调用 `bind_auth_code`: ```http POST /mcp X-Gateway-Session: GWS_xxx Content-Type: application/json ``` ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "bind_auth_code", "arguments": { "auth_code": "AC_xxx" } } } ``` Gateway 会调用 base 项目的 `/mcp/auth/exchange`,并把返回的 `mcp_token` 存入 Redis: ```text fms:mcp:gateway:session:{sha256(gateway_session_id)} ``` Redis value 中保存 `mcp_token`、`admin_id`、`company_id`、`client_type`、`expire_time` 等信息。Redis key 不保存明文 `gateway_session_id`。 ### 3. 调用工具 后续工具调用必须继续带同一个 `gateway_session_id`: ```json { "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "query_order", "arguments": { "keyword": "USC26070371955", "page": 1, "limit": 20 } } } ``` Gateway 根据 `gateway_session_id` 从 Redis 读取当前员工的 `mcp_token`,再把请求转发到 fmsoperate 的 `/mcp/tools/queryOrder`。 ### 4. 撤销与过期 - 员工在后台取消 Workbuddy 授权后,ThinkPHP 会让对应 `mcp_token` 失效。 - 公网 Gateway Redis session 会在 TTL 到期后自动过期。 - 当前公网模式默认 TTL 为 30 天,可通过 `FMS_GATEWAY_SESSION_TTL_SECONDS` 调整。 - 如果 token 已失效但 Redis session 仍存在,下一次工具调用会被 ThinkPHP 拒绝。 ## 常用命令 查看工具列表: ```powershell python app.py list-tools ``` 开发/排障时手动绑定授权码: ```powershell python app.py bind --auth-code AUTH123 ``` 直接调用订单查询: ```powershell python app.py call --tool query_order --keyword USC26070371955 --page 1 --limit 20 ``` 直接调用轨迹查询: ```powershell python app.py call --tool query_track --order-number USC26070371955 ``` 运行全部测试: ```powershell python -m unittest discover -s tests -p "test_*.py" ``` ## 环境变量 基础配置: ```dotenv FMS_AUTH_BASE=http://chenjiacheng.base.dahuo.fudingri.com FMS_TOOLS_BASE=http://chenjiacheng.fmsoperate.dahuo.fudingri.com FMS_CLIENT_TYPE=workbuddy FMS_TIMEOUT_SECONDS=10 FMS_REFRESH_SKEW_SECONDS=120 FMS_LOG_LEVEL=info ``` 本地 stdio 推荐 Redis token store: ```dotenv FMS_TOKEN_STORE=redis FMS_REDIS_HOST=192.168.1.241 FMS_REDIS_PORT=6379 FMS_REDIS_DB=20 FMS_REDIS_PASSWORD= FMS_REDIS_PREFIX=fms:mcp:workbuddy: ``` 公网模式推荐配置: ```dotenv FMS_GATEWAY_MODE=public FMS_GATEWAY_PUBLIC_BASE=https://mcp.example.com FMS_GATEWAY_SESSION_TTL_SECONDS=2592000 FMS_TOKEN_STORE=redis FMS_REDIS_HOST=127.0.0.1 FMS_REDIS_PORT=6379 FMS_REDIS_DB=20 FMS_REDIS_PASSWORD=change-me FMS_REDIS_PREFIX=fms:mcp:gateway: ``` 兼容旧配置键: - `MCP_AUTH_BASE_URL` - `MCP_TOOLS_BASE_URL` - `MCP_CLIENT_TYPE` - `MCP_TIMEOUT_SECONDS` - `MCP_REFRESH_SKEW_SECONDS` - `MCP_LOG_LEVEL` - `MCP_TOKEN_STORE` - `MCP_TOKEN_STORE_PATH` - `MCP_REDIS_HOST` - `MCP_REDIS_PORT` - `MCP_REDIS_DB` - `MCP_REDIS_PASSWORD` - `MCP_REDIS_PREFIX` - `MCP_SESSION_KEY` 说明: - `config.py` 会自动读取 `mcp/.env`。 - 同名配置以 `.env` 优先,系统环境变量只作为兜底。 - 如果只配置 `FMS_API_BASE`,会同时作为 auth 和 tools 的基础地址。 - 如果 base 与 fmsoperate 是不同域名,应分别配置 `FMS_AUTH_BASE` 和 `FMS_TOOLS_BASE`。 ## Token 与 Session 存储 本地 stdio 模式: - Redis key 默认为 `fms:mcp:workbuddy:{session_key}`。 - `session_key` 默认由员工本机 `COMPUTERNAME` / `USERNAME` 生成。 - 共享 `.env` 中不要写死 `FMS_SESSION_KEY`。 公网 HTTP 模式: - Redis key 默认为 `fms:mcp:gateway:session:{sha256(gateway_session_id)}`。 - 每个公网客户端必须有独立 `gateway_session_id`。 - Gateway 不把 `mcp_token` 返回给 Workbuddy 展示层。 - Gateway 不在日志中记录明文 `gateway_session_id`、`mcp_token` 或授权码。 文件存储仅用于开发排障: ```dotenv FMS_TOKEN_STORE=file FMS_TOKEN_STORE_PATH=C:/fms-mcp/.mcp_token.json ``` 如果使用网络共享 `app.py`,不要把文件 token store 指向 `\\192.168.1.241\chenjiacheng\mcp\` 共享目录。 ## 公网安全注意事项 - 公网入口必须使用 HTTPS。 - Redis 只允许内网访问。 - Redis 密码不能提交到仓库。 - 反向代理必须覆盖而不是透传用户伪造的转发头。 - 当前 Python 内置限流默认使用真实 TCP 连接 IP;公网生产建议同时在 Nginx、负载均衡或 API 网关层配置限流。 - 审计日志只记录 session hash 的短前缀、员工 ID、公司 ID、工具名和 request_id。 - 第一版公网只开放查询型工具,不开放审批、费用修改、状态流转、批量写入。 ## ThinkPHP 路由口径 ThinkPHP MCP 路由位于各后端项目的 `route/mcp/mcp_route.php`。 Gateway 会追加以下路径: - Auth:`/mcp/auth/exchange`、`/mcp/auth/refresh`、`/mcp/auth/revoke` - Tools:`/mcp/tools/queryOrder`、`/mcp/tools/queryTrack` `FMS_AUTH_BASE` 和 `FMS_TOOLS_BASE` 只配置域名或基础地址,不要包含 `/admin/mcp`。 ## 排障 ### query_order 返回 UTF-8 BOM 错误 如果 Workbuddy 提示: ```text Unexpected UTF-8 BOM (decode using utf-8-sig) ``` 说明后端工具接口返回的 JSON 前面带了 UTF-8 BOM。当前 Gateway 已在 HTTP JSON 解码层使用 `utf-8-sig` 兼容。先运行全量测试确认当前文件是否已包含修复: ```powershell python -m unittest discover -s tests -p "test_*.py" ``` ### stdio 中文乱码 Windows 下 Python 标准输出可能使用本机控制台编码。当前 `mcp_protocol.py` 已将 stdio JSON-RPC 响应改为 `ensure_ascii=True`,传输行只包含 ASCII JSON 转义,客户端解析后仍是正常中文。 如果仍然乱码,优先确认 Workbuddy 启动的是最新的 `\\192.168.1.241\chenjiacheng\mcp\app.py`。 ## 当前边界 - Gateway 不是业务系统,不直接查 MySQL。 - Gateway 不替代 ThinkPHP 权限体系。 - 公网 Gateway 是否能生产放量,取决于 Workbuddy 远程 MCP 是否能稳定传递 `gateway_session_id`。 - 写入型 MCP 工具需要单独安全评审后再开放。