@ppcassist/amazon-ads-mcp 1.0.4 → 1.0.5

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 (3) hide show
  1. package/index.js +116 -117
  2. package/package.json +2 -3
  3. package/setup.js +109 -60
package/index.js CHANGED
@@ -1,149 +1,148 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { TokenManager } = require("./lib/auth");
4
- const { McpProxy } = require("./lib/proxy");
5
-
6
- const log = (msg) => process.stderr.write(`[amazon-ads-mcp] ${msg}\n`);
7
-
8
- function requireEnv(name) {
9
- const value = process.env[name];
10
- if (!value) {
11
- log(`ERROR: Missing required environment variable: ${name}`);
12
- process.exit(1);
3
+ // Mode detection: "npx @ppcassist/amazon-ads-mcp setup" runs onboarding
4
+ if (process.argv[2] === "setup") {
5
+ require("./setup");
6
+ // setup.js handles its own exit — nothing else to do here
7
+ } else {
8
+ // MCP proxy mode (default — used by Claude Desktop)
9
+ const { TokenManager } = require("./lib/auth");
10
+ const { McpProxy } = require("./lib/proxy");
11
+
12
+ const log = (msg) => process.stderr.write(`[amazon-ads-mcp] ${msg}\n`);
13
+
14
+ function requireEnv(name) {
15
+ const value = process.env[name];
16
+ if (!value) {
17
+ log(`ERROR: Missing required environment variable: ${name}`);
18
+ process.exit(1);
19
+ }
20
+ return value;
13
21
  }
14
- return value;
15
- }
16
22
 
17
- async function main() {
18
- const clientId = requireEnv("CLIENT_ID");
19
- const clientSecret = requireEnv("CLIENT_SECRET");
20
- const refreshToken = requireEnv("REFRESH_TOKEN");
21
- const profileId = requireEnv("PROFILE_ID");
22
- const region = requireEnv("REGION");
23
-
24
- // Initialize OAuth token manager
25
- const tokenManager = new TokenManager({ clientId, clientSecret, refreshToken });
26
- try {
27
- await tokenManager.init();
28
- } catch (err) {
29
- log(`Failed to obtain access token: ${err.message}`);
30
- process.exit(1);
31
- }
23
+ async function main() {
24
+ const clientId = requireEnv("CLIENT_ID");
25
+ const clientSecret = requireEnv("CLIENT_SECRET");
26
+ const refreshToken = requireEnv("REFRESH_TOKEN");
27
+ const profileId = requireEnv("PROFILE_ID");
28
+ const region = requireEnv("REGION");
29
+
30
+ const tokenManager = new TokenManager({ clientId, clientSecret, refreshToken });
31
+ try {
32
+ await tokenManager.init();
33
+ } catch (err) {
34
+ log(`Failed to obtain access token: ${err.message}`);
35
+ process.exit(1);
36
+ }
32
37
 
33
- // Initialize MCP proxy
34
- const proxy = new McpProxy({ tokenManager, clientId, profileId, region });
38
+ const proxy = new McpProxy({ tokenManager, clientId, profileId, region });
35
39
 
36
- log(`Started (region=${region})`);
40
+ log(`Started (region=${region})`);
37
41
 
38
- // Read JSON-RPC messages from stdin (newline-delimited)
39
- let buffer = "";
42
+ let buffer = "";
40
43
 
41
- process.stdin.setEncoding("utf8");
42
- process.stdin.on("data", (chunk) => {
43
- buffer += chunk;
44
+ process.stdin.setEncoding("utf8");
45
+ process.stdin.on("data", (chunk) => {
46
+ buffer += chunk;
44
47
 
45
- // Process all complete lines in the buffer
46
- let newlineIdx;
47
- while ((newlineIdx = buffer.indexOf("\n")) !== -1) {
48
- const line = buffer.slice(0, newlineIdx).trim();
49
- buffer = buffer.slice(newlineIdx + 1);
48
+ let newlineIdx;
49
+ while ((newlineIdx = buffer.indexOf("\n")) !== -1) {
50
+ const line = buffer.slice(0, newlineIdx).trim();
51
+ buffer = buffer.slice(newlineIdx + 1);
50
52
 
51
- if (!line) continue;
53
+ if (!line) continue;
54
+
55
+ let request;
56
+ try {
57
+ request = JSON.parse(line);
58
+ } catch (err) {
59
+ log(`Failed to parse JSON-RPC message: ${err.message}`);
60
+ continue;
61
+ }
52
62
 
53
- let request;
54
- try {
55
- request = JSON.parse(line);
56
- } catch (err) {
57
- log(`Failed to parse JSON-RPC message: ${err.message}`);
58
- continue;
63
+ handleRequest(proxy, request);
59
64
  }
65
+ });
60
66
 
61
- handleRequest(proxy, request);
62
- }
63
- });
67
+ process.stdin.on("end", () => {
68
+ log("stdin closed, shutting down");
69
+ tokenManager.destroy();
70
+ process.exit(0);
71
+ });
72
+ }
64
73
 
65
- process.stdin.on("end", () => {
66
- log("stdin closed, shutting down");
67
- tokenManager.destroy();
68
- process.exit(0);
69
- });
70
- }
74
+ async function handleRequest(proxy, request) {
75
+ const { id, method } = request;
76
+
77
+ try {
78
+ const result = await proxy.forward(request);
79
+
80
+ if (result.type === "stream") {
81
+ let sseBuf = "";
82
+ result.stream.setEncoding("utf8");
83
+
84
+ result.stream.on("data", (chunk) => {
85
+ sseBuf += chunk;
86
+ let lineEnd;
87
+ while ((lineEnd = sseBuf.indexOf("\n")) !== -1) {
88
+ const line = sseBuf.slice(0, lineEnd).trim();
89
+ sseBuf = sseBuf.slice(lineEnd + 1);
90
+ if (line.startsWith("data: ")) {
91
+ const json = line.slice(6);
92
+ if (json) {
93
+ process.stdout.write(json + "\n");
94
+ }
95
+ }
96
+ }
97
+ });
71
98
 
72
- async function handleRequest(proxy, request) {
73
- const { id, method } = request;
74
-
75
- try {
76
- const result = await proxy.forward(request);
77
-
78
- if (result.type === "stream") {
79
- // SSE stream: buffer chunks, extract JSON from "data: ..." lines,
80
- // and write as newline-delimited JSON-RPC for Claude Desktop
81
- let sseBuf = "";
82
- result.stream.setEncoding("utf8");
83
-
84
- result.stream.on("data", (chunk) => {
85
- sseBuf += chunk;
86
- let lineEnd;
87
- while ((lineEnd = sseBuf.indexOf("\n")) !== -1) {
88
- const line = sseBuf.slice(0, lineEnd).trim();
89
- sseBuf = sseBuf.slice(lineEnd + 1);
90
- if (line.startsWith("data: ")) {
91
- const json = line.slice(6);
99
+ result.stream.on("end", () => {
100
+ const remaining = sseBuf.trim();
101
+ if (remaining.startsWith("data: ")) {
102
+ const json = remaining.slice(6);
92
103
  if (json) {
93
104
  process.stdout.write(json + "\n");
94
105
  }
95
106
  }
96
- }
97
- });
98
-
99
- result.stream.on("end", () => {
100
- // Flush any remaining data in buffer
101
- const remaining = sseBuf.trim();
102
- if (remaining.startsWith("data: ")) {
103
- const json = remaining.slice(6);
104
- if (json) {
105
- process.stdout.write(json + "\n");
106
- }
107
- }
108
- });
107
+ });
109
108
 
110
- result.stream.on("error", (err) => {
111
- log(`SSE stream error (id=${id}): ${err.message}`);
109
+ result.stream.on("error", (err) => {
110
+ log(`SSE stream error (id=${id}): ${err.message}`);
111
+ writeJsonRpc({
112
+ jsonrpc: "2.0",
113
+ id,
114
+ error: { code: -32000, message: `Stream error: ${err.message}` },
115
+ });
116
+ });
117
+ } else if (result.type === "json") {
118
+ writeJsonRpc(result.data);
119
+ } else {
120
+ log(`Unexpected response (status=${result.statusCode}): ${result.data}`);
112
121
  writeJsonRpc({
113
122
  jsonrpc: "2.0",
114
123
  id,
115
- error: { code: -32000, message: `Stream error: ${err.message}` },
124
+ error: {
125
+ code: -32000,
126
+ message: `Upstream error (HTTP ${result.statusCode})`,
127
+ },
116
128
  });
117
- });
118
- } else if (result.type === "json") {
119
- writeJsonRpc(result.data);
120
- } else {
121
- // Raw / unexpected response
122
- log(`Unexpected response (status=${result.statusCode}): ${result.data}`);
129
+ }
130
+ } catch (err) {
131
+ log(`Proxy error (method=${method}, id=${id}): ${err.message}`);
123
132
  writeJsonRpc({
124
133
  jsonrpc: "2.0",
125
134
  id,
126
- error: {
127
- code: -32000,
128
- message: `Upstream error (HTTP ${result.statusCode})`,
129
- },
135
+ error: { code: -32000, message: err.message },
130
136
  });
131
137
  }
132
- } catch (err) {
133
- log(`Proxy error (method=${method}, id=${id}): ${err.message}`);
134
- writeJsonRpc({
135
- jsonrpc: "2.0",
136
- id,
137
- error: { code: -32000, message: err.message },
138
- });
139
138
  }
140
- }
141
139
 
142
- function writeJsonRpc(obj) {
143
- process.stdout.write(JSON.stringify(obj) + "\n");
144
- }
140
+ function writeJsonRpc(obj) {
141
+ process.stdout.write(JSON.stringify(obj) + "\n");
142
+ }
145
143
 
146
- main().catch((err) => {
147
- log(`Fatal: ${err.message}`);
148
- process.exit(1);
149
- });
144
+ main().catch((err) => {
145
+ log(`Fatal: ${err.message}`);
146
+ process.exit(1);
147
+ });
148
+ }
package/package.json CHANGED
@@ -1,11 +1,10 @@
1
1
  {
2
2
  "name": "@ppcassist/amazon-ads-mcp",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
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",
8
- "amazon-ads-setup": "./setup.js"
7
+ "amazon-ads-mcp": "./index.js"
9
8
  },
10
9
  "keywords": [
11
10
  "amazon-ads",
package/setup.js CHANGED
@@ -1,5 +1,3 @@
1
- #!/usr/bin/env node
2
-
3
1
  const http = require("http");
4
2
  const https = require("https");
5
3
  const fs = require("fs");
@@ -8,9 +6,6 @@ const os = require("os");
8
6
  const { exec } = require("child_process");
9
7
  const readline = require("readline");
10
8
 
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
9
  const REDIRECT_URI_BASE = "http://localhost";
15
10
  const CALLBACK_PATH = "/callback";
16
11
 
@@ -26,6 +21,57 @@ function print(msg) {
26
21
  process.stdout.write(msg + "\n");
27
22
  }
28
23
 
24
+ function ask(question) {
25
+ return new Promise((resolve) => {
26
+ const rl = readline.createInterface({
27
+ input: process.stdin,
28
+ output: process.stdout,
29
+ });
30
+ rl.question(question, (answer) => {
31
+ rl.close();
32
+ resolve(answer.trim());
33
+ });
34
+ });
35
+ }
36
+
37
+ function askSecret(question) {
38
+ return new Promise((resolve) => {
39
+ process.stdout.write(question);
40
+ const rl = readline.createInterface({ input: process.stdin });
41
+
42
+ if (process.stdin.isTTY) {
43
+ process.stdin.setRawMode(true);
44
+ }
45
+
46
+ let secret = "";
47
+ const onData = (key) => {
48
+ const ch = key.toString();
49
+ if (ch === "\n" || ch === "\r" || ch === "\u0004") {
50
+ if (process.stdin.isTTY) {
51
+ process.stdin.setRawMode(false);
52
+ }
53
+ process.stdin.removeListener("data", onData);
54
+ rl.close();
55
+ process.stdout.write("\n");
56
+ resolve(secret.trim());
57
+ } else if (ch === "\u0003") {
58
+ // Ctrl+C
59
+ process.stdout.write("\n");
60
+ process.exit(1);
61
+ } else if (ch === "\u007F" || ch === "\b") {
62
+ // Backspace
63
+ secret = secret.slice(0, -1);
64
+ } else {
65
+ secret += ch;
66
+ process.stdout.write("*");
67
+ }
68
+ };
69
+
70
+ process.stdin.on("data", onData);
71
+ process.stdin.resume();
72
+ });
73
+ }
74
+
29
75
  function openBrowser(url) {
30
76
  const platform = process.platform;
31
77
  const cmd =
@@ -101,16 +147,6 @@ function httpGet(urlStr, headers) {
101
147
  });
102
148
  }
103
149
 
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
150
  function getConfigPath() {
115
151
  if (process.platform === "darwin") {
116
152
  return path.join(os.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
@@ -118,13 +154,10 @@ function getConfigPath() {
118
154
  if (process.platform === "win32") {
119
155
  return path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
120
156
  }
121
- // Linux fallback
122
157
  return path.join(os.homedir(), ".config", "Claude", "claude_desktop_config.json");
123
158
  }
124
159
 
125
160
  function detectRegion(profiles) {
126
- // Use the first profile's country to guess region
127
- // EU marketplaces
128
161
  const euCountries = ["UK", "GB", "DE", "FR", "IT", "ES", "NL", "SE", "PL", "BE", "TR", "AE", "SA", "EG", "IN"];
129
162
  const feCountries = ["JP", "AU", "SG"];
130
163
 
@@ -138,8 +171,10 @@ function detectRegion(profiles) {
138
171
 
139
172
  // ─── Start local server and wait for callback ───
140
173
 
141
- function waitForCallback(port) {
174
+ function startCallbackServer(port) {
142
175
  return new Promise((resolve, reject) => {
176
+ let settled = false;
177
+
143
178
  const server = http.createServer((req, res) => {
144
179
  if (!req.url.startsWith(CALLBACK_PATH)) {
145
180
  res.writeHead(404);
@@ -154,6 +189,7 @@ function waitForCallback(port) {
154
189
  if (error) {
155
190
  res.writeHead(200, { "Content-Type": "text/html" });
156
191
  res.end("<html><body><h2>Authorization denied.</h2><p>You can close this tab.</p></body></html>");
192
+ settled = true;
157
193
  server.close();
158
194
  reject(new Error(`Amazon returned error: ${error}`));
159
195
  return;
@@ -167,28 +203,33 @@ function waitForCallback(port) {
167
203
 
168
204
  res.writeHead(200, { "Content-Type": "text/html" });
169
205
  res.end("<html><body><h2>Authorization successful!</h2><p>You can close this tab and return to the terminal.</p></body></html>");
206
+ settled = true;
170
207
  server.close();
171
- resolve(code);
208
+ resolve({ code, port });
172
209
  });
173
210
 
174
211
  const timeout = setTimeout(() => {
175
- server.close();
176
- reject(new Error("Timed out waiting for authorization (2 minutes). Please try again."));
212
+ if (!settled) {
213
+ settled = true;
214
+ server.close();
215
+ reject(new Error("Timed out waiting for authorization (2 minutes). Please try again."));
216
+ }
177
217
  }, TIMEOUT_MS);
178
218
 
179
219
  server.on("close", () => clearTimeout(timeout));
180
220
 
181
221
  server.on("error", (err) => {
182
- if (err.code === "EADDRINUSE") {
183
- resolve(null); // Signal to try next port
184
- } else {
185
- reject(err);
222
+ if (!settled) {
223
+ settled = true;
224
+ if (err.code === "EADDRINUSE") {
225
+ resolve(null); // Signal to try next port
226
+ } else {
227
+ reject(err);
228
+ }
186
229
  }
187
230
  });
188
231
 
189
- server.listen(port, () => {
190
- resolve._server = server; // store for reference
191
- });
232
+ server.listen(port);
192
233
  });
193
234
  }
194
235
 
@@ -199,63 +240,72 @@ async function main() {
199
240
  print("\u{1F680} Amazon Ads MCP Setup");
200
241
  print("\u2500".repeat(35));
201
242
 
202
- // Find an available port
203
- let port = 8080;
204
- let code = null;
243
+ // Ask for credentials
244
+ const clientId = await ask("\u2192 Enter your Amazon Ads Client ID: ");
245
+ if (!clientId) {
246
+ print("\n\u274C Client ID is required.");
247
+ process.exit(1);
248
+ }
205
249
 
206
- while (port <= 8082) {
250
+ const clientSecret = await askSecret("\u2192 Enter your Amazon Ads Client Secret: ");
251
+ if (!clientSecret) {
252
+ print("\n\u274C Client Secret is required.");
253
+ process.exit(1);
254
+ }
255
+
256
+ print("");
257
+
258
+ // Find an available port and start callback server
259
+ let result = null;
260
+
261
+ for (let port = 8080; port <= 8082; port++) {
207
262
  const redirectUri = `${REDIRECT_URI_BASE}:${port}${CALLBACK_PATH}`;
208
263
  const authUrl =
209
- `${OAUTH_AUTHORIZE_URL}?client_id=${encodeURIComponent(PPCASSIST_CLIENT_ID)}` +
264
+ `${OAUTH_AUTHORIZE_URL}?client_id=${encodeURIComponent(clientId)}` +
210
265
  `&scope=advertising::campaign_management` +
211
266
  `&response_type=code` +
212
267
  `&redirect_uri=${encodeURIComponent(redirectUri)}`;
213
268
 
214
- const promise = waitForCallback(port);
269
+ const promise = startCallbackServer(port);
215
270
 
216
- // Check if port was available (give server a moment to start)
217
- await new Promise((r) => setTimeout(r, 100));
271
+ // Give server a moment to bind or fail
272
+ await new Promise((r) => setTimeout(r, 150));
218
273
 
219
- // Try to get the code; null means port was in use
220
- const result = await Promise.race([
274
+ // Quick check if port was in use, promise resolves to null immediately
275
+ const quick = await Promise.race([
221
276
  promise,
222
- new Promise((r) => setTimeout(() => r("pending"), 200)),
277
+ new Promise((r) => setTimeout(() => r("waiting"), 300)),
223
278
  ]);
224
279
 
225
- if (result === null) {
280
+ if (quick === null) {
226
281
  print(` Port ${port} in use, trying ${port + 1}...`);
227
- port++;
228
282
  continue;
229
283
  }
230
284
 
231
- // Port is available — open browser and wait
285
+ // Server is listening — open browser
232
286
  print(`\u2192 Opening Amazon authorization in your browser...`);
233
287
  openBrowser(authUrl);
234
288
  print(`\u2192 Waiting for authorization... (press Ctrl+C to cancel)`);
235
289
 
236
- if (result === "pending") {
237
- code = await promise;
238
- } else {
239
- code = result;
240
- }
290
+ result = quick === "waiting" ? await promise : quick;
241
291
  break;
242
292
  }
243
293
 
244
- if (!code) {
294
+ if (!result) {
245
295
  print("\n\u274C Could not find an available port (tried 8080-8082). Please free one and try again.");
246
296
  process.exit(1);
247
297
  }
248
298
 
249
299
  print("\u2705 Authorization received");
250
300
 
251
- // Exchange code for tokens
252
- const redirectUri = `${REDIRECT_URI_BASE}:${port}${CALLBACK_PATH}`;
301
+ // Exchange code for tokens using the actual port that was bound
302
+ const redirectUri = `${REDIRECT_URI_BASE}:${result.port}${CALLBACK_PATH}`;
253
303
  const tokenBody = new URLSearchParams({
254
304
  grant_type: "authorization_code",
255
- code,
305
+ code: result.code,
256
306
  redirect_uri: redirectUri,
257
- client_id: PPCASSIST_CLIENT_ID,
258
- client_secret: PPCASSIST_CLIENT_SECRET,
307
+ client_id: clientId,
308
+ client_secret: clientSecret,
259
309
  }).toString();
260
310
 
261
311
  let tokenData;
@@ -280,7 +330,7 @@ async function main() {
280
330
  try {
281
331
  profiles = await httpGet(PROFILES_URL, {
282
332
  Authorization: `Bearer ${access_token}`,
283
- "Amazon-Advertising-API-ClientId": PPCASSIST_CLIENT_ID,
333
+ "Amazon-Advertising-API-ClientId": clientId,
284
334
  });
285
335
  } catch (err) {
286
336
  print(`\n\u274C Failed to fetch profiles: ${err.message}`);
@@ -303,10 +353,10 @@ async function main() {
303
353
  profiles.forEach((p, i) => {
304
354
  const name = p.accountInfo?.name || `Profile ${p.profileId}`;
305
355
  const cc = p.countryCode || "";
306
- print(` ${i + 1}. ${name} ${cc ? `(${cc})` : ""} profile_id: ${p.profileId}`);
356
+ print(` ${i + 1}. ${name} ${cc ? `(${cc})` : ""} \u2014 profile_id: ${p.profileId}`);
307
357
  });
308
358
 
309
- const answer = await askQuestion(`\u2192 Select a profile [1-${profiles.length}]: `);
359
+ const answer = await ask(`\u2192 Select a profile [1-${profiles.length}]: `);
310
360
  const idx = parseInt(answer, 10) - 1;
311
361
 
312
362
  if (isNaN(idx) || idx < 0 || idx >= profiles.length) {
@@ -344,15 +394,14 @@ async function main() {
344
394
  command: "npx",
345
395
  args: ["-y", "@ppcassist/amazon-ads-mcp"],
346
396
  env: {
347
- CLIENT_ID: PPCASSIST_CLIENT_ID,
348
- CLIENT_SECRET: PPCASSIST_CLIENT_SECRET,
397
+ CLIENT_ID: clientId,
398
+ CLIENT_SECRET: clientSecret,
349
399
  REFRESH_TOKEN: refresh_token,
350
400
  PROFILE_ID: profileId,
351
401
  REGION: region,
352
402
  },
353
403
  };
354
404
 
355
- // Ensure directory exists
356
405
  fs.mkdirSync(configDir, { recursive: true });
357
406
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
358
407