Social PostLint-MCP

Social PostLint-MCP

Checks a social post against a platform's real character limit before it ships — X bills every URL at 23 characters and weights non-Latin script at 2, while Bluesky and Mastodon count grapheme clusters. Covers X, X Premium, Bluesky, LinkedIn, Threads, Mastodon, and Discord, with no credentials and no network access.

Category
访问服务器

README

<div align="center">

postlint-mcp

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.

npm version License: Apache 2.0 Node Glama score Podcast X

<img src="docs/demo.gif" alt="Three drafts checked in a terminal: an X post at 308 of 280 with three URLs billed at 23 each, a Bluesky post at 302 of 300, and that same draft with its link still a [URL] placeholder, which a length check reads as 277 but the server prices at exactly 300 of 300" width="800">

<sub>Recorded from <a href="docs/demo.tape">docs/demo.tape</a> with <a href="https://github.com/charmbracelet/vhs">vhs</a>. The posts and counts come from <a href="scripts/fixtures.mjs">scripts/fixtures.mjs</a>, which the regression tests import too.</sub>

</div>


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.

Tools

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.

How each platform counts

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.

Setup

Published on npm. The config blocks below use npx, which fetches it on first run; no clone required.

git clone https://github.com/conorbronsdon/postlint-mcp.git
cd postlint-mcp
npm install
npm run build

Claude Code

Add to your .mcp.json:

{
  "mcpServers": {
    "postlint": {
      "command": "node",
      "args": ["/absolute/path/to/postlint-mcp/dist/index.js"]
    }
  }
}

Claude Desktop

Same block, in claude_desktop_config.json.

Codex

Add to ~/.codex/config.toml:

[mcp_servers.postlint]
command = "npx"
args = ["-y", "@conorbronsdon/postlint-mcp"]

No token, no environment variables, no network access. Once the package is published, npx -y @conorbronsdon/postlint-mcp replaces the node invocation everywhere above.

Verify

Ask your assistant: "Check this post for X and Bluesky," and paste something with a couple of links in it.

A worked example

The X post that started this, run through check_post with platform: "x":

{
  "platform": "x",
  "limit": 280,
  "unit": "weighted characters",
  "length": 308,
  "over": true,
  "remaining": -28,
  "drivers": [
    "3 URLs counted as 23 each = 69",
    "239 other characters counted as 1 each"
  ],
  "warnings": []
}

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:

{
  "fits": ["x_premium", "linkedin", "threads", "mastodon", "discord"],
  "over": ["x", "bluesky"],
  "rows": [
    { "platform": "x", "length": 308, "limit": 280, "over": true, "drivers": ["3 URLs counted as 23 each = 69", "239 other characters counted as 1 each"] },
    { "platform": "bluesky", "length": 330, "limit": 300, "over": true, "drivers": ["3 URLs counted in full = 91 (Bluesky does not shorten links)", "239 other graphemes"] },
    { "platform": "mastodon", "length": 308, "limit": 500, "over": false, "remaining": 192 }
  ]
}

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.

Draft placeholders

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.

What it does not do

  • It does not post anything. There is no write path, no credential, and no network call of any kind. That last one is enforced rather than asserted: a test replaces fetch, XMLHttpRequest, and WebSocket with throws and drives every tool, so a call added later fails CI instead of quietly making this sentence false.
  • It does not check an instance's actual limit. Mastodon servers configure their own; this reports the 500 default and tells you to read configuration.statuses.max_characters from the target server yourself.
  • It does not truncate. A 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.
  • It does not detect every URL a platform would. Links with a scheme and 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.
  • It does not count media, polls, quote posts, or link cards. Those have their own rules and this measures text.
  • It does not know about content warnings. On Mastodon a CW counts toward the same 500. This checks the body alone.
  • It does not carry limits it cannot source. See the platform table.

Development

npm install
npm run build
npm test

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.

Contributing

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.

About

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:

  • op3-mcp: podcast analytics through OP3 — downloads, geography, apps, per-episode breakdowns.
  • podcastindex-mcp: the Podcast Index MCP server, search by person or topic, trending shows, feed health.
  • substack-mcp: read posts and manage drafts on Substack, safe for agent workflows.
  • Transistor-MCP: the Transistor.fm MCP server. Episodes, transcripts, download counts.
  • ai-tools-for-creators: a curated list of AI skills and MCP servers for people who ship ideas for a living.

More at chainofthought.show and on X.


Disclaimer

This is an independent personal project, not affiliated with, sponsored by, or endorsed by any company. All views expressed are my own.

License

Apache-2.0

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选