What I Learned After Turning the Same Tool Into Both a CLI and an MCP Server
This post records the system design of making content-i18n support both CLI and MCP server
The point is not MCP protocol itself, but how the same workflow can be shared by two entrypoints
Core questions:
- what CLI is
- what MCP is
- what each layer should own
- why they should not grow separate workflows
- how
content-i18nsplits core, CLI, and MCP
What CLI is
CLI means command-line interface, an interface where users operate a program through terminal commands
Example:
|
|
This command has several parts:
content-i18n: program to runreview: action to execute--config: config file path--file: target file--source: source file
CLI users are usually humans, shell scripts, CI jobs, or other automation
A good CLI usually needs:
- clear command names
- understandable flags
- readable error messages
- exit codes scripts can check
- output that works for both humans and shell usage
CLI is the human-facing entrypoint
How CLI works
CLI is roughly this flow:
|
|
For content-i18n review:
- read command and flags
- load
content-i18n.yaml - find source and target file
- call the real review workflow
- print result for the user
- return exit code based on result
CLI can decide how output is printed, how errors are shown, and how commands are named
But what review checks, how queue state is calculated, and when sync-status can update should not live in the CLI layer
How to design the CLI layer
The CLI layer should own:
- command tree
- flag parsing
- config path loading
- shell-friendly output
- human-readable error
- exit code
The CLI layer should not own:
- prepare business rule
- review business rule
- queue state derivation
- sync-status decision
- batch orchestration
- source / target mapping rule
CLI is an adapter:
|
|
It converts human commands into input for core workflow, then converts core workflow results into human-readable output
What MCP is
MCP means Model Context Protocol
In this project, MCP lets an agent runtime operate a local tool through structured tool calls
If CLI is the contract between human and local program, MCP is the contract between agent runtime and local program
CLI call:
|
|
MCP tool call:
CLI is command + flags
MCP is tool name + structured arguments
The form is different, but the final operation can be the same
How MCP works
MCP layer is roughly this flow:
|
|
An MCP tool usually defines:
- tool name
- description
- input schema
- handler
- response shape
For content_i18n_review_translation:
- declare the tool exists
- define required arguments
- receive request from agent runtime
- decode it into input for core workflow
- call review workflow
- return structured response to agent
MCP output needs to let the agent continue the next step
So response usually includes:
- issue list
- file path
- source path
- sync readiness
- next action hint
But MCP should not implement review by itself
How to design the MCP layer
The MCP layer should own:
- public tool contract
- argument schema
- request decoding
- handler registration
- structured response
- agent-friendly result shape
The MCP layer should not own:
- deciding review rules
- deriving queue state
- deciding completion
- handling source / target mapping
- adding batch orchestration rules
content-i18n MCP server was organized like this:
|
|
This structure makes responsibilities clearer:
- definition stores tool contract
- handler receives request and calls core
- registration attaches tools to server
- response shapes agent-facing output
Registration also changed to a registry / spec pattern
That keeps the public contract out of a long manual registration block, and makes adding or removing tools less error-prone
Why CLI and MCP should share the same core workflow
When one tool supports both CLI and MCP, the biggest risk is that the two entrypoints slowly become two products
If both sides implement logic by themselves, drift appears quickly:
- CLI review passes, MCP review fails
- MCP sync succeeds, CLI queue still shows stale
- batch path and single-file path check different things
- agent bypasses review and edits target file directly
- completion rule means different things in different entrypoints
So workflow needs one authority
In content-i18n, that authority is internal/core
|
|
CLI and MCP can have different input / output
But prepare, review, sync, queue, and batch can only have one meaning
content-i18n core boundary
Final stable layering:
|
|
Responsibility split:
| 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 and MCP call internal/core
internal/core does not need to know whether the caller is CLI or MCP
Review rule changes once, and both CLI and MCP get the same behaviour
How MCP tool surface should be designed
MCP tool surface should not be too granular
Early low-level primitives can look flexible:
- read source
- create work packet
- validate translation
- write translation target
- next translation
- repair translation
But they are risky for agents
Tool surface is not only an API list. It also hints at how the agent should work
With too many raw primitives, the agent can assemble its own workflow:
|
|
That is tool bypass
So content-i18n reduced the public MCP surface to workflow-level tools:
content_i18n_statuscontent_i18n_prepare_translationcontent_i18n_review_translationcontent_i18n_sync_statuscontent_i18n_translation_queuecontent_i18n_translate_batchcontent_i18n_validate_site
These 7 tools fit agents better than low-level primitives:
prepare_translationis closer to a real workflow step thanread_source+create_work_packettranslation_queuecan already return candidate, so extranext_translationis not neededreview_translationreturns structured issues and sync readiness, so it is more complete than a thin validator wrappertranslate_batchkeeps batch orchestration inside the tool, instead of making agent build its own batch loopsync_statusturns completion into official state, not only file existence
A better MCP tool design has:
- fewer public tools
- higher-level operations
- fuller responses
- less room for caller-invented workflow
How queue state works
Queue model turns translation workflow from a one-time task into a maintainable system
content-i18n uses three states:
completedstalemissing
These states come from:
- source discovery
- expected target path
- source hash
- translation status
Meanings:
| 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 |
Translation is not once-and-done
A target completed today can become stale tomorrow if source changes
Queue is not only a list of files to translate. It answers:
- which targets do not exist
- which targets are no longer synced with source
- which targets are complete
- which post should be handled next
MCP tools also need to respect queue model
If agent can skip queue, review, and sync, workflow authority fails
Failure modes and final rules
This design avoids several failure modes
CLI / MCP drift
If CLI and MCP each implement review, they become inconsistent
Fix:
- CLI and MCP only call core
- review rule exists once
- queue state derives from core
Wrapper logic leak
If MCP wrapper starts adding workflow rules, wrapper keeps getting thicker
Fix:
- MCP handler decodes request
- handler calls core
- handler formats structured response
- handler does not rejudge business rule
Low-level tool bypass
Too many low-level tools let agent build its own workflow
Fix:
- public surface uses workflow-level tools
- response carries enough information
- review and sync-status become official path
Fuzzy completion
If completion only means target file exists, queue state becomes unreliable
Fix:
- review / validation passes
- source-language leftovers are removed
- sync-status succeeds
Duplicated state logic
If CLI, MCP, and batch each calculate queue state, result can diverge
Fix:
- queue state derives in core
- callers use core result
Final rules:
- CLI and MCP are entrypoints, not two products
internal/coreis the only workflow owner- MCP tools should be workflow-level
- Public surface should be small, response should be complete
- Queue state must be system-derived
- Completion needs an official path
- Tool design should prevent agent bypass
This keeps CLI and MCP using the same tool core, instead of slowly becoming two different systems