paragraph-cli

Use the Paragraph CLI and MCP server to manage posts, publications, subscribers, and coins on paragraph.com. Trigger when the user asks to publish, create, update, or manage newsletter content on Paragraph via CLI or MCP.

paragraph-xyz/skill36 installsMITSynced Aug 26

Works with

Claude CodeCursorCodex CLIGitHub CopilotGemini CLI
---
name: paragraph-cli
description: Use the Paragraph CLI and MCP server to manage posts, publications, subscribers, and coins on paragraph.com. Trigger when the user asks to publish, create, update, or manage newsletter content on Paragraph via CLI or MCP.
license: MIT
---

# Paragraph CLI

CLI for [Paragraph](https://paragraph.com) — a web3 publishing and newsletter platform. Use it to manage posts, publications, subscribers, and coins.

For direct HTTP or SDK access without installing anything, see the **paragraph-api** skill instead.

## MCP Server (recommended for MCP-compatible clients)

If your client supports [MCP](https://modelcontextprotocol.io), use the Paragraph MCP server instead of the CLI for a more integrated experience.

### Remote server (recommended)

Use the hosted server at `mcp.paragraph.com` — no installation or API key management required. Authenticates via your Paragraph account in the browser.

```bash
claude mcp add paragraph --transport http https://mcp.paragraph.com/mcp
```

For other clients, add `https://mcp.paragraph.com/mcp` as a server URL in your MCP configuration.

### Local server

If you prefer to run the server locally (requires Node.js 18+):

```bash
claude mcp add paragraph -- npx @paragraph-com/mcp
```

Local mode requires an API key via `PARAGRAPH_API_KEY` env var or `paragraph login` from the CLI.

The MCP server exposes 23 tools (posts, publications, subscribers, coins, search, feed, users, me, analytics, emails) and shares authentication with the CLI. See [full docs](https://paragraph.com/docs/development/mcp).

Notable tools added recently: `send-custom-email` (markdown email blast to a recipient list — requires publication approval), `update-publication` (settings, featured post, pinned posts, email-notification toggles), `remove-subscriber` (hard delete by email or wallet), `update-post` accepts `publishedAt` for backdating, and `update-post` accepts `imageUrl` (set/replace the cover image) and `clearImage` (remove the existing cover).

## CLI Setup

Install globally:

```bash
npm install -g @paragraph-com/cli
```

Authenticate — login persists the key to `~/.paragraph/config.json`:

```bash
paragraph login --token <api-key>
echo "$PARAGRAPH_API_KEY" | paragraph login --with-token
```

Or skip login and pass the key per-command via env var:

```bash
PARAGRAPH_API_KEY=<api-key> paragraph post list --json
```

Verify: `paragraph whoami --json`

## Working agreement

- **Always use `--json`** for parseable output. Data goes to stdout, status/errors to stderr.
- **Always use `--yes`** on `delete` to skip confirmation prompts.
- **Use `--dry-run`** before `delete`, `publish`, and `archive` to preview what will happen.
- **Use flags, not just positional args.** Every identifier accepts `--id` so you can chain commands.
- **Pipe content via stdin** when creating or updating posts from files: `cat draft.md | paragraph post create --title "My Post"`
- **Paginate with `--limit` and `--cursor`.** The JSON response includes `pagination.cursor` and `pagination.hasMore`.
- **Do not use interactive login.** Use `--token` or `--with-token` for non-interactive auth.
- **Check auth before running commands.** Run `paragraph whoami --json` to verify credentials are valid.
- **Do not publish without explicit user approval.** Publishing sends content live and optionally emails subscribers.
- **Default to draft.** `post create` creates drafts. Only call `post publish` when the user asks.
- **Do not send custom emails without explicit user approval.** `paragraph email send` delivers real email and can't be undone. Draft the subject and body first; use `--dry-run` to preview filtering before a real send. On a `403`, surface "this publication isn't approved for custom email yet" and stop — do not retry.
- **Respect rate limits.** If you get `RATE_LIMITED`, wait and retry. Avoid tight loops between paginated requests.

## Commands

### Posts

```bash
# Create a draft
paragraph post create --title "My Post" --file ./post.md --tags "web3,defi" --json
paragraph post create --title "My Post" --text "# Hello World" --subtitle "A subtitle" --json
cat content.md | paragraph post create --title "My Post" --json

# List
paragraph post list --json
paragraph post list --status draft --limit 20 --json
paragraph post list --status scheduled --json
paragraph post list --publication <slug-or-id> --json

# Get (by ID, URL, or @pub/slug)
paragraph post get --id <post-id> --json
paragraph post get @my-blog/post-slug --json

# Extract a single field
paragraph post get --id <post-id> --field markdown > post.md
paragraph post get --id <post-id> --field title

# Update
paragraph post update --id <id-or-slug> --title "New Title" --json
paragraph post update --id <id-or-slug> --text "Updated content" --subtitle "New subtitle" --json
paragraph post update --id <id-or-slug> --file ./updated.md --tags "new,tags" --json

# Backdate (publishedAt sticks across re-publishes — useful for imported content)
paragraph post update --id <id-or-slug> --published-at "2024-01-01T00:00:00Z" --json

# Set/replace cover image (URL is fetched + re-hosted on Paragraph's CDN)
paragraph post update --id <id-or-slug> --image-url https://example.com/cover.jpg --json

# Remove the existing cover image
paragraph post update --id <id-or-slug> --clear-image --json

# Publish
paragraph post publish --id <id-or-slug> --json
paragraph post publish --id <id-or-slug> --newsletter --json

# Revert to draft
paragraph post draft --id <id-or-slug> --json

# Archive
paragraph post archive --id <id-or-slug> --json

# Schedule a post for future publication
paragraph post schedule --id <id-or-slug> --at "2026-05-01T09:00:00Z" --json
paragraph post schedule --id <id-or-slug> --at "2026-05-01T09:00:00Z" --newsletter --json

# Cancel a scheduled publication
paragraph post unschedule --id <id-or-slug> --json

# Create and schedule in one step
paragraph post create --title "Launch Day" --file ./post.md --schedule-at "2026-05-01T09:00:00Z" --newsletter --json

# Preview destructive actions
paragraph post delete --id <id-or-slug> --dry-run --json
paragraph post publish --id <id-or-slug> --dry-run --json

# Delete
paragraph post delete --id <id-or-slug> --yes --json

# Send test newsletter email
paragraph post test-email --id <post-id> --json

# Browse
paragraph post feed --limit 10 --json
paragraph post by-tag --tag web3 --limit 20 --json
```

### Publications

```bash
paragraph publication get --id <slug-or-id-or-domain> --json

# Update settings (only provided fields change). The publication ID must
# match the publication that owns your API key — run `paragraph whoami` to
# look it up.
paragraph publication update <publication-id> --name "My Blog" --theme-color purple-600 --json

# Featured post: a post ID, or one of "latest" | "popular" | "disabled"
paragraph publication update <publication-id> --featured-post latest --json

# Pinned posts (replaces the existing list, max 50, IDs must belong to this publication)
paragraph publication update <publication-id> --pinned-post-ids id1,id2,id3 --json

# Comment visibility: true | false | on-platform
paragraph publication update <publication-id> --disable-comments on-platform --json

# Owner email-notification toggles (key=value pairs).
# Allowed keys: newComment, newSubscriber, newPaidSubscriber, newContentCollected.
paragraph publication update <publication-id> --email-notifications newSubscriber=true,newComment=false --json

# Apply a full settings body from a JSON file. Explicit flags override file values.
paragraph publication update <publication-id> --from-json ./settings.json --json
```

### Search

```bash
paragraph search post --query "ethereum" --json
paragraph search blog --query "web3" --json
```

### Subscribers

```bash
paragraph subscriber list --limit 100 --json
paragraph subscriber count --publication <id> --json
paragraph subscriber add --email user@example.com --json
paragraph subscriber add --wallet 0x1234...abcd --json
paragraph subscriber import --csv ./subscribers.csv --json

# Remove (hard delete, irreversible — prompts for confirmation; use --yes to skip)
paragraph subscriber remove --email user@example.com --yes --json
paragraph subscriber remove --wallet 0x1234...abcd --yes --json
```

### Coins

```bash
paragraph coin get --id <id-or-address> --json
paragraph coin popular --limit 10 --json
paragraph coin search --query "ethereum" --json
paragraph coin holders --id <id-or-address> --limit 50 --json
paragraph coin quote --id <id-or-address> --amount <wei> --json
```

### Users

```bash
paragraph user get --id <user-id-or-wallet> --json
```

### Analytics

Run read-only SQL against your publication's analytics schema. Scoped automatically to the publication that owns your API key — do not include blog filters in WHERE clauses.

```bash
# Discover tables and columns
paragraph analytics schema --json
paragraph analytics schema --table post_analytics_summary --json

# Run a query
paragraph analytics query --sql "SELECT title, total_views, open_rate FROM post_analytics_summary ORDER BY total_views DESC LIMIT 5" --json

# Read SQL from a file or stdin
paragraph analytics query --file ./query.sql --json
echo "SELECT active_subscriber_count FROM blog_subscriber_counts" | paragraph analytics query --json
```

Prefer the pre-aggregated views (`post_analytics_summary`, `subscriber_engagement_scores`, `blog_subscriber_counts`) over raw tables — they're sub-second and cover most reporting questions. SELECT/WITH only, no semicolons, max 10,000 rows, 30s statement timeout.

### Custom emails

Send a one-off markdown email from your publication to a specific recipient list you supply. Each recipient gets it individually with a mandatory unsubscribe footer.

**Use this for:**
- **Targeted segment sends** — "email everyone who opened my last post", "follow-up to my 50 most engaged subscribers", "reach out to this curated list of 20 readers". Build the segment via `paragraph analytics query` or `paragraph subscriber list`.
- **Self-notifications to the writer** — "email me when I hit 1,000 subscribers", "weekly analytics digest". Use `paragraph whoami` to look up the writer's email if needed.
- **Outreach to non-subscriber addresses** — a CSV of conference contacts, a press list, an intro to friends-of-friends. Recipients can come from anywhere; they don't have to be in `paragraph subscriber list`. (Anyone who previously unsubscribed will still come back as `suppressed`.)
- **Re-engagement of inactive subscribers** — "email everyone who hasn't opened in 90 days." Identify the segment via `paragraph analytics query` against `subscriber_engagement_scores` or `newsletter_metrics`.
- **Draft review to a few collaborators** — "send this draft pitch to my 3 co-authors for feedback." Use this when you need to email people other than the publication owner; `paragraph post test-email` only goes to the owner.

**Do NOT use this for newsletter blasts.** To email all subscribers with a post, use `paragraph post publish --newsletter` (or `paragraph post create ... --newsletter`). That's the newsletter pipeline; `email send` is for targeted lists you supply.

The publication must be approved by Paragraph for custom email; ineligible publications get a `403`. Up to 10,000 recipients per call. Always confirm with the user before sending — emails go out for real and can't be undone.

```bash
# Send (prompts for confirmation; pass --yes to skip)
paragraph email send --subject "Hello" --body "# Hi" --to reader@example.com --yes --json

# Read body from a file
paragraph email send --subject "Update" --body-file ./body.md --to a@x.com --to b@x.com --yes --json

# Comma-separated recipients (--to is repeatable)
paragraph email send --subject "Update" --body-file ./body.md --to "a@x.com,b@x.com" --yes --json

# Pipe body via stdin
cat body.md | paragraph email send --subject "Update" --to reader@example.com --yes --json

# Dry run — preview the accepted/skipped split without sending
paragraph email send --subject "Update" --body "# Hi" --to a@x.com --dry-run --json
```

The JSON response includes `accepted` (queued for delivery) and `skipped` — each skipped recipient has a `reason`: `invalid` (malformed address), `suppressed` (previously unsubscribed), or `scheduling_failed` (queue failure — the only reason safe to retry).

### Auth

```bash
paragraph login --token <api-key>
echo "<api-key>" | paragraph login --with-token
paragraph whoami --json
paragraph logout
```

## JSON response shapes

Paginated list (note: the CLI wraps items under `data`; the REST API and SDK use `items` instead):
```json
{
  "data": [{ "id": "...", "title": "..." }],
  "pagination": { "cursor": "abc123", "hasMore": true }
}
```

Single item:
```json
{ "id": "...", "title": "...", "markdown": "..." }
```

Mutation:
```json
{ "id": "...", "status": "published" }
```

Error (on stderr):
```json
{ "error": "Not found.", "code": "NOT_FOUND", "status": 404 }
```

Error codes: UNAUTHORIZED, FORBIDDEN, NOT_FOUND, RATE_LIMITED, SERVER_ERROR, REQUEST_FAILED, CLIENT_ERROR, UNKNOWN.

## Common patterns

### Create and publish in one flow
```bash
ID=$(paragraph post create --title "My Post" --file ./post.md --json | jq -r '.id')
paragraph post publish --id "$ID" --newsletter --json
```

### Export all posts as markdown
```bash
paragraph post list --limit 100 --json | jq -r '.data[].id' | while read id; do
  SLUG=$(paragraph post get --id "$id" --json | jq -r '.slug')
  paragraph post get --id "$id" --field markdown > "${SLUG}.md"
done
```

### Paginate through all subscribers
```bash
CURSOR=""
while true; do
  RESULT=$(paragraph subscriber list --limit 100 ${CURSOR:+--cursor "$CURSOR"} --json)
  echo "$RESULT" | jq '.data[]'
  CURSOR=$(echo "$RESULT" | jq -r '.pagination.cursor // empty')
  HAS_MORE=$(echo "$RESULT" | jq '.pagination.hasMore')
  [ "$HAS_MORE" = "true" ] || break
done
```

## Environment variables

| Variable | Purpose |
|----------|---------|
| PARAGRAPH_API_KEY | API key (skip login) |
| PARAGRAPH_API_URL | Custom API base URL |
| PARAGRAPH_NON_INTERACTIVE | Set to 1 to force CLI mode |
| CI | Set to true to force CLI mode |

## Troubleshooting

### Authentication errors

If commands fail with `UNAUTHORIZED`:

```bash
# Check if logged in
paragraph whoami --json

# Re-authenticate
paragraph login --token <api-key>
```

The CLI auto-clears stored credentials on 401. Re-login if credentials were revoked.

### Rate limiting

If you get `RATE_LIMITED`, wait and retry. The error includes a `429` status code. Avoid tight loops — add a delay between paginated requests.

### Command hangs

If a command appears to hang, it may be waiting for stdin. Ensure you're passing content via `--text`, `--file`, or piping to stdin. The CLI times out after 30 seconds if stdin is piped but no data arrives.

### CLI not found after install

```bash
npm install -g @paragraph-com/cli
# Verify
npx paragraph --version
```

More AI & ML skills

← All AI & ML skills

Check your AI visibility

One URL in, a 0–100 score and the exact fixes out.

RUN THE CHECK

Browse all the tools

15 tools across six categories
13 of them never send your data anywhere

Free · No signup · No trial clock

SEE THE DIRECTORY