# Python MCP Gateway 这是一个轻量的 Python MCP Gateway,用来把 Workbuddy 的 MCP 工具调用转发到现有 ThinkPHP 授权接口和工具接口。 ## 当前状态 当前仓库已经具备一个可运行的 stdio MCP 入口,以及一套经过测试的核心模块,覆盖: - `.env` 优先的配置加载 - Redis token 存储,按员工本机 `session_key` 隔离 - 文件型 token 存储兜底 - `auth_code` 换票、续期、失效的认证客户端 - `bind_auth_code` 授权绑定工具 - 工具 HTTP 转发客户端 - `query_order` 工具注册 - Gateway 运行时会话管理 - MCP `initialize` / `tools/list` / `tools/call` 请求处理 - 本地命令模式下的 stdio MCP 启动入口 当前实现仍然保持“薄网关”原则: - 业务认证和权限判断仍然留在 ThinkPHP 项目中 - Gateway 只负责协议适配、会话管理和 HTTP 转发 - 第一阶段只开放查询型工具,当前从 `query_order` 开始 ## Workbuddy 配置 员工侧 Workbuddy 使用网络 UNC 路径启动 Gateway: ```json { "mcpServers": { "fms": { "command": "python", "args": [ "\\\\192.168.1.241\\mcp\\app.py", "serve-stdio" ] } } } ``` 不要把 `Y:\mcp\app.py` 写进 Workbuddy 配置;那只是开发机映射盘视角。 ## 常用命令 以下命令在 `mcp/` 目录下执行。 ### 查看工具列表 ```powershell python app.py list-tools ``` ### 绑定授权码(开发/排障) ```powershell python app.py bind --auth-code AUTH123 ``` 员工正式使用时不需要执行这条命令。Workbuddy 拉起 `serve-stdio` 后,员工应在 Workbuddy 中调用 `bind_auth_code` 工具完成绑定。 ### 直接调用 query_order(开发/排障) ```powershell python app.py call --tool query_order --keyword SO20260706001 --page 1 --limit 20 ``` ### 启动 MCP stdio 服务 ```powershell python \\192.168.1.241\chenjiacheng\mcp\app.py serve-stdio ``` 这个命令可作为 Workbuddy 本地命令模式下的 MCP 服务入口,由 Workbuddy 自动拉起,不要求员工手动执行。 ## 环境变量 当前代码直接支持以下文档口径的配置键: - `FMS_API_BASE` - `FMS_AUTH_BASE` - `FMS_TOOLS_BASE` - `FMS_CLIENT_TYPE` - `FMS_TIMEOUT_MS` - `FMS_TIMEOUT_SECONDS` - `FMS_REFRESH_SKEW_SECONDS` - `FMS_LOG_LEVEL` - `FMS_TOKEN_STORE` - `FMS_TOKEN_STORE_PATH` - `FMS_REDIS_HOST` - `FMS_REDIS_PORT` - `FMS_REDIS_DB` - `FMS_REDIS_PASSWORD` - `FMS_REDIS_PREFIX` - `FMS_SESSION_KEY` 同时兼容早期实现中的这些键名: - `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` 优先,系统环境变量只在 `.env` 缺少该配置时兜底。 - 如果只提供 `FMS_API_BASE`,当前实现会同时把它作为 auth 和 tools 的基础地址。 - 如果 `base` 与 `fmsoperate` 部署在不同域名,应分别提供 `FMS_AUTH_BASE` 与 `FMS_TOOLS_BASE`。 ## .env 文件 当前 `mcp/` 目录建议使用 `.env` 保存共享 Gateway 配置。 推荐配置如下: ```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 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: ``` 不要在共享 `.env` 中写死 `FMS_SESSION_KEY`。默认情况下 Gateway 会用员工本机的 `COMPUTERNAME` / `USERNAME` 自动生成稳定 `session_key`,Redis key 形如: ```text fms:mcp:workbuddy:{session_key} ``` 这样多个员工通过同一个 `\\192.168.1.241\chenjiacheng\mcp\app.py` 启动时,也不会共用同一个 token。 ## Token 存储 当前推荐使用 Redis: ```dotenv FMS_TOKEN_STORE=redis ``` Redis 存储行为: - `bind_auth_code` 成功后把 `mcp_token` 写入 Redis - Redis key 使用 `FMS_REDIS_PREFIX + session_key` - `session_key` 优先读取 `FMS_SESSION_KEY`,未配置时自动从员工本机信息生成 - token 过期时间会同步设置为 Redis TTL - `revoke` 成功后会删除当前 session 的 Redis key 文件存储仍保留为兜底方式: ```dotenv FMS_TOKEN_STORE=file FMS_TOKEN_STORE_PATH=C:/fms-mcp/.mcp_token.json ``` 如果使用网络共享 `app.py`,不要把 `FMS_TOKEN_STORE_PATH` 指向 `\\192.168.1.241\chenjiacheng\mcp\` 共享目录。 ## 本地接入建议 当前员工正式使用路径是: 1. 员工在后台获取一次性 `auth_code` 2. Workbuddy 自动通过 `python \\192.168.1.241\chenjiacheng\mcp\app.py serve-stdio` 拉起 Gateway 3. 员工在 Workbuddy 中调用 `bind_auth_code`,输入授权码完成绑定 4. Gateway 调用 `exchange` 时带上 `session_key` 5. token 保存到 Redis 当前员工对应的 key 6. 后续直接使用 `query_order` 等工具 `python app.py bind --auth-code ...` 仅保留给开发、联调和排障使用。 ## 测试命令 ```powershell python -m unittest discover -s tests -p "test_*.py" ``` ## 当前边界 当前 Gateway 已可作为本地 stdio MCP 进程使用,但仍保持最小边界: - 不在 Python 中沉淀额外业务规则 - 不直接连接业务数据库 - 不做动态插件加载 - 第一阶段不开放写入型工具 ## query_order BOM 排障 如果 Workbuddy 调用 `query_order` 时提示: ```text Unexpected UTF-8 BOM (decode using utf-8-sig) ``` 说明后端工具接口返回的 JSON 前面带了 UTF-8 BOM。当前 Gateway 已在 HTTP JSON 解码层使用 `utf-8-sig` 做兼容。排查时先在 `mcp/` 目录运行: ```powershell python -m unittest discover -s tests -p "test_*.py" ``` 若全量测试通过但 Workbuddy 仍报同样错误,优先确认 Workbuddy 启动的 `app.py` 是否为 `\\192.168.1.241\chenjiacheng\mcp\app.py` 的最新文件。 ## 2026-07-07 ThinkPHP MCP route path ThinkPHP MCP routes now live in each backend project's `route/mcp/mcp_route.php`. The Gateway appends these paths to `FMS_AUTH_BASE` and `FMS_TOOLS_BASE`: - Auth: `/mcp/auth/exchange`, `/mcp/auth/refresh`, `/mcp/auth/revoke` - Tools: `/mcp/tools/queryOrder` Keep `FMS_AUTH_BASE` and `FMS_TOOLS_BASE` as host/base-domain values. Do not include `/admin/mcp` in these environment variables. ## query_track / stdio 中文乱码排障 如果后端 HTTP 返回、保存到 UTF-8 文件都正常,但 Workbuddy/AI 调 MCP 工具时中文显示成乱码,优先检查 Python Gateway 的 `serve-stdio` 输出层。 本次踩坑原因:Windows 下 Python 标准输出可能使用本机控制台编码,而 MCP stdio 客户端按 JSON/UTF-8 读取时会出现中文乱码。当前 `mcp_protocol.py` 已将 JSON-RPC stdio 响应改为 `json.dumps(..., ensure_ascii=True)`,传输行只包含 ASCII JSON 转义;客户端解析 JSON 后仍是正常中文。 排查顺序: - 先确认 HTTP 层 `ApiClient` 能用 `utf-8-sig` 正常解析后端 JSON。 - 再确认 `serve-stdio` 输出不是直接吐本机编码中文。 - 修改后需要重启 Workbuddy 拉起的 MCP Gateway 进程。 验证命令: ```powershell python -m unittest discover -s tests -p "test_*.py" ``` ## 2026-07-07 query_track direct tracking-number lookup `query_track` now accepts `order_id`, `order_number`, or `tracking_number`. - With `order_id`, the backend keeps using `TrackLogic::fmsOrderQueryTrack($orderId)`. - With `tracking_number`, or with only `order_number`, the backend calls `TrackLogic::queryTrack($number)` directly. This matches the existing fmsoperate page `admin/Order/trackInfo.html?order_number=xxx`. - Python Gateway only forwards the arguments to `/mcp/tools/queryTrack`; it does not resolve orders locally or connect to the business database. Debug command: ```powershell python app.py call --tool query_track --tracking-number 1471904540000000301 --page 1 --limit 20 ``` ## 2026-07-07 query_track timezone alignment Root cause: the backend page has `session('timezone')` from the normal login session, while MCP tool calls rebuild session data from `mcp_token`. The fmsoperate MCP middleware was reading timezone from `st_user`, but the normal backend session uses the company's timezone (`st_company.timezone_id -> st_timezone.value`). When timezone was empty, `CommonHelper::changeDateByTimeZone($time, 0, '')` returned the original UTC time. Fix: fmsoperate `McpTokenLogic::checkToken()` now loads the company timezone and returns a normalized `session_context`; `CheckMcpToken` writes `session('timezone')` from that context before calling tool logic. This keeps `query_track` aligned with `admin/Order/trackInfo.html?order_number=xxx`.