Reply to your Medium
from Cursor.
Without an API bill.
medium-ops is a standalone Medium CLI anda 23-tool MCP server. Hybrid auth: public RSS for reads, undocumented dashboard GraphQL for writes, legacy Integration Token when you have one. Your IDE's LLM does the drafting through propose_reply / confirm_reply.
$ uvx medium-ops mcp install cursor
Writes ~/.cursor/mcp.json. Restart Cursor.
One package. Three Medium APIs.
Medium exposes three usable surfaces. We use all of them — public RSS by default, dashboard GraphQL when you need writes, official REST when you have a legacy token.
medium.com/feed/@you returns ~10 latest stories with body_html. Default for list_posts / get_post when limit fits.
medium.com/_/graphql with sid + xsrf. Powers stats, full archive, post_response, publishPost, deletePost.
api.medium.com/v1/* with Bearer token. Optional: Medium stopped issuing new tokens in 2023, kept for backward compat.
Drop-in for Cursor, Claude Desktop, Claude Code. Your IDE's LLM does the drafting.
Posts, responses, claps — one import.
RSS-first. No credentials needed for the common case. Drops to authenticated GraphQL automatically when you ask for more than the feed contains.
- ·Zero-auth defaults — public RSS first, GQL only when needed.
- ·Markdown-clean body extraction from messy Medium HTML.
- ·Stats + archive behind one helper, when you authenticate.
# 3 browser tabs · 14 clicks · ~5 min
1. open medium.com/me/stats
2. switch to Responses tab
3. scroll, copy/paste threads…
4. tab to Cursor, paste, ask LLM
5. copy reply back, paste in browser
6. submit, wait, repeatfrom medium_ops import MediumClient
c = MediumClient.create()
posts = c.list_posts(limit=20, source="rss") # zero-auth
for p in posts:
threads = c.list_responses(p.post_id)
for r in threads:
... # one shot, scriptable, auditedToken-gated propose → confirm.
The host LLM drafts. You see the preview. You confirm. The token expires in 5 minutes. SQLite dedup means a stuck loop replays as a no-op. The actual write goes through Medium's savePostResponse mutation with the indexed Delta payload format we reverse-engineered.
// you, in chat
"draft replies to my unanswered Medium responses"
// LLM:
"sure, can you paste them here?"
... 12 more turns of copy/paste ...
$$$ tokens spent retyping context $$$// host LLM, via MCP
1. tool: get_unanswered_responses(post_id=…)
2. tool: propose_reply(token=A, body="…")
3. you: ✓ ship it
4. tool: confirm_reply(token=A)
→ savePostResponse, audited, deduped.
→ token TTL 5 min, single-use.Replays are no-ops. Mistakes leave a trail.
Plus a Medium-specific guard: HAR re-snapshot. When the dashboard schema drifts, capture a devtools export and re-pin in seconds.
Hash of (kind, target, body) deduped at write. Re-runs return the original audit entry.
Append-only, grep-friendly. Every dry-run and every real call lands in one file.
When Medium changes the GraphQL schema, medium-ops auth har file.har ingests a devtools export and snapshots the live wire format.
Per-target spacing, configurable seconds. Gentle to Medium, gentle to your reputation.
Every write is a preview unless you flip --dry-run=false. Newcomers can't fire blanks.
Public path is the default. Auth is only required when you exceed RSS coverage or write.
Medium has three half-APIs. We stitched them.
No single Medium endpoint covers everything. Public RSS is stable but read-only and capped. The dashboard GraphQL covers stats and writes but is undocumented. The legacy Integration Token works for publishing if you have one. We wrap all three.
Stable. Public. ~10 most recent posts with body, tags, hero image. We default to it for list_posts / get_post.
Undocumented. Powers stats, full archive, all writes. We reverse-engineered createPost / publishPost / savePostResponse via probe payloads.
Official REST at api.medium.com/v1/*. Medium stopped issuing new tokens in 2023; if you have one, it still works for publish_post.
How we compare to the field.
One toolkit, two real alternatives, one row per capability. No asterisks.
| capability | medium-ops | Medium SDK | medium-unofficial |
|---|---|---|---|
| Auth surface | Hybrid (RSS / GQL / Token) | Token only | Cookie only |
| Reads | RSS-first, GQL fallback | Public posts only | GQL only |
| Write paths | Token + dashboard GQL | Token only | GQL only |
| Schema-drift recovery | auth har snapshot diff | — | — |
| MCP tools | 23 | — | — |
| License | MIT | MIT | Various |
Public package, public registry, public PRs.
Six surfaces of receipts. Click any card to verify.
Open an issue. Open a PR. Or just install it and tell me what broke.