把同一個工具同時做成 CLI 和 MCP 之後, 我學到的事

這篇記錄 content-i18n 同時支援 CLI 和 MCP server 時的 system design

重點不是 MCP protocol 本身, 而是同一個 workflow 要怎麼被兩種 entrypoint 共用

核心問題:

  • CLI 是什麼
  • MCP 是什麼
  • 它們各自應該負責什麼
  • 為什麼不能各自長一套 workflow
  • content-i18n 最後怎麼切 core, CLI, MCP

什麼是 CLI

CLI 是 command-line interface, 也就是人透過 terminal 下 command 操作程式

例如:

1
content-i18n review --config ./content-i18n.yaml --file target.md --source source.md

這個 command 裡有幾個部分:

  • content-i18n: 要執行的程式
  • review: 要執行的 action
  • --config: config file path
  • --file: target file
  • --source: source file

CLI 的使用者通常是人, shell script, CI job, 或其他 automation

一個好的 CLI 通常要處理:

  • command name 清楚
  • flag 好理解
  • error message 看得懂
  • exit code 可以給 script 判斷
  • output 可以給人讀, 也可以給 shell 接

CLI 是 human-facing entrypoint

CLI 怎麼運作

CLI 大概是這條 flow:

1
2
3
4
5
command
  -> argument parsing
  -> config loading
  -> core workflow call
  -> human-facing output

content-i18n review 來說:

  1. 讀 command 和 flags
  2. 載入 content-i18n.yaml
  3. 找到 source 和 target file
  4. 呼叫真正的 review workflow
  5. 把結果印給使用者
  6. 根據結果回傳 exit code

CLI 可以決定 output 怎麼印, 錯誤訊息怎麼呈現, command 怎麼命名

但 review 檢查什麼, queue state 怎麼算, sync-status 什麼時候能更新, 不應該放在 CLI layer

CLI layer 要怎麼設計

CLI layer 適合負責:

  • command tree
  • flag parsing
  • config path loading
  • shell-friendly output
  • human-readable error
  • exit code

CLI layer 不適合負責:

  • prepare 的 business rule
  • review 的 business rule
  • queue state 推導
  • sync-status 判斷
  • batch orchestration
  • source / target mapping rule

CLI 是 adapter:

1
2
3
human
  -> CLI
  -> internal/core

它把人的 command 轉成 core workflow 可以吃的 input, 再把 core workflow 的 result 轉成人看得懂的 output

什麼是 MCP

MCP 是 Model Context Protocol

在這個專案裡, MCP 讓 agent runtime 可以用 structured tool call 操作本機工具

如果 CLI 是 human 跟 local program 之間的 contract, MCP 就是 agent runtime 跟 local program 之間的 contract

CLI call:

1
content-i18n review --config ./content-i18n.yaml --file target.md --source source.md

MCP tool call:

CLI 是 command + flags

MCP 是 tool name + structured arguments

形式不同, 但最後要做的事情可以是同一件事

MCP 怎麼運作

MCP layer 大概是這條 flow:

1
2
3
4
tool definition
  -> request decoding
  -> core workflow call
  -> structured response

一支 MCP tool 通常要定義:

  • tool name
  • description
  • input schema
  • handler
  • response shape

content_i18n_review_translation 來說:

  1. 宣告這支 tool 存在
  2. 定義它需要哪些 arguments
  3. 接 agent runtime 傳進來的 request
  4. decode 成 core workflow 可以用的 input
  5. 呼叫 review workflow
  6. 回傳 structured response 給 agent

MCP 的 output 要讓 agent 可以接著做下一步

所以 response 通常要包含:

  • issue list
  • file path
  • source path
  • sync readiness
  • next action hint

但 MCP 不該自己實作 review

MCP layer 要怎麼設計

MCP layer 適合負責:

  • public tool contract
  • argument schema
  • request decoding
  • handler registration
  • structured response
  • agent-friendly result shape

MCP layer 不適合負責:

  • 自己決定 review rule
  • 自己推導 queue state
  • 自己判斷 completion
  • 自己處理 source / target mapping
  • 自己補 batch orchestration rule

content-i18n 的 MCP server 後來整理成:

1
2
3
4
5
6
7
8
9
internal/mcp/
  server.go
  tools.go
  tool_defs.go
  tool_handlers.go
  resources.go
  resource_defs.go
  resource_handlers.go
  response.go

這個結構讓責任比較清楚:

  • definition 放 tool contract
  • handler 接 request 並呼叫 core
  • registration 負責把 tool 掛到 server
  • response 負責 agent-facing output shape

registration 也改成 registry / spec pattern

這樣 public contract 不會藏在一大段手工註冊裡, 新增或移除 tool 時也比較不容易漏掉

為什麼 CLI 和 MCP 要共用同一個 core workflow

同一個工具支援 CLI 和 MCP 時, 最大風險是兩邊慢慢變成兩個產品

如果兩邊各自實作邏輯, 很快就會 drift:

  • CLI review pass, MCP review fail
  • MCP sync 成功, CLI queue 還是 stale
  • batch path 和 single-file path 檢查不一致
  • agent 繞過 review, 直接改 target file
  • completion rule 在不同入口有不同解釋

所以 workflow 必須只有一個 authority

content-i18n 裡, 這個 authority 是 internal/core

1
2
CLI / MCP = interface
internal/core = workflow owner

CLI 和 MCP 可以有不同 input / output

但 prepare, review, sync, queue, batch 的 meaning 只能有一份

content-i18n 的 core boundary

最後比較穩定的分層:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
consumer repo
  ├─ content roots
  ├─ content-i18n.yaml
  ├─ glossary
  └─ style pack
CLI / MCP
internal/core
  ├─ init
  ├─ prepare/review/sync
  ├─ queue
  ├─ batch orchestration
  └─ site validation entrypoint
        ├─ validator
        ├─ structure
        ├─ frontmatter
        ├─ content
        └─ providers

責任切分:

Layer Responsibility
CLI command parsing, flags, config path, exit code, human-readable output
MCP tool definition, request decoding, handler registration, structured response
internal/core prepare, review, sync, queue, batch, validation workflow
validator / structure / frontmatter / content low-level document rules
providers DeepL, Google, ai-harness provider boundary

CLI 和 MCP 呼叫 internal/core

internal/core 不需要知道 caller 是 CLI 還是 MCP

Review rule 改一次, CLI 和 MCP 都會吃到同一個行為

MCP tool surface 要怎麼設計

MCP tool surface 不能太碎

早期如果把 tool 拆成 low-level primitive, 看起來很有彈性:

  • read source
  • create work packet
  • validate translation
  • write translation target
  • next translation
  • repair translation

但這對 agent 反而危險

因為 tool surface 不只是 API 清單, 它也在暗示 agent 應該怎麼做事

raw primitive 太多, agent 很容易自己組 workflow:

1
2
3
4
5
read source
  -> inspect file
  -> edit target directly
  -> run partial validation
  -> skip sync-status

這就是 tool bypass

所以 content-i18n 後來把 public MCP surface 收斂成 workflow-level tools:

  • content_i18n_status
  • content_i18n_prepare_translation
  • content_i18n_review_translation
  • content_i18n_sync_status
  • content_i18n_translation_queue
  • content_i18n_translate_batch
  • content_i18n_validate_site

這 7 支 tool 比 low-level primitive 更適合 agent:

  • prepare_translationread_source + create_work_packet 更接近真實 workflow step
  • translation_queue 已經能帶出 candidate, 不需要額外 next_translation
  • review_translation 回 structured issue 和 sync readiness, 比單純 validator wrapper 更完整
  • translate_batch 保留 batch orchestration, 不讓 agent 自己拼 batch loop
  • sync_status 把 completion 變成正式狀態, 不只是檔案存在

比較好的 MCP tool design:

  • public tool 少
  • operation 高階
  • response 完整
  • caller 不容易自己補 workflow

Queue state 怎麼運作

Queue model 讓 translation workflow 從一次性任務變成可維護系統

content-i18n 用三種 state:

  • completed
  • stale
  • missing

這些 state 從下面資料推導:

  • source discovery
  • expected target path
  • source hash
  • translation status

三種 state 的意思:

State Meaning
missing source exists, expected target does not exist
stale target exists, but source changed after last sync
completed review / validation passed, target synced with source

翻譯不是 once-and-done

今天完成的 target, 只要 source 改了, 明天就可能 stale

所以 queue 不只是列出待翻譯檔案, 它是在回答:

  • 哪些 target 不存在
  • 哪些 target 已經跟 source 不同步
  • 哪些 target 真的完成
  • 下一個該處理哪一篇

MCP tool 也要尊重 queue model

如果 agent 可以跳過 queue, review, sync, workflow authority 會失效

Failure modes 和最後規則

這個設計主要避免幾種 failure mode

CLI / MCP drift

CLI 和 MCP 如果各自實作 review, 很快就會不一致

修法:

  • CLI 和 MCP 都只呼叫 core
  • review rule 只放一份
  • queue state 只從 core 推導

Wrapper logic leak

MCP wrapper 如果開始補 workflow rule, wrapper 會越來越厚

修法:

  • MCP handler 只 decode request
  • handler 呼叫 core
  • handler format structured response
  • 不在 handler 裡重新判斷 business rule

Low-level tool bypass

low-level tools 太多, agent 會自己組 workflow

修法:

  • public surface 收斂成 workflow-level tools
  • response 帶足夠資訊
  • review 和 sync-status 成為 official path

Fuzzy completion

如果 completion 只是 target file exists, queue 會失真

修法:

  • review / validation 要 pass
  • source-language leftover 要清掉
  • sync-status 要成功

Duplicated state logic

如果 queue state 在 CLI, MCP, batch 各算一次, 結果會不一致

修法:

  • queue state 推導集中在 core
  • caller 只使用 core result

最後規則:

  1. CLI 和 MCP 是 entrypoint, 不是兩個產品
  2. internal/core 是唯一 workflow owner
  3. MCP tool 要偏 workflow-level
  4. Public surface 要少, response 要完整
  5. Queue state 必須由系統推導
  6. Completion 要有正式 path
  7. Tool design 要防 agent bypass

這樣 CLI 和 MCP 才能共用同一個工具核心, 不會慢慢長成兩套系統

References

  1. content-i18n GitHub Repository
  2. Model Context Protocol — What is MCP?
  3. Model Context Protocol — Architecture Overview
  4. Model Context Protocol Specification — Basic Overview
  5. Model Context Protocol — SDKs