# Python MCP Gateway 这是物流系统给 Workbuddy 使用的轻量 MCP Gateway。它负责接收 MCP 请求、解析并转发设备会话,并把工具调用转发到现有 ThinkPHP 项目的 MCP 接口;员工认证与设备会话签发仍由 `base` 负责。 Gateway 保持“薄网关”边界: - 不直接连接业务数据库。 - 不复制 ThinkPHP 的业务权限逻辑。 - 不在 Python 内判断订单、费用、轨迹等业务权限。 - ThinkPHP 继续负责 `mcp_token`、员工状态、公司、工具白名单和业务数据权限的最终校验。 ## 当前能力 - 支持 `.env` 优先的配置加载。 - 支持本地 stdio MCP 模式:`serve-stdio`。 - 支持公网 HTTP JSON-RPC 模式:`serve-public`。 - 支持 Redis token/session 存储。 - 支持文件 token store 作为开发排障兜底。 - 不再支持授权码绑定工具;正式接入只使用后台生成的 `GWS_xxx` 设备配置。 - 本地 stdio 与公网 HTTP 注册同一组 11 个查询、筛选和导出工具。 - 支持订单、轨迹、报关资料、排舱列表与详情查询。 - 支持订单与排舱筛选项,以及未排舱订单和省外进港资料导出。 - 支持 MCP `initialize`、`tools/list`、`tools/call`。 - 公网模式支持 `gateway_session_id` 请求级隔离、Redis Gateway session、审计日志和基础限流。 ## 工具目录 `GatewayApp` 与 `PublicGatewayApp` 当前注册以下 11 个候选工具: | MCP 工具 | 用途 | ThinkPHP 路由 | 最终展示 | |---|---|---|---| | `query_order` | 普通订单列表查询 | `/mcp/tools/queryOrder` | 旧协议兼容 | | `query_track` | 按订单或物流号码查询轨迹 | `/mcp/tools/queryTrack` | 安全表格 | | `query_order_exact` | 按明确号码类型精准查询订单 | `/mcp/tools/queryOrderExact` | 安全表格 | | `query_customs_declaration_files` | 按订单号或排舱单号查询报关资料 | `/mcp/tools/queryCustomsDeclarationFiles` | 安全表格 | | `query_outbound_list` | 按业务阶段和筛选条件查询排舱列表 | `/mcp/tools/queryOutboundList` | 安全表格 | | `query_outbound_detail` | 按排舱单号查询排舱汇总与订单明细 | `/mcp/tools/queryOutboundDetail` | 安全详情 | | `list_outbound_filter_options` | 查询七类排舱筛选项 | `/mcp/tools/listOutboundFilterOptions` | 安全筛选项 | | `list_order_filter_options` | 查询精准订单筛选项 | `/mcp/tools/listOrderFilterOptions` | 安全筛选项 | | `export_pending_outbound_orders` | 导出未排舱订单 | `/mcp/tools/exportPendingOutboundOrders` | 安全文件链接 | | `export_out_of_province_port_data` | 导出省外进港资料 | `/mcp/tools/exportOutOfProvincePortData` | 安全文件链接 | | `list_pending_outbound_export_filter_options` | 查询未排舱导出筛选项 | `/mcp/tools/listPendingOutboundExportFilterOptions` | 安全筛选项 | 这 11 个名称只是 Gateway 的本地候选集合。员工在 `tools/list` 中实际看到、在 `tools/call` 中实际可调用的工具,始终是“Gateway 本地注册集合”与 fmsoperate 当前动态启用列表的交集;动态列表缺失、格式错误或查询失败时关闭访问,不回退为全量开放。 MCP 能力声明为 `tools.listChanged=false`。工具名称、Schema、说明或注册集合变化后,必须重启对应 Gateway 进程并让客户端重新连接,客户端才会重新获取工具列表。 ## 工具结果展示协议 Gateway 在 `tools/call` 最终边界处理展示字段,不改变 ThinkPHP 内部接口和工具入参: - `query_order` 保持原有 `columns + records` 结果和文本展示,不参与本次转换。 - `services/output_presenter.py` 对其余 10 个安全工具执行显式白名单展示。 - `query_order_exact`、`query_track`、`query_customs_declaration_files`、`query_outbound_list` 对外使用中文 `headers + rows + pagination`,不返回内部字段键。 - `query_outbound_detail` 使用中文 `summary + details + pagination` 两层结构。 - 三个筛选项工具保留“可传值、显示名称、业务编码”,确保返回值可继续传给查询或导出工具。 - 两个导出工具返回 `files[].label + files[].url`,不暴露后端 `file_url` 键。 - `request_id` 位于 MCP 结果 `_meta`;参数错误使用业务名称,未知异常不透传后端细节。 - stdio 与 public 模式共用 `services/output_presenter.py`;未知工具或畸形响应关闭失败。 详细设计见 [技术规格的 Presenter 分类](project-docs/tech-specs.md#presenter-分类)。 省外进港资料导出使用 `outbound_numbers`、`container_codes`、`bl_numbers`、`so_numbers` 四个号码数组之一,并同时提供 `file_type=NB/SH/MS`。用户未明确号码类型时,AI 必须先让用户从排舱单号、柜号、提单号、SO 号中选择,确认前不得调用;提单号指后台排舱单列表的普通提单号,SO 号对应 `fms_booking_detail.so_number`。 排舱列表查询的 `outbound_status` 必填,用户未说明排舱阶段时必须先询问;`shipping_method` 可选,未提供时查询空运、海运和陆运全部,只有用户明确指定后才按运输方式筛选。 ## 文档入口 - `AGENTS.md`:项目所有权、红线、协作规则和验证命令。 - `README.md`:当前工具目录、接入配置、运行方式、运维与排障。 - `project-docs/overview.md`:当前系统概况与跨仓职责。 - `project-docs/requirements.md`:现行协议、安全、工具选择和输出合同。 - `project-docs/tech-specs.md`:组件、会话、展示、限流、追踪与日志设计。 - `project-docs/user-structure.md`:员工、调试、工具选择和跨仓开发流程。 - `project-docs/timeline.md`:短里程碑和当前交接状态。 临时规划与已完成的执行计划不作为现行合同;交付后应把稳定事实并回上述入口,并从活动工作区清理过程文件。 ## 两种运行模式 ### 本地 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:` 只是开发机映射盘视角,员工机器未必存在。 本地 stdio 模式只保留给开发排障和已有 token 会话的兼容调用;员工正式使用路径统一走公网 HTTP 模式,由后台直接生成 Workbuddy 设备配置,不再通过授权码绑定。 ### 公网 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. 后台生成设备配置 员工在后台或 home 的“连接 Workbuddy”入口新增设备配置。base 会一次性完成: - 生成 `GWS_xxx`。 - 写入 `st_mcp_gateway_session`。 - 创建 `st_mcp_token_session`。 - 写入 Redis Gateway session。 - 返回只包含 `GWS_xxx` 的 Workbuddy 配置。 Workbuddy 配置示例: ```json { "mcpServers": { "fms-public": { "url": "http://192.168.1.241:8765/mcp", "headers": { "X-Gateway-Session": "GWS_xxx" } } } } ``` ### 2. 调用工具 后续工具调用必须继续带同一个 `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`。 ### 3. 撤销与过期 - 员工在后台取消 Workbuddy 授权后,ThinkPHP 会让对应 `mcp_token` 失效。 - 公网 Gateway Redis session 会在 TTL 到期后自动过期。 - 当前公网模式默认 TTL 为 30 天,可通过 `FMS_GATEWAY_SESSION_TTL_SECONDS` 调整。 - 如果 token 已失效但 Redis session 仍存在,下一次工具调用会被 ThinkPHP 拒绝。 ## 重启公网 MCP 服务 当前公网 MCP 服务端口是 `8765`,对外地址类似: ```text http://192.168.1.241:8765/mcp ``` 注意:当前项目没有单独的 `restart` 子命令,也没有 Windows 服务管理脚本。这里说的“重启”就是先停掉旧的 `serve-public` 进程,再用启动命令重新拉起。 ### 只停止旧服务 如果你只是想停掉旧服务,不马上重新启动,执行这两行就够了: ```powershell $conn = Get-NetTCPConnection -LocalPort 8765 -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1 if ($conn) { Stop-Process -Id $conn.OwningProcess -Force } ``` 第一行是查找谁在监听 `8765` 端口;第二行才是真正停止这个进程。执行后如果 `$conn` 为空,说明当前没有旧服务在运行。 想先确认要停的是哪个进程,可以在 `Stop-Process` 前执行: ```powershell if ($conn) { Get-Process -Id $conn.OwningProcess } ``` 只停止以后,`/health` 检查会连不上,这是正常的;要恢复服务再执行下面的启动命令。 ### 推荐命令:按端口停止旧服务后启动 ```powershell $conn = Get-NetTCPConnection -LocalPort 8765 -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1 if ($conn) { Stop-Process -Id $conn.OwningProcess -Force } Set-Location Y:\mcp python app.py serve-public --host 0.0.0.0 --port 8765 ``` 如果服务就在你当前这个 PowerShell 窗口里运行,也可以先按 `Ctrl+C` 停掉旧服务,然后再执行启动命令: ```powershell Set-Location Y:\mcp python app.py serve-public --host 0.0.0.0 --port 8765 ``` 后续如果接入 NSSM、计划任务、Windows 服务或其他进程管理器,再把这里改成对应的 `restart` 命令。 ### 健康检查 重启后用下面命令确认服务已经起来: ```powershell Invoke-WebRequest http://127.0.0.1:8765/health -UseBasicParsing ``` 正常情况下会返回包含 `ok` 的 JSON。 ### 部署状态核对 用户已确认 `Y:\fmsoperate\sql` 中 10 个 MCP SQL 和 `Y:\base\sql` 中 5 个 MCP SQL 均已执行。SQL 已执行不等于工具当前处于启用状态;实际可见工具仍以 fmsoperate 动态注册表的当前启用值为准。 仓库静态内容无法证明运行中的 Gateway 是否已加载当前代码,也无法证明 Workbuddy 是否已重新连接。发布工具或 Schema 变更后,运维交接必须分别确认: 1. 公网或本地 Gateway 已按实际部署方式重启。 2. Workbuddy 已断开并重新连接,重新执行 `initialize` 和 `tools/list`。 3. `tools/list` 返回的动态工具集合符合当前员工权限与预期启用状态。 ## 常用命令 查看工具列表: ```powershell python app.py list-tools ``` 直接调用订单查询: ```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 app.py call --tool query_customs_declaration_files --order-numbers ORD001,ORD002 --page 1 --limit 20 ``` 按排舱单号批量查询报关资料: ```powershell python app.py call --tool query_customs_declaration_files --outbound-numbers PC001,PC002 --page 1 --limit 20 ``` `order_numbers` 只接收后台订单列表及排舱详情“订单号”列展示的订单号,不接收系统单号、内部 `order_id/id`、客户参考号、快递单号或排舱单号。只有用户明确说明“订单号”或“排舱单号”后才能调用;如果未明确号码类型,AI 必须先提问让用户二选一,确认前不得调用。两种号码数组不能同时传入。 运行全部测试: ```powershell python -m unittest discover -s tests -p "test_*.py" ``` 严格覆盖率验收: ```powershell python -m coverage run -m unittest discover -s tests -p 'test_*.py' python -m coverage report -m --fail-under=100 ``` ## 环境变量 基础配置: ```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`。 - 动态工具列表:`/mcp/tools/listEnabledTools`。 - 查询:`/mcp/tools/queryOrder`、`/mcp/tools/queryTrack`、`/mcp/tools/queryOrderExact`、`/mcp/tools/queryCustomsDeclarationFiles`、`/mcp/tools/queryOutboundList`、`/mcp/tools/queryOutboundDetail`。 - 筛选项:`/mcp/tools/listOutboundFilterOptions`、`/mcp/tools/listOrderFilterOptions`、`/mcp/tools/listPendingOutboundExportFilterOptions`。 - 导出:`/mcp/tools/exportPendingOutboundOrders`、`/mcp/tools/exportOutOfProvincePortData`。 `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`。 - fmsoperate 和 base 的 MCP SQL 已确认执行;工具当前动态启用值仍必须从运行环境核验。 - 仓库无法证明运行中 Gateway 已重启或客户端已重连,发布交接必须显式确认这两项。 - 写入型 MCP 工具需要单独安全评审后再开放。