@ppcassist/amazon-ads-mcp 1.0.0 → 1.0.2
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 +25 -3
- package/lib/auth.js +5 -1
- package/lib/proxy.js +15 -3
- package/package.json +4 -2
- package/setup.js +369 -0
package/index.js
CHANGED
|
@@ -76,13 +76,35 @@ async function handleRequest(proxy, request) {
|
|
|
76
76
|
const result = await proxy.forward(request);
|
|
77
77
|
|
|
78
78
|
if (result.type === "stream") {
|
|
79
|
-
// SSE 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
|
+
|
|
80
84
|
result.stream.on("data", (chunk) => {
|
|
81
|
-
|
|
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
|
+
}
|
|
82
97
|
});
|
|
83
98
|
|
|
84
99
|
result.stream.on("end", () => {
|
|
85
|
-
//
|
|
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
|
+
}
|
|
86
108
|
});
|
|
87
109
|
|
|
88
110
|
result.stream.on("error", (err) => {
|
package/lib/auth.js
CHANGED
|
@@ -14,7 +14,11 @@ class TokenManager {
|
|
|
14
14
|
|
|
15
15
|
async init() {
|
|
16
16
|
await this.refresh();
|
|
17
|
-
this.timer = setInterval(() =>
|
|
17
|
+
this.timer = setInterval(() => {
|
|
18
|
+
this.refresh().catch((err) => {
|
|
19
|
+
process.stderr.write(`[auth] Token refresh failed: ${err.message}\n`);
|
|
20
|
+
});
|
|
21
|
+
}, REFRESH_INTERVAL_MS);
|
|
18
22
|
this.timer.unref(); // don't keep process alive just for refresh
|
|
19
23
|
}
|
|
20
24
|
|
package/lib/proxy.js
CHANGED
|
@@ -19,7 +19,20 @@ class McpProxy {
|
|
|
19
19
|
this.endpoint = new URL(endpoint);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
async forward(jsonRpcRequest) {
|
|
22
|
+
async forward(jsonRpcRequest, _isRetry) {
|
|
23
|
+
const result = await this._doRequest(jsonRpcRequest);
|
|
24
|
+
|
|
25
|
+
// Retry once on 401 (token expired between refreshes)
|
|
26
|
+
if (!_isRetry && result.statusCode === 401) {
|
|
27
|
+
process.stderr.write("[proxy] Got 401, refreshing token and retrying\n");
|
|
28
|
+
await this.tokenManager.refresh();
|
|
29
|
+
return this.forward(jsonRpcRequest, true);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_doRequest(jsonRpcRequest) {
|
|
23
36
|
const body = JSON.stringify(jsonRpcRequest);
|
|
24
37
|
const token = this.tokenManager.getToken();
|
|
25
38
|
|
|
@@ -51,9 +64,8 @@ class McpProxy {
|
|
|
51
64
|
res.on("end", () => {
|
|
52
65
|
const raw = Buffer.concat(chunks).toString();
|
|
53
66
|
try {
|
|
54
|
-
resolve({ type: "json", data: JSON.parse(raw) });
|
|
67
|
+
resolve({ type: "json", data: JSON.parse(raw), statusCode: res.statusCode });
|
|
55
68
|
} catch {
|
|
56
|
-
// Return raw text if not valid JSON (error pages, etc.)
|
|
57
69
|
resolve({ type: "raw", data: raw, statusCode: res.statusCode });
|
|
58
70
|
}
|
|
59
71
|
});
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ppcassist/amazon-ads-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
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
|
+
});
|