jwb-mcp-server · v26.11.0 · 2026-11-01 · sha256 76a6b2d71b9ed96a

jwb-mcp-server v26.11.0A

Immutable. This exact content never changes and is served at /api/v1/blob/76a6b2d71b9ed96a.

---
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 各写一套判据 | 同一数据两种结论 | 复用领域函数并注释差异 |