@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.
- package/index.js +116 -117
- package/package.json +2 -3
- package/setup.js +109 -60
package/index.js
CHANGED
|
@@ -1,149 +1,148 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
34
|
-
const proxy = new McpProxy({ tokenManager, clientId, profileId, region });
|
|
38
|
+
const proxy = new McpProxy({ tokenManager, clientId, profileId, region });
|
|
35
39
|
|
|
36
|
-
|
|
40
|
+
log(`Started (region=${region})`);
|
|
37
41
|
|
|
38
|
-
|
|
39
|
-
let buffer = "";
|
|
42
|
+
let buffer = "";
|
|
40
43
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
process.stdin.setEncoding("utf8");
|
|
45
|
+
process.stdin.on("data", (chunk) => {
|
|
46
|
+
buffer += chunk;
|
|
44
47
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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: {
|
|
124
|
+
error: {
|
|
125
|
+
code: -32000,
|
|
126
|
+
message: `Upstream error (HTTP ${result.statusCode})`,
|
|
127
|
+
},
|
|
116
128
|
});
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
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
|
-
|
|
144
|
-
}
|
|
140
|
+
function writeJsonRpc(obj) {
|
|
141
|
+
process.stdout.write(JSON.stringify(obj) + "\n");
|
|
142
|
+
}
|
|
145
143
|
|
|
146
|
-
main().catch((err) => {
|
|
147
|
-
|
|
148
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
176
|
-
|
|
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 (
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
269
|
+
const promise = startCallbackServer(port);
|
|
215
270
|
|
|
216
|
-
//
|
|
217
|
-
await new Promise((r) => setTimeout(r,
|
|
271
|
+
// Give server a moment to bind or fail
|
|
272
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
218
273
|
|
|
219
|
-
//
|
|
220
|
-
const
|
|
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("
|
|
277
|
+
new Promise((r) => setTimeout(() => r("waiting"), 300)),
|
|
223
278
|
]);
|
|
224
279
|
|
|
225
|
-
if (
|
|
280
|
+
if (quick === null) {
|
|
226
281
|
print(` Port ${port} in use, trying ${port + 1}...`);
|
|
227
|
-
port++;
|
|
228
282
|
continue;
|
|
229
283
|
}
|
|
230
284
|
|
|
231
|
-
//
|
|
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
|
-
|
|
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 (!
|
|
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:
|
|
258
|
-
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":
|
|
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})` : ""}
|
|
356
|
+
print(` ${i + 1}. ${name} ${cc ? `(${cc})` : ""} \u2014 profile_id: ${p.profileId}`);
|
|
307
357
|
});
|
|
308
358
|
|
|
309
|
-
const answer = await
|
|
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:
|
|
348
|
-
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
|
|