# Medium Ops

> Medium CLI + 23-tool MCP server. Your IDE drafts replies. No API keys.

- **Type:** MCP server
- **Install:** `agentstack add mcp-06ketan-medium-ops`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [06ketan](https://agentstack.voostack.com/s/06ketan)
- **Installs:** 0
- **Category:** [Integrations](https://agentstack.voostack.com/c/integrations)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [06ketan](https://github.com/06ketan)
- **Source:** https://github.com/06ketan/medium-ops
- **Website:** https://medium-ops.chavan.in

## Install

```sh
agentstack add mcp-06ketan-medium-ops
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# medium-ops

[](https://pypi.org/project/medium-ops/)
[](https://pypi.org/project/medium-ops/)
[](https://www.python.org/downloads/)
[](LICENSE)
[](https://modelcontextprotocol.io)
[](https://registry.modelcontextprotocol.io/v0/servers?search=io.github.06ketan/medium-ops)
[](https://github.com/06ketan/medium-ops/releases/latest)
[](https://glama.ai/mcp/servers/06ketan/medium-ops)
[](https://lobehub.com/mcp/06ketan-medium-ops)

> **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.**

Stories, responses, claps, feed, profiles, stats, reply engine, MCP server.
One Python install, one binary, MIT licensed. Sibling of
[substack-ops](https://github.com/06ketan/substack-ops).

## TL;DR — MCP-native (no API key, one command)

```bash
uvx medium-ops mcp install cursor          # or claude-desktop, claude-code, opencode, print
# Restart your host. Then in chat:
#   "list unanswered responses on post abc123def456"
#   "draft a warm reply to response r1"
#   "post that draft"
```

Your **host's** LLM (Cursor's, Claude's) does the drafting via the
`propose_reply` / `confirm_reply` tools. No `ANTHROPIC_API_KEY` /
`OPENAI_API_KEY` needed.

### Wrong install?

This 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)**.

## Works with (MCP)

These rows help discovery (search keywords); **confirm each host’s current MCP docs** before upgrading.

### Open source–oriented hosts

| Host | Documentation | Typical wire-up |
|------|---------------|-----------------|
| **OpenCode** | [OpenCode MCP servers](https://open-code.ai/en/docs/mcp-servers) | `uvx medium-ops mcp install opencode` |
| **Continue** | [Continue](https://docs.continue.dev) | `uvx medium-ops mcp install print` — paste the snippet into Continue’s MCP settings |
| **Zed** | [Zed](https://zed.dev/docs) | Configure stdio MCP per Zed’s docs |
| **Cline** | [Cline](https://github.com/cline/cline) | MCP setup per extension / marketplace docs |
| **Goose** | [Goose](https://block.github.io/goose/) | MCP extensions per Goose docs |

### Large commercial stacks

| Host | Documentation | Typical wire-up |
|------|---------------|-----------------|
| **Cursor** | [Cursor MCP](https://docs.cursor.com/context/model-context-protocol) | `uvx medium-ops mcp install cursor` |
| **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` |
| **GitHub Copilot** | [Copilot](https://docs.github.com/en/copilot) | MCP in VS Code where supported — use `print` + host docs |
| **ChatGPT** | [OpenAI](https://platform.openai.com/docs) | Connector flows — often REST for tools without MCP |
| **Google Gemini** | [Gemini](https://ai.google.dev/docs) | Gemini CLI / IDE features per Google docs |

### OpenCode (copy-paste)

Auto-install:

```bash
uvx medium-ops mcp install opencode
```

Manual (`~/.config/opencode/opencode.json`):

```json
{
  "mcp": {
    "medium-ops": {
      "type": "local",
      "command": ["uvx", "medium-ops", "mcp", "serve"],
      "enabled": true
    }
  }
}
```

Optional version pin: `["uvx", "medium-ops==0.1.2", "mcp", "serve"]` (replace with current PyPI release).

## Why a hybrid

Medium exposes three usable surfaces and we use all of them:

1. **Public RSS (reads, no auth).** `medium.com/feed/@{user}` returns the
   author's ~10 most recent stories with `body_html`, `pubDate`, `tags`,
   hero image, and `dc:creator`. Zero credentials, faster than GraphQL,
   stable. Used by default for `list_posts` / `get_post` / `get_post_content`.
   Inspired by [Portfolio_V2's blog page](https://github.com/06ketan/Portfolio_V2/blob/main/src/utils/medium/parser.ts).
2. **Dashboard GraphQL (authenticated reads).** `medium.com/_/graphql` +
   `medium.com/_/api/*` with the `sid` cookie. Used as a **fallback** when
   you ask for more than ~10 posts, when the post isn't in the RSS window,
   or for things RSS can't give you (responses, claps, feed, stats, search).
3. **Official REST (writes).** `api.medium.com/v1/*` with an
   Integration Token. Supports `createPost`, `createPostInPublication`,
   `getUser`, `getPublications`. That's it.

Force a specific transport with `--source rss|graphql|auto` on `posts list`,
`posts show`, and `posts content`. The dashboard + GraphQL endpoints are
undocumented and Medium can change them at any time. See
[Known gaps](#known-gaps).

## Setup (dev / from source)

```bash
git clone https://github.com/06ketan/medium-ops && cd medium-ops
uv sync
uv sync --extra mcp     # mcp SDK for the MCP server (recommended)
uv sync --extra tui     # textual for the TUI
```

Auth is read from `~/.cursor/mcp.json`'s `mcpServers.medium-ops.env` (or
`medium-api` / `medium`). Override with env or `.env`.

```bash
uv run medium-ops auth verify
uv run medium-ops quickstart
```

## Command surface

Every write defaults to `--dry-run`. Flip with `--no-dry-run`. All writes
land in `.cache/audit.jsonl` and are dedup-checked against
`.cache/actions.db`.

### Auth (3)

| Command | What it does |
|---|---|
| `auth verify` | Probe both integration token (/me) and sid cookie (GraphQL Viewer). |
| `auth test` | Same but exits non-zero on failure (CI-friendly). |
| `auth setup` | Interactive: paste token / sid / uid / username to `.env`. |

### Read — Stories (5)

| Command | What it does |
|---|---|
| `posts list [--user] [--limit]` | Latest stories by a user (default: self). |
| `posts show ` | Story metadata (title, clap count, response count). |
| `posts content  [--md]` | Body HTML (or Markdown with `--md`). |
| `posts search  [--limit]` | Medium-side full-text search. |
| `posts publish -t "..." -f body.md [--pub] [--status draft|public|unlisted]` | Publish via integration token. |

### Read + Write — Responses (3)

| Command | What it does |
|---|---|
| `responses list  [--limit]` | Top-level responses table. |
| `responses tree  [--out file.json]` | Full response + reply tree JSON. |
| `responses add  "body" [--parent ] [--no-dry-run]` | Post a response or reply. |

### Read + Write — Claps (2)

| Command | What it does |
|---|---|
| `claps count ` | Total claps. |
| `claps give  [--claps N] [--no-dry-run]` | Clap 1-50 times. Dedup-protected. |

### Read — Discovery + Profile (5)

| Command | What it does |
|---|---|
| `feed list [--tab home\|following\|tag-{slug}] [--limit]` | Reader feed. |
| `profile me` | Your full profile (GraphQL). |
| `profile get ` | Any user's public profile. |
| `profile stats [--days N]` | Per-post views / reads / fans (dashboard scrape). |
| `profile publications` | Publications you can publish to (integration token). |

### Reply engine (3)

| Command | What it does |
|---|---|
| `reply template  --template thanks` | Rule-based replies (no LLM). |
| `reply bulk  --out drafts.json` | Draft every response to a file. |
| `reply bulk-send drafts.json [--no-dry-run]` | Post only `action=approved` rows. Dedup-checked. |

### Operations + safety (2)

| Command | What it does |
|---|---|
| `audit search [--kind] [--target] [--status] [--since 7d]` | Query the JSONL audit log. |
| `audit dedup-status` | Counts in the dedup SQLite DB. |

### MCP server (3)

| Command | What it does |
|---|---|
| `mcp install  [--dry-run]` | Auto-merge config into your host. |
| `mcp serve` | stdio MCP server (22 tools). |
| `mcp list-tools` | Print the tool registry. |

### Other (1)

| Command | What it does |
|---|---|
| `quickstart` | Print a quickstart checklist. |

## Reply modes

| Mode | What it does | Safety |
|------|--------------|--------|
| `template` | YAML keyword rules under `src/medium_ops/templates/*.yaml` | dry-run default |
| `bulk` | LLM drafts every response to `drafts.json`. Edit, set `action: "approved"` | offline review, dedup-checked on send |
| `bulk-send` | Posts only items with `action: "approved"` | dry-run default; dedup DB prevents dup replies |
| MCP `propose_reply` → `confirm_reply` | Host LLM drafts, you approve per-item, token-gated | 5-min token TTL, idempotent, no API key |

## MCP server

```bash
medium-ops mcp install opencode          # ~/.config/opencode/opencode.json
medium-ops mcp install cursor              # auto-add to ~/.cursor/mcp.json
medium-ops mcp install claude-desktop      # auto-add to claude_desktop_config.json
medium-ops mcp install claude-code         # uses `claude mcp add`
medium-ops mcp install print               # print the snippet only
medium-ops mcp serve                       # stdio server
medium-ops mcp list-tools                  # 22 tools
```

Manual config snippet:

```json
{
  "mcpServers": {
    "medium-ops": {
      "command": "medium-ops",
      "args": ["mcp", "serve"]
    }
  }
}
```

If the `mcp` SDK is not installed, the server falls back to a minimal
stdin/stdout JSON-line dispatcher:

```bash
echo '{"tool":"list_posts","args":{"limit":3}}' | medium-ops mcp serve
```

### MCP-native draft loop (no API key)

The safety + drafting stack that makes the unattended mode safe:

| Tool | What it does |
|------|--------------|
| `get_unanswered_responses` | Worklist — responses where you haven't replied. |
| `propose_reply` | Dry-run only. Returns a `token` + payload preview. |
| `confirm_reply` | Posts the staged reply by token. Idempotent via dedup DB. Token TTL 5 min. |
| `bulk_draft_replies` / `send_approved_drafts` | File-based offline review loop. |
| `audit_search` / `dedup_status` | Read the audit log + dedup counts. |

## LLM strategy

Two layers, both free:

1. **MCP-native (default).** Host LLM drafts via `propose_reply` /
   `confirm_reply`. No env vars, no API key. Use this for interactive replies.
2. **Subprocess CLI (daemon path).** For `reply bulk` when no human is in the
   loop. Auto-detects `claude` (Claude Code), `cursor-agent`, or `codex` on
   PATH. Override with `MEDIUM_OPS_LLM_CMD`.

There is no paid-API-key path.

## Auth setup

Medium has two auth layers that map to different feature surfaces:

1. **Integration Token** — `Authorization: Bearer `. Used against
   `api.medium.com/v1/*`. Gets you: `publish_post`,
   `list_own_publications`. Token generation at
   https://medium.com/me/settings → "Integration tokens".
   **Note: Medium stopped issuing new tokens in 2023.** If you never
   generated one, the write path will 401 and you'll have to use the
   sid-cookie response path for any writes.
2. **sid cookie** — from `medium.com` (Application → Cookies → `sid`).
   Used against `medium.com/_/graphql` and `medium.com/_/api/*`. Gets you:
   all reads (stories, responses, claps, feed, stats, profile), plus
   `clap_post` and `post_response` (fragile — undocumented).

```bash
medium-ops auth verify
medium-ops auth test
medium-ops auth setup
medium-ops auth har ./medium.har    # ingest a Chrome devtools HAR export
```

### Refreshing auth from a HAR

When cookies rotate or Medium changes a GraphQL schema, the fastest fix is:

1. Open `medium.com` in Chrome with devtools → Network panel.
2. Reproduce the failing action (publish a draft, post a response, etc.).
3. Right-click any request → "Save all as HAR with content".
4. `medium-ops auth har ./medium.har`

This:

- merges fresh `sid`, `uid`, `xsrf`, `cf_clearance` cookies into `.env`
  (preserving everything else)
- writes a redacted snapshot to `.cache/har-snapshot.json` listing every
  Medium GraphQL operation observed plus its request-variable / response-data
  key shapes — useful for diffing against the queries hard-coded in
  `client.py` to spot schema drift before users hit it.

> **Don't have these yet?** See [docs/AUTH-SETUP.md](./docs/AUTH-SETUP.md) for a
> 5-minute browser-DevTools walkthrough. The Medium Integration Token API has
> been deprecated since 2023 — most users today use cookie-based auth via
> `MEDIUM_SID`.

Env vars (or `~/.cursor/mcp.json` → `mcpServers.medium-ops.env`):

```bash
MEDIUM_INTEGRATION_TOKEN=2fb00...     # optional, for writes
MEDIUM_SID=1:...                      # optional, for reads
MEDIUM_UID=...                        # optional
MEDIUM_USERNAME=yourhandle            # optional but recommended
```

## Architecture

```text
mcp.json | env                  →  auth.py
                                      │
                            MediumConfig (token? sid? uid? username?)
                                      │
                                MediumClient (httpx)
                              ┌───────┼──────────┐
                              ▼       ▼          ▼
                   api.medium.com  medium.com/  medium.com/_/
                     /v1/* (REST)    _/graphql   api/* (dashboard)
                       │              │           │
                   Bearer token    sid cookie    sid cookie
                                      │
          ┌──────┬──────┬────────────┬──────┬────────┬──────────┐
          ▼      ▼      ▼            ▼      ▼        ▼          ▼
        posts  responses  claps  profile  stats  feed  reply_engine
                                                            │
                                        ┌───────────────────┼───────────────┐
                                        ▼                   ▼               ▼
                                    template            ai_bulk       MCP propose/confirm
                                        └───────────────────┬───────────────┘
                                                            ▼
                                                   base.post_response
                                                            │
                                                  ┌─────────┼─────────┐
                                                  ▼         ▼         ▼
                                                dedup     audit   dry_run
                                                (SQLite)  (jsonl)

 mcp/server.py ──── 22 tools ─── all share MediumClient
```

## Endpoints used

| Action | Method + URL |
|--------|--------------|
| Auth: integration token | `GET https://api.medium.com/v1/me` |
| Auth: sid cookie | `POST https://medium.com/_/graphql` (`Viewer`) |
| User profile | `POST /_/graphql` (`UserProfileQuery`) |
| List stories | `POST /_/graphql` (`UserStreamOverview`) |
| Story metadata | `POST /_/graphql` (`PostViewer`) |
| Story body | `POST /_/graphql` (`PostContent`) |
| Story search | `POST /_/graphql` (`SearchPosts`) |
| Responses | `POST /_/graphql` (`PostResponses`) |
| Feed | `POST /_/graphql` (`HomeFeed` / `FollowingFeed` / `TagFeed`) |
| Publish story | `POST https://api.medium.com/v1/users/{id}/posts` |
| Publish to pub | `POST https://api.medium.com/v1/publications/{pub_id}/posts` |
| Own pubs | `GET https://api.medium.com/v1/users/{id}/publications` |
| Clap | `POST https://medium.com/_/api/posts/{id}/clap` (undocumented) |
| Post response | `POST https://medium.com/_/api/posts` (undocumented) |
| Stats | `GET https://medium.com/@{username}/stats?count=...` (undocumented) |

## Related MCPs

- **[slideshot](https://github.com/06ketan/slideshot)** — HTML → slides (PNG / PDF / PPTX); npm [`slideshot-mcp`](https://www.npmjs.com/package/slideshot-mcp).
- **[substack-ops](https://github.com/06ketan/substack-ops)** — Substack posts, notes, comments + MCP (PyPI `substack-ops`).

## Tests

```bash
uv run pytest -q
```

Coverage: auth loading, client transports + XSSI stripping, dedup DB, audit
log search, MCP tool registry + dispatcher, MCP install host-config merging,
propose/confirm flow + token expiry, reply-engine template matching +
dedup+audit flow, subprocess LLM detection.

## Known gaps

- **Medium stopped issuing new Integration Tokens in 2023.** If you never
  got one, `publi

…

## Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [06ketan](https://github.com/06ketan)
- **Source:** [06ketan/medium-ops](https://github.com/06ketan/medium-ops)
- **License:** MIT
- **Homepage:** https://medium-ops.chavan.in

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/mcp-06ketan-medium-ops
- Seller: https://agentstack.voostack.com/s/06ketan
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
