mcp-meta-ads-incrementality 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +131 -0
- package/dist/auth-cli.d.ts +3 -0
- package/dist/auth-cli.d.ts.map +1 -0
- package/dist/auth-cli.js +342 -0
- package/dist/auth-cli.js.map +1 -0
- package/dist/credentials.d.ts +25 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +101 -0
- package/dist/credentials.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +163 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/attribution.d.ts +65 -0
- package/dist/lib/attribution.d.ts.map +1 -0
- package/dist/lib/attribution.js +103 -0
- package/dist/lib/attribution.js.map +1 -0
- package/dist/lib/insights.d.ts +75 -0
- package/dist/lib/insights.d.ts.map +1 -0
- package/dist/lib/insights.js +73 -0
- package/dist/lib/insights.js.map +1 -0
- package/dist/lib/meta-client.d.ts +69 -0
- package/dist/lib/meta-client.d.ts.map +1 -0
- package/dist/lib/meta-client.js +129 -0
- package/dist/lib/meta-client.js.map +1 -0
- package/dist/platform.d.ts +5 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +34 -0
- package/dist/platform.js.map +1 -0
- package/dist/tools/insights-incrementality.d.ts +86 -0
- package/dist/tools/insights-incrementality.d.ts.map +1 -0
- package/dist/tools/insights-incrementality.js +102 -0
- package/dist/tools/insights-incrementality.js.map +1 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 drak-marketing
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# mcp-meta-ads-incrementality
|
|
2
|
+
|
|
3
|
+
> An MCP server for Meta Marketing API insights, built around incrementality.
|
|
4
|
+
> The default attribution window is `incrementality`, not `7d_click + 1d_view`.
|
|
5
|
+
|
|
6
|
+
Most agency reports today use Meta's default 7-day-click + 1-day-view attributed conversions. That number systematically overstates ad impact: it credits clicks that would have converted anyway and gives view-credit for impressions that didn't change behavior. Meta exposes an `incrementality` breakdown on its insights API that returns the actual incremental conversion count.
|
|
7
|
+
|
|
8
|
+
This MCP server makes that the default, and lets you ask for the standard windows in the same call to quantify the gap.
|
|
9
|
+
|
|
10
|
+
## Status
|
|
11
|
+
|
|
12
|
+
`v0.1.0` — early. One tool today (`meta_ads_insights_incrementality`). Comprehensive unit-test coverage of the incrementality math, attribution-window handling, and Meta SDK request shape (120+ tests). Real-API integration is unverified at this version — we recommend running it against a sandboxed account first.
|
|
13
|
+
|
|
14
|
+
This package is not affiliated with, endorsed by, or sponsored by Meta Platforms, Inc. "Meta" and "Facebook" are trademarks of Meta Platforms, Inc.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install -g mcp-meta-ads-incrementality
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or run directly via `npx`:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx mcp-meta-ads-incrementality --help
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Auth
|
|
29
|
+
|
|
30
|
+
Set `META_ACCESS_TOKEN` to a Meta Marketing API access token. A **System User token** from Business Manager is strongly recommended — it doesn't expire and is scoped to the ad accounts the System User has access to.
|
|
31
|
+
|
|
32
|
+
To create one:
|
|
33
|
+
|
|
34
|
+
1. Open [Business Manager](https://business.facebook.com/) → Business Settings → Users → System Users.
|
|
35
|
+
2. Create a new System User and assign it to the ad accounts you want to manage.
|
|
36
|
+
3. Generate a token with `ads_read` and `ads_management` permissions.
|
|
37
|
+
4. Export it: `export META_ACCESS_TOKEN=EAAB...`
|
|
38
|
+
|
|
39
|
+
User access tokens (60-day) also work. For agency use, get **Advanced Access** to the Marketing API via App Review — Standard Access only works against ad accounts the developer owns.
|
|
40
|
+
|
|
41
|
+
## Configure in your MCP client
|
|
42
|
+
|
|
43
|
+
Claude Desktop or Claude Code, in `.mcp.json`:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"mcpServers": {
|
|
48
|
+
"meta-ads-incrementality": {
|
|
49
|
+
"command": "npx",
|
|
50
|
+
"args": ["-y", "mcp-meta-ads-incrementality"],
|
|
51
|
+
"env": {
|
|
52
|
+
"META_ACCESS_TOKEN": "EAAB..."
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Tools
|
|
60
|
+
|
|
61
|
+
### `meta_ads_insights_incrementality`
|
|
62
|
+
|
|
63
|
+
Pull Meta Ads insights for a date range with one or more attribution windows. Returns per-row conversion counts and a summary.
|
|
64
|
+
|
|
65
|
+
**Inputs:**
|
|
66
|
+
|
|
67
|
+
| field | required | default | notes |
|
|
68
|
+
|---|---|---|---|
|
|
69
|
+
| `ad_account_id` | yes | — | with or without `act_` prefix |
|
|
70
|
+
| `date_range` | yes | — | `{ since: "YYYY-MM-DD", until: "YYYY-MM-DD" }` |
|
|
71
|
+
| `conversion_event_name` | yes | — | e.g. `offsite_conversion.fb_pixel_purchase` |
|
|
72
|
+
| `attribution_windows` | no | `["incrementality"]` | array of valid windows |
|
|
73
|
+
| `level` | no | `"campaign"` | `account`, `campaign`, `adset`, `ad` |
|
|
74
|
+
| `campaign_id` | no | — | restrict to a single campaign |
|
|
75
|
+
| `adset_id` | no | — | restrict to a single ad set |
|
|
76
|
+
| `breakdowns` | no | — | Meta breakdown fields |
|
|
77
|
+
|
|
78
|
+
**Valid attribution windows:** `incrementality`, `1d_click`, `7d_click`, `28d_click`, `1d_view`, `1d_ev`, `dda`, `skan_view`, `skan_click`. (Note: `7d_view` and `28d_view` were deprecated by Meta on Jan 12, 2026 and are not accepted.)
|
|
79
|
+
|
|
80
|
+
**Output shape:**
|
|
81
|
+
|
|
82
|
+
```jsonc
|
|
83
|
+
{
|
|
84
|
+
"meta": {
|
|
85
|
+
"ad_account_id": "act_123",
|
|
86
|
+
"date_range": { "since": "2026-03-01", "until": "2026-03-31" },
|
|
87
|
+
"level": "campaign",
|
|
88
|
+
"conversion_event_name": "offsite_conversion.fb_pixel_purchase",
|
|
89
|
+
"attribution_windows": ["incrementality", "7d_click", "1d_view"]
|
|
90
|
+
},
|
|
91
|
+
"rows": [
|
|
92
|
+
{
|
|
93
|
+
"campaign_id": "...",
|
|
94
|
+
"campaign_name": "Brand",
|
|
95
|
+
"spend": 5000,
|
|
96
|
+
"conversions": { "incrementality": 40, "7d_click": 80, "1d_view": 20 },
|
|
97
|
+
"default_attributed": 100,
|
|
98
|
+
"inflation_factor": 2.5,
|
|
99
|
+
"overstatement_pct": 60
|
|
100
|
+
}
|
|
101
|
+
],
|
|
102
|
+
"summary": {
|
|
103
|
+
"total_spend": 5000,
|
|
104
|
+
"total_conversions": { "incrementality": 40, "7d_click": 80, "1d_view": 20 },
|
|
105
|
+
"cpa": { "incrementality": 125, "7d_click": 62.5, "1d_view": 250 },
|
|
106
|
+
"inflation_factor": 2.5
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
When `incrementality` and at least one of `7d_click` / `1d_view` are both requested, every row gets:
|
|
112
|
+
|
|
113
|
+
- `default_attributed` — sum of `7d_click` + `1d_view` (the standard agency number)
|
|
114
|
+
- `inflation_factor` — `default_attributed / incrementality` (how much larger the reported number is)
|
|
115
|
+
- `overstatement_pct` — `(default - incrementality) / default × 100`
|
|
116
|
+
|
|
117
|
+
## Pitch for agencies
|
|
118
|
+
|
|
119
|
+
Most agency reports show the default 7d-click + 1d-view conversion number. That number is systematically inflated. Meta's own `incrementality` breakdown is the true incremental count — and if you put both numbers next to each other in a client report, the gap is usually large. The `inflation_factor` field in this MCP's output is the headline metric: "the number you've been reporting is X× the actual incremental impact."
|
|
120
|
+
|
|
121
|
+
Use this MCP as the data source for your AI-driven reporting workflows. The defaults are set up so that the right number leads.
|
|
122
|
+
|
|
123
|
+
## Caveats
|
|
124
|
+
|
|
125
|
+
- The `incrementality` attribution window is referenced in Meta's Breakdowns documentation but is not in the canonical `action_attribution_windows` enum on the Ad Account Insights reference page. We've defaulted to it because it's the right thing to report — but if Meta changes the API, this MCP will need to follow. Pin the version if stability matters more than correctness.
|
|
126
|
+
- For agencies running campaigns where true incrementality matters at the dollar level, **Conversion Lift studies** (set up in advance with a randomized holdout group) remain the gold standard. This MCP does not yet expose those — that's planned for v0.2.
|
|
127
|
+
- Built on `facebook-nodejs-business-sdk` (Meta's official Node SDK). Inherits any quirks of that SDK including a known appsecret_proof pagination bug.
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
MIT. See [LICENSE](./LICENSE).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-cli.d.ts","sourceRoot":"","sources":["../src/auth-cli.ts"],"names":[],"mappings":";AAgUA,wBAAsB,GAAG,CAAC,IAAI,GAAE,MAAM,EAA0B,GAAG,OAAO,CAAC,IAAI,CAAC,CA4E/E"}
|
package/dist/auth-cli.js
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ============================================
|
|
3
|
+
// mcp-meta-ads-auth -- one-time Meta OAuth + long-lived token exchange
|
|
4
|
+
// ============================================
|
|
5
|
+
// Flow:
|
|
6
|
+
// 1. Loopback HTTP listener on a free port
|
|
7
|
+
// 2. Open browser to Facebook OAuth dialog (implicit flow, response_type=token)
|
|
8
|
+
// 3. Callback page uses JS to extract token from URL fragment, POSTs to local server
|
|
9
|
+
// 4. Exchange short-lived token for long-lived token (60 days) using app_secret
|
|
10
|
+
// 5. Write credentials to ~/.config/mcp-meta-ads-nodejs/credentials.json
|
|
11
|
+
//
|
|
12
|
+
// Priority chain at runtime:
|
|
13
|
+
// 1. META_ACCESS_TOKEN env var (System User token -- never expires, recommended)
|
|
14
|
+
// 2. Cached OAuth token from this CLI (long-lived, ~60 days)
|
|
15
|
+
import http from "http";
|
|
16
|
+
import { URL } from "url";
|
|
17
|
+
import { writeStoredCredentials, credentialsFilePath, CREDENTIALS_FILE_VERSION } from "./credentials.js";
|
|
18
|
+
import { findFreeLoopbackPort, openBrowser } from "./platform.js";
|
|
19
|
+
const META_GRAPH_VERSION = "v24.0";
|
|
20
|
+
const META_AUTH_URL = `https://www.facebook.com/${META_GRAPH_VERSION}/dialog/oauth`;
|
|
21
|
+
const META_GRAPH_URL = `https://graph.facebook.com/${META_GRAPH_VERSION}`;
|
|
22
|
+
const AUTH_SCOPE = "business_management,ads_read,ads_management,pages_show_list,pages_read_engagement";
|
|
23
|
+
function parseArgs(argv) {
|
|
24
|
+
return { help: argv.includes("--help") || argv.includes("-h") };
|
|
25
|
+
}
|
|
26
|
+
function printHelp() {
|
|
27
|
+
process.stdout.write([
|
|
28
|
+
"mcp-meta-ads-auth -- authorize Claude to access your Meta ad accounts",
|
|
29
|
+
"",
|
|
30
|
+
"Usage:",
|
|
31
|
+
" npx -p mcp-meta-ads-incrementality mcp-meta-ads-auth",
|
|
32
|
+
"",
|
|
33
|
+
"Options:",
|
|
34
|
+
" -h, --help Show this help",
|
|
35
|
+
"",
|
|
36
|
+
`Credentials are written to: ${credentialsFilePath}`,
|
|
37
|
+
"",
|
|
38
|
+
"Note: Meta long-lived tokens expire after ~60 days.",
|
|
39
|
+
" Re-run this command when your token expires.",
|
|
40
|
+
" For permanent access, use a System User token from Business Manager",
|
|
41
|
+
" and set META_ACCESS_TOKEN in your environment.",
|
|
42
|
+
"",
|
|
43
|
+
].join("\n"));
|
|
44
|
+
}
|
|
45
|
+
// ============================================
|
|
46
|
+
// OAUTH: IMPLICIT FLOW WITH JS FRAGMENT EXTRACTION
|
|
47
|
+
// ============================================
|
|
48
|
+
function buildAuthUrl(appId, redirectUri, state) {
|
|
49
|
+
const params = new URLSearchParams({
|
|
50
|
+
client_id: appId,
|
|
51
|
+
redirect_uri: redirectUri,
|
|
52
|
+
response_type: "token",
|
|
53
|
+
scope: AUTH_SCOPE,
|
|
54
|
+
state,
|
|
55
|
+
});
|
|
56
|
+
return `${META_AUTH_URL}?${params.toString()}`;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Meta's implicit OAuth puts the token in the URL fragment (#access_token=...).
|
|
60
|
+
* Fragments are never sent to the server, so the callback page needs JavaScript
|
|
61
|
+
* to extract it and POST it to us.
|
|
62
|
+
*/
|
|
63
|
+
function renderCallbackPage(port) {
|
|
64
|
+
return `<!DOCTYPE html>
|
|
65
|
+
<html lang="en">
|
|
66
|
+
<head>
|
|
67
|
+
<meta charset="utf-8">
|
|
68
|
+
<title>Meta Authentication</title>
|
|
69
|
+
<style>
|
|
70
|
+
body { font: 15px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
71
|
+
max-width: 480px; margin: 80px auto; padding: 0 24px; color: #222; }
|
|
72
|
+
h1 { font-size: 22px; margin-bottom: 12px; }
|
|
73
|
+
p { line-height: 1.5; }
|
|
74
|
+
.success { color: #16a34a; }
|
|
75
|
+
.error { color: #dc2626; }
|
|
76
|
+
</style>
|
|
77
|
+
</head>
|
|
78
|
+
<body>
|
|
79
|
+
<h1 id="title">Authenticating...</h1>
|
|
80
|
+
<p id="message">Processing your login...</p>
|
|
81
|
+
<script>
|
|
82
|
+
(function() {
|
|
83
|
+
var hash = window.location.hash.substring(1);
|
|
84
|
+
var params = new URLSearchParams(hash);
|
|
85
|
+
var token = params.get('access_token');
|
|
86
|
+
var state = params.get('state');
|
|
87
|
+
var error = params.get('error');
|
|
88
|
+
var errorDesc = params.get('error_description');
|
|
89
|
+
|
|
90
|
+
if (error) {
|
|
91
|
+
document.getElementById('title').textContent = 'Authorization denied';
|
|
92
|
+
document.getElementById('title').className = 'error';
|
|
93
|
+
document.getElementById('message').textContent = errorDesc || error;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!token) {
|
|
98
|
+
document.getElementById('title').textContent = 'No token received';
|
|
99
|
+
document.getElementById('title').className = 'error';
|
|
100
|
+
document.getElementById('message').textContent = 'Facebook did not return an access token. Please try again.';
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// POST the token to our local server
|
|
105
|
+
fetch('http://127.0.0.1:${port}/token', {
|
|
106
|
+
method: 'POST',
|
|
107
|
+
headers: { 'Content-Type': 'application/json' },
|
|
108
|
+
body: JSON.stringify({ access_token: token, state: state })
|
|
109
|
+
}).then(function(resp) {
|
|
110
|
+
if (resp.ok) {
|
|
111
|
+
document.getElementById('title').textContent = 'Signed in successfully';
|
|
112
|
+
document.getElementById('title').className = 'success';
|
|
113
|
+
document.getElementById('message').textContent = 'You can close this tab and return to the terminal.';
|
|
114
|
+
} else {
|
|
115
|
+
document.getElementById('title').textContent = 'Error';
|
|
116
|
+
document.getElementById('title').className = 'error';
|
|
117
|
+
document.getElementById('message').textContent = 'Failed to send token to local server.';
|
|
118
|
+
}
|
|
119
|
+
}).catch(function() {
|
|
120
|
+
document.getElementById('title').textContent = 'Error';
|
|
121
|
+
document.getElementById('title').className = 'error';
|
|
122
|
+
document.getElementById('message').textContent = 'Could not connect to local server.';
|
|
123
|
+
});
|
|
124
|
+
})();
|
|
125
|
+
</script>
|
|
126
|
+
</body>
|
|
127
|
+
</html>`;
|
|
128
|
+
}
|
|
129
|
+
function renderSimplePage(title, message, isError) {
|
|
130
|
+
const cls = isError ? "error" : "success";
|
|
131
|
+
return `<!DOCTYPE html>
|
|
132
|
+
<html><head><meta charset="utf-8"><title>${escapeHtml(title)}</title>
|
|
133
|
+
<style>body{font:15px -apple-system,sans-serif;max-width:480px;margin:80px auto;padding:0 24px;color:#222}h1{font-size:22px}.success{color:#16a34a}.error{color:#dc2626}</style>
|
|
134
|
+
</head><body><h1 class="${cls}">${escapeHtml(title)}</h1><p>${escapeHtml(message)}</p></body></html>`;
|
|
135
|
+
}
|
|
136
|
+
function escapeHtml(s) {
|
|
137
|
+
return s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
138
|
+
}
|
|
139
|
+
async function waitForToken(port, expectedState, authUrl) {
|
|
140
|
+
return new Promise((resolve, reject) => {
|
|
141
|
+
let settled = false;
|
|
142
|
+
const finish = (fn) => { if (settled)
|
|
143
|
+
return; settled = true; fn(); };
|
|
144
|
+
const callbackHtml = renderCallbackPage(port);
|
|
145
|
+
const server = http.createServer((req, res) => {
|
|
146
|
+
if (!req.url) {
|
|
147
|
+
res.writeHead(404).end();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const parsed = new URL(req.url, `http://127.0.0.1:${port}`);
|
|
151
|
+
// Serve the callback page (Facebook redirects here with #access_token=...)
|
|
152
|
+
if (parsed.pathname === "/callback" || parsed.pathname === "/") {
|
|
153
|
+
res.writeHead(200, {
|
|
154
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
155
|
+
"Access-Control-Allow-Origin": "*",
|
|
156
|
+
});
|
|
157
|
+
res.end(callbackHtml);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
// CORS preflight for the POST
|
|
161
|
+
if (req.method === "OPTIONS" && parsed.pathname === "/token") {
|
|
162
|
+
res.writeHead(200, {
|
|
163
|
+
"Access-Control-Allow-Origin": "*",
|
|
164
|
+
"Access-Control-Allow-Methods": "POST",
|
|
165
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
166
|
+
});
|
|
167
|
+
res.end();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
// Receive the token POSTed by the callback page's JavaScript
|
|
171
|
+
if (req.method === "POST" && parsed.pathname === "/token") {
|
|
172
|
+
let body = "";
|
|
173
|
+
req.on("data", (chunk) => { body += chunk; });
|
|
174
|
+
req.on("end", () => {
|
|
175
|
+
try {
|
|
176
|
+
const data = JSON.parse(body);
|
|
177
|
+
if (!data.access_token) {
|
|
178
|
+
res.writeHead(400, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
|
|
179
|
+
res.end("Missing access_token");
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (data.state !== expectedState) {
|
|
183
|
+
res.writeHead(400, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
|
|
184
|
+
res.end("State mismatch");
|
|
185
|
+
finish(() => { server.close(); reject(new Error("OAuth state mismatch")); });
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
res.writeHead(200, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
|
|
189
|
+
res.end("OK");
|
|
190
|
+
finish(() => {
|
|
191
|
+
setTimeout(() => server.close(), 200);
|
|
192
|
+
resolve({ access_token: data.access_token, state: data.state || "" });
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
res.writeHead(400, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*" });
|
|
197
|
+
res.end("Invalid JSON");
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
res.writeHead(404).end();
|
|
203
|
+
});
|
|
204
|
+
server.on("error", (err) => {
|
|
205
|
+
finish(() => reject(new Error(`Loopback server failed: ${err.message}`)));
|
|
206
|
+
});
|
|
207
|
+
server.listen(port, "127.0.0.1", () => {
|
|
208
|
+
process.stderr.write(`\nOpening your browser to sign in with Facebook...\n`);
|
|
209
|
+
process.stderr.write(`If it doesn't open automatically, visit:\n ${authUrl}\n\n`);
|
|
210
|
+
openBrowser(authUrl).catch(() => { });
|
|
211
|
+
});
|
|
212
|
+
setTimeout(() => {
|
|
213
|
+
finish(() => { server.close(); reject(new Error("Timed out waiting for OAuth callback (5 minutes).")); });
|
|
214
|
+
}, 5 * 60 * 1000);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
async function exchangeForLongLivedToken(shortLivedToken, appId, appSecret) {
|
|
218
|
+
const params = new URLSearchParams({
|
|
219
|
+
grant_type: "fb_exchange_token",
|
|
220
|
+
client_id: appId,
|
|
221
|
+
client_secret: appSecret,
|
|
222
|
+
fb_exchange_token: shortLivedToken,
|
|
223
|
+
});
|
|
224
|
+
const res = await fetch(`${META_GRAPH_URL}/oauth/access_token?${params.toString()}`);
|
|
225
|
+
const json = (await res.json());
|
|
226
|
+
if (!res.ok || json.error) {
|
|
227
|
+
const errMsg = json.error?.message || json.error || res.statusText;
|
|
228
|
+
throw new Error(`Long-lived token exchange failed: ${errMsg}`);
|
|
229
|
+
}
|
|
230
|
+
if (!json.access_token) {
|
|
231
|
+
throw new Error("No access_token in exchange response");
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
access_token: json.access_token,
|
|
235
|
+
token_type: json.token_type || "bearer",
|
|
236
|
+
expires_in: json.expires_in || 5184000,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
// ============================================
|
|
240
|
+
// VERIFY TOKEN + GET USER INFO
|
|
241
|
+
// ============================================
|
|
242
|
+
async function verifyToken(token) {
|
|
243
|
+
const res = await fetch(`${META_GRAPH_URL}/me?access_token=${encodeURIComponent(token)}`);
|
|
244
|
+
const json = (await res.json());
|
|
245
|
+
if (!res.ok || json.error) {
|
|
246
|
+
throw new Error(`Token verification failed: ${json.error?.message || json.error || res.statusText}`);
|
|
247
|
+
}
|
|
248
|
+
return { name: json.name || "Unknown", id: json.id || "" };
|
|
249
|
+
}
|
|
250
|
+
async function listAdAccounts(token) {
|
|
251
|
+
const res = await fetch(`${META_GRAPH_URL}/me/adaccounts?fields=name,account_id&limit=100&access_token=${encodeURIComponent(token)}`);
|
|
252
|
+
const json = (await res.json());
|
|
253
|
+
if (!res.ok || json.error) {
|
|
254
|
+
throw new Error(`Failed to list ad accounts: ${json.error?.message || res.statusText}`);
|
|
255
|
+
}
|
|
256
|
+
return (json.data || []).map((a) => ({
|
|
257
|
+
id: a.id,
|
|
258
|
+
name: a.name || `(unnamed ${a.account_id})`,
|
|
259
|
+
account_id: a.account_id,
|
|
260
|
+
}));
|
|
261
|
+
}
|
|
262
|
+
// ============================================
|
|
263
|
+
// MAIN
|
|
264
|
+
// ============================================
|
|
265
|
+
export async function run(argv = process.argv.slice(2)) {
|
|
266
|
+
const args = parseArgs(argv);
|
|
267
|
+
if (args.help) {
|
|
268
|
+
printHelp();
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const appId = (process.env.META_APP_ID ?? "").trim();
|
|
272
|
+
const appSecret = (process.env.META_APP_SECRET ?? "").trim();
|
|
273
|
+
if (!appId) {
|
|
274
|
+
process.stderr.write("META_APP_ID environment variable is required.\n" +
|
|
275
|
+
"Get your App ID from https://developers.facebook.com/apps/\n");
|
|
276
|
+
process.exit(2);
|
|
277
|
+
}
|
|
278
|
+
if (!appSecret) {
|
|
279
|
+
process.stderr.write("META_APP_SECRET environment variable is required for long-lived token exchange.\n" +
|
|
280
|
+
"Find it in your Facebook App settings under App Secret.\n");
|
|
281
|
+
process.exit(2);
|
|
282
|
+
}
|
|
283
|
+
const port = await findFreeLoopbackPort();
|
|
284
|
+
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
285
|
+
const state = randomState();
|
|
286
|
+
const authUrl = buildAuthUrl(appId, redirectUri, state);
|
|
287
|
+
process.stderr.write("\n=== Meta Ads authentication ===\n");
|
|
288
|
+
const { access_token: shortLivedToken } = await waitForToken(port, state, authUrl);
|
|
289
|
+
process.stderr.write("Short-lived token received. Exchanging for long-lived token...\n");
|
|
290
|
+
const longLived = await exchangeForLongLivedToken(shortLivedToken, appId, appSecret);
|
|
291
|
+
const expiryDays = Math.round(longLived.expires_in / 86400);
|
|
292
|
+
process.stderr.write(`Long-lived token obtained (expires in ~${expiryDays} days).\n`);
|
|
293
|
+
process.stderr.write("Verifying token...\n");
|
|
294
|
+
const user = await verifyToken(longLived.access_token);
|
|
295
|
+
process.stderr.write("Fetching accessible ad accounts...\n");
|
|
296
|
+
const accounts = await listAdAccounts(longLived.access_token);
|
|
297
|
+
const stored = {
|
|
298
|
+
version: CREDENTIALS_FILE_VERSION,
|
|
299
|
+
access_token: longLived.access_token,
|
|
300
|
+
expires_in: longLived.expires_in,
|
|
301
|
+
obtained_at: new Date().toISOString(),
|
|
302
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
303
|
+
user_name: user.name,
|
|
304
|
+
user_id: user.id,
|
|
305
|
+
ad_accounts: accounts.map((a) => ({ id: a.id, name: a.name, account_id: a.account_id })),
|
|
306
|
+
};
|
|
307
|
+
writeStoredCredentials(stored);
|
|
308
|
+
const expiryDate = new Date(Date.now() + longLived.expires_in * 1000).toISOString().slice(0, 10);
|
|
309
|
+
process.stderr.write([
|
|
310
|
+
"",
|
|
311
|
+
"Done.",
|
|
312
|
+
"",
|
|
313
|
+
` User: ${user.name} (${user.id})`,
|
|
314
|
+
` Ad accounts: ${accounts.length} accessible`,
|
|
315
|
+
...accounts.slice(0, 5).map((a) => ` - ${a.name} (${a.account_id})`),
|
|
316
|
+
accounts.length > 5 ? ` ... and ${accounts.length - 5} more` : "",
|
|
317
|
+
` Expires: ~${expiryDate} (${expiryDays} days)`,
|
|
318
|
+
` Saved to: ${credentialsFilePath}`,
|
|
319
|
+
"",
|
|
320
|
+
"Next step: fully quit Claude Desktop (Cmd+Q / File > Exit) and reopen it.",
|
|
321
|
+
'Then try: "Get Meta ad performance for last 7 days"',
|
|
322
|
+
"",
|
|
323
|
+
`Note: This token expires in ~${expiryDays} days. Re-run this command to refresh.`,
|
|
324
|
+
"For permanent access, use a System User token and set META_ACCESS_TOKEN.",
|
|
325
|
+
"",
|
|
326
|
+
].filter(Boolean).join("\n"));
|
|
327
|
+
}
|
|
328
|
+
function randomState() {
|
|
329
|
+
const bytes = new Uint8Array(16);
|
|
330
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
331
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
332
|
+
}
|
|
333
|
+
const isMain = import.meta.url === `file://${process.argv[1]}` ||
|
|
334
|
+
process.argv[1]?.endsWith("/auth-cli.js") ||
|
|
335
|
+
process.argv[1]?.endsWith("\\auth-cli.js");
|
|
336
|
+
if (isMain) {
|
|
337
|
+
run().catch((err) => {
|
|
338
|
+
process.stderr.write(`\n${err.message}\n`);
|
|
339
|
+
process.exit(1);
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
//# sourceMappingURL=auth-cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-cli.js","sourceRoot":"","sources":["../src/auth-cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare const CREDENTIALS_FILE_VERSION = 1;
|
|
2
|
+
export interface StoredCredentials {
|
|
3
|
+
version: number;
|
|
4
|
+
access_token: string;
|
|
5
|
+
expires_in: number;
|
|
6
|
+
obtained_at: string;
|
|
7
|
+
created_at: number;
|
|
8
|
+
user_name?: string;
|
|
9
|
+
user_id?: string;
|
|
10
|
+
ad_accounts?: Array<{
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
account_id: string;
|
|
14
|
+
}>;
|
|
15
|
+
}
|
|
16
|
+
export interface ResolvedCredentials {
|
|
17
|
+
access_token: string;
|
|
18
|
+
source: "env" | "file";
|
|
19
|
+
expires_at?: number;
|
|
20
|
+
}
|
|
21
|
+
export declare function readStoredCredentials(filePath?: string): StoredCredentials | null;
|
|
22
|
+
export declare function writeStoredCredentials(creds: StoredCredentials, filePath?: string): void;
|
|
23
|
+
export declare function resolveCredentials(credsFilePath?: string): ResolvedCredentials;
|
|
24
|
+
export { configDir, credentialsFilePath } from "./platform.js";
|
|
25
|
+
//# sourceMappingURL=credentials.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAE1C,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACvE;AAED,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,KAAK,GAAG,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AASD,wBAAgB,qBAAqB,CAAC,QAAQ,GAAE,MAA4B,GAAG,iBAAiB,GAAG,IAAI,CAUtG;AAED,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,iBAAiB,EACxB,QAAQ,GAAE,MAA4B,GACrC,IAAI,CAON;AAMD,wBAAgB,kBAAkB,CAChC,aAAa,GAAE,MAA4B,GAC1C,mBAAmB,CAqCrB;AA6BD,OAAO,EAAE,SAAS,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// CREDENTIAL LOADING & PERSISTENCE
|
|
3
|
+
// ============================================
|
|
4
|
+
// Priority order:
|
|
5
|
+
// 1. META_ACCESS_TOKEN env var (System User token -- never expires, recommended)
|
|
6
|
+
// 2. Cached OAuth token from mcp-meta-ads-auth (long-lived, ~60 days)
|
|
7
|
+
//
|
|
8
|
+
// System User tokens are preferred for production. OAuth is the fallback
|
|
9
|
+
// for users who don't have Business Manager admin access.
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from "fs";
|
|
11
|
+
import path from "path";
|
|
12
|
+
import { credentialsFilePath } from "./platform.js";
|
|
13
|
+
export const CREDENTIALS_FILE_VERSION = 1;
|
|
14
|
+
const envTrimmed = (key) => (process.env[key] || "").trim().replace(/^["']|["']$/g, "");
|
|
15
|
+
// ============================================
|
|
16
|
+
// FILE I/O
|
|
17
|
+
// ============================================
|
|
18
|
+
export function readStoredCredentials(filePath = credentialsFilePath) {
|
|
19
|
+
if (!existsSync(filePath))
|
|
20
|
+
return null;
|
|
21
|
+
try {
|
|
22
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
23
|
+
const parsed = JSON.parse(raw);
|
|
24
|
+
if (parsed.version !== CREDENTIALS_FILE_VERSION)
|
|
25
|
+
return null;
|
|
26
|
+
return parsed;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function writeStoredCredentials(creds, filePath = credentialsFilePath) {
|
|
33
|
+
const dir = path.dirname(filePath);
|
|
34
|
+
if (!existsSync(dir)) {
|
|
35
|
+
mkdirSync(dir, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
writeFileSync(filePath, JSON.stringify(creds, null, 2), { encoding: "utf-8" });
|
|
38
|
+
try {
|
|
39
|
+
chmodSync(filePath, 0o600);
|
|
40
|
+
}
|
|
41
|
+
catch { }
|
|
42
|
+
}
|
|
43
|
+
// ============================================
|
|
44
|
+
// RESOLVE (read-time priority chain)
|
|
45
|
+
// ============================================
|
|
46
|
+
export function resolveCredentials(credsFilePath = credentialsFilePath) {
|
|
47
|
+
// 1. Environment variable takes absolute priority (System User token)
|
|
48
|
+
const envToken = envTrimmed("META_ACCESS_TOKEN");
|
|
49
|
+
if (envToken) {
|
|
50
|
+
return { access_token: envToken, source: "env" };
|
|
51
|
+
}
|
|
52
|
+
// 2. Cached OAuth token from auth CLI
|
|
53
|
+
const stored = readStoredCredentials(credsFilePath);
|
|
54
|
+
if (stored?.access_token) {
|
|
55
|
+
const expiresAt = stored.created_at + stored.expires_in;
|
|
56
|
+
const now = Math.floor(Date.now() / 1000);
|
|
57
|
+
if (now >= expiresAt) {
|
|
58
|
+
// Token expired
|
|
59
|
+
throw new Error(buildExpiredMessage(stored));
|
|
60
|
+
}
|
|
61
|
+
const daysLeft = Math.round((expiresAt - now) / 86400);
|
|
62
|
+
if (daysLeft <= 7) {
|
|
63
|
+
process.stderr.write(`[warning] Meta OAuth token expires in ${daysLeft} day(s). ` +
|
|
64
|
+
`Re-run: npx -p mcp-meta-ads-incrementality mcp-meta-ads-auth\n`);
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
access_token: stored.access_token,
|
|
68
|
+
source: "file",
|
|
69
|
+
expires_at: expiresAt,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
// 3. Nothing found
|
|
73
|
+
throw new Error(buildMissingMessage());
|
|
74
|
+
}
|
|
75
|
+
function buildExpiredMessage(stored) {
|
|
76
|
+
return [
|
|
77
|
+
`Meta OAuth token has expired (obtained ${stored.obtained_at}).`,
|
|
78
|
+
``,
|
|
79
|
+
`To re-authenticate, run:`,
|
|
80
|
+
` npx -p mcp-meta-ads-incrementality mcp-meta-ads-auth`,
|
|
81
|
+
``,
|
|
82
|
+
`Or for permanent access, set META_ACCESS_TOKEN to a System User token`,
|
|
83
|
+
`from Meta Business Manager (never expires).`,
|
|
84
|
+
].join("\n");
|
|
85
|
+
}
|
|
86
|
+
function buildMissingMessage() {
|
|
87
|
+
return [
|
|
88
|
+
`No Meta access token found.`,
|
|
89
|
+
``,
|
|
90
|
+
`Option 1 (recommended): System User token`,
|
|
91
|
+
` Create one at business.facebook.com > Business Settings > System Users`,
|
|
92
|
+
` Then set META_ACCESS_TOKEN in your environment.`,
|
|
93
|
+
``,
|
|
94
|
+
`Option 2: Browser sign-in (expires in ~60 days)`,
|
|
95
|
+
` Run: npx -p mcp-meta-ads-incrementality mcp-meta-ads-auth`,
|
|
96
|
+
``,
|
|
97
|
+
`Credentials are stored at: ${credentialsFilePath}`,
|
|
98
|
+
].join("\n");
|
|
99
|
+
}
|
|
100
|
+
export { configDir, credentialsFilePath } from "./platform.js";
|
|
101
|
+
//# sourceMappingURL=credentials.js.map
|