Problem
MCP 工具边界是本系统的公共 API(44+ 工具,被 Qoder / Cursor / Claude Desktop 等 IDE agent 消费),但它当前是全仓库架构摩擦最集中的区域:
1. Schema 与实现分离 2600 行,无静态匹配保证
codewiki/mcp/registry.py 共 2622 行,约 85% 是手写 inputSchema JSON 字符串;dispatch(registry.py:2552-2622)靠字符串 handler_path + importlib 动态导入,6 条调用分支(3 mode × takes_store)
- schema 声明的参数与 handler 实际读取的参数之间没有任何静态保证;工具行为描述在四处重复(registry description /
server.py:71-127 instructions / prompts.py 1627 行 / AGENTS.md)
2. 薄壳样板批量重复
- 53 个工具文件共用
(arguments, store) -> str 薄壳签名,各自重写 json.dumps(ensure_ascii=False)、错误捕获、output_dir 解析
_resolve_output_dir 有 4 份独立副本(capture_conversation.py:186 / distill_conversation.py:200 / note_consolidation.py:306 / source_ingest.py);wiki 场景块记载的收敛点 resolve_workspace 尚未落地
knowledge_loop.py(2394 行)单文件 51 处函数内局部 import 绕循环依赖
3. 横切关注点硬编码在 dispatcher 里
- CBM enrichment 在
registry.py:2450-2549 按工具名匹配分支,加一种后处理就要改 dispatch 核心
4. 集成风险集中在无人测试的缝隙
- 31 个测试文件、437 个测试全部直接 import handler,零测试经过 dispatch
- 仅有的端到端冒烟
tests/smoke_test_mcp.py 文件名不匹配 pytest 收集;okf_regression_test.py 收集 0 条
store 参数对大部分文件型工具是残留噪音:测试每次调用新建空 SessionStore
Proposed Interface
选定方案:D* 杂交设计——以 ports & adapters 骨架(深模块 ToolKernel + 协议适配器 + 内存测试适配器),保留手写 schema(公共 API 即 schema 字节,不做签名派生,避免对冻结契约的回归风险),吸收中间件方案中的"边界契约快照测试"作为迁移护栏。
# codewiki/mcp/protocol.py —— 领域契约,零 mcp import
@dataclasses.dataclass(frozen=True)
class ToolSpec:
name: str
description: str
input_schema: dict[str, Any] # JSON Schema 手写,随工具文件同址维护
mode: str = "thread" # "main_thread" | "thread" | "async"
takes_store: bool = True
@dataclasses.dataclass(frozen=True)
class ToolResult:
payload: Any # 已解码结构——测试断言这个
text: str # 线上字节——legacy str 原样透传,逐字节兼容
class ToolSurface(Protocol): # 入站端口:生产与测试驱动同一表面
def list_tools(self) -> list[ToolSpec]: ...
async def call(self, name: str, arguments: dict[str, Any]) -> ToolResult: ...
# codewiki/mcp/kernel.py —— 端口的唯一实现(深模块,约 150 行)
class ToolKernel: # implements ToolSurface
def __init__(self, store, *, enrichers: tuple[Enricher, ...] = ()) -> None: ...
def register(self, spec: ToolSpec, handler) -> None: ...
def register_legacy(self, schema, handler_path: str, mode: str,
takes_store: bool = True) -> None: # 桥接旧 _register,一行不改即可运行
async def call(self, name, arguments) -> ToolResult:
# 查表 → 按 mode 调度(main_thread 直调 / thread 走 asyncio.to_thread / async await)
# → 结果归一化(str/dict/TextContent/list[TextContent] 四种形状)
# → enrichers 作用于 dict 型成功结果(CBM 不再二次 json.loads)
# → 任何异常塌缩为 ToolResult({"error": str(e)}, ...),永不抛出边界
@tool(name=..., description=..., input_schema=..., mode=...) # 装饰器:工具文件内声明式注册
def handle_xxx(arguments: dict, store) -> dict: # 返回 dict 即可,封包/兜底在墙内
...
# codewiki/mcp/mcp_adapter.py —— 唯一 import mcp.types 的地方(加 server.py)
class McpAdapter: # ToolSpec→mcp.types.Tool、ToolResult.text→TextContent
# codewiki/mcp/testing.py —— 测试适配器:同一端口,无 mcp、无子进程
class InMemoryClient:
def __init__(self, *, with_cbm: bool = False) -> None: ...
def call(self, name: str, arguments: dict) -> Any: # 返回 payload
使用示例:
# server.py 接线(仍约 10 行)
_adapter = McpAdapter(build_kernel(_store, with_cbm=True))
@server.list_tools()
async def list_tools() -> list[Tool]: return _adapter.list_tools()
@server.call_tool()
async def call_tool(name, arguments) -> list[TextContent]:
return await _adapter.call_tool(name, arguments)
# 边界测试(与生产同一表面,tmp_path 即文件系统替身)
def test_capture_dedup(tmp_path):
client = InMemoryClient()
out = str(tmp_path / "repowiki")
assert client.call("capture_conversation",
{"output_dir": out, "conversation": CONV})["status"] == "captured"
assert client.call("capture_conversation",
{"output_dir": out, "conversation": CONV})["status"] == "duplicate"
隐藏的复杂度: 三种执行模式调度(调用点零分支)、统一异常兜底(50+ 处手写 try/except 收敛为一处)、JSON 编码单点(ensure_ascii=False)、结果形状归一化、CBM 后处理(从按名匹配分支 → 可注册/可关闭的 enricher)、importlib 风险限定在 legacy 桥接(新工具直接引用函数对象,拼写错误 import 期暴露)、output_dir 解析(收敛至 resolve_workspace 单点:纯解析、不 mkdir、抛 ValueError 由边界兜底)。
Dependency Strategy
依赖类别:In-process(核心逻辑纯进程内,可直接合并与真文件系统边界测试),协议边界用 ports & adapters 隔离:
| 依赖 |
处置 |
| mcp SDK(Tool/TextContent/Server) |
仅 mcp_adapter.py + server.py import;ToolSpec/ToolResult 是我方契约,SDK 升级只动适配器 |
| SessionStore |
注入的具体依赖,不做端口(进程内线程安全对象,测试随手 SessionStore() 即建——现有 31 个测试已证明) |
repowiki/ 文件布局 |
拥抱不隔离——本来就是 local-substitutable 的持久状态(tmp_path 可替换);真正的债务是 4 份 _resolve_output_dir,合并进 workspace.py::resolve_workspace 单点 |
| CBM |
Enricher 中间件,build_kernel(with_cbm=...) 控制,测试默认关闭保证确定性 |
Testing Strategy
- 新边界测试:
tests/test_mcp_boundary.py——把 smoke_test_mcp.py 的 19 步改写为 InMemoryClient.call(...) 形式纳入 pytest 收集,成为端到端回归网
- 边界契约快照测试:遍历注册表对 (schema, 样例调用输出键集) 做快照,迁移期只许审批变更(防 schema/行为漂移)
- 逐工具迁移时补
InMemoryClient 边界测试(每 PR 一工具一测试)
- 旧测试处置:437 个 handler 直连测试迁移期共存不动(测试直连不受枢纽改造影响);随工具文件触碰逐步替换为边界测试;断言
json.dumps/try-except 样板的测试在对应工具迁移后删除
- 测试环境:
tmp_path + 真 SessionStore,无需 mock;CBM 默认关闭;不引入新基础设施
Implementation Recommendations
模块职责(不绑定当前文件路径):
- 该拥有:工具注册表、执行模式调度、异常→错误 JSON 封包、结果编码与归一化、横切后处理管线
- 该隐藏:importlib 桥接细节、结果形状归一化、必填参数校验、enricher 组合机制
- 该暴露:
ToolSurface(list/call 两个方法)、@tool 装饰器、InMemoryClient 测试适配器
不变量(迁移全程必须保持):
- 工具名、参数名、JSON 响应语义逐字不变(公共 API);legacy str 结果经
ToolResult.text 原样透传
- 异常兜底契约不变:handler 抛异常是安全的,边界统一返回
{"error": str(e)}
- 三种执行模式语义不变(tree-sitter 线程亲和性)
渐进迁移路径(无 big-bang):
- P0 零行为差枢纽:新增 protocol/kernel/mcp_adapter/testing;桥接器收编全部旧
REGISTRY 条目;server.py 改接 McpAdapter;旧 dispatch() 保留为弃用薄壳。437 个现有测试不改、全绿
- P1 先建回归网再动工具:CBM 抽为
enrichment.py 中间件;smoke 移植为边界测试;建契约快照
- P2 路径收敛:4 份
_resolve_output_dir 合并进 workspace.resolve_workspace(行为逐字一致),各副本随工具迁移逐个删除
- P3 主体(5-10 个小 PR):逐工具把
registry.py 的 Tool(...) 块剪贴进对应工具文件成 @tool;每批由 P1 边界测试护驾
- P4 收尾:删除 importlib 路径与
registry.py;评估 takes_store 退役(目前仅 3 个工具为 False);smoke_test_mcp.py 删除
约束未来的规则: 新工具只许新式注册(装饰器直引函数对象);横切关注点一律走 enricher,禁止再往 dispatch 核心加按名匹配分支。
本 RFC 由 improve-codebase-architecture 工作流产出:两个并行探索代理走读全仓库识别摩擦 → 6 个深化候选 → 4 个激进不同的接口设计(最小化/灵活性/作者体验/端口适配)并行竞标 → 用户选定杂交方案。
Problem
MCP 工具边界是本系统的公共 API(44+ 工具,被 Qoder / Cursor / Claude Desktop 等 IDE agent 消费),但它当前是全仓库架构摩擦最集中的区域:
1. Schema 与实现分离 2600 行,无静态匹配保证
codewiki/mcp/registry.py共 2622 行,约 85% 是手写inputSchemaJSON 字符串;dispatch(registry.py:2552-2622)靠字符串handler_path+importlib动态导入,6 条调用分支(3 mode × takes_store)server.py:71-127instructions /prompts.py1627 行 / AGENTS.md)2. 薄壳样板批量重复
(arguments, store) -> str薄壳签名,各自重写json.dumps(ensure_ascii=False)、错误捕获、output_dir 解析_resolve_output_dir有 4 份独立副本(capture_conversation.py:186/distill_conversation.py:200/note_consolidation.py:306/source_ingest.py);wiki 场景块记载的收敛点resolve_workspace尚未落地knowledge_loop.py(2394 行)单文件 51 处函数内局部 import 绕循环依赖3. 横切关注点硬编码在 dispatcher 里
registry.py:2450-2549按工具名匹配分支,加一种后处理就要改 dispatch 核心4. 集成风险集中在无人测试的缝隙
tests/smoke_test_mcp.py文件名不匹配 pytest 收集;okf_regression_test.py收集 0 条store参数对大部分文件型工具是残留噪音:测试每次调用新建空SessionStoreProposed Interface
选定方案:D* 杂交设计——以 ports & adapters 骨架(深模块
ToolKernel+ 协议适配器 + 内存测试适配器),保留手写 schema(公共 API 即 schema 字节,不做签名派生,避免对冻结契约的回归风险),吸收中间件方案中的"边界契约快照测试"作为迁移护栏。使用示例:
隐藏的复杂度: 三种执行模式调度(调用点零分支)、统一异常兜底(50+ 处手写 try/except 收敛为一处)、JSON 编码单点(
ensure_ascii=False)、结果形状归一化、CBM 后处理(从按名匹配分支 → 可注册/可关闭的 enricher)、importlib 风险限定在 legacy 桥接(新工具直接引用函数对象,拼写错误 import 期暴露)、output_dir 解析(收敛至resolve_workspace单点:纯解析、不 mkdir、抛 ValueError 由边界兜底)。Dependency Strategy
依赖类别:In-process(核心逻辑纯进程内,可直接合并与真文件系统边界测试),协议边界用 ports & adapters 隔离:
mcp_adapter.py+server.pyimport;ToolSpec/ToolResult是我方契约,SDK 升级只动适配器SessionStore()即建——现有 31 个测试已证明)repowiki/文件布局tmp_path可替换);真正的债务是 4 份_resolve_output_dir,合并进workspace.py::resolve_workspace单点build_kernel(with_cbm=...)控制,测试默认关闭保证确定性Testing Strategy
tests/test_mcp_boundary.py——把smoke_test_mcp.py的 19 步改写为InMemoryClient.call(...)形式纳入 pytest 收集,成为端到端回归网InMemoryClient边界测试(每 PR 一工具一测试)json.dumps/try-except 样板的测试在对应工具迁移后删除tmp_path+ 真SessionStore,无需 mock;CBM 默认关闭;不引入新基础设施Implementation Recommendations
模块职责(不绑定当前文件路径):
ToolSurface(list/call 两个方法)、@tool装饰器、InMemoryClient测试适配器不变量(迁移全程必须保持):
ToolResult.text原样透传{"error": str(e)}渐进迁移路径(无 big-bang):
REGISTRY条目;server.py改接McpAdapter;旧dispatch()保留为弃用薄壳。437 个现有测试不改、全绿enrichment.py中间件;smoke 移植为边界测试;建契约快照_resolve_output_dir合并进workspace.resolve_workspace(行为逐字一致),各副本随工具迁移逐个删除registry.py的Tool(...)块剪贴进对应工具文件成@tool;每批由 P1 边界测试护驾registry.py;评估takes_store退役(目前仅 3 个工具为 False);smoke_test_mcp.py删除约束未来的规则: 新工具只许新式注册(装饰器直引函数对象);横切关注点一律走 enricher,禁止再往 dispatch 核心加按名匹配分支。
本 RFC 由 improve-codebase-architecture 工作流产出:两个并行探索代理走读全仓库识别摩擦 → 6 个深化候选 → 4 个激进不同的接口设计(最小化/灵活性/作者体验/端口适配)并行竞标 → 用户选定杂交方案。