
Shipping an MCP Server in Four Days — Without Shipping Junk
Most people treat “AI-assisted development” as a speed story: more code, faster. The part nobody talks about is what fast actually costs you if nothing is checking the work — silent asymmetries, tests that lie, features that should never have shipped in the first place.
Using my Continuum process, I just finished the iching.rocks MCP server — a public Model Context Protocol endpoint (mcp.iching.rocks, currently MCP 1.12.30) that lets any compatible agent look up hexagrams and trigrams, trace a reading through changing lines, and search a complete original-translation I Ching corpus. Built from scratch on July 2, 2026, it went through nine upgrade cycles by July 6 — four days. That’s fast. It’s also fast in a way that held up, because every cycle ran the same loop: draft a design spec, run it past a separate pre-review and an independent cross-check, resolve disagreements explicitly instead of picking one silently, implement, then verify against a live smoke test hitting the real server — not a mock.
That loop caught four things worth writing up on their own.
Table of Contents
1. The whitelist that quietly hid part of your searchable text
First, the data. The corpus isn’t a blob of prose per hexagram — it’s structured JSON, and every hexagram carries the same named fields: a name and pinyin, the traditional-Chinese name (trad_chinese), short essence and symbolic_meaning lines, and then the layered classical material — judgment, image, and the six lines, each split into a canonical text and an interpretive explanation. Two hexagrams (1 and 2) also carry a final_comment. Alongside all of that sits original_text, holding the source Chinese and its pinyin. search_hexagrams doesn’t search all of it by default; it searches a whitelisted subset, and which fields are on that list turns out to matter.
search_hexagrams runs against a closed field whitelist — deliberately, so an unknown field name is a structured error instead of a silent broaden. The bug wasn’t in the matching engine. It was in which fields were in the default set.
search_hexagrams("利涉大川") → 0 matches (default fields)
search_hexagrams("利涉大川",
fields: ["original_text.chinese"]) → hexagrams 5, 6, 13, 18, 26, 27, 42, 59, 61, 64
Folding, normalization, tie-breaking — all correct. The default field list just didn’t include the field holding the classical Chinese text:
// before
private static readonly string[] DefaultFields = {
"name", "pinyin", "trad_chinese", "essence", "symbolic_meaning",
"judgment.text", "image.text", "lines[].text",
"trigram_above.english", "trigram_below.english"
};Code language: C# (cs)
Once you frame the whitelist as a policy decision rather than a config detail, the actual shape of the bug is visible: hexagram names in Chinese script were searchable by default (trad_chinese), but the classical text in Chinese script wasn’t. Same gap for pinyin — original_text.pinyin was opt-in while the name-level pinyin field wasn’t. It’s not “one field is missing.” It’s “the whole non-English-literate-but-Han-capable query path was opt-in by accident.”
// after
private static readonly string[] DefaultFields = {
"name", "pinyin", "trad_chinese", "essence", "symbolic_meaning",
"judgment.text", "image.text", "lines[].text",
"trigram_above.english", "trigram_below.english",
"original_text.chinese", "original_text.pinyin", "final_comment.text"
};Code language: C# (cs)
The design-spec process is what kept this from shipping half-finished — but not by the mechanism you’d guess. Three passes touched this field. The draft spec recommended promoting all three fields together. An earlier ChatGPT pre-review had argued the opposite for one of them — that final_comment.text belonged with the interpretive-prose fields that stay opt-in by design — and the draft recorded that objection instead of quietly overriding it. The Codex cross-check that followed didn’t dispute the promotion; it independently agreed with it. So the process didn’t earn its keep by the cross-check catching an error — it earned it by forcing the pre-review’s objection into an explicit, recorded ruling rather than a silent pick either way.
That ruling went against the exclusion, and the reasoning is the point. final_comment.text — the canonical text of the 用九/用六 lines, unique to hexagrams 1 and 2 — is the one member of the “canonical text” class (alongside judgment.text, image.text, lines[].text) the default set had missed; the interpretive layer is a separate field, final_comment.explanation, which correctly stays opt-in. Taking the exclusion at face value wouldn’t have preserved the status quo — it would have inverted the asymmetry. The aggregation behind original_text.* already includes the final-comment source text, so promoting the others without final_comment.text would have made the Chinese of those lines default-searchable while their English translation stayed opt-in: a fresh asymmetry in place of the one being closed.
2. Saying no to an MCP server feature on purpose
Some background this one needs. Most English I Chings descend from the Wilhelm/Baynes translation — still under copyright, and so embedded in reference tools that models reach for its phrasing by default. Guarding against that is where this corpus started, but it didn’t stay a copyright exercise: it became a from-scratch translation of the classical Chinese Zhouyi, built as a system-dynamics model rather than moral commentary, with a standardized vocabulary applied across all 64 hexagrams so the same source phrase always yields the same English. The wording is deliberately its own — which is what makes the next proposal a trap.
Not every open item gets built. One proposal — a “classic vocabulary bridge” that would let searches using standard Wilhelm/Baynes-style terms find hits in this corpus’s differently-worded original translation — got explicitly declined, not deferred. Building searchable proximity to another translator’s licensed phrasing is a copyright risk regardless of how indirect the mapping is, and no amount of clever implementation makes that tradeoff worth it.
What shipped instead is smaller and safer: a zero-result response includes a note explaining, in the corpus’s own terms, why a classic-vocabulary query came up empty, rather than silently returning nothing. The interesting part isn’t the feature — it’s that the decision to not build the fuller version is recorded in the same design-spec artifact as everything that did ship, with the reasoning attached. Six months from now, nobody has to re-litigate why that field doesn’t exist.
3. The rate-limit test that was lying to itself
The stress-test harness for the MCP rate limiter started throwing intermittent false failures on a specific sub-check: fire a burst of calls, expect the last one to get blocked, sometimes it didn’t.
The rate limiter’s windows are fixed to wall-clock time — not relative to when a burst starts. The harness waited a flat 65 seconds between phases, which guarantees crossing a window boundary but not avoiding straddling one:
window closes at :00 window opens at :00
...call 5, 6, 7 (blocked) | call 8, 9 (fresh window)...
If a burst starts at the wrong offset, five charges land in the closing window and two in the fresh one — and the seventh call, expected to be blocked, legitimately succeeds. The test wasn’t wrong about the limiter. It was wrong about its own timing.
The fix uses information the server already hands back on every breach. The limiter lives inside a JSON-RPC transport, so an over-limit call returns a JSON-RPC error object (code -32000), not an HTTP 429 — and its message carries a Retry after N seconds hint. (It’s prose, parsed out of the message: SDK 1.4.0 gives a thrown exception no structured error data channel to put it in.) That hint pins the exact boundary:
def learn_boundary(entry):
# first breach's hint pins the window boundary, absolutely
return (entry.breach_epoch + entry.retry_seconds) % 60
def wait_for_fresh_window(reason, boundary, margin=3):
now = time.time()
target = next_boundary_after(now, boundary) + margin
time.sleep(target - now)Code language: Python (python)
The harness now self-heals on the first run: it charges through the initial burst at whatever offset it happens to start at, learns the boundary from the resulting breach, then aligns every subsequent burst to start just past it. The flat 65-second wait stays only as a fallback for when no hint has been observed yet. Twelve offline unit cases — including landing exactly at, just before, and just after the boundary, plus the minute-mark wraparound — pin the math before it ever runs live again.
4. Ruling yourself out before blaming the server
A ChatGPT connector session produced a wrong hexagram ordering and what looked like a truncated tool payload. The investigation went outward in layers instead of guessing:
- Is the underlying data wrong? No — re-verified against the canonical sequence array served by
list_sequences. - Does a docs page contradict that array? Yes — a prose description of one hexagram-ordering scheme stated the wrong cycling rule. Real bug, but content, not server logic.
- Is the server actually truncating large payloads? No — the live smoke run confirmed the server returns both
structuredContentand an agreeing text fallback for every largesearch_hexagramsresult.
The truncation was happening client-side, in ChatGPT’s own MCP bridge. Each layer got checked against something re-derivable — the sequence array, the smoke-test output — before moving to the next, which is the only way “not our bug” is a conclusion instead of a guess.
What this bought
Four ordinary bugs — a field policy gap, a declined feature, a flaky test, a client-side red herring — aren’t individually interesting. What’s worth taking from this is that shipping fast and shipping caught aren’t in tension, provided the process treats every result as something to re-derive and check rather than something to trust because a model produced it. The rate-limit fix above only exists because the harness re-checked its own assumptions with the same rigor it applied to the server. That’s the whole trick.
The server’s live at mcp.iching.rocks if you want to try search_hexagrams against Chinese text, pinyin, or English directly. It’s free to use; the MCP Terms cover attribution and fair use, worth a skim before you build anything on top of it.
More coding articles