meta-mcp
Health Warn
- No license — Repository has no license file
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Community trust — 179 GitHub stars
Code Warn
- network request — Outbound network request in cloudflare-entry.mjs
- network request — Outbound network request in src/lib/meta/client.ts
- network request — Outbound network request in src/lib/oauth/meta-oauth.ts
Permissions Pass
- Permissions — No dangerous permissions requested
No AI report is available for this listing yet.
MCP Server for connecting to the Meta Marketing API
Meta Ads MCP Server
A Cloudflare Workers MCP server for Meta Ads account setup, campaign management, ad sets, creatives, audiences, reporting, and batch workflows.
This repo is built on xmcp and exposes a Streamable HTTP MCP endpoint plus browser-facing Meta OAuth routes.
Open Source / Self-Hosted
This repository is intended to be deployed in your own Cloudflare account with your own Meta app credentials.
It does not ship with:
- a hosted control plane
- a shared Meta app
- a built-in end-user dashboard
- a JWT issuer for your users and workspaces
You bring:
- your Cloudflare Worker deployment
- your Meta developer app
- your JWT issuer or auth provider
- your own UI or backend that initiates the OAuth flow
What It Does
- Runs as a Cloudflare Worker
- Uses direct Meta Graph API
fetchcalls instead of the Meta SDK - Stores Meta user connections per workspace in D1
- Stores short-lived OAuth state in KV
- Encrypts stored Meta access tokens
- Protects MCP requests with your app-issued JWTs
Endpoints
GET /healthGET /appPOST /mcpGET /oauth/meta/startGET /oauth/meta/callback
Auth Model
This server is multi-tenant. Every MCP request must include a bearer JWT issued by your app.
If you are open-sourcing this project, the important implication is that consumers must wire it into their own auth system. The server does not know how to identify a user or workspace without that JWT.
Required JWT claims:
suboruserIdworkspaceId- optional
roles
Example payload:
{
"sub": "user_123",
"workspaceId": "workspace_abc",
"roles": ["admin"]
}
Why /oauth/meta/start is not a generic public link:
- the server must know which workspace the Meta account should be attached to
- that workspace context comes from the JWT
- without it, the server cannot safely bind the resulting Meta token
Tool Surface
Implemented tool families:
- Account and setup
- Campaign management
- Ad set management
- Creative and ads
- Audience and targeting
- Reporting and insights
- Batch helpers
The server currently registers 39 tools.
Project Layout
src/toolstool definitions grouped by domainsrc/libauth, storage, OAuth, runtime, and Meta client helperssrc/servicesdomain-specific Meta service logicsrc/middleware.tsOAuth routing and MCP JWT authcloudflare-entry.mjsWorker wrapper entry for Cloudflare-specific route interceptionschema.sqlD1 schematestunit and contract-style tests
Local Development
Install dependencies:
pnpm install
Run local dev:
pnpm dev
Useful scripts:
pnpm build
pnpm test
pnpm deploy
Cloudflare Bindings
Required bindings:
- D1 database bound as
META_DB - KV namespace bound as
META_OAUTH_STATE
Required secrets:
JWT_SECRETorJWT_JWKS_URLMETA_APP_IDMETA_APP_SECRETMETA_TOKEN_ENCRYPTION_KEYAPP_UI_PASSWORDfor the built-in admin page at/app
Optional configuration:
JWT_ISSUERJWT_AUDIENCEAPP_SESSION_SECRETAPP_UI_WORKSPACE_IDAPP_UI_USER_IDMETA_REDIRECT_URIMETA_GRAPH_VERSIONMETA_OAUTH_SCOPESMETA_OAUTH_ALLOWED_RETURN_ORIGINS
Defaults:
META_GRAPH_VERSION=v25.0META_OAUTH_SCOPES=ads_management,business_managementAPP_UI_WORKSPACE_ID=workspace_adminAPP_UI_USER_ID=app_admin
Built-In Admin UI
The Worker now includes a small browser UI at /app.
What it does:
- prompts for an admin password
- starts the existing Meta OAuth flow without requiring you to manually mint a bearer JWT
- shows whether a Meta account is connected for the admin workspace
- loads accessible ad accounts using the same service logic as
get_ad_accounts
Required setup:
- Set
APP_UI_PASSWORDon the Worker. - Make sure
META_REDIRECT_URImatches your public host, for example:
https://meta-mcp.gestalt.xyz/oauth/meta/callback
- Open:
https://meta-mcp.gestalt.xyz/app
Meta App Setup
In your Meta app:
- Add the Marketing API product.
- Add a Website platform.
- Set the Website platform URL to your Worker origin.
- Set
App Domainsto your Worker domain. - Set the callback URL to:
https://<your-worker-host>/oauth/meta/callback
If your app uses Facebook Login or Facebook Login for Business, also add that exact callback URL to the product-specific redirect URI settings.
For a Worker deployed on workers.dev, these fields usually need to match the Worker host exactly.
Database
Apply the D1 schema:
pnpm wrangler d1 execute META_DB --remote --file schema.sql -y
Tables:
meta_connectionsmeta_ad_accounts_cache
Deployment
Deploy the Worker:
pnpm deploy
After deploy:
- note the public Worker URL
- set
META_REDIRECT_URItohttps://<your-worker-host>/oauth/meta/callback - update the same callback in the Meta app settings
If you plan to use a separate frontend or dashboard on another origin, allow that origin for post-OAuth browser redirects:
META_OAUTH_ALLOWED_RETURN_ORIGINS=https://your-ui.example.com,http://localhost:3000
Use your real frontend origin in production.
Manual Test Flow
1. Generate a short-lived JWT
Use the same JWT secret your app uses for the Worker.
export JWT_SECRET="YOUR_JWT_SECRET"
TOKEN=$(node --input-type=module <<'NODE'
import { SignJWT } from 'jose';
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
const token = await new SignJWT({ workspaceId: 'workspace_test', roles: ['admin'] })
.setProtectedHeader({ alg: 'HS256' })
.setSubject('user_test')
.setIssuedAt()
.setExpirationTime('10m')
.sign(secret);
console.log(token);
NODE
)
2. Start Meta OAuth
curl -i \
-H "Authorization: Bearer $TOKEN" \
"https://<your-worker-host>/oauth/meta/start?workspace_id=workspace_test"
Copy the Location header into your browser and complete the Meta login flow.
Expected success page:
Meta account connected.
3. Initialize MCP
curl -s https://<your-worker-host>/mcp \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"jsonrpc":"2.0","id":"init-1","method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"manual-test","version":"1.0.0"}}}'
4. List Tools
curl -s https://<your-worker-host>/mcp \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"jsonrpc":"2.0","id":"tools-1","method":"tools/list","params":{}}'
5. Call a Real Tool
After OAuth succeeds, this should return the accessible ad accounts for that workspace:
curl -s https://<your-worker-host>/mcp \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"jsonrpc":"2.0","id":"call-1","method":"tools/call","params":{"name":"get_ad_accounts","arguments":{}}}'
If you get a connect/reconnect error, the OAuth flow and the MCP call used different workspaceId values.
Notes
- Cloudflare
workers.devdomains can require extra care in Meta app settings. - The Worker entrypoint explicitly intercepts OAuth routes before delegating to the generated XMCP Worker.
- The Cloudflare Worker build path is not identical to local
xmcp dev, so always verify the deployed routes after OAuth-related changes.
References
Reviews (0)
Sign in to leave a review.
Leave a reviewNo results found