The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the Postlint MCP Server listing page.
Check a social post against a platform's real character limit before it ships. X, Bluesky, LinkedIn, Threads, Mastodon, Discord. Pure compute — no API, no auth, no network.
Recorded from docs/demo.tape with vhs. The posts and counts come from scripts/fixtures.mjs, which the regression tests import too.
An MCP server that answers one question: does this post fit?
A language model cannot count characters by inspection, and on these platforms neither can you. The limits are not what they look like. X bills every URL at 23 characters through t.co whether the link is 12 characters or 200. Bluesky counts extended grapheme clusters, so a four-person family emoji is 1 and not 11. Mastodon charges nothing for the domain on a remote mention. Getting any of that wrong shows up as a rejected post, or a truncated one, at publish time.
Counting is what a tool call is for. The model cannot do it by inspection, and a deterministic function can do it exactly.
Why this exists. Two posts went out of a podcast promo workflow over the limit. A Bluesky post shipped at 302 against 300, with the line "Under 300 graphemes. Audit clean." sitting directly beneath it. An X post was drafted at 308 against 280 and would have been rejected on launch morning. Both were invisible to eyeballing, because in both cases the count was a claim and not a measurement. Both are regression tests in this repo.
| Tool | What it returns |
|---|---|
check_post | Verdict for one platform: counted length, the limit, headroom, and what drove the count |
check_post_all | One row per platform, with the breakdown attached only to the rows that fail |
platform_limits | Each platform's limit, its counting unit, why that unit is not a character count, and the source |
Responses are small on purpose. check_post_all omits the arithmetic on passing rows because agents pay tokens per response.
| Platform | Limit | Unit | The part that surprises people | Source |
|---|---|---|---|---|
x | 280 | weighted characters | Every URL costs exactly 23. CJK, Hangul, and emoji cost 2 each; Latin, Greek, Cyrillic, Hebrew, and Arabic cost 1. An emoji sequence is one unit of 2, not 2 per code point. | twitter-text v3 config |
x_premium | 25,000 | weighted characters | Same weighting, higher ceiling. | X help center |
bluesky | 300 | graphemes | Flags, ZWJ emoji, skin-tone modifiers, and combining accents each count as 1. URLs count in full. A second cap of 3,000 UTF-8 bytes can bind first on ZWJ-heavy text. | atproto lexicon |
linkedin | 3,000 | characters | The 3,000 is generous; the fold is the real constraint. The feed collapses the post behind "see more" after a few lines. | LinkedIn help |
threads | 500 | characters | The September 2025 change added a 10,000-character attachment. The post body is still 500. | Meta newsroom |
mastodon | 500 | graphemes | URLs cost 23, as on X. On @user@example.social only @user counts. The limit is per-instance and plenty of servers run higher. | Mastodon API docs |
discord | 2,000 | characters | 4,000 with Nitro. Embeds have a separate 6,000 total. | Discord support |
Every number above traces to a published source. Widely repeated figures that no primary source states — the Facebook post limit, the YouTube community post limit, Reddit's title cap, Instagram's organic caption cap — are deliberately absent. A limit that cannot be defended makes a passing check worth nothing.
Published on npm. The config blocks below use npx, which fetches it on first run; no clone required.
Add to your .mcp.json:
Same block, in claude_desktop_config.json.
Add to ~/.codex/config.toml:
No token, no environment variables, no network access. Once the package is published, npx -y @conorbronsdon/postlint-mcp replaces the node invocation everywhere above.
Ask your assistant: "Check this post for X and Bluesky," and paste something with a couple of links in it.
The X post that started this, run through check_post with platform: "x":
The drivers line is the useful part. 69 of the budget went to links before a word was written, which tells you to move two of them into a reply rather than trimming prose.
The same post through check_post_all:
One post, three different lengths — 308, 330, and 308 again — from the same 330 characters of text. That gap is the whole reason this exists.
Drafts carry link placeholders, and [URL] is five characters while a real link is not. A post measured with the placeholder in place and posted with the link filled in is a post measured wrong; one draft came in at 264 that way and posted at 282.
So [URL], [LINK], [YOUTUBE URL], [SUBSTACK URL], and similar are priced as a real link (a 28-character YouTube short link, the shortest thing normally posted) and the response carries a warning saying the count is a floor.
fetch, XMLHttpRequest, and WebSocket with throws and drives every tool, so a call added later fails CI instead of quietly making this sentence false.configuration.statuses.max_characters from the target server yourself.truncate_to helper was considered and left out. Cutting a post at a character offset splits URLs, breaks grapheme clusters, and lands mid-sentence, and cutting it at a "safe" boundary silently drops whichever clause happened to be last. Either way the tool would be deciding what the post says. It reports the number and leaves the edit to you.www.-prefixed hosts always match. A bare domain matches only on a common TLD (src/count.ts holds the list), where the real twitter-text implementation carries the full IANA registry. Write https:// in front of a link and the count is exact.Tests make no network calls, because the server makes none. The two historical over-limit posts are regression fixtures in src/__tests__/lint.test.ts, alongside grapheme cases for ZWJ family emoji, regional-indicator flags, skin-tone modifiers, combining accents, and CJK.
Issues and pull requests are welcome. A new platform needs three things: the limit, the unit it is measured in, and a published source. A new counting rule needs a test that fails without it. Numbers repeated by third parties are not sources.
Built and maintained by Conor Bronsdon. I host the Chain of Thought podcast, which covers AI infrastructure, developer tools, and how practitioners actually use this stuff. I built this after shipping two over-limit posts in a workflow that was supposed to catch them.
Companion tools:
More at chainofthought.show and on X.
This is an independent personal project, not affiliated with, sponsored by, or endorsed by any company. All views expressed are my own.
Apache-2.0