The design decision that shapes everything here is that the server refuses to guess who is calling: no JWT, no workspace, no Meta token binding. That is more setup than a single-user server — you supply the auth issuer — but it is the reason one deployment can serve several workspaces without their ad accounts leaking into each other. The `/app` page exists so you do not have to hand-sign a token just to connect the first account.
A Cloudflare Worker that speaks MCP over Streamable HTTP and calls the Meta Graph API directly, with browser-facing Meta OAuth routes alongside it. It is multi-tenant by design: Meta connections are stored per workspace in D1, access tokens are encrypted at rest, and every MCP request must carry a JWT your own application issued. The server registers 39 tools.
- 39 registered tools grouped by domain — account and setup, campaign management, ad set management, creatives and ads, audiences and targeting, reporting and insights, and batch helpers
- The accessible ad accounts for a workspace, once its Meta OAuth flow has completed — `get_ad_accounts`
- OAuth handled in the Worker: `GET /oauth/meta/start` and `GET /oauth/meta/callback` bind the resulting Meta token to the workspace named in the caller's JWT, so the start link is never a generic public URL
- Per-workspace isolation backed by D1 (`meta_connections`, `meta_ad_accounts_cache`) with short-lived OAuth state in KV
- A small admin page at `/app` that takes a password, runs the OAuth flow without you minting a bearer JWT by hand, and shows whether a Meta account is connected
- Direct Graph API `fetch` calls rather than the Meta SDK, with `META_GRAPH_VERSION` defaulting to `v25.0`
Your own Cloudflare Worker deployment, your own Meta developer app with the Marketing API product added, and your own JWT issuer — the repository ships no hosted control plane, no shared Meta app and no auth provider. Required bindings are a D1 database as `META_DB` and a KV namespace as `META_OAUTH_STATE`; required secrets are `JWT_SECRET` or `JWT_JWKS_URL`, `META_APP_ID`, `META_APP_SECRET`, `META_TOKEN_ENCRYPTION_KEY`, and `APP_UI_PASSWORD` for the admin page. Every MCP request needs a bearer JWT carrying `sub` (or `userId`) and `workspaceId`. Default OAuth scopes are `ads_management,business_management`. The Meta app's callback URL must match `https://<your-worker-host>/oauth/meta/callback` exactly.
One command plus a key — npx -y meta-mcp, then supply credentials
