Skip to main content
Glama
README.md
# meta-mcp

Read-only MCP server for **Meta (Facebook) Graph API**. Plugs into Claude Code (and any other MCP-compatible client) over **stdio** (local) or **Streamable HTTP** (remote).

Covers four API surfaces with a single long-lived token:

- **Marketing API** — ad accounts, campaigns, ad sets, ads, creatives, insights
- **Facebook Pages** — pages, posts, comments, page insights
- **Instagram Graph** — business accounts, media, media/account insights, hashtag search
- **WhatsApp Business** — WABAs, phone numbers, message templates, analytics

Plus `debug_token` (inspect any token) and `graph_get` (raw GET escape-hatch).

HTTP method is hard-coded to `GET` everywhere — Claude cannot accidentally create, mutate, or delete anything.

## Tools

Specialized (validated with zod):

| Surface | Tools |
|---|---|
| Common | `graph_get`, `debug_token` |
| Pages | `pages_list`, `page_get`, `page_posts`, `post_get`, `post_comments`, `page_insights` |
| Marketing | `ad_accounts_list`, `ad_account_get`, `campaigns_list`, `campaign_get`, `adsets_list`, `ads_list`, `adcreatives_list`, `adcreative_get`, `insights` |
| Instagram | `ig_business_account_get`, `ig_media_list`, `ig_media_get`, `ig_media_insights`, `ig_user_insights`, `ig_hashtag_search`, `ig_hashtag_media` |
| WhatsApp | `wa_business_owned_wabas`, `wa_phone_numbers`, `wa_phone_number_get`, `wa_message_templates`, `wa_template_get`, `wa_analytics` |

## 1. Get an access token

The token prefix `EAA...` is the same for every Graph token type — **you can't tell which kind you have from the prefix.** Run `debug_token` after first start to identify it. The four kinds:

| Kind | When to use | How to obtain |
|---|---|---|
| **System User** | Recommended for ads / business automation — long-lived, doesn't tie to a human account | Business Manager → Business settings → Users → System users → Generate new token |
| **Page Access Token** | Single page only | Graph Explorer with a user token: `GET /me/accounts?fields=access_token` |
| **User OAuth token** | Personal access, expires in ~60 days | Standard FB Login OAuth flow |
| **App access token** | Server-to-server calls that don't need user context (e.g. `debug_token`) | `APPID|APPSECRET` literal |

**For `page_insights` and any Instagram tool** you need a Page Access Token. This server resolves it lazily: if `FB_ACCESS_TOKEN` is a user/system token with admin rights, calling any Page/IG tool triggers a one-time `/me/accounts` fetch that caches each page's token. No manual swap needed.

## 2. App credentials (recommended)

If you set both `FB_APP_ID` and `FB_APP_SECRET` in `.env`:

- Every request is signed with `appsecret_proof` — Meta's defence against token replay if the access token leaks.
- `debug_token` works (Meta requires `APPID|APPSECRET` to call that endpoint).

Find them at **developers.facebook.com → My Apps → {your app} → Settings → Basic**.

## 3. Three ways to run

| Mode | When to use |
|---|---|
| **npm (`npx`)** | Easiest for end users — no clone, no build |
| **stdio (from source)** | Hacking on the server locally |
| **Docker (Ubuntu)** | Shared server, multiple devices, mobile, teammates |

### A. Install from npm

```bash
claude mcp add meta \
  --env FB_ACCESS_TOKEN=<your-token> \
  --env FB_APP_ID=<your-app-id> \
  --env FB_APP_SECRET=<your-app-secret> \
  -- npx -y meta-mcp
```

Or manually in `~/.claude.json`:

```json
{
  "mcpServers": {
    "meta": {
      "command": "npx",
      "args": ["-y", "meta-mcp"],
      "env": {
        "FB_ACCESS_TOKEN": "EAA...",
        "FB_APP_ID": "<your-app-id>",
        "FB_APP_SECRET": "<your-app-secret>"
      }
    }
  }
}
```

In Claude Code: `/mcp` → `meta: connected`.

### B. Local — stdio from source

```bash
git clone https://github.com/nourgithub/meta-mcp.git
cd meta-mcp
cp .env.example .env
# edit .env — set FB_ACCESS_TOKEN at minimum
# (also FB_APP_ID + FB_APP_SECRET for debug_token / appsecret_proof)
npm install
npm run build
claude mcp add meta -- node "$(pwd)/dist/index.js"
```

### C. Remote — Docker on Ubuntu

Everything ships in [`deploy/`](deploy/):

- `deploy/Dockerfile` — multi-stage Node 20 alpine build, runs as non-root with `tini` for clean signals
- `deploy/docker-compose.yml` — two services: the app + Caddy as TLS-terminating reverse proxy
- `deploy/Caddyfile` — auto-HTTPS via Let's Encrypt, proxies `/mcp` (SSE-aware) and `/health`

Pre-requisites: Ubuntu server with public IP and a DNS A-record pointing your domain at it.

```bash
# 1) On the server — install Docker
ssh root@SERVER_IP
apt-get update
apt-get install -y ca-certificates curl
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" \
  > /etc/apt/sources.list.d/docker.list
apt-get update
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

# 2) Open ports 80 and 443 (skip if no ufw)
ufw allow 80/tcp
ufw allow 443/tcp
```

**From your laptop**, push the project to the server:

```bash
tar -czf - -C /d/meta-mcp \
  --exclude='./node_modules' --exclude='./dist' --exclude='./.env' --exclude='./.git' . \
  | ssh root@SERVER_IP "mkdir -p /opt/meta-mcp && tar -xzf - -C /opt/meta-mcp"
```

Back on the **server**, prepare `.env`:

```bash
ssh root@SERVER_IP
cd /opt/meta-mcp
cp .env.example .env
nano .env
```

Fill at minimum:

```
FB_ACCESS_TOKEN=EAA...
FB_APP_ID=...
FB_APP_SECRET=...

MCP_TRANSPORT=http
MCP_HTTP_PORT=8765
MCP_HTTP_AUTH_TOKEN=<openssl rand -hex 32>
MCP_HTTP_ALLOWED_HOSTS=meta-mcp.example.com

META_MCP_DOMAIN=meta-mcp.example.com
ACME_EMAIL=you@example.com
```

`chmod 600 .env`. Build and start:

```bash
cd /opt/meta-mcp/deploy
docker compose build
docker compose up -d
docker compose ps      # both services should be "running" / "healthy"
docker compose logs -f # watch Caddy fetch the cert
```

Verify:

```bash
curl https://meta-mcp.example.com/health
# {"ok":true,"name":"meta-mcp"}

curl -i -X POST https://meta-mcp.example.com/mcp
# 401 — missing bearer (expected)
```

Connect Claude Code:

```bash
claude mcp add --transport http meta https://meta-mcp.example.com/mcp \
  --header "Authorization: Bearer <your MCP_HTTP_AUTH_TOKEN>"
```

## Pagination

All `*_list` tools accept:

- `limit` — page size (default 25)
- `auto_paginate=true` — walk `paging.next` up to `max_pages` (default 4) times
- `max_pages` — safety cap

When `auto_paginate=true` the response shape is:

```json
{
  "items": [ ...flattened from all pages ],
  "pages": 3,
  "truncated": false,
  "next_cursor": "QVFIUjF..."
}
```

Cursors are short-lived (~30 min). Don't reuse a stale `next_cursor` after a long delay.

## Rate limits

Meta's actual budget depends on app tier (Standard / Advanced), token type, and per-object usage — there is no single fixed RPS for Graph API. The server applies:

- **Client-side**: soft cap of `META_RPS_LIMIT` requests / second (default 25, env-tunable).
- **Header-aware cooldown**: after each response we parse `X-App-Usage`, `X-Ad-Account-Usage`, `X-Business-Use-Case-Usage`. If any bucket > 90%, the next request is delayed (using `estimated_time_to_regain_access` if present).
- **Retry**: on HTTP 429 or Graph error codes `4`, `17`, `32`, `613` the request is retried with exponential backoff (max 2 retries).

## Security

- `FB_ACCESS_TOKEN` is sent only as `Authorization: Bearer` header, never as a query parameter — keeps it out of proxy / CDN logs.
- If `FB_APP_SECRET` is set, every request includes `appsecret_proof` (HMAC-SHA256 of token with secret). Token replay from another machine fails.
- Graph error bodies are scrubbed before being returned: occurrences of the token, app secret and `appsecret_proof` are replaced with `***` to prevent leakage via tool output.
- HTTP mode: `MCP_HTTP_AUTH_TOKEN` is mandatory, compared with constant-time. `MCP_HTTP_ALLOWED_HOSTS` enables DNS-rebinding protection.
- HTTPS in Docker mode handled by Caddy automatically.

## Caveats

- **Insights async jobs not supported.** For huge Ads Insights ranges Meta recommends `POST /insights` → poll `report_run_id`. This server is GET-only; use shorter ranges or export from Ads Manager.
- **Webhook subscriptions out of scope.** MCP is a request/response model; subscribing to realtime updates needs a separate server.
- **Page-token cache is per-process.** Restarting the server clears it. First IG/Page-Insights call after restart triggers one extra `/me/accounts` round-trip.

## Project layout

```
meta-mcp/
├── src/
│   ├── index.ts          # entry: picks transport, registers tools
│   ├── config.ts         # loads .env, validates required vars
│   ├── client.ts         # fetch wrapper, rate limiter, paginator, page-token cache
│   ├── http-server.ts    # Express + StreamableHTTPServerTransport + bearer auth
│   └── tools/
│       ├── raw.ts        # graph_get
│       ├── debug.ts      # debug_token
│       ├── pages.ts      # Facebook Pages
│       ├── marketing.ts  # Ads
│       ├── instagram.ts  # Instagram Graph
│       └── whatsapp.ts   # WhatsApp Business
├── deploy/
│   ├── Dockerfile
│   ├── docker-compose.yml
│   └── Caddyfile
├── .env.example
├── package.json
└── tsconfig.json
```

## Smoke test without Claude

stdio:

```bash
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | node dist/index.js
```

HTTP:

```bash
curl -sS -X POST https://meta-mcp.example.com/mcp \
  -H "Authorization: Bearer $MCP_HTTP_AUTH_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}' \
  -i
```

## License

MIT.

TDQS

C2.7/5.0

Scored across 30 tools

Disambiguation5/5

Every tool has a clearly distinct purpose targeting a specific resource (ad account, campaign, Instagram media, WhatsApp phone number, etc.). Even similar tools like ads_list and adsets_list are distinguished by the parent entity (account, campaign, adset). No ambiguity.

Naming Consistency4/5

Naming follows a consistent snake_case pattern (resource_verb) for most tools (e.g., ad_account_get, ads_list, campaign_get). A few tools like graph_get and debug_token deviate slightly but are still readable and serve special purposes.

Tool Count4/5

30 tools is on the higher side but justified by the broad scope covering Facebook Ads, Instagram, WhatsApp Business, and Page insights. Each tool addresses a specific read operation, and the count is manageable for such a large API surface.

Completeness3/5

The tool set is comprehensive for read operations on Meta's Graph API, covering ads, Instagram, WhatsApp, and Pages. However, it lacks write operations (create/update/delete) which are available via graph_get escape hatch only. This is acceptable for a read-only server, but some common workflows (e.g., posting content) are absent.

Maintenance

ActivityInactive
ResponsivenessNo issues