How I Built the MCP Server That Publishes This Blog, Part 2: What Broke and What It Taught Me
Three failures in the order I found them, and the check each one left behind
Written 2 September 2026. Three failures, in the order I found them. The third one was still in the code when I started writing this, which is the only reason it is in here at all: a postmortem you write about yourself is easy to round off, and this one refuses to round off.

Part 2 of the series: what broke
Part one was about why this server exists. This one is about the three ways it has been wrong, and the fact that they are the same way three times.
One: the tab that closed too early
One: the tab that closed too early
The full account of this one, with the audit table and the script, is its own article, so here is the short version and the part that belongs to the server rather than to the incident.
The Medium adapter waited for the browser's address to change to the new draft's edit URL, treated that as done, and closed the tab in a finally block. Medium was still saving the body over several background requests. On 25 July 2026 an audit of all twenty-five calendar entries carrying a published URL found twelve live posts truncated to between 41% and 72% of the text I had sent, some missing their code blocks; a thirteenth URL answered HTTP 410. Every one of them had returned a valid URL and ok.
The part that belongs here is what it did to the design. Before this, the server had one kind of check: a pre-publish verifier, running on the text before it went anywhere. After it, there are three, and the reason there are three is that each answers a question the other two cannot.
- Before publishing, per platform and per entry type: will this text survive the adapter at all. Tables, unclosed fences, an H1 duplicating the title field.
- After publishing, reachability and activity: is the URL alive, is it picking up views. This is the check that existed and lied to me for weeks.
- After publishing, completeness: is what is now on the server the same as what I sent. This is the one that did not exist.
Three checks is not thoroughness. It is an admission that a check can only answer the question it asks, and I had been treating one question as though it covered the other two.
Two: the detector that read the article it was publishing
Two: the detector that read the article it was publishing
The adapter also needed to notice when Medium's editor failed to save, so it searched the page for the phrases Medium shows on failure — one of which is "something is wrong". It searched the whole page's text, which on that page includes the article being published.
One of the articles contained the sentence "if something is wrong you flip back".
A run that had worked perfectly was declared a failure. The retry ran. A duplicate draft appeared. The fix is small — look only at the elements a page uses to announce status, [role=alert], [role=status], [aria-live] — and the rule it leaves behind is not small: never read status out of prose. If the only way to learn an outcome is to hunt for words in running text, you guessed it. Prose will eventually contain your keyword by accident, and it will pick the worst possible moment, which is when the prose is about the thing you are checking for.
Three: the guardrail that says ok about platforms it has never heard of
Three: the guardrail that says ok about platforms it has never heard of
This is the one I am least comfortable publishing, and therefore the one worth publishing.
The pre-publish check dispatches on platform. Medium has rules, dev.to has rules, Telegram, Hacker News and the Stack Exchange sites have rules. Anything else falls through to a default branch, which records an informational finding saying there is no verifier for this platform. The overall verdict is then computed as "no error-level findings", so an informational finding is not an error, and the entry passes green.
Read that again with the rest of this series in mind. My guardrail against tools that report success without doing the work reports success without doing any work, whenever it meets a platform it does not recognise.
It is not hypothetical. The campaign that published this very series runs across platforms with no verifier at all: the posts to X, the post to LinkedIn, the newsletter, and the blog entries that are checklists rather than publications. Every one of those passes the pre-publish check with a clean bill, and the clean bill means nothing whatsoever. If I ever add a platform — and the plan for this series named one, a Russian-language site with no adapter and no verifier and no mention anywhere in the codebase — the check would wave it through and the publish step would fail.
The fix is two lines: make the default finding a warning rather than information. A warning is visible and does not block. That is the correct severity for "I have no idea whether this will render", and the reason it was information rather than a warning is that I wrote the default branch before there was anything for it to catch, and never went back.
There is a second version of the same shape, and this one behaves correctly, which is why it is instructive. An entry with an empty body produces an error rather than a note, and the publisher refuses it outright. Every one of the twenty entries of this campaign went into the calendar with an empty body, so every one of them is an entry the check will refuse until I write the thing. That is the verifier doing exactly its job, and it is also an accurate measurement of how much of what I call "planning" is a title, a date, and the article not being written yet.
What the verifier knows, and why every rule in it is a scar
What the verifier knows, and why every rule in it is a scar
The pre-publish check is a mirror of the publishing dispatcher: for every platform it knows what that publishing path can actually render. Reading its rules is the fastest tour of the platforms I have.
Medium has no table block in its editor, so a markdown table renders as literal pipes; the check refuses one. It also refuses an unclosed fence and a leading H1 that repeats the title field, because Medium has no separate title field at all — the first line of the editor is the title, which is a fact that has bitten me before and will bite you once.
Telegram gets plain text, because the adapter sends it with no parse mode, so any bold markers, backticks, links, headings or tables would appear verbatim in the message; and there is a 4,096-character cap, which is a truncation waiting to happen if nobody checks.
Hacker News renders no markdown at all, and its title has an 80-character cap that the platform would otherwise truncate silently. Silently is the operative word: that is a third-level failure with a valid URL waiting to be born, and the only reason it has not happened to me is that the check counts characters.
dev.to renders GFM, so tables and lists are fine and the rules are shorter: unclosed fences, a title-duplicating H1, an empty body.
The Stack Exchange sites want no repeated title line at the top of the body, a parent question's URL for an answer, real tags on a question rather than the placeholder that the entry template leaves behind — and they refuse outright to automate a comment on somebody else's answer, because there is no fixed target for it and it is not the kind of thing a machine should be posting under my name.
None of those rules were designed. Every one of them is a scar.
Repair is its own gamble
Repair is its own gamble
Twelve truncated posts had to be re-uploaded, and I want to be honest about how that went rather than end on the fix.
Re-uploading a story replays the same autosave race that truncated it in the first place, so a repair run can leave a post worse than it found it. The repair script prints FAIL when its final verification times out, even in cases where the save went through, which means its exit code is not the thing to trust — the completeness diff is. Stories that carry a newsletter block are excluded from the script entirely, because replacing the body breaks that block; those are hand work. And on some stories an edit to a published piece goes live immediately while on others it sits as an unpublished revision, so after every repair run the anonymous mode has to be run again to see what a stranger actually gets.
The general lesson is unwelcome: the cost of a silent failure is not the failed run. It is that the repair path was never designed, because nobody designs a repair path for a failure they believe cannot happen.
Twenty tools, and the honest arithmetic
Twenty tools, and the honest arithmetic
I recommend fifteen and I have twenty, and after all of the above I can be more precise about why that matters here rather than in the abstract. Every tool is a description billed on every request, and every description is a chance for the model to choose the wrong item from the list. But the specific cost in this server is different: five of the twenty belong to a completely different domain that crept in because this was the server that was already wired up, and those five have no verifier, no adapter and no place in a publishing workflow. They are not expensive. They are noise in a catalogue that a model reads before deciding what to do about an article.
What I would do differently
What I would do differently
Write the completeness check first. Not the adapter, not the tool descriptions — the thing that reads the result back and compares it against what was sent. It is twenty minutes of work in the version that only counts code blocks and checks for the last link's text, which is all mine does, and it would have caught every single failure in this article except the third one.
And for the third one: make the default branch of anything that dispatches on a name a warning, not a silence. The whole series is about tools reporting success they cannot vouch for. The most likely place for that to happen in your own code is the branch you wrote for cases you had not thought of yet.
Part one is why I built it. The truncation incident in full, with the audit table and the completeness script, is here. The protocol itself: where MCP and REST came from, what MCP costs in tokens, what an MCP server is made of.
Sources
Sources
My own promotion-calendar MCP server: its publishing dispatcher, its verifiers, its README, and the publishing runbook docs/publishing.md, written up on 25 July 2026. The audit of all 25 calendar entries carrying a published URL, run the same day. scripts/check-published-completeness.mjs, scripts/check-published-health.mjs, and the Medium repair script. The MCP specification is at modelcontextprotocol.io, revisions 2025-06-18 and 2026-07-28.