---
name: jwb-mcp-server
description: Use when 为求职工作台或类似的本地优先工具构建、维护 MCP server（stdio 形态）时，需要按只读优先、写入确认、越界拒绝与可测试性约定行事。English triggers：MCP server, stdio, tool design, host integration, local-first tools.
compatibility: 需要 Python 3.12+（MCP SDK 与领域包同一条基线）；领域层已抽成可安装包 jobws-core，**装上即用、不需要仓库在侧**（少数仓库侧写操作会返回 unknown_operation，属能力说明而非依赖）；jobws 指仓库内的 python tools/jobws.py。
license: MIT
metadata:
  version: 26.11.0
---

# MCP server 构建与维护（stdio 形态）

设计参照：MCP 官方规范，以及 Anthropic 的 mcp-builder 技能所倡导的工程思路。
（该仓库未声明开源许可，本文件为独立撰写，不复制其文本；思路层面的对齐是刻意的。）

## 硬性纪律（每条都对应真实事故）

- **stdout 是协议通道**：诊断一律走 stderr——一个 print 就能破坏整个会话；
- **越界拒绝**：宿主可能由模型代传参数——工作区解析 `realpath` 后校验在允许根
  之内，越界**拒绝**而不是警告（少了这道检查等于给出任意目录的读写能力）；
- **只读优先**：只读与写工具分开；写工具一律两段式（preview 拿令牌 → apply
  落盘），且「必须先给用户看过」这一步**放在协议里**（令牌的一次性 / 期限 /
  绑定），不靠提示词约束——模型看一遍摘要就调 apply 的成本几乎为零；
- **返回规范数据**：底层工具返回 dict（可脱离 SDK 单测），序列化集中在 server
  层；工具层把「拒绝」表达为 `ok:false` + 可读理由，而不是抛栈。

## 工具设计

| 维度 | 做法 |
|---|---|
| 命名 | 动词_名词 + 服务前缀（如 list_applications / preview_import_applications） |
| 描述 | docstring 写清三步用法：预览 → 把 diff 展示给用户 → 用户确认后 apply |
| 口径 | 与 CLI / 网页端**同源**：复用领域函数，不另写判据；有意差异必须注释说明 |
| 返回体 | 控制体积：列表给上限 + total；「摘要」类是完整视图的子集，不是搬家 |
| 错误 | 令牌不可用 / 冲突 → ok:false + 理由（宿主能自行决定「重新预览」） |

## 可测试性（不依赖宿主）

- 工具层与 SDK 分离：工具是普通函数、返回 dict——可直接单测；
- 两条测试线：① 造真实工作区跑口径（数据真落盘、断言真字节；mock 掉调用等于
  什么都没测）；② stdio 冒烟：真起子进程，走真通道——initialize、列出工具、
  调用工具三步全走一遍；
- 令牌目录在测试里挪进临时目录——不碰真实临时目录，用例之间不互相看见。

## 分发形态

- 独立小包（**职责分层**优先——MCP 是宿主里跑的可选组件，主干是应用本体，依赖面
  与发版节奏都不同）；`[project.scripts]` 提供命令入口；
- **别把「依赖集互斥」当成天然的独立理由**：本包曾据此自成环境（主干兼容 Py3.8 钉
  `pydantic<2.10`，而 SDK 要 `>=2.12`）。Python 3.12 基线落地后该前提消失，两者已
  有非空交集（`>=2.12,<2.14`，实测 2.13.5 下主干 679 项（2026-09-16 当时用例数）与 MCP 23 项同环境同过）。
  **判断独立与否要看职责边界，而不是当时恰好冲突的版本区间**——冲突会随基线移动而蒸发；
- 宿主配置样板写进 README（command / args / type: stdio），并写明工作区解析
  优先级（参数 > 环境变量 > 默认）；**领域层抽包之后不再需要「仓库在侧」**——
  剩下的是「哪几个写操作只在仓库形态下可用」这类**能力**说明（说清它返回什么码，
  而不是含糊地说"需要仓库"）；
- 语义化版本只在有实际兼容影响时动；依赖**不在 PyPI 的本地包**时把安装顺序写
  清楚（先装依赖包再装本包，CI 同序）；本地开发的坑也值得写进文档——例如非
  editable 安装的静态模块映射对**新增模块**不可见（搬新模块后不重装就
  ModuleNotFoundError），而 `--config-settings editable_mode=compat` 能回避它。

## 常见错误

| 错误 | 后果 | 正确做法 |
|---|---|---|
| stdout 打日志 | 协议会话损坏、宿主报解析错 | 日志走 stderr |
| 写工具直接落盘 | 模型可绕过用户确认 | 两段式令牌 |
| 参数直接拼路径 | 任意目录读写 | realpath + 允许根校验 |
| 返回体巨型列表 | 撑爆宿主上下文 | 上限 + total |
| 与 CLI 各写一套判据 | 同一数据两种结论 | 复用领域函数并注释差异 |
