Package not found. Please check the package name and try again.

@ppcassist/amazon-ads-mcp 1.0.1 → 1.0.4

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.
Files changed (2) hide show
  1. package/package.json +4 -2
  2. package/setup.js +369 -0
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@ppcassist/amazon-ads-mcp",
3
- "version": "1.0.1",
3
+ "version": "1.0.4",
4
4
  "description": "Local MCP proxy for Amazon Ads API - authenticates and forwards requests to Amazon's official MCP server",
5
5
  "main": "index.js",
6
6
  "bin": {
7
- "amazon-ads-mcp": "./index.js"
7
+ "amazon-ads-mcp": "./index.js",
8
+ "amazon-ads-setup": "./setup.js"
8
9
  },
9
10
  "keywords": [
10
11
  "amazon-ads",
@@ -21,6 +22,7 @@
21
22
  },
22
23
  "files": [
23
24
  "index.js",
25
+ "setup.js",
24
26
  "lib/",
25
27
  "README.md"
26
28
  ]
package/setup.js ADDED
@@ -0,0 +1,369 @@
1
+ #!/usr/bin/env node
2
+
3
+ const http = require("http");
4
+ const https = require("https");
5
+ const fs = require("fs");
6
+ const path = require("path");
7
+ const os = require("os");
8
+ const { exec } = require("child_process");
9
+ const readline = require("readline");
10
+
11
+ // ─── PPC Assist OAuth credentials (placeholders) ───
12
+ const PPCASSIST_CLIENT_ID = "amzn1.application-oa2-client.REPLACE_ME";
13
+ const PPCASSIST_CLIENT_SECRET = "REPLACE_ME";
14
+ const REDIRECT_URI_BASE = "http://localhost";
15
+ const CALLBACK_PATH = "/callback";
16
+
17
+ const OAUTH_AUTHORIZE_URL = "https://www.amazon.com/ap/oa";
18
+ const OAUTH_TOKEN_URL = "https://api.amazon.com/auth/o2/token";
19
+ const PROFILES_URL = "https://advertising-api-eu.amazon.com/v2/profiles";
20
+
21
+ const TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes
22
+
23
+ // ─── Helpers ───
24
+
25
+ function print(msg) {
26
+ process.stdout.write(msg + "\n");
27
+ }
28
+
29
+ function openBrowser(url) {
30
+ const platform = process.platform;
31
+ const cmd =
32
+ platform === "darwin" ? "open" :
33
+ platform === "win32" ? "start" :
34
+ "xdg-open";
35
+
36
+ exec(`${cmd} "${url}"`, (err) => {
37
+ if (err) {
38
+ print(`\n Could not open browser automatically.`);
39
+ print(` Open this URL manually:\n`);
40
+ print(` ${url}\n`);
41
+ }
42
+ });
43
+ }
44
+
45
+ function httpPost(urlStr, body, headers) {
46
+ return new Promise((resolve, reject) => {
47
+ const url = new URL(urlStr);
48
+ const options = {
49
+ hostname: url.hostname,
50
+ path: url.pathname,
51
+ method: "POST",
52
+ headers: {
53
+ "Content-Type": "application/x-www-form-urlencoded",
54
+ "Content-Length": Buffer.byteLength(body),
55
+ ...headers,
56
+ },
57
+ };
58
+
59
+ const req = https.request(options, (res) => {
60
+ let chunks = [];
61
+ res.on("data", (c) => chunks.push(c));
62
+ res.on("end", () => {
63
+ const raw = Buffer.concat(chunks).toString();
64
+ try {
65
+ resolve(JSON.parse(raw));
66
+ } catch {
67
+ reject(new Error(`Invalid JSON from ${urlStr}: ${raw.slice(0, 200)}`));
68
+ }
69
+ });
70
+ });
71
+ req.on("error", reject);
72
+ req.write(body);
73
+ req.end();
74
+ });
75
+ }
76
+
77
+ function httpGet(urlStr, headers) {
78
+ return new Promise((resolve, reject) => {
79
+ const url = new URL(urlStr);
80
+ const options = {
81
+ hostname: url.hostname,
82
+ path: url.pathname + url.search,
83
+ method: "GET",
84
+ headers,
85
+ };
86
+
87
+ const req = https.request(options, (res) => {
88
+ let chunks = [];
89
+ res.on("data", (c) => chunks.push(c));
90
+ res.on("end", () => {
91
+ const raw = Buffer.concat(chunks).toString();
92
+ try {
93
+ resolve(JSON.parse(raw));
94
+ } catch {
95
+ reject(new Error(`Invalid JSON from ${urlStr}: ${raw.slice(0, 200)}`));
96
+ }
97
+ });
98
+ });
99
+ req.on("error", reject);
100
+ req.end();
101
+ });
102
+ }
103
+
104
+ function askQuestion(prompt) {
105
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
106
+ return new Promise((resolve) => {
107
+ rl.question(prompt, (answer) => {
108
+ rl.close();
109
+ resolve(answer.trim());
110
+ });
111
+ });
112
+ }
113
+
114
+ function getConfigPath() {
115
+ if (process.platform === "darwin") {
116
+ return path.join(os.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
117
+ }
118
+ if (process.platform === "win32") {
119
+ return path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
120
+ }
121
+ // Linux fallback
122
+ return path.join(os.homedir(), ".config", "Claude", "claude_desktop_config.json");
123
+ }
124
+
125
+ function detectRegion(profiles) {
126
+ // Use the first profile's country to guess region
127
+ // EU marketplaces
128
+ const euCountries = ["UK", "GB", "DE", "FR", "IT", "ES", "NL", "SE", "PL", "BE", "TR", "AE", "SA", "EG", "IN"];
129
+ const feCountries = ["JP", "AU", "SG"];
130
+
131
+ for (const p of profiles) {
132
+ const cc = (p.countryCode || "").toUpperCase();
133
+ if (euCountries.includes(cc)) return "EU";
134
+ if (feCountries.includes(cc)) return "FE";
135
+ }
136
+ return "NA";
137
+ }
138
+
139
+ // ─── Start local server and wait for callback ───
140
+
141
+ function waitForCallback(port) {
142
+ return new Promise((resolve, reject) => {
143
+ const server = http.createServer((req, res) => {
144
+ if (!req.url.startsWith(CALLBACK_PATH)) {
145
+ res.writeHead(404);
146
+ res.end("Not found");
147
+ return;
148
+ }
149
+
150
+ const params = new URL(req.url, `http://localhost:${port}`).searchParams;
151
+ const code = params.get("code");
152
+ const error = params.get("error");
153
+
154
+ if (error) {
155
+ res.writeHead(200, { "Content-Type": "text/html" });
156
+ res.end("<html><body><h2>Authorization denied.</h2><p>You can close this tab.</p></body></html>");
157
+ server.close();
158
+ reject(new Error(`Amazon returned error: ${error}`));
159
+ return;
160
+ }
161
+
162
+ if (!code) {
163
+ res.writeHead(400, { "Content-Type": "text/html" });
164
+ res.end("<html><body><h2>Missing authorization code.</h2></body></html>");
165
+ return;
166
+ }
167
+
168
+ res.writeHead(200, { "Content-Type": "text/html" });
169
+ res.end("<html><body><h2>Authorization successful!</h2><p>You can close this tab and return to the terminal.</p></body></html>");
170
+ server.close();
171
+ resolve(code);
172
+ });
173
+
174
+ const timeout = setTimeout(() => {
175
+ server.close();
176
+ reject(new Error("Timed out waiting for authorization (2 minutes). Please try again."));
177
+ }, TIMEOUT_MS);
178
+
179
+ server.on("close", () => clearTimeout(timeout));
180
+
181
+ server.on("error", (err) => {
182
+ if (err.code === "EADDRINUSE") {
183
+ resolve(null); // Signal to try next port
184
+ } else {
185
+ reject(err);
186
+ }
187
+ });
188
+
189
+ server.listen(port, () => {
190
+ resolve._server = server; // store for reference
191
+ });
192
+ });
193
+ }
194
+
195
+ // ─── Main ───
196
+
197
+ async function main() {
198
+ print("");
199
+ print("\u{1F680} Amazon Ads MCP Setup");
200
+ print("\u2500".repeat(35));
201
+
202
+ // Find an available port
203
+ let port = 8080;
204
+ let code = null;
205
+
206
+ while (port <= 8082) {
207
+ const redirectUri = `${REDIRECT_URI_BASE}:${port}${CALLBACK_PATH}`;
208
+ const authUrl =
209
+ `${OAUTH_AUTHORIZE_URL}?client_id=${encodeURIComponent(PPCASSIST_CLIENT_ID)}` +
210
+ `&scope=advertising::campaign_management` +
211
+ `&response_type=code` +
212
+ `&redirect_uri=${encodeURIComponent(redirectUri)}`;
213
+
214
+ const promise = waitForCallback(port);
215
+
216
+ // Check if port was available (give server a moment to start)
217
+ await new Promise((r) => setTimeout(r, 100));
218
+
219
+ // Try to get the code; null means port was in use
220
+ const result = await Promise.race([
221
+ promise,
222
+ new Promise((r) => setTimeout(() => r("pending"), 200)),
223
+ ]);
224
+
225
+ if (result === null) {
226
+ print(` Port ${port} in use, trying ${port + 1}...`);
227
+ port++;
228
+ continue;
229
+ }
230
+
231
+ // Port is available — open browser and wait
232
+ print(`\u2192 Opening Amazon authorization in your browser...`);
233
+ openBrowser(authUrl);
234
+ print(`\u2192 Waiting for authorization... (press Ctrl+C to cancel)`);
235
+
236
+ if (result === "pending") {
237
+ code = await promise;
238
+ } else {
239
+ code = result;
240
+ }
241
+ break;
242
+ }
243
+
244
+ if (!code) {
245
+ print("\n\u274C Could not find an available port (tried 8080-8082). Please free one and try again.");
246
+ process.exit(1);
247
+ }
248
+
249
+ print("\u2705 Authorization received");
250
+
251
+ // Exchange code for tokens
252
+ const redirectUri = `${REDIRECT_URI_BASE}:${port}${CALLBACK_PATH}`;
253
+ const tokenBody = new URLSearchParams({
254
+ grant_type: "authorization_code",
255
+ code,
256
+ redirect_uri: redirectUri,
257
+ client_id: PPCASSIST_CLIENT_ID,
258
+ client_secret: PPCASSIST_CLIENT_SECRET,
259
+ }).toString();
260
+
261
+ let tokenData;
262
+ try {
263
+ tokenData = await httpPost(OAUTH_TOKEN_URL, tokenBody);
264
+ } catch (err) {
265
+ print(`\n\u274C Token exchange failed: ${err.message}`);
266
+ process.exit(1);
267
+ }
268
+
269
+ if (tokenData.error) {
270
+ print(`\n\u274C Amazon OAuth error: ${tokenData.error} - ${tokenData.error_description || ""}`);
271
+ process.exit(1);
272
+ }
273
+
274
+ const { access_token, refresh_token } = tokenData;
275
+
276
+ // Fetch advertiser profiles
277
+ print("\u2192 Fetching your Amazon Ads profiles...");
278
+
279
+ let profiles;
280
+ try {
281
+ profiles = await httpGet(PROFILES_URL, {
282
+ Authorization: `Bearer ${access_token}`,
283
+ "Amazon-Advertising-API-ClientId": PPCASSIST_CLIENT_ID,
284
+ });
285
+ } catch (err) {
286
+ print(`\n\u274C Failed to fetch profiles: ${err.message}`);
287
+ process.exit(1);
288
+ }
289
+
290
+ if (!Array.isArray(profiles) || profiles.length === 0) {
291
+ print("\n\u274C No advertising profiles found on this account.");
292
+ process.exit(1);
293
+ }
294
+
295
+ // Select profile
296
+ let selectedProfile;
297
+
298
+ if (profiles.length === 1) {
299
+ selectedProfile = profiles[0];
300
+ print(`\u2192 Found 1 profile: ${selectedProfile.accountInfo?.name || "Account"} (${selectedProfile.profileId})`);
301
+ } else {
302
+ print(`\u2192 Found ${profiles.length} profiles:`);
303
+ profiles.forEach((p, i) => {
304
+ const name = p.accountInfo?.name || `Profile ${p.profileId}`;
305
+ const cc = p.countryCode || "";
306
+ print(` ${i + 1}. ${name} ${cc ? `(${cc})` : ""} — profile_id: ${p.profileId}`);
307
+ });
308
+
309
+ const answer = await askQuestion(`\u2192 Select a profile [1-${profiles.length}]: `);
310
+ const idx = parseInt(answer, 10) - 1;
311
+
312
+ if (isNaN(idx) || idx < 0 || idx >= profiles.length) {
313
+ print("\n\u274C Invalid selection.");
314
+ process.exit(1);
315
+ }
316
+
317
+ selectedProfile = profiles[idx];
318
+ }
319
+
320
+ const profileName = selectedProfile.accountInfo?.name || "Amazon Ads";
321
+ const profileId = String(selectedProfile.profileId);
322
+ const region = detectRegion([selectedProfile]);
323
+
324
+ print(`\u2705 Profile selected: ${profileName} (${region})`);
325
+
326
+ // Write Claude Desktop config
327
+ const configPath = getConfigPath();
328
+ const configDir = path.dirname(configPath);
329
+
330
+ let config = {};
331
+ if (fs.existsSync(configPath)) {
332
+ try {
333
+ config = JSON.parse(fs.readFileSync(configPath, "utf8"));
334
+ } catch {
335
+ print(` Warning: existing config was invalid JSON, creating fresh.`);
336
+ }
337
+ }
338
+
339
+ if (!config.mcpServers) {
340
+ config.mcpServers = {};
341
+ }
342
+
343
+ config.mcpServers["amazon-ads"] = {
344
+ command: "npx",
345
+ args: ["-y", "@ppcassist/amazon-ads-mcp"],
346
+ env: {
347
+ CLIENT_ID: PPCASSIST_CLIENT_ID,
348
+ CLIENT_SECRET: PPCASSIST_CLIENT_SECRET,
349
+ REFRESH_TOKEN: refresh_token,
350
+ PROFILE_ID: profileId,
351
+ REGION: region,
352
+ },
353
+ };
354
+
355
+ // Ensure directory exists
356
+ fs.mkdirSync(configDir, { recursive: true });
357
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
358
+
359
+ print(`\u2705 Claude Desktop config updated`);
360
+ print(` ${configPath}`);
361
+ print("\u2500".repeat(35));
362
+ print(`\u2705 Setup complete! Restart Claude Desktop to activate.`);
363
+ print("");
364
+ }
365
+
366
+ main().catch((err) => {
367
+ print(`\n\u274C Fatal error: ${err.message}`);
368
+ process.exit(1);
369
+ });