Skip to main content
Glama

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

Related MCP server: flin-meta-ads-mcp

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

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.

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

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:

{
  "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

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

# 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:

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:

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:

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:

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:

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:

{
  "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:

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

HTTP:

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.

Available Tools

30 tools
ad_account_getGet one ad accountD

GET /act_{id}.

Inputs: account_id (required), fields.

ad_accounts_listList ad accounts accessible to the tokenD

GET /me/adaccounts.

Inputs: fields, limit.

adcreative_getGet one ad creativeD

GET /{creative_id}.

Inputs: id (required), fields.

adcreatives_listList ad creatives under an ad accountD

GET /act_{id}/adcreatives.

Inputs: account_id (required), fields, limit, auto_paginate, +1 more.

adsets_listList ad setsC

GET /{parent}/adsets. parent.type selects the path: 'account' → /act_{id}/adsets, 'campaign' → /{campaign_id}/adsets.

Inputs: parent (required), fields, filtering, effective_status, +3 more.

ads_listList adsC

GET /{parent}/ads. parent.type selects the path: 'account' → /act_{id}/ads, 'campaign' → /{campaign_id}/ads, 'adset' → /{adset_id}/ads.

Inputs: parent (required), fields, filtering, effective_status, +3 more.

campaign_getGet one campaignD

GET /{campaign_id}.

Inputs: id (required), fields.

campaigns_listList campaigns under an ad accountD

GET /act_{id}/campaigns.

Inputs: account_id (required), fields, filtering, effective_status, +3 more.

debug_tokenInspect a Meta access tokenA

GET /debug_token — returns token type (USER / PAGE / APP / SYSTEM_USER), app_id, expires_at, scopes, is_valid. If input_token is omitted, inspects the server's configured FB_ACCESS_TOKEN. Requires FB_APP_ID and FB_APP_SECRET to be configured (Meta requires APPID|APPSECRET as the access_token for this endpoint).

Inputs: input_token.

graph_getRaw GET to Meta Graph APIA

Escape-hatch: raw GET to any Graph API endpoint. Path must start with '/' and contain only safe characters. If the path starts with /v{X.Y}/ the version is stripped (server uses FB_GRAPH_VERSION). Always GET (read-only). Use only when no specialized tool fits.

Inputs: path (required), params.

ig_business_account_getGet Instagram Business / Creator accountB

GET /{ig_user_id}. Note: ig_user_id is NOT the @username — it's the IG Business Account id obtained via /{page_id}?fields=instagram_business_account. Requires the Page Access Token of the linked Facebook Page (resolved automatically via via_page_id).

Inputs: ig_user_id (required), via_page_id (required), fields.

ig_hashtag_mediaHashtag media — top or recentC

GET /{hashtag_id}/top_media or /{hashtag_id}/recent_media. user_id is required by Meta.

Inputs: hashtag_id (required), kind (required), user_id (required), fields, +3 more.

ig_media_getGet one IG media itemC

GET /{media_id}.

Inputs: media_id (required), via_page_id (required), fields, locale.

ig_media_insightsInsights for one IG media itemB

GET /{media_id}/insights. Metric set depends on media_type: IMAGE/CAROUSEL_ALBUM: impressions, reach, engagement, saved. VIDEO/REELS: plays, reach, engagement, video_views, saved. STORY: impressions, reach, replies, exits, taps_forward, taps_back.

Inputs: media_id (required), via_page_id (required), metric (required).

ig_media_listList IG media (posts) of a Business accountD

GET /{ig_user_id}/media.

Inputs: ig_user_id (required), via_page_id (required), fields, limit, +2 more.

ig_user_insightsAccount-level Instagram insightsB

GET /{ig_user_id}/insights. Period is required. Common metrics: impressions, reach, profile_views, follower_count (day); audience_country, audience_gender_age (lifetime).

Inputs: ig_user_id (required), via_page_id (required), metric (required), period (required), +2 more.

insightsAds Insights (sync only)A

GET /{object_id}/insights. Sync only — for huge date ranges Meta recommends async jobs (POST /insights → polling), which this read-only server does not support. object_id can be account (use 'act_{id}' or numeric and we'll prefix), campaign, adset or ad — level must match. Common fields: spend, impressions, clicks, ctr, cpc, cpm, reach, frequency, actions, action_values.

Inputs: object_id (required), level (required), fields, date_preset, +8 more.

page_getGet one Facebook PageC

GET /{page_id} — page metadata.

Inputs: page_id (required), fields, locale.

page_insightsPage Insights metricsA

GET /{page_id}/insights. Requires a Page Access Token (resolved automatically). Pass an array of metric names (e.g. [page_impressions, page_fans, page_post_engagements]). Period default is day; pass since/until as YYYY-MM-DD or unix seconds.

Inputs: page_id (required), metric (required), period, since, +1 more.

page_postsList posts on a Facebook PageA

Default edge is published_posts (Page-owned posts paged by created time). Other edges: posts (default Graph posts edge), feed (everything on the wall, including visitor posts), tagged (posts where the page is tagged).

Inputs: page_id (required), edge, fields, since, +4 more.

pages_listList Facebook Pages owned/managed by the tokenA

GET /me/accounts — returns pages with id, name and (importantly) access_token for each. Run this first to discover page_id values for other Page tools.

Inputs: fields, limit.

post_commentsList comments on a postD

GET /{post_id}/comments.

Inputs: post_id (required), via_page_id (required), fields, order, +3 more.

post_getGet one Facebook PostA

GET /{post_id}. Post id format is usually {page_id}_{post_id}. Use a Page Access Token via via_page_id (extracted from the post id prefix).

Inputs: post_id (required), via_page_id (required), fields.

wa_analyticsWhatsApp Business analyticsC

GET /{waba_id}?fields=analytics.start({start}).end({end}).granularity({granularity}) — messages sent/delivered/read by phone number, by country, etc. start/end are unix timestamps in seconds.

Inputs: waba_id (required), start (required), end (required), granularity (required), +2 more.

wa_business_owned_wabasList WhatsApp Business Accounts owned by a BusinessB

GET /{business_id}/owned_whatsapp_business_accounts — returns WABAs owned by the parent Business. Find business_id via Business Manager or /me/businesses.

Inputs: business_id (required), fields, limit.

wa_message_templatesList WA message templates under a WABAD

GET /{waba_id}/message_templates.

Inputs: waba_id (required), fields, limit, auto_paginate, +1 more.

wa_phone_number_getGet one WA phone number — quality rating / messaging tierA

GET /{phone_number_id} — includes quality_rating, messaging_limit_tier, name_status, verified_name.

Inputs: phone_number_id (required), fields.

wa_phone_numbersList phone numbers under a WABAC

GET /{waba_id}/phone_numbers.

Inputs: waba_id (required), fields.

wa_template_getGet one WA message templateC

GET /{template_id}.

Inputs: template_id (required), fields.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    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 recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      A
      quality
      D
      maintenance
      MCP Server for the Meta Marketing API. Gives Claude Desktop direct access to your ad account data — campaign performance, creative analysis, audience breakdowns, and budget pacing.
      10
      344 npm
      MIT
    • A
      license
      B
      quality
      B
      maintenance
      Read-only MCP server for Meta Ads that lists and reads ad accounts, campaigns, ad sets, ads, ad images, creatives, and fetches insights at various levels.
      18
      MIT
    • F
      license
      A
      quality
      D
      maintenance
      Local MCP server for operating Instagram, Meta Ads, and Facebook Page from Claude Code via Meta's Graph API. Provides 20 tools for publishing, campaign management, and insights.
      20
      -
    • F
      license
      A
      quality
      B
      maintenance
      MCP server for reading Meta Business (Facebook Pages and Instagram) data via Graph API, with cross-account portfolio aggregation to answer questions about follower growth, page insights, and metrics. It runs as a stdio or HTTP server for Claude Desktop.
      9
      -