{
  "markdown": "# medium-ops\n\n<!-- mcp-name: io.github.06ketan/medium-ops -->\n\n[![PyPI version](https://img.shields.io/pypi/v/medium-ops?color=00ab6c&label=pypi)](https://pypi.org/project/medium-ops/)\n[![PyPI downloads](https://img.shields.io/pypi/dm/medium-ops?color=00ab6c&label=downloads)](https://pypi.org/project/medium-ops/)\n[![Python 3.12+](https://img.shields.io/badge/python-3.12%2B-3776AB)](https://www.python.org/downloads/)\n[![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)\n[![MCP compatible](https://img.shields.io/badge/MCP-compatible-8A2BE2)](https://modelcontextprotocol.io)\n[![MCP Registry](https://img.shields.io/badge/MCP_Registry-verified-blue)](https://registry.modelcontextprotocol.io/v0/servers?search=io.github.06ketan/medium-ops)\n[![Anthropic DXT](https://img.shields.io/badge/Anthropic_DXT-.mcpb-D97706)](https://github.com/06ketan/medium-ops/releases/latest)\n[![Glama MCP server](https://glama.ai/mcp/servers/06ketan/medium-ops/badges/score.svg)](https://glama.ai/mcp/servers/06ketan/medium-ops)\n[![MCP Badge](https://lobehub.com/badge/mcp/06ketan-medium-ops)](https://lobehub.com/mcp/06ketan-medium-ops)\n\n> **Standalone Medium CLI + 22-tool MCP server for Cursor MCP, Claude MCP, OpenCode MCP, and any stdio MCP host. Your IDE drafts the replies. Zero AI API keys.**\n\nStories, responses, claps, feed, profiles, stats, reply engine, MCP server.\nOne Python install, one binary, MIT licensed. Sibling of\n[substack-ops](https://github.com/06ketan/substack-ops).\n\n## TL;DR — MCP-native (no API key, one command)\n\n```bash\nuvx medium-ops mcp install cursor          # or claude-desktop, claude-code, opencode, print\n# Restart your host. Then in chat:\n#   \"list unanswered responses on post abc123def456\"\n#   \"draft a warm reply to response r1\"\n#   \"post that draft\"\n```\n\nYour **host's** LLM (Cursor's, Claude's) does the drafting via the\n`propose_reply` / `confirm_reply` tools. No `ANTHROPIC_API_KEY` /\n`OPENAI_API_KEY` needed.\n\n### Wrong install?\n\nThis project is **`medium-ops` on [PyPI](https://pypi.org/project/medium-ops/)** — install with **`uv` / `uvx`**, not unrelated **`npx`** packages from “Medium MCP” searches. Canonical listing: **[Glama — 06ketan/medium-ops](https://glama.ai/mcp/servers/06ketan/medium-ops)**.\n\n## Works with (MCP)\n\nThese rows help discovery (search keywords); **confirm each host’s current MCP docs** before upgrading.\n\n### Open source–oriented hosts\n\n| Host | Documentation | Typical wire-up |\n|------|---------------|-----------------|\n| **OpenCode** | [OpenCode MCP servers](https://open-code.ai/en/docs/mcp-servers) | `uvx medium-ops mcp install opencode` |\n| **Continue** | [Continue](https://docs.continue.dev) | `uvx medium-ops mcp install print` — paste the snippet into Continue’s MCP settings |\n| **Zed** | [Zed](https://zed.dev/docs) | Configure stdio MCP per Zed’s docs |\n| **Cline** | [Cline](https://github.com/cline/cline) | MCP setup per extension / marketplace docs |\n| **Goose** | [Goose](https://block.github.io/goose/) | MCP extensions per Goose docs |\n\n### Large commercial stacks\n\n| Host | Documentation | Typical wire-up |\n|------|---------------|-----------------|\n| **Cursor** | [Cursor MCP](https://docs.cursor.com/context/model-context-protocol) | `uvx medium-ops mcp install cursor` |\n| **Claude** (Desktop / Code) | [Claude Desktop](https://support.anthropic.com/en/articles/10065433-installing-claude-for-desktop), [Claude Code](https://docs.claude.com/en/docs/claude-code) | `mcp install claude-desktop` / `claude-code` |\n| **GitHub Copilot** | [Copilot](https://docs.github.com/en/copilot) | MCP in VS Code where supported — use `print` + host docs |\n| **ChatGPT** | [OpenAI](https://platform.openai.com/docs) | Connector flows — often REST for tools without MCP |\n| **Google Gemini** | [Gemini](https://ai.google.dev/docs) | Gemini CLI / IDE features per Google docs |\n\n### OpenCode (copy-paste)\n\nAuto-install:\n\n```bash\nuvx medium-ops mcp install opencode\n```\n\nManual (`~/.config/opencode/opencode.json`):\n\n```json\n{\n  \"mcp\": {\n    \"medium-ops\": {\n      \"type\": \"local\",\n      \"command\": [\"uvx\", \"medium-ops\", \"mcp\", \"serve\"],\n      \"enabled\": true\n    }\n  }\n}\n```\n\nOptional version pin: `[\"uvx\", \"medium-ops==0.1.2\", \"mcp\", \"serve\"]` (replace with current PyPI release).\n\n## Why a hybrid\n\nMedium exposes three usable surfaces and we use all of them:\n\n1. **Public RSS (reads, no auth).** `medium.com/feed/@{user}` returns the\n   author's ~10 most recent stories with `body_html`, `pubDate`, `tags`,\n   hero image, and `dc:creator`. Zero credentials, faster than GraphQL,\n   stable. Used by default for `list_posts` / `get_post` / `get_post_content`.\n   Inspired by [Portfolio_V2's blog page](https://github.com/06ketan/Portfolio_V2/blob/main/src/utils/medium/parser.ts).\n2. **Dashboard GraphQL (authenticated reads).** `medium.com/_/graphql` +\n   `medium.com/_/api/*` with the `sid` cookie. Used as a **fallback** when\n   you ask for more than ~10 posts, when the post isn't in the RSS window,\n   or for things RSS can't give you (responses, claps, feed, stats, search).\n3. **Official REST (writes).** `api.medium.com/v1/*` with an\n   Integration Token. Supports `createPost`, `createPostInPublication`,\n   `getUser`, `getPublications`. That's it.\n\nForce a specific transport with `--source rss|graphql|auto` on `posts list`,\n`posts show`, and `posts content`. The dashboard + GraphQL endpoints are\nundocumented and Medium can change them at any time. See\n[Known gaps](#known-gaps).\n\n## Setup (dev / from source)\n\n```bash\ngit clone https://github.com/06ketan/medium-ops && cd medium-ops\nuv sync\nuv sync --extra mcp     # mcp SDK for the MCP server (recommended)\nuv sync --extra tui     # textual for the TUI\n```\n\nAuth is read from `~/.cursor/mcp.json`'s `mcpServers.medium-ops.env` (or\n`medium-api` / `medium`). Override with env or `.env`.\n\n```bash\nuv run medium-ops auth verify\nuv run medium-ops quickstart\n```\n\n## Command surface\n\nEvery write defaults to `--dry-run`. Flip with `--no-dry-run`. All writes\nland in `.cache/audit.jsonl` and are dedup-checked against\n`.cache/actions.db`.\n\n### Auth (3)\n\n| Command | What it does |\n|---|---|\n| `auth verify` | Probe both integration token (/me) and sid cookie (GraphQL Viewer). |\n| `auth test` | Same but exits non-zero on failure (CI-friendly). |\n| `auth setup` | Interactive: paste token / sid / uid / username to `.env`. |\n\n### Read — Stories (5)\n\n| Command | What it does |\n|---|---|\n| `posts list [--user] [--limit]` | Latest stories by a user (default: self). |\n| `posts show <id_or_url>` | Story metadata (title, clap count, response count). |\n| `posts content <id> [--md]` | Body HTML (or Markdown with `--md`). |\n| `posts search <query> [--limit]` | Medium-side full-text search. |\n| `posts publish -t \"...\" -f body.md [--pub] [--status draft|public|unlisted]` | Publish via integration token. |\n\n### Read + Write — Responses (3)\n\n| Command | What it does |\n|---|---|\n| `responses list <post_id> [--limit]` | Top-level responses table. |\n| `responses tree <post_id> [--out file.json]` | Full response + reply tree JSON. |\n| `responses add <post_id> \"body\" [--parent <r_id>] [--no-dry-run]` | Post a response or reply. |\n\n### Read + Write — Claps (2)\n\n| Command | What it does |\n|---|---|\n| `claps count <post_id>` | Total claps. |\n| `claps give <post_id> [--claps N] [--no-dry-run]` | Clap 1-50 times. Dedup-protected. |\n\n### Read — Discovery + Profile (5)\n\n| Command | What it does |\n|---|---|\n| `feed list [--tab home\\|following\\|tag-{slug}] [--limit]` | Reader feed. |\n| `profile me` | Your full profile (GraphQL). |\n| `profile get <username>` | Any user's public profile. |\n| `profile stats [--days N]` | Per-post views / reads / fans (dashboard scrape). |\n| `profile publications` | Publications you can publish to (integration token). |\n\n### Reply engine (3)\n\n| Command | What it does |\n|---|---|\n| `reply template <post_id> --template thanks` | Rule-based replies (no LLM). |\n| `reply bulk <post_id> --out drafts.json` | Draft every response to a file. |\n| `reply bulk-send drafts.json [--no-dry-run]` | Post only `action=approved` rows. Dedup-checked. |\n\n### Operations + safety (2)\n\n| Command | What it does |\n|---|---|\n| `audit search [--kind] [--target] [--status] [--since 7d]` | Query the JSONL audit log. |\n| `audit dedup-status` | Counts in the dedup SQLite DB. |\n\n### MCP server (3)\n\n| Command | What it does |\n|---|---|\n| `mcp install <cursor\\|claude-desktop\\|claude-code\\|opencode\\|print> [--dry-run]` | Auto-merge config into your host. |\n| `mcp serve` | stdio MCP server (22 tools). |\n| `mcp list-tools` | Print the tool registry. |\n\n### Other (1)\n\n| Command | What it does |\n|---|---|\n| `quickstart` | Print a quickstart checklist. |\n\n## Reply modes\n\n| Mode | What it does | Safety |\n|------|--------------|--------|\n| `template` | YAML keyword rules under `src/medium_ops/templates/*.yaml` | dry-run default |\n| `bulk` | LLM drafts every response to `drafts.json`. Edit, set `action: \"approved\"` | offline review, dedup-checked on send |\n| `bulk-send` | Posts only items with `action: \"approved\"` | dry-run default; dedup DB prevents dup replies |\n| MCP `propose_reply` → `confirm_reply` | Host LLM drafts, you approve per-item, token-gated | 5-min token TTL, idempotent, no API key |\n\n## MCP server\n\n```bash\nmedium-ops mcp install opencode          # ~/.config/opencode/opencode.json\nmedium-ops mcp install cursor              # auto-add to ~/.cursor/mcp.json\nmedium-ops mcp install claude-desktop      # auto-add to claude_desktop_config.json\nmedium-ops mcp install claude-code         # uses `claude mcp add`\nmedium-ops mcp install print               # print the snippet only\nmedium-ops mcp serve                       # stdio server\nmedium-ops mcp list-tools                  # 22 tools\n```\n\nManual config snippet:\n\n```json\n{\n  \"mcpServers\": {\n    \"medium-ops\": {\n      \"command\": \"medium-ops\",\n      \"args\": [\"mcp\", \"serve\"]\n    }\n  }\n}\n```\n\nIf the `mcp` SDK is not installed, the server falls back to a minimal\nstdin/stdout JSON-line dispatcher:\n\n```bash\necho '{\"tool\":\"list_posts\",\"args\":{\"limit\":3}}' | medium-ops mcp serve\n```\n\n### MCP-native draft loop (no API key)\n\nThe safety + drafting stack that makes the unattended mode safe:\n\n| Tool | What it does |\n|------|--------------|\n| `get_unanswered_responses` | Worklist — responses where you haven't replied. |\n| `propose_reply` | Dry-run only. Returns a `token` + payload preview. |\n| `confirm_reply` | Posts the staged reply by token. Idempotent via dedup DB. Token TTL 5 min. |\n| `bulk_draft_replies` / `send_approved_drafts` | File-based offline review loop. |\n| `audit_search` / `dedup_status` | Read the audit log + dedup counts. |\n\n## LLM strategy\n\nTwo layers, both free:\n\n1. **MCP-native (default).** Host LLM drafts via `propose_reply` /\n   `confirm_reply`. No env vars, no API key. Use this for interactive replies.\n2. **Subprocess CLI (daemon path).** For `reply bulk` when no human is in the\n   loop. Auto-detects `claude` (Claude Code), `cursor-agent`, or `codex` on\n   PATH. Override with `MEDIUM_OPS_LLM_CMD`.\n\nThere is no paid-API-key path.\n\n## Auth setup\n\nMedium has two auth layers that map to different feature surfaces:\n\n1. **Integration Token** — `Authorization: Bearer <token>`. Used against\n   `api.medium.com/v1/*`. Gets you: `publish_post`,\n   `list_own_publications`. Token generation at\n   https://medium.com/me/settings → \"Integration tokens\".\n   **Note: Medium stopped issuing new tokens in 2023.** If you never\n   generated one, the write path will 401 and you'll have to use the\n   sid-cookie response path for any writes.\n2. **sid cookie** — from `medium.com` (Application → Cookies → `sid`).\n   Used against `medium.com/_/graphql` and `medium.com/_/api/*`. Gets you:\n   all reads (stories, responses, claps, feed, stats, profile), plus\n   `clap_post` and `post_response` (fragile — undocumented).\n\n```bash\nmedium-ops auth verify\nmedium-ops auth test\nmedium-ops auth setup\nmedium-ops auth har ./medium.har    # ingest a Chrome devtools HAR export\n```\n\n### Refreshing auth from a HAR\n\nWhen cookies rotate or Medium changes a GraphQL schema, the fastest fix is:\n\n1. Open `medium.com` in Chrome with devtools → Network panel.\n2. Reproduce the failing action (publish a draft, post a response, etc.).\n3. Right-click any request → \"Save all as HAR with content\".\n4. `medium-ops auth har ./medium.har`\n\nThis:\n\n- merges fresh `sid`, `uid`, `xsrf`, `cf_clearance` cookies into `.env`\n  (preserving everything else)\n- writes a redacted snapshot to `.cache/har-snapshot.json` listing every\n  Medium GraphQL operation observed plus its request-variable / response-data\n  key shapes — useful for diffing against the queries hard-coded in\n  `client.py` to spot schema drift before users hit it.\n\n> **Don't have these yet?** See [docs/AUTH-SETUP.md](./docs/AUTH-SETUP.md) for a\n> 5-minute browser-DevTools walkthrough. The Medium Integration Token API has\n> been deprecated since 2023 — most users today use cookie-based auth via\n> `MEDIUM_SID`.\n\nEnv vars (or `~/.cursor/mcp.json` → `mcpServers.medium-ops.env`):\n\n```bash\nMEDIUM_INTEGRATION_TOKEN=2fb00...     # optional, for writes\nMEDIUM_SID=1:...                      # optional, for reads\nMEDIUM_UID=...                        # optional\nMEDIUM_USERNAME=yourhandle            # optional but recommended\n```\n\n## Architecture\n\n```text\nmcp.json | env                  →  auth.py\n                                      │\n                            MediumConfig (token? sid? uid? username?)\n                                      │\n                                MediumClient (httpx)\n                              ┌───────┼──────────┐\n                              ▼       ▼          ▼\n                   api.medium.com  medium.com/  medium.com/_/\n                     /v1/* (REST)    _/graphql   api/* (dashboard)\n                       │              │           │\n                   Bearer token    sid cookie    sid cookie\n                                      │\n          ┌──────┬──────┬────────────┬──────┬────────┬──────────┐\n          ▼      ▼      ▼            ▼      ▼        ▼          ▼\n        posts  responses  claps  profile  stats  feed  reply_engine\n                                                            │\n                                        ┌───────────────────┼───────────────┐\n                                        ▼                   ▼               ▼\n                                    template            ai_bulk       MCP propose/confirm\n                                        └───────────────────┬───────────────┘\n                                                            ▼\n                                                   base.post_response\n                                                            │\n                                                  ┌─────────┼─────────┐\n                                                  ▼         ▼         ▼\n                                                dedup     audit   dry_run\n                                                (SQLite)  (jsonl)\n\n mcp/server.py ──── 22 tools ─── all share MediumClient\n```\n\n## Endpoints used\n\n| Action | Method + URL |\n|--------|--------------|\n| Auth: integration token | `GET https://api.medium.com/v1/me` |\n| Auth: sid cookie | `POST https://medium.com/_/graphql` (`Viewer`) |\n| User profile | `POST /_/graphql` (`UserProfileQuery`) |\n| List stories | `POST /_/graphql` (`UserStreamOverview`) |\n| Story metadata | `POST /_/graphql` (`PostViewer`) |\n| Story body | `POST /_/graphql` (`PostContent`) |\n| Story search | `POST /_/graphql` (`SearchPosts`) |\n| Responses | `POST /_/graphql` (`PostResponses`) |\n| Feed | `POST /_/graphql` (`HomeFeed` / `FollowingFeed` / `TagFeed`) |\n| Publish story | `POST https://api.medium.com/v1/users/{id}/posts` |\n| Publish to pub | `POST https://api.medium.com/v1/publications/{pub_id}/posts` |\n| Own pubs | `GET https://api.medium.com/v1/users/{id}/publications` |\n| Clap | `POST https://medium.com/_/api/posts/{id}/clap` (undocumented) |\n| Post response | `POST https://medium.com/_/api/posts` (undocumented) |\n| Stats | `GET https://medium.com/@{username}/stats?count=...` (undocumented) |\n\n## Related MCPs\n\n- **[slideshot](https://github.com/06ketan/slideshot)** — HTML → slides (PNG / PDF / PPTX); npm [`slideshot-mcp`](https://www.npmjs.com/package/slideshot-mcp).\n- **[substack-ops](https://github.com/06ketan/substack-ops)** — Substack posts, notes, comments + MCP (PyPI `substack-ops`).\n\n## Tests\n\n```bash\nuv run pytest -q\n```\n\nCoverage: auth loading, client transports + XSSI stripping, dedup DB, audit\nlog search, MCP tool registry + dispatcher, MCP install host-config merging,\npropose/confirm flow + token expiry, reply-engine template matching +\ndedup+audit flow, subprocess LLM detection.\n\n## Known gaps\n\n- **Medium stopped issuing new Integration Tokens in 2023.** If you never\n  got one, `publish_post` / `list_own_publications` will 401. The\n  read + response + clap paths still work via sid. The RSS read path needs\n  no credentials at all.\n- **RSS is capped at ~10 posts** and lacks clap/response counts and stats.\n  When you need more, pass `--source graphql` (requires sid) or set\n  `--limit > 10` and the client will auto-fall back to GraphQL.\n- **GraphQL operation names and schemas change silently.** The queries in\n  `client.py` mirror what the dashboard uses today — expect breakage every\n  couple of months. Pin this package's version.\n- **`post_response`** uses GraphQL `savePostResponse(deltas: [Delta!]!,\n  inResponseToPostId: ID!)`. Delta shape is\n  `{type: 1, index: N, paragraph: {type: 1, text, markups: []}}` (type=1 means\n  insert; paragraph.type=1 is P). Reverse-engineered from error messages.\n- **`update_draft_content`** uses dashboard `POST /p/{id}/deltas` with\n  `{baseRev, rev, deltas}`. For a brand-new draft, `baseRev=-1, rev=0`.\n  Subsequent edits should bump both.\n- **`clap_post`** still uses the undocumented `/_/api/posts/{id}/clap` shape;\n  not yet re-validated against the new GraphQL surface. Dry-run first.\n- **Members-only stories** return a paywall preview unless the `sid` belongs\n  to a paying member.\n- **No \"restack\" equivalent.** Medium doesn't have reshares; the closest is\n  a clap + a response. Use `clap_post` + `post_response` together for that.\n- **No notes / short-form.** Medium killed short-form in 2018.\n- **Chrome cookie auto-grab** (the `auth_chrome` flow from substack-ops) is\n  not yet implemented. Paste your `sid` into `.env` for now.\n- **TUI** not yet implemented; the extras pin is there for future work.\n\n## License\n\nMIT. See [LICENSE](LICENSE).\n",
  "bytes": 18588,
  "sha": "77e731c5cf5554f18cd81cac297d54786c4a982a5389eb4136af00cf91c12a22",
  "repo_slug": "06ketan/medium-ops",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_06ketan_medium_ops_64b75b4a/readme"
}