Sandbox
@sysprog21/zhtw-mcp

MCP server for zh-TW text linting

zhtw-mcp checks Traditional Chinese text for Taiwan-specific vocabulary, punctuation, and character forms. It runs as an MCP server for agent workflows and as a CLI for direct file linting, with rules and fixes driven by the bundled ruleset and MoE references.

477 stars45 forksRustUpdated 7d ago
Who it's for

Builders who use Claude Code, Codex, or other MCP clients and need zh-TW text checked in their workflow.

What it delivers

You can catch and fix zh-TW terminology and punctuation issues before they reach readers.

What it does

Taiwan standard checks

Enforces MoE punctuation, character shapes, and cross-strait vocabulary rules.

MCP integration

Exposes the linter as an MCP server so agents can call it during writing and editing.

Automatic fixes

Can return deterministic fixes for safe lexical issues and preview them with `--dry-run`.

Strict and relaxed profiles

Lets you switch between base and strict enforcement, with relaxed mode for UI text.

AI writing review

Can flag filler phrases, safety words, and other AI-writing patterns with `detect_ai`.

Caching and telemetry

Includes a persistent judgment cache and telemetry output for repeated runs.

How to get it

  1. 1On any system with Nix and flakes enabled
    # builds and drops into a temporary shell with `zhtw-mcp` on `$PATH`
    nix shell "github:sysprog21/zhtw-mcp"
    
    # builds and runs zhtw-mcp (you can use this command to register with an MCP
    # client as shown in the Installing section)
    nix run "github:sysprog21/zhtw-mcp"
  2. 2Requires stable Rust 1.91+.
    make
  3. 3Run
    make check           # the gate CI runs: tests, clippy, formatting, hooks
    make indent          # run the formatters; the gate checks their result
    make hooks           # install the git hooks; uninstall-hooks removes them
    make corpus          # precision, recall and false-positive metrics
  4. 4The quickest way to build, install to $XDG_BIN_HOME (or ~/.local/bin), stop older server…
    make install      # build release, install binary, register detected MCP clients
    make uninstall    # remove binary and detected MCP registrations
    make status       # check binary freshness, process, and registration state
  5. 5For manual setup or other MCP clients
    # Claude Code
    claude mcp add zhtw-mcp -- /path/to/zhtw-mcp
    
    # Codex CLI
    codex mcp add zhtw -- /path/to/zhtw-mcp
    
    # OpenCode
    opencode mcp add zhtw-mcp /path/to/zhtw-mcp
  6. 6Run
    zhtw-mcp lint README.md                 # lint a file
    zhtw-mcp lint file.md --fix             # auto-fix in place
    zhtw-mcp lint file.md --fix --dry-run   # preview fixes
    zhtw-mcp lint file.md --telemetry       # print stderr summary counters
    zhtw-mcp cache clear                    # clear persistent judgment cache

README

zhtw-mcp

A linguistic linter for Traditional Chinese (zh-TW) that enforces Taiwan Ministry of Education (MoE) standards on vocabulary, punctuation, and character shapes. It plugs into AI coding assistants through the Model Context Protocol (MCP) and catches Mainland Chinese (zh-CN) regional drift before it reaches the user.

The tool enforces three official Taiwan standards:

Over 1100 vocabulary rules and 15 casing rules are compiled into the binary. For ambiguous terms, the server asks the AI assistant it runs inside for help deciding -- no extra API keys required.

Why this exists

Modern Chinese is an inadequately standardized language

In the late Qing dynasty, scholars had to express Western concepts in a writing system with no native vocabulary for them. Whether coining new words or importing translations via Japanese (和製漢語), they assembled a literary system under enormous time pressure. Many translated terms were inconsistent, ambiguous, or contradictory. The Chinese-speaking world has lived with these deficiencies for over a century.

Simplified Chinese made it worse

The PRC simplification effort reduced not just stroke counts but vocabulary precision. Terms that should vary by domain got flattened into single catch-all translations. Many PRC translations were coined hastily: if a term worked in one context, it spread uncritically to others.

AI models amplify the problem

AI language models learn from web text where Simplified Chinese vastly outweighs Traditional Chinese (roughly 2.6:1 in CC-100). Major datasets like CulturaX do not even track Traditional Chinese separately. A FAccT 2025 study confirmed that most models favor zh-CN terminology when asked to write zh-TW. The output looks plausible but is not how people in Taiwan actually write.

This goes beyond character conversion. The same word often means different things across the strait:

Englishzh-CNzh-TWWhy it matters
concurrency並發並行In zh-CN, 並行 means "parallel" -- a different concept entirely
parallel並行平行zh-CN 並行 = "parallel"; in Taiwan, 並行 = "concurrent"
process (OS)進程行程進程 in Taiwan means "progress," not an OS process
file / document文件 / 文檔檔案 / 文件文件 in China = "file"; in Taiwan = "document"
render渲染算繪渲染 in Taiwan = "exaggerate" (a painting technique)
traverse遍歷走訪遍歷 in Taiwan is reserved for Ergodic theory (遍歷理論)

What this project does

Automatically check and correct zh-TW text produced by AI, catching cross-strait terminology leaks:

  • Half-width punctuation (, . :) that should be full-width ( )
  • Mainland-style "" curly quotes replaced with Taiwan-style 「」 corner brackets
  • Missing or extra CJK-Latin/digit spacing when the project's spacing policy requires it
  • Mainland vocabulary -- 軟件→軟體, 內存→記憶體, 默認→預設, etc.
  • Non-standard character variants -- 裏→裡, 着→著 per MoE standard forms in the strict profile; form preferences are advisory
  • Politically colored terms -- 祖國, 內地
  • Casing -- JavaScript, GitHub, macOS

These standards are enforced through two profiles on the strictness axis, plus orthogonal capability flags:

ProfilePurpose
baseCross-strait vocabulary, punctuation, casing, grammar, politically colored terms
strictFull MoE enforcement: character variants (裏→裡), grammar (臺/台), all punctuation
FlagPurpose
relaxedRelaxed for software UI: disables colon/dunhao enforcement and grammar checks; uses en-dash for ranges
detect_aiAI writing review: filler phrase detection, semantic safety words, copula/passive voice checks, density-based pattern detection
spacingCJK boundary policy, --spacing require|strip: require stores spaces (default); strip leaves the gap to a controlled HTML renderer. The one flag here that takes a value, and a different thing from --off spacing, which turns the whole family off

For unsupported authority attributions, select document_genre in MCP or --document-genre casual|technical|financial in the CLI. The check runs only with AI detection on (--detect-ai / detect_ai), and never suggests an edit in any genre: deleting an attribution changes what the sentence claims, so the genre selects the advice rather than a rewrite. Casual prose is told to name the source or drop the appeal; technical and financial prose are told the claim needs a citation.

Profiles control how strict the zh-TW norm enforcement is. Flags are orthogonal -- detect_ai works with either profile, relaxed can combine with strict if you want variant normalization but lenient punctuation.

spacing=require is the default because source text also appears outside CSS-capable renderers. Projects that control their HTML can select strip and use text-autospace, a Baseline newly-available property since November 2025. UTR #59 is a draft for layout-time autospacing, not a requirement to remove source-text spaces. Either way the policy governs the U+0020 space, and only where CJK meets an ASCII letter or an ASCII digit.

See docs/rules.md for the full rule reference.

Naming convention: cn and tw

This project follows BCP 47. The region subtag comes from ISO 3166-1 alpha-2, where "region" can denote a sovereign state, territory, or economic area -- not necessarily a "country."

  • zh-CN: Chinese as written in the CN region (Simplified)
  • zh-TW: Chinese as written in the TW region (Traditional)

Throughout the codebase, cn and tw denote regional writing conventions, not a political statement.

Getting started

Pre-built binaries

Every successful push to main refreshes the rolling latest release, which is what GitHub reports as the latest release. No version tag is involved. Each archive holds the binary, LICENSE, and README.md, and SHA256SUMS ships next to them.

PlatformAsset
Linux x86_64 (glibc 2.39 or newer)zhtw-mcp-x86_64-unknown-linux-gnu.tar.gz
Linux arm64 (glibc 2.39 or newer)zhtw-mcp-aarch64-unknown-linux-gnu.tar.gz
macOS arm64zhtw-mcp-aarch64-apple-darwin.tar.gz
Windows x86_64zhtw-mcp-x86_64-pc-windows-msvc.tar.gz

On any other platform, use Nix below or build from source.

The browser extension is packaged onto the same release as zhtw-mcp-extension.zip. Unpack it and load it through chrome://extensions with developer mode on.

macOS / Linux

base=https://github.com/sysprog21/zhtw-mcp/releases/download/latest
case "$(uname -sm)" in
  "Darwin arm64")  asset=zhtw-mcp-aarch64-apple-darwin.tar.gz ;;
  "Linux x86_64")  asset=zhtw-mcp-x86_64-unknown-linux-gnu.tar.gz ;;
  "Linux aarch64") asset=zhtw-mcp-aarch64-unknown-linux-gnu.tar.gz ;;
  *) asset=""; echo "no pre-built binary for $(uname -sm)" >&2 ;;
esac
[ -n "$asset" ] &&
  curl -fsSLO "$base/$asset" -O "$base/SHA256SUMS" &&
  shasum -a 256 --ignore-missing -c SHA256SUMS &&
  tar -xzf "$asset" zhtw-mcp

On Linux without shasum, use sha256sum --ignore-missing -c SHA256SUMS instead.

Windows (PowerShell)

$base = "https://github.com/sysprog21/zhtw-mcp/releases/download/latest"
$asset = "zhtw-mcp-x86_64-pc-windows-msvc.tar.gz"
irm "$base/$asset" -OutFile $asset
irm "$base/SHA256SUMS" -OutFile SHA256SUMS
$want = ((Select-String -Path SHA256SUMS -SimpleMatch $asset).Line -split '\s+')[0]
if ((Get-FileHash -Algorithm SHA256 $asset).Hash -ine $want) { throw "checksum mismatch" }
tar -xzf $asset zhtw-mcp.exe

Both snippets leave the binary in the current directory; move it somewhere on your PATH to run it by name.

Nix

On any system with Nix and flakes enabled:

# builds and drops into a temporary shell with `zhtw-mcp` on `$PATH`
nix shell "github:sysprog21/zhtw-mcp"

# builds and runs zhtw-mcp (you can use this command to register with an MCP
# client as shown in the Installing section)
nix run "github:sysprog21/zhtw-mcp"

Note: The first run compiles the project from source, which can take several minutes. Subsequent runs reuse the Nix store cache and start instantly. To speed up the initial build, run nix build --cores 0 "github:sysprog21/zhtw-mcp" first. And --cores 0 tells Nix to use all available CPU cores.

Building from source

Requires stable Rust 1.91+.

make

The binary is at target/release/zhtw-mcp.

Python 3 is a build requirement, not just a test requirement: the OpenCC conversion tables are generated rather than committed.

Working on the code

make check           # the gate CI runs: tests, clippy, formatting, hooks
make indent          # run the formatters; the gate checks their result
make hooks           # install the git hooks; uninstall-hooks removes them
make corpus          # precision, recall and false-positive metrics

scripts/indent.sh holds the formatter chain: comment reflow with commentflow, then cargo fmt, black, shfmt, and the assets/ruleset.json normalization that scripts/check-ruleset.py owns. make indent runs it with --write and the gate runs it with --check, against a copy of the tree so a check never rewrites what it is judging. The chain runs to a fixed point, since reindenting a block can invalidate the wrap of a comment inside it.

No formatter here is passed a style flag. shfmt takes its settings from .editorconfig and commentflow takes its column limit from .clang-format, which exists only for that number: comments wrap at 80 while rustfmt allows code 100.

Every lane reports that it skipped rather than failing when its tool is missing, which is why a green local run is weaker evidence than a green CI run. CI installs commentflow, shfmt and shellcheck on the Linux leg and sets ZHTW_REQUIRE_TOOLS=1 there, which turns a skip into a failure.

Any cargo build installs the git hooks, through build.rs, and make hooks does it on its own. A configured core.hooksPath is left untouched, since it may be shared by unrelated repositories. The pre-commit hook runs rustfmt, black, shellcheck, shfmt, commentflow and the ruleset checks over a checkout of the index, so an unstaged edit neither fails a commit nor rides along in one. The commit-msg hook holds the subject to 50 columns and the body to 72, imperative and free of em dashes, counting a CJK character as the two columns a terminal spends on it. The pre-push hook replays those rules over commits a rebase or an amend rewrote, and CI runs the same script over a pull request's own commits, so the rules bind a contributor who never installed the hooks as well.

scripts/check-comments.sh holds source comments to the two prose rules no formatter knows about: no em dash, and no backtick outside a /// or //! doc comment, where backticks are rustdoc markup rather than prose. It runs in the gate and in the pre-commit hook.

scripts/test-git-hooks.sh drives all four hooks against a scratch repository and runs in the gate, so a hook that stops rejecting fails there rather than on somebody's next commit.

Installing

The quickest way to build, install to $XDG_BIN_HOME (or ~/.local/bin), stop older server processes, and register with detected MCP clients (Claude Code and/or Codex):

make install      # build release, install binary, register detected MCP clients
make uninstall    # remove binary and detected MCP registrations
make status       # check binary freshness, process, and registration state

For manual setup or other MCP clients:

# Claude Code
claude mcp add zhtw-mcp -- /path/to/zhtw-mcp

# Codex CLI
codex mcp add zhtw -- /path/to/zhtw-mcp

# OpenCode
opencode mcp add zhtw-mcp /path/to/zhtw-mcp

Other MCP clients may use .mcp.json in your project root:

{
  "mcpServers": {
    "zhtw-mcp": {
      "command": "/path/to/zhtw-mcp",
      "args": []
    }
  }
}

Replace /path/to/zhtw-mcp with the actual binary path (e.g., target/release/zhtw-mcp).

CLI quick start

zhtw-mcp lint README.md                 # lint a file
zhtw-mcp lint file.md --fix             # auto-fix in place
zhtw-mcp lint file.md --fix --dry-run   # preview fixes
zhtw-mcp lint file.md --telemetry       # print stderr summary counters
zhtw-mcp cache clear                    # clear persistent judgment cache

See docs/cli.md for the full CLI reference and docs/mcp.md for MCP tool/resource/prompt details.

Common prompts

When running as an MCP server, you interact through natural language. The assistant translates your intent into zhtw tool calls:

IntentSayMaps toWhat happens
Lint text"Check this paragraph for mainland terms"zhtw({ "text": "..." })Returns issues with line/column, suggestions, and rule type
Auto-fix"Fix the zh-TW issues in this document"zhtw({ "text": "...", "fix_mode": "lexical_safe" })Deterministic fixes applied; corrected text returned
Quality gate"Reject if more than 3 zh-TW errors"zhtw({ "text": "...", "max_errors": 3 })accepted: true/false verdict based on error count
Strict MoE"Check this with strict MoE rules"zhtw({ "text": "...", "profile": "strict" })Adds character variant (裏→裡) and full punctuation enforcement
UI strings"Lint this UI string, skip grammar"zhtw({ "text": "...", "relaxed": true })Disables colon/dunhao/grammar enforcement; uses en-dash for ranges
AI writing review"Review this for AI writing artifacts"zhtw({ "text": "...", "detect_ai": true })Flags filler phrases, semantic safety words, copula/passive overuse
Markdown-aware"Lint this markdown, skip code blocks"zhtw({ "text": "...", "content_type": "markdown" })Fenced code, inline code, and HTML blocks excluded from scanning
Cost telemetry"Lint this and include telemetry"zhtw({ "text": "...", "include_telemetry": true })Returns estimated token/caching metrics for the call

Each zhtw call is stateless -- parameters like profile are per-call, not session state. Omitting profile defaults to base.

The server also exposes two read-only resources for assistants to consult: zh-tw://style-guide/moe (MoE standards) and zh-tw://dictionary/ambiguous (cross-strait term disambiguation). See docs/mcp.md for the full prompt catalog.

Further reading

  • docs/cli.md -- full CLI reference, config files, CI/CD integration, S2T conversion
  • docs/mcp.md -- MCP tool parameters, resources, prompts, sampling, usage examples
  • docs/internals.md -- processing pipeline, script detection, design decisions, testing
  • docs/rules.md -- rule type reference, extending the ruleset, runtime overrides

License

zhtw-mcp is available under a permissive MIT-style license. Use of this source code is governed by a MIT license that can be found in the LICENSE file.

Files in the repo

Repository payload27 top-level entries
  • .agents
  • .claude
  • .github
  • assets
  • benches
  • build
  • docs
  • extension
  • scripts
  • src
  • tests
  • .clang-format
  • .editorconfig
  • .envrc
  • .gitattributes
  • .gitignore
  • .shellcheckrc
  • build.rs
  • Cargo.lock
  • Cargo.toml
  • flake.lock
  • flake.nix
  • LICENSE
  • Makefile
  • README.md
  • rust-toolchain.toml
  • THIRD-PARTY-NOTICES.md

Discussion (0)

Ask about usage, or say what you built with it

Sign in to join the discussion.

No comments yet. Be the first to say what this is good for.

More connectors

High-performance code intelligence MCP server. Indexes codebases into a persistent knowledge graph — average repo in milliseconds. 158 languages, sub-ms queries, 99% fewer tokens. Single static binary, zero dependencies.

43k

Universal provider proxy for OpenAI Codex & Claude Code — use any LLM (Claude, Gemini, Grok, DeepSeek, Ollama…) with Codex CLI, App, SDK, and Claude Code

14k
okf-memory/
okf-agent-memory

Git-native persistent memory for AI coding agents. Implements Google OKF v0.2 with sub-300µs in-memory BM25 search, embedded MCP server, and progressive disclosure. Slashes token bloat by 80% with zero external databases or dependencies. Built in pure Go.

547
tirth8205/
code-review-graph

Local-first code intelligence graph for MCP and CLI. Builds a persistent map of your codebase so AI coding tools read only what matters, with benchmarked context reductions on reviews and large-repo workflows.

31k
2akouwu/
reverify

Stop your AI from making things up — it proposes, deterministic tools decide, every claim checked against ground truth with evidence. Grounded facts and context survive resets. Reverse engineering is the proving ground. MCP server + CLI.

1.1k
t8y2/dbxConnectors

20 MB lightweight cross-platform database client for 90+ databases, including MySQL, PostgreSQL, SQLite, Redis, MongoDB, DuckDB, SQL Server, and Dameng. Built-in AI, MCP Server, CLI, desktop and Docker. | 轻量级跨平台数据库管理工具,支持 MySQL、PostgreSQL、SQLite、Redis、MongoDB、达梦等 90+ 数据库,提供桌面端、Docker、CLI、内置 AI 助手和 MCP Server。

19k