# Onlypult MCP Server

Connect AI assistants to Onlypult using the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/). Works with Claude, Cursor, ChatGPT, Windsurf, Codex, Antigravity, and any MCP-compatible client.

**MCP Server URL:** `https://mcp.onlypult.com/mcp`  
**Transport:** Streamable HTTP (hosted at `mcp.onlypult.com` — no local install required)  
**Local package (file uploads):** `npx -y @onlypult/mcp@latest` — stdio MCP on your machine for `media_upload_file`  
**Human-readable docs:** https://onlypult.com/dev/mcp.html
**REST API reference:** https://onlypult.com/dev/api.html
**OpenAPI spec:** https://onlypult.com/dev/openapi.yaml

---

## Use Onlypult in your AI client

Pick **hosted HTTP** (`mcp.onlypult.com`) for zero install, or **local stdio** (`@onlypult/mcp`) when you need `media_upload_file` from disk paths. Same JSON works in Cursor, Claude Desktop, Windsurf, Codex, Antigravity, and other clients — only the config file path differs.

### Cursor — two options

| Setup | Config | Local file upload |
|-------|--------|-------------------|
| **`@onlypult/mcp` (recommended for disk paths)** | stdio via `npx` | `media_upload_file` |
| **Hosted HTTP** | `https://mcp.onlypult.com/mcp` | Browser flow only |

**Cursor — local package (file uploads)** — add to `.cursor/mcp.json`:

```json
{
  "mcpServers": {
    "onlypult": {
      "command": "npx",
      "args": ["-y", "@onlypult/mcp@latest"],
      "env": {
        "ONLYPULT_API_KEY": "op_your_api_key_here"
      }
    }
  }
}
```

Requires Node.js 18+. No manual install — Cursor runs the package on first use.

**Cursor — hosted HTTP (no install)** — add to `.cursor/mcp.json`:

```json
{
  "mcpServers": {
    "onlypult": {
      "type": "http",
      "url": "https://mcp.onlypult.com/mcp",
      "headers": {
        "Authorization": "Bearer op_your_api_key_here"
      }
    }
  }
}
```

**Claude Desktop** — `claude_desktop_config.json` is **stdio-only** (`command` + `args`). Two options:

**Local stdio (file uploads)** — macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`, Windows: `%APPDATA%\Claude\claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "onlypult": {
      "command": "npx",
      "args": ["-y", "@onlypult/mcp@latest"],
      "env": {
        "ONLYPULT_API_KEY": "op_your_api_key_here"
      }
    }
  }
}
```

**Remote (no install)** — Claude Desktop → **Settings → Connectors → Add custom connector** → Name `Onlypult`, URL `https://mcp.onlypult.com/mcp` → Connect → OAuth. Do **not** use `type: http` in `claude_desktop_config.json` (not supported). Same steps as Claude.ai — guide: `/dev/mcp-claude.html`

### Other MCP clients (Windsurf, Codex, Antigravity, …)

| Goal | Config |
|------|--------|
| Posts, scheduling, browser media | Hosted `https://mcp.onlypult.com/mcp` + API key or OAuth |
| `media_upload_file` from disk | Local stdio — same `npx -y @onlypult/mcp@latest` block as Cursor |

Put the JSON in your app's MCP settings (e.g. `.windsurf/mcp.json`, `.cursor/mcp.json`, Codex/Antigravity integrations UI). See hosted vs local examples under **Cursor** above.

**Claude.ai** — Settings → Connectors → Add → Add custom connector → Name `Onlypult`, URL `https://mcp.onlypult.com/mcp` → Connect → OAuth. Free plan: one custom connector. Full guide: `/dev/mcp-claude.html`

**ChatGPT (Plus/Pro)** — Settings → Connectors → Add custom connector → URL: `https://mcp.onlypult.com/mcp`. Sign in via OAuth.

**ChatGPT (Developer mode, free)** — Settings → App Preferences → Apps → enable Developer mode → Create app with Server URL `https://mcp.onlypult.com/mcp`, OAuth auth → Connect → Sign in with Onlypult. In chat, mention `@Onlypult` before each request. Full guide: `/dev/mcp-chatgpt.html`

Restart your client after saving configuration.

**Troubleshooting — Cursor shows connected but 0 tools:** Reload MCP in Cursor Settings and open a new chat. Hosted `mcp.onlypult.com` must run a build with `enableJsonResponse: true` (Onlypult team deploy).

---

## What you can do

Ask your AI assistant things like:

- "Show my connected social profiles"
- "Post 'Hello world!' to Instagram now"
- "Schedule a LinkedIn post for tomorrow at 9am"
- "Cross-post this to Instagram and Facebook"
- "Post this image to Instagram" (browser upload flow)
- "List my failed posts and retry them"
- "Create an Instagram Reels post with this video"

---

## Setup

1. **Get your API key** — create at https://app.onlypult.com/settings → API Keys. Key format: `op_` + 64 hex chars. Shown only once. Full guide: https://onlypult.com/dev/index.html#authentication
2. **Configure your client** — hosted HTTP, local stdio (`@onlypult/mcp`), or OAuth (Claude.ai, ChatGPT, Claude Desktop Connectors).
3. **Restart your client** — close and reopen completely after config changes.

---

## Authentication

| Method | When to use | How |
|--------|-------------|-----|
| API Key | Hosted HTTP or local stdio (`@onlypult/mcp`) | `Authorization: Bearer op_...` or `ONLYPULT_API_KEY` in stdio `env` |
| OAuth 2.1 | Claude.ai, ChatGPT, Claude Desktop (Connectors) | Authorization Code + PKCE — user signs in via browser |

### OAuth 2.1 flow (summary)

1. AI client discovers `GET /.well-known/oauth-authorization-server`
2. `GET /authorize` → redirect to `app.onlypult.com/mcp-oauth/authorize`
3. User logs in and clicks Allow access
4. `POST /token` with code + PKCE verifier (or `grant_type=refresh_token` later)
5. MCP server exchanges code for `omt_...` access + `opr_...` refresh via internal Onlypult API (`api_oauth_tokens`)
6. Subsequent requests use `Authorization: Bearer omt_...` (access expires in 30 days; refresh via `/token`)

Pre-registered clients: `claude`, `chatgpt`. Others: `POST /register` (RFC 7591).

---

## Response format

MCP tools call the Onlypult REST API (`https://api.onlypult.com/v1`). Responses use a standard envelope.

**Success** — wrapped in `data`:

```json
{ "data": { ... } }
```

**Paginated lists** — include `meta`:

```json
{
  "data": [...],
  "meta": {
    "total": 100,
    "page": 1,
    "limit": 20,
    "pages": 5
  }
}
```

**Errors** — HTTP errors (404, 401):

```json
{
  "name": "Not Found",
  "message": "Profile not found",
  "code": 0,
  "status": 404
}
```

**Validation errors (422)** — MCP tools return a readable message with `code` and `retryable`:

```json
{
  "error": {
    "code": "publish_date_after_plan",
    "message": "Cannot schedule posts after your plan end date (2026-07-05). Choose an earlier date or renew your subscription.",
    "retryable": false,
    "field": "publish_at"
  }
}
```

When `retryable` is `false`, do not retry the same request — explain the issue to the user (e.g. renew plan, pick an earlier date).

**Scheduling time** — use `publish_at` in format `"Y-m-d H:i"` in your account timezone (e.g. `"2026-07-07 14:00"`). Call `account_get` first for `timezone` and `plan_end_at`.

Full quickstart with curl examples: https://onlypult.com/dev/index.html#response-format

---

## Available tools

Call `account_get` before scheduling posts. Call `profiles_list` to get profile IDs before creating posts.

### Account

| Tool | Description |
|------|-------------|
| `account_get` | Get timezone, plan end date, and publish_at format |

### Profiles

| Tool | Description |
|------|-------------|
| `profiles_list` | List all connected social media profiles |
| `profiles_get` | Get a specific profile by ID |
| `profiles_platforms_list` | List supported platforms |
| `profiles_get_pinterest_boards` | List Pinterest boards for a profile (use `id` in `platform_options.pinterest.board_id`) |
| `profiles_get_youtube_categories` | List YouTube categories for a profile (use `id` in `platform_options.youtube.category`) |

### Posts

| Tool | Description |
|------|-------------|
| `posts_create` | Create post — draft, scheduled, or publish immediately |
| `posts_create_story` | Create Story (Instagram, Facebook, VK) |
| `posts_create_reels` | Create Instagram/Facebook Reels |
| `posts_create_shorts` | Create YouTube Shorts |
| `posts_publish_now` | Publish immediately (shortcut) |
| `posts_cross_post` | Post same content to 2+ profiles |
| `posts_list` | List posts with filters |
| `posts_list_failed` | List failed posts |
| `posts_get` | Get single post |
| `posts_update` | Update draft or scheduled post |
| `posts_delete` | Delete draft or scheduled post |
| `posts_retry` | Retry failed post |
| `posts_retry_all_failed` | Retry all failed posts |

### Media

| Tool | Description |
|------|-------------|
| `media_get_upload_url` | Browser upload URL (remote MCP, manual upload) |
| `media_upload_file` | Direct upload from local file path (`@onlypult/mcp` stdio only — not hosted `mcp.onlypult.com`) |
| `media_check_status` | Poll browser upload session, get file IDs |

#### Choosing a media upload method

| Situation | Tool / approach |
|-----------|-----------------|
| **Local stdio** (`@onlypult/mcp`), file path on same machine (e.g. `./assets/photo.jpg`) | `media_get_upload_url` → token → `media_upload_file` → `posts_create` |
| **`mcp.onlypult.com`**, Claude.ai, ChatGPT — file on user's computer | `media_get_upload_url` → user uploads in browser → `media_check_status` |
| File already at public **HTTP(S) URL** | `media_urls` in `posts_create` / `posts_update` (no upload tools) |
| Multiple local files in one post | `media_upload_file` per file, or one browser session via `media_get_upload_url` |

**Agents:** with `@onlypult/mcp`, prefer `media_get_upload_url` + `media_upload_file` when `file_path` is known. On hosted MCP, use browser flow only.

---

## Tool reference

### Account

#### `account_get`

No parameters. Returns scheduling context:

| Field | Description |
|-------|-------------|
| `timezone` | Account timezone — `publish_at` is interpreted in this timezone |
| `plan_active` | Whether the current plan is active |
| `plan_end_date` | Last date posts can be scheduled (`Y-m-d`) |
| `plan_end_at` | Last datetime posts can be scheduled (`Y-m-d H:i`) |
| `publish_at_format` | Required format for `publish_at` — always `"Y-m-d H:i"` |

Call this before scheduling to avoid plan limit errors.

---

### Profiles

#### `profiles_list`

No parameters. Returns `id`, `name`, `platform`, `username`, `status` for each profile.

#### `profiles_get`

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `profile_id` | string | Yes | Profile ID from profiles_list |

#### `profiles_platforms_list`

No parameters. Returns supported platforms.

#### `profiles_get_pinterest_boards`

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `profile_id` | string | Yes | Pinterest profile ID |

#### `profiles_get_youtube_categories`

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `profile_id` | string | Yes | YouTube profile ID |

---

### Posts

When multiple profiles exist on the same platform, pass explicit `profile_ids`. Call `profiles_list` first.

#### `posts_create`

Create a social media post.

**Choose the correct mode based on user intent:**

- **DRAFT MODE** (`is_draft=true`): user says "draft", "save for later", "don't publish"
- **IMMEDIATE MODE** (`publish_now=true`): user says "publish now", "post now", "immediately"
- **SCHEDULED MODE** (default): user says "schedule", "tomorrow at 2pm" — call `account_get` first, then provide `publish_at` as `"Y-m-d H:i"` in account timezone

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `profile_ids` | string[] | Yes | — | Profile IDs to post to |
| `content` | string | No* | — | Post text. Required unless media_ids or media_urls provided |
| `is_draft` | boolean | No | false | Save as draft |
| `publish_now` | boolean | No | false | Publish immediately |
| `publish_at` | string | No | — | Schedule time in account timezone, format `"Y-m-d H:i"` (e.g. `"2026-07-07 14:00"`). Required if not draft and not publish_now |
| `title` | string | No | — | Required for YouTube, recommended for Pinterest |
| `media_ids` | string[] | No | — | From media_check_status |
| `media_urls` | string[] | No | — | Public URLs (downloaded server-side, up to 10) |
| `platform_options` | object | No | — | Per-platform settings (see below) |

#### `posts_create_story` / `posts_create_reels` / `posts_create_shorts`

Same parameters as `posts_create`. Automatically sets `is_story`, `is_reels`, or `is_shorts` for supported platforms.

#### `posts_publish_now`

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `profile_ids` | string[] | Yes | Profile IDs |
| `content` | string | No* | Post text |
| `title` | string | No | Post title |
| `media_ids` | string[] | No | Media file IDs |
| `media_urls` | string[] | No | Public media URLs |
| `platform_options` | object | No | Per-platform settings |

#### `posts_cross_post`

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `profile_ids` | string[] | Yes | — | At least 2 profile IDs |
| `content` | string | No* | — | Post text |
| `publish_now` | boolean | No | false | Publish immediately |
| `publish_at` | string | No | — | Schedule time in account timezone, format `"Y-m-d H:i"` |
| `media_ids` | string[] | No | — | Media file IDs |
| `media_urls` | string[] | No | — | Public media URLs |
| `platform_options` | object | No | — | Per-platform settings |

#### `posts_list`

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `status` | string | No | — | draft, scheduled, published, failed |
| `profile_id` | string | No | — | Filter by profile |
| `limit` | integer | No | 20 | Max 100 |
| `page` | integer | No | 1 | Page number |

#### `posts_list_failed`

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `profile_id` | string | No | — | Filter by profile |
| `limit` | integer | No | 20 | Max 100 |

#### `posts_get` / `posts_delete` / `posts_retry`

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `post_id` | string | Yes | Post ID |

#### `posts_update`

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `post_id` | string | Yes | Post ID to update |
| `content` | string | No | New text |
| `publish_at` | string | No | New schedule time in account timezone, format `"Y-m-d H:i"` |
| `title` | string | No | New title |
| `media_ids` | string[] | No | Replaces existing media |
| `media_urls` | string[] | No | Merged with media_ids |
| `platform_options` | object | No | Per-platform settings |

#### `posts_retry_all_failed`

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `profile_id` | string | No | Only retry for this profile |

---

### platform_options

Per-platform settings for post create/update. Keys: `instagram`, `facebook`, `youtube`, `tiktok`, `telegram`, `linkedin`, `pinterest`, `twitter`, `threads`, `bluesky`, `wordpress`, `google_my_business`, `vkontakte`.

```json
{
  "instagram": { "is_reels": true },
  "youtube": { "is_shorts": true, "privacy": "public", "category": "22" }
}
```

| Field | Type | Description |
|-------|------|-------------|
| `is_story` | boolean | Instagram/Facebook/VK Story (exclusive with is_reels, is_shorts) |
| `is_reels` | boolean | Instagram/Facebook Reels |
| `is_shorts` | boolean | YouTube Shorts |
| `title` | string | Platform-specific title |
| `privacy` | string | YouTube: public\|unlisted\|private. TikTok: PUBLIC_TO_EVERYONE\|SELF_ONLY\|… |
| `category` | string | YouTube category ID — use profiles_get_youtube_categories |
| `disable_comment` | boolean | TikTok |
| `disable_duet` | boolean | TikTok |
| `disable_stitch` | boolean | TikTok |
| `disable_notification` | boolean | Telegram: send silently |
| `pin_message` | boolean | Telegram: pin after publish |
| `visibility` | string | LinkedIn: PUBLIC or CONNECTIONS |
| `board_id` | string | Pinterest — use profiles_get_pinterest_boards |
| `link` | string | Pinterest destination link |
| `alt_text` | string | Pinterest image alt text |

---

### Media

#### `media_get_upload_url`

**When to use:** hosted MCP or browser upload. Requires **profile_id** from `profiles_list` (same profile as target post — drives transform/watermark).

**When NOT to use:** local stdio with readable `file_path` → `media_upload_file`. Public URL → `media_urls`.

Flow:

1. `profiles_list` → `profile_id`
2. Call this tool with `profile_id` → `upload_url` + `token`
2. Ask user to open URL in browser
3. User uploads via drag & drop page (multiple files OK)
4. Call `media_check_status` with token
5. Use file IDs in `media_ids` for posts_create / posts_update / story / reels tools

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `profile_id` | string | Yes | — | Profile ID from profiles_list |
| `is_story` | boolean | No | false | Upload as Instagram/Facebook story |
| `is_reels` | boolean | No | false | Upload as Instagram Reels |

Match `is_story` / `is_reels` with post tools (`posts_create_story`, `posts_create_reels`) and `platform_options`.

Upload page: `https://app.onlypult.com/mcp-upload?token=...` — no login, 10 min TTL.

#### `media_upload_file`

**When to use:** local stdio via `@onlypult/mcp` — any client that runs MCP on your machine (Cursor, Claude Desktop, Windsurf, Codex, Antigravity, …).

**When NOT to use:** hosted `mcp.onlypult.com`, Claude.ai, ChatGPT — server cannot access your filesystem. Chat attachment without a disk path. Public URL → `media_urls`.

**Reliable flow:** `media_get_upload_url` → token → `media_upload_file` with `file_path` + token → `posts_create` / `posts_update` with `media_ids`.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `file_path` | string | Yes | Local path readable by MCP server |
| `profile_id` | string | No* | Profile ID from profiles_list (required when token omitted) |
| `token` | string | No | Upload session token from `media_get_upload_url` (strongly recommended; auto-created if omitted) |
| `is_story` | boolean | No | Upload as story |
| `is_reels` | boolean | No | Upload as Reels |

Returns file `id` immediately for `media_ids`. Uses multipart upload internally for files &gt; 5MB.

#### `media_check_status`

**When to use:** after `media_get_upload_url` browser flow when user says "done". Not needed after `media_upload_file`.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `token` | string | Yes | Token from media_get_upload_url |

Returns `status`: pending, done, or failed; and `files[]` with `id`, `url`, `type`, `filename`.

**Supported:** JPG, PNG, WebP, GIF, MP4, MOV, WebM. **Max file size:** depends on your Onlypult plan.

---

## Uploading images and videos

### Which method to use

| Situation | What to use |
|-----------|-------------|
| Local stdio (`@onlypult/mcp`), file on disk | `media_get_upload_url` → `media_upload_file` with `file_path` |
| `mcp.onlypult.com`, Claude.ai, ChatGPT | `media_get_upload_url` → browser → `media_check_status` |
| Image/video already at public URL | `media_urls` in `posts_create` |

### Browser flow (remote MCP)

#### Step 1: Ask to create an upload link

User: "I want to post an image to Instagram"

Assistant calls `media_get_upload_url`, returns browser link.

### Step 2: Upload your file

User opens link, chooses or drags files. Page shows uploaded file IDs. Link expires in 10 minutes.

### Step 3: Confirm upload

User says "done". Assistant calls `media_check_status`, then `posts_create` with `media_ids`.

### Example: local file (stdio)

> **You:** Post `./assets/product.jpg` to Instagram with caption "New collection"
> **Assistant:** media_upload_file(file_path="./assets/product.jpg") → posts_create with media_ids

### Example with image (browser)

> **You:** I want to post a product photo to Instagram with caption "New collection is live!"
> **Assistant:** Upload link generated! Open https://app.onlypult.com/mcp-upload?token=… Say "done" when finished.
> **You:** done
> **Assistant:** Upload completed — 1 file ready. Post published to Instagram.

### Example: Reels with video

> **You:** Post this vertical video as an Instagram Reel: "Behind the scenes"
> **Assistant:** media_get_upload_url (is_reels=true) → upload → media_check_status → posts_create_reels

---

## Example conversations

**Simple post**

> User: Post "Just launched our new feature!" to Instagram now
> Assistant: profiles_list → posts_publish_now with profile_ids

**Scheduled post**

> User: Schedule a LinkedIn post for tomorrow at 2pm
> Assistant: account_get (check plan_end_at and timezone) → posts_create with publish_at `"2026-07-07 14:00"`

**Cross-posting**

> User: Post "Big news coming soon…" to Instagram and Facebook
> Assistant: posts_cross_post with two profile_ids

**With image (local path)**

> User: Post ./marketing/hero.png to Instagram with caption "Launch day"
> Assistant: media_upload_file → posts_create with media_ids

**With image (browser / remote MCP)**

> User: I want to post a product photo to Instagram
> Assistant: media_get_upload_url → user uploads → media_check_status → posts_create with media_ids

---

## Troubleshooting

**Invalid API key**

1. Check key at https://app.onlypult.com/settings → API Keys
2. No extra spaces when copying
3. Key must start with `op_`

**No profiles connected**

Connect social accounts in Onlypult dashboard before posting.

**Changes not taking effect**

Restart the AI client completely after editing MCP config.

**OAuth failed on Claude.ai**

Remove the connector and add it again — stale failed auth may be cached.

**Scheduling failed with plan error**

If the tool returns `code: plan_expired` or `publish_date_after_plan` with `retryable: false`, do not retry. Call `account_get` to see `plan_end_at`, then either choose an earlier `publish_at` or ask the user to renew their Onlypult plan.

---

## Links

- [Onlypult Dashboard](https://app.onlypult.com)
- [API Keys](https://app.onlypult.com/settings)
- [Developer Docs (HTML)](https://onlypult.com/dev/mcp.html)
- [REST API Reference](https://onlypult.com/dev/api.html)
- [MCP Protocol](https://modelcontextprotocol.io/)
