Pinterest Ads MCP
by dadof3bytes
README.md
# Pinterest Ads MCP — setup guide
Connect your AI assistant (Cursor, Claude Desktop, etc.) to **one Pinterest ad account** so it can read performance data, run account audits, and make **guarded** changes (preview first, then confirm).
You do not need to write code. You will copy settings, run a few terminal commands, click through Pinterest login once, and paste a small JSON config into your AI tool.
**What this is:** a small web app plus an **MCP** (Model Context Protocol) bridge. MCP is how compatible AI apps call structured “tools” instead of guessing from screenshots.
**What this is not:** a multi-brand agency dashboard, billing manager, or delete-anything admin panel. One deployment = one ad account.
---
## Before you start — checklist
Gather these before installing:
| Requirement | Why you need it |
| --- | --- |
| **Pinterest Business account** | Ads API access is tied to business identity. |
| **Ad account** you will manage | This server is locked to a single `PINTEREST_AD_ACCOUNT_ID`. |
| **Ad account role: Admin or Campaign Manager** (Analyst is read-only) | Write tools need create/update permission; Analyst → 403 on applies. |
| **Valid ads billing** on the account (for live ads) | Pinterest may block creation or delivery without billing. |
| **Pinterest developer app** | OAuth client ID and secret for login. |
| **Node.js 20+** and **npm** | Runs the server locally ([nodejs.org](https://nodejs.org)). |
| **Terminal access** on your Mac/PC | For `npm install` and `npm run dev`. |
Optional later:
| Requirement | Why |
| --- | --- |
| **Vercel** (or similar) account | Host the app on a URL instead of only localhost. |
| **Strong secrets** | Protect OAuth and MCP endpoints in production. |
---
## Part 1 — Local setup (recommended first)
### Step 1 — Get the project on your machine
1. Clone or download this repository to a folder you will keep (e.g. `~/pinterest-ads-mcp`).
2. Open Terminal and go to that folder:
```bash
cd /path/to/pinterest-ads-mcp
```
### Step 2 — Install dependencies
```bash
npm install
```
### Step 3 — Create your environment file
1. Copy the template:
```bash
cp .env.example .env.local
```
2. Open `.env.local` in a text editor and fill in values (see **Environment variables** below).
Generate strong random secrets (run in Terminal, paste output into `.env.local`):
```bash
# 32+ characters — used to encrypt the Pinterest refresh token on disk
openssl rand -base64 32
# 16+ characters — protects the MCP HTTP endpoint
openssl rand -base64 24
# 16+ characters — signs OAuth “state” during login (CSRF protection)
openssl rand -base64 24
```
Use **different** values for `TOKEN_ENCRYPTION_KEY`, `MCP_AUTH_TOKEN`, and `OAUTH_STATE_SECRET`.
### Step 4 — Create a Pinterest developer app
1. Go to [Pinterest Developers](https://developers.pinterest.com/) and sign in with the **same business identity** that owns the ad account.
2. Create an app (or use an existing one).
3. Note the **App ID** → `PINTEREST_APP_ID` and **App secret** → `PINTEREST_APP_SECRET`.
4. Add an **OAuth redirect URI** that matches **exactly** (including `http` vs `https`, path, no trailing slash unless you configured one):
| Where you run the app | Redirect URI to register |
| --- | --- |
| Local (default) | `http://localhost:3000/api/oauth/pinterest` |
| Cloud host | `https://YOUR-DOMAIN/api/oauth/pinterest` |
Mismatch is the most common setup failure (“redirect_uri mismatch”).
5. Enable / request access for the **Ads API** scopes your app needs (Pinterest’s developer console labels may vary; the server requests the list below at login).
### Step 5 — Required OAuth scopes
When you click **Start Pinterest OAuth** on `/connect`, the server asks Pinterest for these scopes (comma-separated in the authorize URL):
| Scope | Purpose |
| --- | --- |
| `user_accounts:read` | Confirm which user authorized the app. |
| `ads:read` | Read campaigns, ad groups, ads, analytics, audit data. |
| `ads:write` | Create/update campaigns, ads, keywords (via guarded tools). |
| `pins:read` | Read Pin metadata linked to ads. |
| `pins:write` | Create Pins when building new ads. |
| `boards:read` | Board context for Pins. |
| `boards:write` | Pin creation may require board access. |
| `catalogs:read` | Catalog / shopping audit evidence. |
| `catalogs:write` | Catalog-related writes if your workflows need them. |
If Pinterest shows fewer scopes after login, your developer app may not be approved for Ads API yet — fix that in the developer portal before expecting tools to work.
### Step 6 — Find your ad account ID
In [Pinterest Ads Manager](https://ads.pinterest.com/), open the ad account you want this server to use. The account ID is often visible in the URL or account settings (numeric string). Set:
```env
PINTEREST_AD_ACCOUNT_ID=your_numeric_ad_account_id
```
Use `pinterest_list_ad_accounts` later to confirm the ID matches the connected user.
### Step 7 — Start the server
```bash
npm run dev
```
You should see Next.js running on **http://localhost:3000**.
### Step 8 — Connect Pinterest (OAuth)
1. In a browser, open **http://localhost:3000/connect**.
2. Check the page:
- **Sandbox mode** on/off should match `PINTEREST_USE_SANDBOX` in `.env.local` (see **Sandbox vs live**).
- **Ad account id** should show your configured ID.
3. Click **Start Pinterest OAuth**.
4. Log in to Pinterest and **Allow** the requested permissions.
5. Pinterest sends you back to the app. **Success looks like:**
- Green message: **“Pinterest connected successfully.”**
- A **Stored connection** section with connected time and scope list.
- **No** refresh token shown on the page (by design).
**Where tokens live:**
- After OAuth, an encrypted refresh token is saved in **`.data/oauth.json`** on the machine running the server.
- That file is **secret**. It is listed in `.gitignore` — **never commit it** or paste it into chat.
- Decryption uses `TOKEN_ENCRYPTION_KEY` from `.env.local`. If you lose the key, you must run `/connect` again after generating a new key (old file becomes unusable).
Alternative (advanced): put a plain `PINTEREST_REFRESH_TOKEN` or `PINTEREST_REFRESH_TOKEN_ENCRYPTED` in env instead of using `.data/oauth.json` — useful for cloud hosts without persistent disk.
---
## Part 2 — Connect your AI tool
Two supported paths. Pick **one** (HTTP is easier when the app stays running; stdio is fine for all-local setups).
### Option A — HTTP MCP (`/api/mcp`)
The server exposes MCP at:
```text
http://localhost:3000/api/mcp
```
**Authentication:**
- If `MCP_AUTH_TOKEN` is **unset**, anyone who can reach the URL can use your tools — only acceptable on a locked-down machine.
- If `MCP_AUTH_TOKEN` is **set**:
- On **localhost**, requests without a token are still allowed (convenience for local dev).
- On a **public URL** (e.g. Vercel), every request must include
`Authorization: Bearer YOUR_MCP_AUTH_TOKEN` or you get **401 Unauthorized**.
**Cursor `mcp.json` example** (adjust path and token):
```json
{
"mcpServers": {
"pinterest-ads": {
"url": "http://localhost:3000/api/mcp",
"headers": {
"Authorization": "Bearer YOUR_MCP_AUTH_TOKEN"
}
}
}
}
```
Keep `npm run dev` running while you use HTTP MCP locally.
For production URL:
```json
"url": "https://YOUR-DOMAIN/api/mcp"
```
Always use the `Authorization` header on non-localhost hosts.
### Option B — stdio MCP (`npm run stdio`)
Runs MCP over standard input/output — no browser URL. Your AI app launches the process.
**Cursor `mcp.json` example:**
```json
{
"mcpServers": {
"pinterest-ads": {
"command": "npm",
"args": ["run", "stdio"],
"cwd": "/absolute/path/to/pinterest-ads-mcp",
"env": {
"PINTEREST_APP_ID": "…",
"PINTEREST_APP_SECRET": "…",
"PINTEREST_AD_ACCOUNT_ID": "…",
"TOKEN_ENCRYPTION_KEY": "…",
"PINTEREST_USE_SANDBOX": "true"
}
}
}
}
```
stdio does **not** load `.env.local` automatically. Either:
- Put required variables in the `"env"` block (as above), or
- Export them in your shell before Cursor starts.
OAuth for stdio: complete **`npm run dev`** once and visit `/connect`, **or** set `PINTEREST_REFRESH_TOKEN` in the `"env"` block after connecting elsewhere.
---
## Part 3 — Optional: deploy to Vercel (or similar)
Use cloud hosting when you want MCP available without keeping your laptop on.
1. Push the repo to GitHub (do **not** commit `.env.local` or `.data/`).
2. Import the project in [Vercel](https://vercel.com) as a Next.js app.
3. Set **all** environment variables from `.env.local` in the Vercel project settings.
4. Update:
- `NEXT_PUBLIC_APP_URL` → `https://YOUR-DOMAIN`
- `PINTEREST_REDIRECT_URI` → `https://YOUR-DOMAIN/api/oauth/pinterest`
5. Add the same redirect URI in the Pinterest developer app.
6. Set `CONNECT_PAGE_TOKEN` (see **Security notes**), redeploy, then open
`https://YOUR-DOMAIN/connect?connect_token=YOUR_CONNECT_PAGE_TOKEN` and complete OAuth **on the deployed site**.
**Important for cloud (Vercel):**
- Vercel **cannot** write `.data/oauth.json` under `/var/task`. Tokens must live in environment variables.
- **`TOKEN_ENCRYPTION_KEY`** must already be set in Vercel **before** you run OAuth (same value forever for that deployment, unless you rotate and re-connect).
- After OAuth succeeds, `/connect` shows a yellow **One more step for Vercel** panel with the value for **`PINTEREST_REFRESH_TOKEN_ENCRYPTED`**. Copy the entire string into Vercel → Settings → Environment Variables → add `PINTEREST_REFRESH_TOKEN_ENCRYPTED` → **Redeploy**.
- Until that env var exists, MCP tools will say there is no refresh token — the yellow panel is a one-time handoff, not automatic storage.
- Use the **same** `TOKEN_ENCRYPTION_KEY` that was active when you generated that encrypted blob. Changing the key without re-running OAuth breaks decryption.
- Set a strong `MCP_AUTH_TOKEN` and **always** send the Bearer header from your AI client.
- Do not share the public MCP URL publicly without that token.
Build/start commands (if your host asks):
| Script | Command |
| --- | --- |
| Install | `npm install` |
| Build | `npm run build` |
| Start | `npm run start` |
---
## Environment variables
| Variable | Required when | What it is | If missing / wrong |
| --- | --- | --- | --- |
| `PINTEREST_APP_ID` | OAuth + API | Developer app ID | Cannot start OAuth or call API. |
| `PINTEREST_APP_SECRET` | OAuth + API | Developer app secret | Token exchange fails. |
| `PINTEREST_REDIRECT_URI` | OAuth | Must match Pinterest app setting | Redirect or token exchange errors. |
| `PINTEREST_AD_ACCOUNT_ID` | Most tools | Single ad account for this deployment | Tools error on account resolution. |
| `PINTEREST_USE_SANDBOX` | Always set explicitly | `true` / `false` — picks sandbox vs production **API** host | Wrong environment, confusing empty data or auth errors. |
| `TOKEN_ENCRYPTION_KEY` | Storing token in `.data/oauth.json` | 32+ char secret; encrypts refresh token on disk | Cannot save or read OAuth file. |
| `OAUTH_STATE_SECRET` | OAuth login | 16+ char HMAC secret for login CSRF | `/connect` shows “Set … OAUTH_STATE_SECRET”. |
| `MCP_AUTH_TOKEN` | Recommended | 16+ char Bearer token for `/api/mcp` | Open MCP on public URLs; localhost still works without header. |
| `CONNECT_PAGE_TOKEN` | **Required on public URLs** | Secret to open `/connect` (`?connect_token=…`) | Anyone can start OAuth against your Pinterest app if `/connect` is public. |
| `DRAFT_SIGNING_SECRET` | Optional | Signs write “draft” tokens (defaults to `TOKEN_ENCRYPTION_KEY`) | Write previews fail if neither is set. |
| `NEXT_PUBLIC_APP_URL` | Links / redirects | Base URL of this app (e.g. `http://localhost:3000`) | Wrong redirect targets in some flows. |
| `PINTEREST_REFRESH_TOKEN` | Optional dev | Plain refresh token in env | Bypasses `.data/oauth.json`; dev-only risk. |
| `PINTEREST_REFRESH_TOKEN_ENCRYPTED` | Optional cloud | Encrypted token blob + `TOKEN_ENCRYPTION_KEY` | Used instead of file store. |
---
## Sandbox vs live (`PINTEREST_USE_SANDBOX`)
| Setting | Behavior |
| --- | --- |
| `PINTEREST_USE_SANDBOX=true` | API calls go to `api-sandbox.pinterest.com`. Use for testing without touching live spend. |
| `PINTEREST_USE_SANDBOX=false` | Production API and real ad account data. |
On `/connect`, you can toggle **sandbox OAuth** vs **production OAuth** via the link on the page. The token exchange uses that choice. **Keep `PINTEREST_USE_SANDBOX` aligned** with how you connected; otherwise refresh and API hosts may disagree.
Default in `.env.example` is sandbox (`true`) — switch to `false` only when you intend to manage live ads.
---
## What your AI can do — tools by use case
### Account & structure (read)
| Tool | Use |
| --- | --- |
| `pinterest_get_account` | Account details, user summary, terms-of-service state. |
| `pinterest_list_ad_accounts` | Confirm which ad accounts the login can see vs your configured ID. |
| `pinterest_list_campaigns` / `pinterest_get_campaign` | Campaign list and detail. |
| `pinterest_list_ad_groups` | Ad groups, optionally filtered by campaign. |
| `pinterest_list_ads` | Ads in the account. |
| `pinterest_list_keywords` | Keywords for ad groups. |
### Reporting & analytics
| Tool | Use |
| --- | --- |
| `pinterest_get_analytics` | Quick sync stats for **short** date ranges (≤31 days), ≤100 entities, not hourly. |
| `pinterest_create_report` | Start a **large** or slow report (custom body, or template). |
| `pinterest_get_report` | Poll report job status and download link. |
| `pinterest_list_metrics` | Valid metric column names for reports. |
| `pinterest_metrics_ready` | Whether conversion metrics are ready for a date. |
**Tip:** If sync analytics refuses the request or times out, use **`pinterest_create_report`** then **`pinterest_get_report`**.
### Audit & compliance evidence
| Tool | Use |
| --- | --- |
| `pinterest_audit_snapshot` | Read-only bundle for **ads-pinterest PN-*** style controls (tags, catalogs, campaigns, etc.). Observations only — no “health score.” |
### Targeting research
| Tool | Use |
| --- | --- |
| `pinterest_get_targeting_options` | Interests, locations, geos, audiences, etc. for planning ad groups. |
### Create & change ads (guarded writes)
Nothing deletes campaigns/ads via API here. New entities default to **PAUSED**.
**Workflow:** always **draft** → review preview → **apply** with `confirm: true`. Draft tokens expire in about **5 minutes**.
| Draft tool | Apply tool | What it does |
| --- | --- | --- |
| `pinterest_draft_campaign` | `pinterest_apply_campaign` | Create/update campaigns. **New campaigns:** objectives **`SALES`** or **`LEADS`** only. |
| `pinterest_draft_ad_group` | `pinterest_apply_ad_group` | Create/update ad groups. |
| `pinterest_draft_pin` | `pinterest_apply_pin` | Create a Pin (image URL or media id). |
| `pinterest_draft_ad` | `pinterest_apply_ad` | Create/update ads. |
| `pinterest_draft_keyword` | `pinterest_apply_keyword` | Keywords including negatives. |
| `pinterest_draft_entity_status` | `pinterest_apply_entity_status` | Set ACTIVE / PAUSED / ARCHIVED on campaign, ad group, or ad. |
**Money:** tool inputs/outputs use **major currency** (e.g. dollars); the server converts to Pinterest’s micro-currency internally.
**Objectives:** Pinterest is migrating to simplified objectives. You may still **see** legacy types (`CATALOG_SALES`, `WEB_CONVERSION`, `AWARENESS`, etc.) when reading old campaigns. **Creating** new campaigns through this server only allows **`SALES`** and **`LEADS`**. Ask your AI to map legacy setups to the new model when planning creates.
---
## Quick verification after setup
In your AI chat (with MCP connected), ask it to run these in order:
1. **List tools** — you should see about **25+** `pinterest_*` tools (read, report, audit, draft/apply).
2. **`pinterest_get_account`** — returns your ad account payload without “No Pinterest refresh token”.
3. **`pinterest_audit_snapshot`** — returns a JSON evidence bundle (may take a few seconds).
If all three succeed, reporting and draft tools are ready to use.
---
## Troubleshooting
| Symptom | Likely cause | What to do |
| --- | --- | --- |
| **redirect_uri mismatch** | Pinterest app redirect ≠ `PINTEREST_REDIRECT_URI` | Copy the URI character-for-character into the developer app; redeploy if you changed domain. |
| **401 / 403** on ads tools | Missing scopes, app not approved for Ads API, or user lacks ad account role | Re-check scopes table; confirm you’re admin on `PINTEREST_AD_ACCOUNT_ID`; re-authorize at `/connect`. |
| **No Pinterest refresh token** / tools say not connected | Never finished OAuth, deleted `.data/oauth.json`, missing `PINTEREST_REFRESH_TOKEN_ENCRYPTED` on Vercel, or wrong `TOKEN_ENCRYPTION_KEY` | Visit `/connect` and complete OAuth again; on Vercel, paste encrypted token from setup panel into env and redeploy. |
| **ENOENT mkdir `/var/task/.data`** | Hosted on Vercel without env token storage | Redeploy with this fix, run OAuth again, copy `PINTEREST_REFRESH_TOKEN_ENCRYPTED` from `/connect` into Vercel env. |
| **Token refresh failed** | Revoked app, expired refresh, sandbox/live mismatch | Re-run `/connect`; align `PINTEREST_USE_SANDBOX` with connection mode. |
| **401 Unauthorized MCP request** | Missing/wrong Bearer on a public URL, or wrong token on localhost | Add `Authorization: Bearer` matching `MCP_AUTH_TOKEN`. On localhost you can omit the header, but a **wrong** Bearer still fails. |
| **sync_request_too_large** / analytics timeout | Range too long, too many IDs, or hourly granularity | Use `pinterest_create_report` + `pinterest_get_report`. |
| **Invalid create objective** | Draft used `CATALOG_SALES`, `WEB_CONVERSION`, etc. | Use **`SALES`** or **`LEADS`** for new campaigns only. |
| **Apply failed / confirm required** | Skipped draft step or expired draft | Run `pinterest_draft_*` again, then apply within ~5 minutes with `confirm: true`. |
| **Set PINTEREST_APP_ID… on /connect** | Incomplete `.env.local` | Fill app ID, secret, redirect URI, `OAUTH_STATE_SECRET`; restart `npm run dev`. |
| stdio tools fail immediately | Env vars not visible to subprocess | Add `"env"` in `mcp.json` or export variables before launching Cursor. |
---
## Security notes (please read)
### Lock down `/connect` on Vercel (or any public URL)
`/connect` starts Pinterest login for **whoever** opens it. That does not let strangers control your MCP if you already stored **`PINTEREST_REFRESH_TOKEN_ENCRYPTED`** in Vercel (API calls use env, not a random visitor’s session). It **does** let them authorize **their** Pinterest user against **your** developer app, which is noisy, confusing, and risky during initial setup.
**Fix:** set a long random **`CONNECT_PAGE_TOKEN`** in Vercel env, redeploy, and **never** publish plain `/connect`. Use only:
```text
https://YOUR-DOMAIN/connect?connect_token=YOUR_CONNECT_PAGE_TOKEN
```
After one successful visit, an httpOnly cookie keeps the session for about an hour so OAuth redirect still works. The Pinterest callback URL (`/api/oauth/pinterest?code=…`) stays public — that is required by OAuth.
Also: remove or avoid linking `/connect` from a public homepage; treat the gated URL like a password reset link.
### Other secrets
- **`.env.local`** holds app secrets. Do not email it, commit it to GitHub, or paste into Slack/ChatGPT.
- **`.data/oauth.json`** is as sensitive as a password. Back up only encrypted, store offline, never in git.
- **MCP HTTP URL** on the internet is a remote control for your ad account if `MCP_AUTH_TOKEN` is weak or missing. Use a long random token and HTTPS.
- **Do not paste** refresh tokens, `MCP_AUTH_TOKEN`, or app secrets into AI chats — even “private” threads.
- **Who can run writes:** anyone who can call your MCP server can run **apply** tools after a draft. Lock down network access and tokens.
- This project intentionally excludes agency/business-access APIs, billing APIs, and delete tools.
---
## NPM scripts reference
| Command | Purpose |
| --- | --- |
| `npm run dev` | Local server + `/connect` + HTTP MCP at `/api/mcp` |
| `npm run stdio` | MCP over stdio for AI apps that launch a subprocess |
| `npm run build` | Production build (cloud deploy) |
| `npm run start` | Run production build |
| `npm test` | Automated tests (optional) |
---
## Out of scope
Multi-tenant token vaults, Pinterest Business Access / partner APIs, billing management, entity delete tools, and automated “account health scores.”
For technical architecture (OpenAPI version, libraries), see comments in the repository source.