@contentstack/mcp 0.1.1 → 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/README.md +241 -36
- package/dist/index.js +1024 -97
- package/package.json +6 -3
package/dist/index.js
CHANGED
|
@@ -7,13 +7,13 @@ import {
|
|
|
7
7
|
CallToolRequestSchema,
|
|
8
8
|
ListToolsRequestSchema
|
|
9
9
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
10
|
-
import
|
|
10
|
+
import axios3 from "axios";
|
|
11
11
|
import dotenv from "dotenv";
|
|
12
12
|
|
|
13
13
|
// package.json
|
|
14
14
|
var package_default = {
|
|
15
15
|
name: "@contentstack/mcp",
|
|
16
|
-
version: "0.
|
|
16
|
+
version: "0.2.0",
|
|
17
17
|
main: "./dist/index.js",
|
|
18
18
|
type: "module",
|
|
19
19
|
publishConfig: {
|
|
@@ -46,9 +46,11 @@ var package_default = {
|
|
|
46
46
|
url: "https://github.com/contentstack/mcp.git"
|
|
47
47
|
},
|
|
48
48
|
dependencies: {
|
|
49
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
49
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
50
50
|
axios: "^1.9.0",
|
|
51
|
-
dotenv: "^16.5.0"
|
|
51
|
+
dotenv: "^16.5.0",
|
|
52
|
+
inquirer: "^12.6.3",
|
|
53
|
+
open: "^10.1.2"
|
|
52
54
|
},
|
|
53
55
|
devDependencies: {
|
|
54
56
|
"@commitlint/cli": "^19.8.1",
|
|
@@ -61,6 +63,7 @@ var package_default = {
|
|
|
61
63
|
"eslint-plugin-only-warn": "^1.1.0",
|
|
62
64
|
husky: "^9.1.7",
|
|
63
65
|
jest: "^29.7.0",
|
|
66
|
+
"lint-staged": "^16.0.0",
|
|
64
67
|
prettier: "^3.5.3",
|
|
65
68
|
"ts-jest": "^29.3.2",
|
|
66
69
|
"ts-node": "^10.9.2",
|
|
@@ -98,10 +101,90 @@ var CDA_URLS = {
|
|
|
98
101
|
GCP_NA: "https://gcp-na-cdn.contentstack.com",
|
|
99
102
|
GCP_EU: "https://gcp-eu-cdn.contentstack.com"
|
|
100
103
|
};
|
|
104
|
+
var BRANDKIT_URLS = {
|
|
105
|
+
ai: {
|
|
106
|
+
NA: "https://ai.contentstack.com",
|
|
107
|
+
EU: "https://eu-ai.contentstack.com",
|
|
108
|
+
AZURE_NA: "https://azure-na-ai.contentstack.com",
|
|
109
|
+
AZURE_EU: "https://azure-eu-ai.contentstack.com",
|
|
110
|
+
GCP_NA: "https://gcp-na-ai.contentstack.com",
|
|
111
|
+
GCP_EU: "https://gcp-eu-ai.contentstack.com"
|
|
112
|
+
},
|
|
113
|
+
"brand-kits-api": {
|
|
114
|
+
NA: "https://brand-kits-api.contentstack.com",
|
|
115
|
+
EU: "https://eu-brand-kits-api.contentstack.com",
|
|
116
|
+
AZURE_NA: "https://azure-na-brand-kits-api.contentstack.com",
|
|
117
|
+
AZURE_EU: "https://azure-eu-brand-kits-api.contentstack.com",
|
|
118
|
+
GCP_NA: "https://gcp-na-brand-kits-api.contentstack.com",
|
|
119
|
+
GCP_EU: "https://gcp-eu-brand-kits-api.contentstack.com"
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
var OAUTH_URLS = {
|
|
123
|
+
NA: {
|
|
124
|
+
name: "North America (AWS)",
|
|
125
|
+
uiHost: "https://app.contentstack.com",
|
|
126
|
+
devHub: "https://developerhub-api.contentstack.com"
|
|
127
|
+
},
|
|
128
|
+
EU: {
|
|
129
|
+
name: "Europe (AWS)",
|
|
130
|
+
uiHost: "https://eu-app.contentstack.com",
|
|
131
|
+
devHub: "https://eu-developerhub-api.contentstack.com"
|
|
132
|
+
},
|
|
133
|
+
AZURE_NA: {
|
|
134
|
+
name: "North America (Azure)",
|
|
135
|
+
uiHost: "https://azure-na-app.contentstack.com",
|
|
136
|
+
devHub: "https://azure-na-developerhub-api.contentstack.com"
|
|
137
|
+
},
|
|
138
|
+
AZURE_EU: {
|
|
139
|
+
name: "Europe (Azure)",
|
|
140
|
+
uiHost: "https://azure-eu-app.contentstack.com",
|
|
141
|
+
devHub: "https://azure-eu-developerhub-api.contentstack.com"
|
|
142
|
+
},
|
|
143
|
+
GCP_NA: {
|
|
144
|
+
name: "North America (GCP)",
|
|
145
|
+
uiHost: "https://gcp-na-app.contentstack.com",
|
|
146
|
+
devHub: "https://gcp-na-developerhub-api.contentstack.com"
|
|
147
|
+
},
|
|
148
|
+
GCP_EU: {
|
|
149
|
+
name: "Europe (GCP)",
|
|
150
|
+
uiHost: "https://gcp-eu-app.contentstack.com",
|
|
151
|
+
devHub: "https://gcp-eu-developerhub-api.contentstack.com"
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
var LYTICS_URL = "https://api.lytics.io";
|
|
155
|
+
var PERSONALIZE_URLS = {
|
|
156
|
+
NA: "https://personalize-api.contentstack.com",
|
|
157
|
+
EU: "https://eu-personalize-api.contentstack.com",
|
|
158
|
+
AZURE_NA: "https://azure-na-personalize-api.contentstack.com",
|
|
159
|
+
AZURE_EU: "https://azure-eu-personalize-api.contentstack.com",
|
|
160
|
+
GCP_NA: "https://gcp-na-personalize-api.contentstack.com",
|
|
161
|
+
GCP_EU: "https://gcp-eu-personalize-api.contentstack.com"
|
|
162
|
+
};
|
|
163
|
+
var ANALYTICS_URLS = {
|
|
164
|
+
NA: "https://app.contentstack.com/analytics",
|
|
165
|
+
EU: "https://eu-app.contentstack.com/analytics",
|
|
166
|
+
AZURE_NA: "https://azure-na-app.contentstack.com/analytics",
|
|
167
|
+
AZURE_EU: "https://azure-eu-app.contentstack.com/analytics",
|
|
168
|
+
GCP_NA: "https://gcp-na-app.contentstack.com/analytics",
|
|
169
|
+
GCP_EU: "https://gcp-eu-app.contentstack.com/analytics"
|
|
170
|
+
};
|
|
171
|
+
var LAUNCH_URLS = {
|
|
172
|
+
NA: "https://launch-api.contentstack.com/manage",
|
|
173
|
+
EU: "https://eu-launch-api.contentstack.com/manage",
|
|
174
|
+
AZURE_NA: "https://azure-na-launch-api.contentstack.com/manage",
|
|
175
|
+
AZURE_EU: "https://azure-eu-launch-api.contentstack.com/manage",
|
|
176
|
+
GCP_NA: "https://gcp-na-launch-api.contentstack.com/manage",
|
|
177
|
+
GCP_EU: "https://gcp-eu-launch-api.contentstack.com/manage"
|
|
178
|
+
};
|
|
101
179
|
var GroupEnum = {
|
|
102
180
|
CMA: "cma",
|
|
103
181
|
CDA: "cda",
|
|
104
|
-
ALL: "all"
|
|
182
|
+
ALL: "all",
|
|
183
|
+
BRANDKIT: "brandkit",
|
|
184
|
+
LYTICS: "lytics",
|
|
185
|
+
ANALYTICS: "analytics",
|
|
186
|
+
PERSONALIZE: "personalize",
|
|
187
|
+
LAUNCH: "launch"
|
|
105
188
|
};
|
|
106
189
|
var apiVersionHeaders = [
|
|
107
190
|
"publish_variants_of_an_entry",
|
|
@@ -109,64 +192,706 @@ var apiVersionHeaders = [
|
|
|
109
192
|
"unpublish_an_entry"
|
|
110
193
|
];
|
|
111
194
|
var TOOL_URLS = {
|
|
112
|
-
cma: "
|
|
113
|
-
cda: "
|
|
195
|
+
cma: "https://mcp.contentstack.com/cma/tools",
|
|
196
|
+
cda: "https://mcp.contentstack.com/cda/tools",
|
|
197
|
+
brandkit: "https://mcp.contentstack.com/brandkit/tools",
|
|
198
|
+
lytics: "https://mcp.contentstack.com/lytics/tools",
|
|
199
|
+
personalize: "https://mcp.contentstack.com/personalize/tools",
|
|
200
|
+
analytics: "https://mcp.contentstack.com/analytics/tools",
|
|
201
|
+
launch: "https://mcp.contentstack.com/launch/tools"
|
|
114
202
|
};
|
|
115
203
|
|
|
116
204
|
// src/utils/index.ts
|
|
205
|
+
import axios2 from "axios";
|
|
206
|
+
|
|
207
|
+
// src/oauth.ts
|
|
117
208
|
import axios from "axios";
|
|
118
|
-
|
|
209
|
+
import http from "http";
|
|
210
|
+
import url from "url";
|
|
211
|
+
import fs from "fs/promises";
|
|
212
|
+
import path from "path";
|
|
213
|
+
import os from "os";
|
|
214
|
+
import open from "open";
|
|
215
|
+
import crypto from "crypto";
|
|
216
|
+
import inquirer from "inquirer";
|
|
217
|
+
var ContentstackOAuthHandler = class _ContentstackOAuthHandler {
|
|
218
|
+
appId;
|
|
219
|
+
clientId;
|
|
220
|
+
redirectUri;
|
|
221
|
+
responseType = "code";
|
|
222
|
+
port = 8184;
|
|
223
|
+
region;
|
|
224
|
+
codeVerifier;
|
|
225
|
+
codeChallenge;
|
|
226
|
+
oauthBaseUrl;
|
|
227
|
+
devHubUrl;
|
|
228
|
+
configDir;
|
|
229
|
+
configFile;
|
|
230
|
+
authCompleted = false;
|
|
231
|
+
constructor() {
|
|
232
|
+
this.appId = process.env.CONTENTSTACK_OAUTH_APP_ID ?? "68340a606295230012cb88fd";
|
|
233
|
+
this.clientId = process.env.CONTENTSTACK_OAUTH_CLIENT_ID ?? "cGQZujH3Y_oYkf59";
|
|
234
|
+
this.redirectUri = process.env.CONTENTSTACK_OAUTH_REDIRECT_URI ?? "http://localhost:8184";
|
|
235
|
+
this.configDir = this.getConfigDir();
|
|
236
|
+
this.configFile = path.join(this.configDir, "oauth-config.json");
|
|
237
|
+
}
|
|
238
|
+
getConfigDir() {
|
|
239
|
+
const platform = process.platform;
|
|
240
|
+
const dirName = "ContentstackMCP";
|
|
241
|
+
if (platform === "win32") {
|
|
242
|
+
return path.join(os.homedir(), "AppData", "Local", dirName);
|
|
243
|
+
} else if (platform === "darwin") {
|
|
244
|
+
return path.join(os.homedir(), "Library", "Application Support", dirName);
|
|
245
|
+
} else {
|
|
246
|
+
return path.join(os.homedir(), ".config", dirName);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
async showMainMenu() {
|
|
250
|
+
try {
|
|
251
|
+
await this.ensureConfigDir();
|
|
252
|
+
let exitProgram = false;
|
|
253
|
+
while (!exitProgram) {
|
|
254
|
+
const { action } = await inquirer.prompt([
|
|
255
|
+
{
|
|
256
|
+
type: "list",
|
|
257
|
+
name: "action",
|
|
258
|
+
message: "Select an action:",
|
|
259
|
+
choices: [
|
|
260
|
+
{ name: "Authorization", value: "auth" },
|
|
261
|
+
{ name: "Exit", value: "exit" }
|
|
262
|
+
]
|
|
263
|
+
}
|
|
264
|
+
]);
|
|
265
|
+
switch (action) {
|
|
266
|
+
case "auth":
|
|
267
|
+
await this.showAuthMenu();
|
|
268
|
+
break;
|
|
269
|
+
case "exit":
|
|
270
|
+
exitProgram = true;
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
} catch (error) {
|
|
275
|
+
const err = error;
|
|
276
|
+
console.error("Error:", err.message);
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
async showAuthMenu() {
|
|
281
|
+
const { authAction } = await inquirer.prompt([
|
|
282
|
+
{
|
|
283
|
+
type: "list",
|
|
284
|
+
name: "authAction",
|
|
285
|
+
message: "Authorization actions:",
|
|
286
|
+
choices: [
|
|
287
|
+
{ name: "Login", value: "login" },
|
|
288
|
+
{ name: "Reauthorize", value: "reauth" },
|
|
289
|
+
{ name: "Logout", value: "logout" },
|
|
290
|
+
{ name: "Back", value: "back" }
|
|
291
|
+
]
|
|
292
|
+
}
|
|
293
|
+
]);
|
|
294
|
+
switch (authAction) {
|
|
295
|
+
case "login":
|
|
296
|
+
await this.login();
|
|
297
|
+
break;
|
|
298
|
+
case "logout":
|
|
299
|
+
await this.logout();
|
|
300
|
+
process.exit(0);
|
|
301
|
+
break;
|
|
302
|
+
case "reauth":
|
|
303
|
+
await this.reauthorize();
|
|
304
|
+
break;
|
|
305
|
+
case "back":
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
async login() {
|
|
310
|
+
try {
|
|
311
|
+
await this.selectRegion();
|
|
312
|
+
await this.startOAuthFlow();
|
|
313
|
+
} catch (error) {
|
|
314
|
+
const err = error;
|
|
315
|
+
console.error("Authentication failed:", err.message);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
async reauthorize() {
|
|
319
|
+
try {
|
|
320
|
+
const config = await this.loadConfig();
|
|
321
|
+
if (config && config.region) {
|
|
322
|
+
this.applyRegion(config.region);
|
|
323
|
+
await this.saveConfig({ region: config.region });
|
|
324
|
+
console.log("Existing tokens cleared. Starting reauthorization...");
|
|
325
|
+
await this.startOAuthFlow();
|
|
326
|
+
} else {
|
|
327
|
+
console.log("No existing configuration found. Starting login flow...");
|
|
328
|
+
await this.login();
|
|
329
|
+
}
|
|
330
|
+
} catch (error) {
|
|
331
|
+
const err = error;
|
|
332
|
+
console.error("Reauthorization failed:", err.message);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
applyRegion(region) {
|
|
336
|
+
this.region = region;
|
|
337
|
+
this.oauthBaseUrl = OAUTH_URLS[region]?.uiHost;
|
|
338
|
+
this.devHubUrl = `${OAUTH_URLS[region]?.devHub}`;
|
|
339
|
+
}
|
|
340
|
+
async logout() {
|
|
341
|
+
try {
|
|
342
|
+
const config = await this.loadConfig();
|
|
343
|
+
if (!config || !config.access_token) {
|
|
344
|
+
console.error("No active session found.");
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (config.region) {
|
|
348
|
+
this.applyRegion(config.region);
|
|
349
|
+
try {
|
|
350
|
+
const authorizationId = await this.getOAuthAppAuthorization(
|
|
351
|
+
config.access_token,
|
|
352
|
+
config.user_uid,
|
|
353
|
+
config.organization_uid
|
|
354
|
+
);
|
|
355
|
+
if (authorizationId) {
|
|
356
|
+
await this.revokeOAuthAppAuthorization(
|
|
357
|
+
authorizationId,
|
|
358
|
+
config.access_token,
|
|
359
|
+
config.organization_uid
|
|
360
|
+
);
|
|
361
|
+
console.log("Successfully revoked OAuth app authorization.");
|
|
362
|
+
}
|
|
363
|
+
} catch (error) {
|
|
364
|
+
console.warn(
|
|
365
|
+
`Error revoking OAuth app authorization: ${error.message}. Proceeding with local logout.`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
await fs.unlink(this.configFile).catch(() => {
|
|
370
|
+
});
|
|
371
|
+
console.log("Logged out successfully. All configuration removed.");
|
|
372
|
+
} catch (error) {
|
|
373
|
+
console.error(`Logout failed: ${error.message}`);
|
|
374
|
+
throw new Error("Failed to complete logout");
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
async getOAuthAppAuthorization(accessToken, userUid, organizationUid) {
|
|
378
|
+
try {
|
|
379
|
+
const headers = {
|
|
380
|
+
authorization: `Bearer ${accessToken}`,
|
|
381
|
+
organization_uid: organizationUid,
|
|
382
|
+
"Content-type": "application/json"
|
|
383
|
+
};
|
|
384
|
+
const response = await axios.get(
|
|
385
|
+
`${this.devHubUrl}/manifests/${this.appId}/authorizations`,
|
|
386
|
+
{ headers }
|
|
387
|
+
);
|
|
388
|
+
const data = response.data;
|
|
389
|
+
if (data?.data?.length > 0) {
|
|
390
|
+
if (!userUid) {
|
|
391
|
+
throw new Error("Unable to determine user UID for authorization");
|
|
392
|
+
}
|
|
393
|
+
const currentUserAuthorization = data.data.filter((element) => element.user.uid === userUid) || [];
|
|
394
|
+
if (currentUserAuthorization.length === 0) {
|
|
395
|
+
console.warn("No authorizations found for current user!");
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
return currentUserAuthorization[0].authorization_uid;
|
|
399
|
+
} else {
|
|
400
|
+
console.warn("No authorizations found for the app!");
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
} catch (error) {
|
|
404
|
+
console.error(`Error getting OAuth app authorizations: ${error.message}`);
|
|
405
|
+
throw error;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
async revokeOAuthAppAuthorization(authorizationId, accessToken, organizationUid) {
|
|
409
|
+
if (!authorizationId || authorizationId.length < 1) {
|
|
410
|
+
throw new Error("Invalid authorization ID");
|
|
411
|
+
}
|
|
412
|
+
try {
|
|
413
|
+
const headers = {
|
|
414
|
+
authorization: `Bearer ${accessToken}`,
|
|
415
|
+
organization_uid: organizationUid,
|
|
416
|
+
"Content-type": "application/json"
|
|
417
|
+
};
|
|
418
|
+
await axios.delete(
|
|
419
|
+
`${this.devHubUrl}/manifests/${this.appId}/authorizations/${authorizationId}`,
|
|
420
|
+
{ headers }
|
|
421
|
+
);
|
|
422
|
+
console.log(`Successfully revoked authorization ID: ${authorizationId}`);
|
|
423
|
+
} catch (error) {
|
|
424
|
+
console.error(`Error revoking OAuth app authorization: ${error.message}`);
|
|
425
|
+
throw error;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
static async getAuthHeaders() {
|
|
429
|
+
const handler = new _ContentstackOAuthHandler();
|
|
430
|
+
const tokenData = await handler.ensureValidToken();
|
|
431
|
+
if (!tokenData) {
|
|
432
|
+
throw new Error(
|
|
433
|
+
"Not authenticated. Please run the authentication command first."
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
return {
|
|
437
|
+
Authorization: `Bearer ${tokenData.access_token}`,
|
|
438
|
+
organization_uid: tokenData.organization_uid
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
async selectRegion() {
|
|
442
|
+
const regionKeys = Object.keys(OAUTH_URLS);
|
|
443
|
+
const choices = regionKeys.map((key) => ({
|
|
444
|
+
name: `${OAUTH_URLS[key].name} (${key})`,
|
|
445
|
+
value: key
|
|
446
|
+
}));
|
|
447
|
+
const { selectedRegion } = await inquirer.prompt([
|
|
448
|
+
{
|
|
449
|
+
type: "list",
|
|
450
|
+
name: "selectedRegion",
|
|
451
|
+
message: "Select your Contentstack region:",
|
|
452
|
+
choices
|
|
453
|
+
}
|
|
454
|
+
]);
|
|
455
|
+
this.region = selectedRegion;
|
|
456
|
+
if (!this.region) {
|
|
457
|
+
console.error("Invalid region selected.");
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
this.oauthBaseUrl = OAUTH_URLS[this.region]?.uiHost;
|
|
461
|
+
this.devHubUrl = `${OAUTH_URLS[this.region]?.devHub}/apps`;
|
|
462
|
+
await this.saveConfig({ region: this.region });
|
|
463
|
+
console.log(`Selected region: ${OAUTH_URLS[this.region]?.name}`);
|
|
464
|
+
}
|
|
465
|
+
async saveConfig(config) {
|
|
466
|
+
await fs.writeFile(this.configFile, JSON.stringify(config, null, 2));
|
|
467
|
+
}
|
|
468
|
+
async loadConfig() {
|
|
469
|
+
try {
|
|
470
|
+
const data = await fs.readFile(this.configFile, "utf8");
|
|
471
|
+
return JSON.parse(data);
|
|
472
|
+
} catch (error) {
|
|
473
|
+
if (error.code === "ENOENT") {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
throw new Error(`Failed to load config: ${error.message}`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
async ensureConfigDir() {
|
|
480
|
+
try {
|
|
481
|
+
await fs.mkdir(this.configDir, { recursive: true });
|
|
482
|
+
} catch (error) {
|
|
483
|
+
if (error.code === "EACCES") {
|
|
484
|
+
throw new Error(
|
|
485
|
+
`Permission denied when creating config directory at ${this.configDir}. Please check your permissions.`
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
throw error;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
async startOAuthFlow() {
|
|
492
|
+
if (!this.region) {
|
|
493
|
+
console.log("Region not configured. Please select a region first.");
|
|
494
|
+
await this.selectRegion();
|
|
495
|
+
}
|
|
496
|
+
this.codeVerifier = this.generateCodeVerifier();
|
|
497
|
+
this.codeChallenge = await this.generateCodeChallenge(this.codeVerifier);
|
|
498
|
+
this.authCompleted = false;
|
|
499
|
+
const server = this.createHTTPServer();
|
|
500
|
+
try {
|
|
501
|
+
await this.openAuthorizationUrl();
|
|
502
|
+
let attempts = 0;
|
|
503
|
+
const maxAttempts = 120;
|
|
504
|
+
console.log("Waiting for authentication to complete...");
|
|
505
|
+
while (!this.authCompleted && attempts < maxAttempts) {
|
|
506
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
507
|
+
attempts++;
|
|
508
|
+
}
|
|
509
|
+
if (server.listening) {
|
|
510
|
+
server.close();
|
|
511
|
+
if (!this.authCompleted) {
|
|
512
|
+
console.log("Authentication timed out. Please try again.");
|
|
513
|
+
console.log(
|
|
514
|
+
"If you're experiencing network issues, try running the command again."
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if (this.authCompleted) {
|
|
519
|
+
console.log("Authentication completed successfully.");
|
|
520
|
+
process.exit(0);
|
|
521
|
+
}
|
|
522
|
+
} catch (error) {
|
|
523
|
+
console.error("OAuth flow failed:", error);
|
|
524
|
+
if (server.listening) {
|
|
525
|
+
server.close();
|
|
526
|
+
}
|
|
527
|
+
throw error;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
generateCodeVerifier(length = 128) {
|
|
531
|
+
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
|
|
532
|
+
return Array.from(
|
|
533
|
+
{ length },
|
|
534
|
+
() => charset.charAt(Math.floor(Math.random() * charset.length))
|
|
535
|
+
).join("");
|
|
536
|
+
}
|
|
537
|
+
async generateCodeChallenge(codeVerifier) {
|
|
538
|
+
const hash = crypto.createHash("sha256");
|
|
539
|
+
hash.update(codeVerifier);
|
|
540
|
+
const hashBuffer = hash.digest();
|
|
541
|
+
const base64String = hashBuffer.toString("base64");
|
|
542
|
+
return base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
543
|
+
}
|
|
544
|
+
createHTTPServer() {
|
|
545
|
+
let requestHandled = false;
|
|
546
|
+
const server = http.createServer(
|
|
547
|
+
async (req, res) => {
|
|
548
|
+
if (req.url === "/favicon.ico") {
|
|
549
|
+
res.writeHead(204);
|
|
550
|
+
res.end();
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (requestHandled) {
|
|
554
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
555
|
+
res.end(
|
|
556
|
+
"<html><body><h3>Request already processed.</h3><p>You can close this window and return to the terminal.</p></body></html>"
|
|
557
|
+
);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
const queryObject = url.parse(req.url ?? "", true).query;
|
|
561
|
+
if (!queryObject.code) {
|
|
562
|
+
console.error(
|
|
563
|
+
"Error occurred while logging in with OAuth. No authorization code received."
|
|
564
|
+
);
|
|
565
|
+
this.sendErrorResponse(res);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
requestHandled = true;
|
|
569
|
+
console.log("Authorization code successfully received.");
|
|
570
|
+
try {
|
|
571
|
+
await this.exchangeCodeForToken(queryObject.code);
|
|
572
|
+
this.sendSuccessResponse(res);
|
|
573
|
+
this.authCompleted = true;
|
|
574
|
+
setTimeout(() => {
|
|
575
|
+
if (server.listening) {
|
|
576
|
+
server.close(() => {
|
|
577
|
+
console.log("OAuth flow completed successfully.");
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
}, 1e3);
|
|
581
|
+
} catch (error) {
|
|
582
|
+
const err = error;
|
|
583
|
+
console.error("Error exchanging code for token:", err.message);
|
|
584
|
+
this.sendErrorResponse(res);
|
|
585
|
+
setTimeout(() => {
|
|
586
|
+
if (server.listening) {
|
|
587
|
+
server.close();
|
|
588
|
+
}
|
|
589
|
+
}, 1e3);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
);
|
|
593
|
+
server.listen(this.port, () => {
|
|
594
|
+
console.log(`Waiting for authorization response on port ${this.port}...`);
|
|
595
|
+
});
|
|
596
|
+
server.on("error", (err) => {
|
|
597
|
+
console.error("Server error:", err);
|
|
598
|
+
if (err.code === "EADDRINUSE") {
|
|
599
|
+
console.error(
|
|
600
|
+
`Port ${this.port} is already in use. Please ensure no other instances are running.`
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
process.exit(1);
|
|
604
|
+
});
|
|
605
|
+
return server;
|
|
606
|
+
}
|
|
607
|
+
sendSuccessResponse(res) {
|
|
608
|
+
const successHtml = `
|
|
609
|
+
<!DOCTYPE html>
|
|
610
|
+
<html>
|
|
611
|
+
<head>
|
|
612
|
+
<title>Contentstack MCP - Authorization Successful</title>
|
|
613
|
+
<style>
|
|
614
|
+
body { font-family: Arial, sans-serif; text-align: center; margin-top: 100px; }
|
|
615
|
+
h1 { color: #6c5ce7; }
|
|
616
|
+
p { color: #475161; margin-bottom: 20px; }
|
|
617
|
+
a { color: #6c5ce7; text-decoration: none; }
|
|
618
|
+
</style>
|
|
619
|
+
</head>
|
|
620
|
+
<body>
|
|
621
|
+
<h1>Successfully authorized!</h1>
|
|
622
|
+
<p style="font-size: 16px; font-weight: 600">You can close this window and return to the terminal.</p>
|
|
623
|
+
<p>
|
|
624
|
+
You can review the access permissions on the
|
|
625
|
+
<a href="${this.oauthBaseUrl}/#!/marketplace/authorized-apps" target="_blank">Authorized Apps page</a>.
|
|
626
|
+
</p>
|
|
627
|
+
</body>
|
|
628
|
+
</html>`;
|
|
629
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
630
|
+
res.end(successHtml);
|
|
631
|
+
}
|
|
632
|
+
sendErrorResponse(res) {
|
|
633
|
+
const errorHtml = `
|
|
634
|
+
<!DOCTYPE html>
|
|
635
|
+
<html>
|
|
636
|
+
<head>
|
|
637
|
+
<title>Contentstack MCP - Authorization Failed</title>
|
|
638
|
+
<style>
|
|
639
|
+
body { font-family: Arial, sans-serif; text-align: center; margin-top: 100px; }
|
|
640
|
+
h1 { color: #e74c3c; }
|
|
641
|
+
p { color: #475161; }
|
|
642
|
+
</style>
|
|
643
|
+
</head>
|
|
644
|
+
<body>
|
|
645
|
+
<h1>Authentication Failed</h1>
|
|
646
|
+
<p>Please try again by rerunning the authentication command.</p>
|
|
647
|
+
</body>
|
|
648
|
+
</html>`;
|
|
649
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
650
|
+
res.end(errorHtml);
|
|
651
|
+
}
|
|
652
|
+
async openAuthorizationUrl() {
|
|
653
|
+
if (!this.oauthBaseUrl) throw new Error("oauthBaseUrl not set");
|
|
654
|
+
const authUrl = new URL(
|
|
655
|
+
`${this.oauthBaseUrl}/#!/apps/${this.appId}/authorize`
|
|
656
|
+
);
|
|
657
|
+
authUrl.searchParams.set("response_type", this.responseType);
|
|
658
|
+
authUrl.searchParams.set("client_id", this.clientId);
|
|
659
|
+
authUrl.searchParams.set("redirect_uri", this.redirectUri);
|
|
660
|
+
authUrl.searchParams.set("code_challenge", this.codeChallenge ?? "");
|
|
661
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
662
|
+
const authUrlString = authUrl.toString();
|
|
663
|
+
console.log("Opening browser to authorize application...");
|
|
664
|
+
console.log(
|
|
665
|
+
"If the browser does not open automatically, please open this URL:"
|
|
666
|
+
);
|
|
667
|
+
console.log("\x1B[36m%s\x1B[0m", authUrlString);
|
|
668
|
+
try {
|
|
669
|
+
await open(authUrlString);
|
|
670
|
+
} catch (error) {
|
|
671
|
+
console.error(
|
|
672
|
+
"Failed to open browser automatically. Please copy and paste the URL manually."
|
|
673
|
+
);
|
|
674
|
+
console.log("\x1B[36m%s\x1B[0m", authUrlString);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
async exchangeCodeForToken(code) {
|
|
678
|
+
const params = new URLSearchParams({
|
|
679
|
+
grant_type: "authorization_code",
|
|
680
|
+
client_id: this.clientId,
|
|
681
|
+
code_verifier: this.codeVerifier ?? "",
|
|
682
|
+
redirect_uri: this.redirectUri,
|
|
683
|
+
code
|
|
684
|
+
});
|
|
685
|
+
try {
|
|
686
|
+
const response = await axios.post(
|
|
687
|
+
`${this.devHubUrl}/token`,
|
|
688
|
+
params,
|
|
689
|
+
{
|
|
690
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" }
|
|
691
|
+
}
|
|
692
|
+
);
|
|
693
|
+
const tokenData = response.data;
|
|
694
|
+
if (!tokenData.access_token) {
|
|
695
|
+
throw new Error("Invalid token response");
|
|
696
|
+
}
|
|
697
|
+
const existingConfig = await this.loadConfig();
|
|
698
|
+
const region = this.region || existingConfig?.region;
|
|
699
|
+
if (!region) {
|
|
700
|
+
throw new Error("Region not specified");
|
|
701
|
+
}
|
|
702
|
+
await this.saveConfig({
|
|
703
|
+
...tokenData,
|
|
704
|
+
region,
|
|
705
|
+
token_issued_at: Date.now()
|
|
706
|
+
});
|
|
707
|
+
console.log("Access token obtained successfully.");
|
|
708
|
+
return tokenData;
|
|
709
|
+
} catch (error) {
|
|
710
|
+
console.error(
|
|
711
|
+
"Token exchange failed:",
|
|
712
|
+
error.response?.data || error.message
|
|
713
|
+
);
|
|
714
|
+
throw new Error("Failed to exchange authorization code for tokens");
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
async ensureValidToken() {
|
|
718
|
+
const config = await this.loadConfig();
|
|
719
|
+
if (!config || !config.access_token || !config.region) return null;
|
|
720
|
+
const region = config.region;
|
|
721
|
+
this.applyRegion(region);
|
|
722
|
+
const now = Date.now();
|
|
723
|
+
const expiryTime = config.token_issued_at + config.expires_in * 1e3;
|
|
724
|
+
if (now >= expiryTime - 5 * 60 * 1e3) {
|
|
725
|
+
return this.refreshToken(config.refresh_token);
|
|
726
|
+
}
|
|
727
|
+
return config;
|
|
728
|
+
}
|
|
729
|
+
async refreshToken(refreshToken) {
|
|
730
|
+
const params = new URLSearchParams({
|
|
731
|
+
grant_type: "refresh_token",
|
|
732
|
+
client_id: this.clientId,
|
|
733
|
+
refresh_token: refreshToken,
|
|
734
|
+
redirect_uri: this.redirectUri
|
|
735
|
+
});
|
|
736
|
+
try {
|
|
737
|
+
const response = await axios.post(
|
|
738
|
+
`${this.devHubUrl}/token`,
|
|
739
|
+
params,
|
|
740
|
+
{
|
|
741
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" }
|
|
742
|
+
}
|
|
743
|
+
);
|
|
744
|
+
const tokenData = response.data;
|
|
745
|
+
if (!tokenData.access_token) throw new Error("Invalid token response");
|
|
746
|
+
const existingConfig = await this.loadConfig();
|
|
747
|
+
await this.saveConfig({
|
|
748
|
+
...existingConfig,
|
|
749
|
+
...tokenData,
|
|
750
|
+
refresh_token: tokenData.refresh_token ?? existingConfig?.refresh_token,
|
|
751
|
+
token_issued_at: Date.now()
|
|
752
|
+
});
|
|
753
|
+
return await this.loadConfig();
|
|
754
|
+
} catch (error) {
|
|
755
|
+
console.error(
|
|
756
|
+
"Token refresh failed:",
|
|
757
|
+
error.response?.data || error.message
|
|
758
|
+
);
|
|
759
|
+
throw new Error("Failed to refresh access token");
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
var oauth_default = ContentstackOAuthHandler;
|
|
764
|
+
|
|
765
|
+
// src/utils/index.ts
|
|
766
|
+
var getBaseUrl = (region, group, subgroup) => {
|
|
119
767
|
if (group === GroupEnum.CMA) {
|
|
120
768
|
return CMA_URLS[region];
|
|
121
769
|
} else if (group === GroupEnum.CDA) {
|
|
122
770
|
return CDA_URLS[region];
|
|
771
|
+
} else if (group === "brandkit") {
|
|
772
|
+
if (!subgroup) {
|
|
773
|
+
throw new Error("Subgroup is required for brandkit group");
|
|
774
|
+
}
|
|
775
|
+
return BRANDKIT_URLS[subgroup][region];
|
|
776
|
+
} else if (group === GroupEnum.LYTICS) {
|
|
777
|
+
return LYTICS_URL;
|
|
778
|
+
} else if (group === GroupEnum.PERSONALIZE) {
|
|
779
|
+
return PERSONALIZE_URLS[region];
|
|
780
|
+
} else if (group === GroupEnum.ANALYTICS) {
|
|
781
|
+
return ANALYTICS_URLS[region];
|
|
782
|
+
} else if (group === GroupEnum.LAUNCH) {
|
|
783
|
+
return LAUNCH_URLS[region];
|
|
123
784
|
} else {
|
|
124
785
|
throw new Error(`Invalid group: ${group}`);
|
|
125
786
|
}
|
|
126
787
|
};
|
|
127
|
-
var getTools = async (
|
|
788
|
+
var getTools = async (groups) => {
|
|
128
789
|
try {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
790
|
+
const urlMapping = {
|
|
791
|
+
[GroupEnum.CMA]: TOOL_URLS.cma,
|
|
792
|
+
[GroupEnum.CDA]: TOOL_URLS.cda,
|
|
793
|
+
[GroupEnum.BRANDKIT]: TOOL_URLS.brandkit,
|
|
794
|
+
[GroupEnum.LYTICS]: TOOL_URLS.lytics,
|
|
795
|
+
[GroupEnum.PERSONALIZE]: TOOL_URLS.personalize,
|
|
796
|
+
[GroupEnum.ANALYTICS]: TOOL_URLS.analytics,
|
|
797
|
+
[GroupEnum.LAUNCH]: TOOL_URLS.launch
|
|
798
|
+
};
|
|
799
|
+
if (groups.includes(GroupEnum.ALL)) {
|
|
800
|
+
const [
|
|
801
|
+
cmaRes,
|
|
802
|
+
cdaRes,
|
|
803
|
+
brandkitRes,
|
|
804
|
+
lyticsRes,
|
|
805
|
+
personalizeRes,
|
|
806
|
+
analyticsRes,
|
|
807
|
+
launchRes
|
|
808
|
+
] = await Promise.all([
|
|
809
|
+
axios2.get(TOOL_URLS.cma),
|
|
810
|
+
axios2.get(TOOL_URLS.cda),
|
|
811
|
+
axios2.get(TOOL_URLS.brandkit),
|
|
812
|
+
axios2.get(TOOL_URLS.lytics),
|
|
813
|
+
axios2.get(TOOL_URLS.personalize),
|
|
814
|
+
axios2.get(TOOL_URLS.analytics),
|
|
815
|
+
axios2.get(TOOL_URLS.launch)
|
|
816
|
+
]);
|
|
817
|
+
return {
|
|
818
|
+
...cmaRes.data,
|
|
819
|
+
...cdaRes.data,
|
|
820
|
+
...brandkitRes.data,
|
|
821
|
+
...lyticsRes.data,
|
|
822
|
+
...personalizeRes.data,
|
|
823
|
+
...analyticsRes.data,
|
|
824
|
+
...launchRes.data
|
|
825
|
+
};
|
|
150
826
|
}
|
|
827
|
+
const responses = await Promise.all(
|
|
828
|
+
groups.map((group) => {
|
|
829
|
+
const url2 = urlMapping[group];
|
|
830
|
+
if (!url2) {
|
|
831
|
+
throw new Error(`Invalid group: ${group}`);
|
|
832
|
+
}
|
|
833
|
+
return axios2.get(url2);
|
|
834
|
+
})
|
|
835
|
+
);
|
|
836
|
+
return responses.reduce((allTools, response) => {
|
|
837
|
+
return { ...allTools, ...response.data };
|
|
838
|
+
}, {});
|
|
151
839
|
} catch (error) {
|
|
152
|
-
throw new Error(
|
|
840
|
+
throw new Error(
|
|
841
|
+
`Failed to fetch tools: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
842
|
+
);
|
|
153
843
|
}
|
|
154
844
|
};
|
|
155
|
-
function
|
|
845
|
+
function resolvePathParam(argKey, args, options) {
|
|
846
|
+
if (argKey.startsWith("$")) {
|
|
847
|
+
const realKey = argKey.slice(1);
|
|
848
|
+
return options[realKey];
|
|
849
|
+
}
|
|
850
|
+
return args[argKey] ?? options[argKey];
|
|
851
|
+
}
|
|
852
|
+
async function buildContentstackRequest(actionMapper, args, groupName, subgroupName, options) {
|
|
156
853
|
if (!actionMapper) {
|
|
157
854
|
throw new Error(`Unknown action`);
|
|
158
855
|
}
|
|
159
|
-
|
|
856
|
+
if (actionMapper.type === "graphql") {
|
|
857
|
+
return buildGraphQLRequest(
|
|
858
|
+
actionMapper,
|
|
859
|
+
args,
|
|
860
|
+
groupName,
|
|
861
|
+
subgroupName,
|
|
862
|
+
options
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
let url2 = actionMapper.apiUrl;
|
|
160
866
|
if (actionMapper.params) {
|
|
161
|
-
Object.entries(actionMapper.params).forEach(([
|
|
162
|
-
|
|
867
|
+
Object.entries(actionMapper.params).forEach(([placeholder, argKey]) => {
|
|
868
|
+
const value = resolvePathParam(argKey, args, options);
|
|
869
|
+
if (value === void 0) {
|
|
870
|
+
throw new Error(
|
|
871
|
+
`Missing required path parameter "${argKey}" for URL "${actionMapper.apiUrl}"`
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
url2 = url2.replace(
|
|
875
|
+
new RegExp(placeholder, "g"),
|
|
876
|
+
encodeURIComponent(String(value))
|
|
877
|
+
);
|
|
163
878
|
});
|
|
164
879
|
}
|
|
165
880
|
const queryParams = {};
|
|
166
881
|
if (actionMapper.queryParams) {
|
|
167
882
|
Object.entries(actionMapper.queryParams).forEach(([paramName, argName]) => {
|
|
168
883
|
if (args[argName] !== void 0) {
|
|
169
|
-
|
|
884
|
+
const value = args[argName];
|
|
885
|
+
if (paramName.endsWith("[]")) {
|
|
886
|
+
const cleanParamName = paramName.slice(0, -2);
|
|
887
|
+
if (Array.isArray(value)) {
|
|
888
|
+
queryParams[cleanParamName] = value;
|
|
889
|
+
} else {
|
|
890
|
+
queryParams[cleanParamName] = [value];
|
|
891
|
+
}
|
|
892
|
+
} else {
|
|
893
|
+
queryParams[paramName] = value;
|
|
894
|
+
}
|
|
170
895
|
}
|
|
171
896
|
});
|
|
172
897
|
}
|
|
@@ -196,16 +921,40 @@ function buildContentstackRequest(actionMapper, args, groupName, options) {
|
|
|
196
921
|
}
|
|
197
922
|
}
|
|
198
923
|
}
|
|
199
|
-
const returnObj = buildReturnValue(
|
|
924
|
+
const returnObj = await buildReturnValue(
|
|
200
925
|
groupName,
|
|
926
|
+
subgroupName,
|
|
201
927
|
actionMapper.method,
|
|
202
|
-
|
|
928
|
+
url2,
|
|
203
929
|
body,
|
|
204
930
|
queryParams,
|
|
205
931
|
options
|
|
206
932
|
);
|
|
207
933
|
return returnObj;
|
|
208
934
|
}
|
|
935
|
+
async function buildGraphQLRequest(actionMapper, args, groupName, subgroupName, options) {
|
|
936
|
+
const { query, variables: variableMapping } = actionMapper;
|
|
937
|
+
const variables = {};
|
|
938
|
+
Object.entries(variableMapping).forEach(([varName, config]) => {
|
|
939
|
+
const sourceKey = config["x-mapFrom"];
|
|
940
|
+
if (args[sourceKey] !== void 0) {
|
|
941
|
+
variables[varName] = args[sourceKey];
|
|
942
|
+
}
|
|
943
|
+
});
|
|
944
|
+
const graphqlBody = {
|
|
945
|
+
query,
|
|
946
|
+
variables
|
|
947
|
+
};
|
|
948
|
+
return buildReturnValue(
|
|
949
|
+
groupName,
|
|
950
|
+
subgroupName,
|
|
951
|
+
"POST",
|
|
952
|
+
actionMapper.apiUrl,
|
|
953
|
+
graphqlBody,
|
|
954
|
+
{},
|
|
955
|
+
options
|
|
956
|
+
);
|
|
957
|
+
}
|
|
209
958
|
function buildBodyPayload(schema, data) {
|
|
210
959
|
function walk(sch) {
|
|
211
960
|
if (sch.type === "object") {
|
|
@@ -263,47 +1012,109 @@ function buildBodyPayload(schema, data) {
|
|
|
263
1012
|
}
|
|
264
1013
|
return walk(schema).value;
|
|
265
1014
|
}
|
|
266
|
-
function buildReturnValue(groupName, method,
|
|
267
|
-
if (!
|
|
1015
|
+
async function buildReturnValue(groupName, subgroupName, method, url2, data, queryParams, options) {
|
|
1016
|
+
if (!url2) {
|
|
268
1017
|
throw new Error("URL is required");
|
|
269
1018
|
}
|
|
270
|
-
const {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
1019
|
+
const {
|
|
1020
|
+
apiKey,
|
|
1021
|
+
deliveryToken,
|
|
1022
|
+
brandKitUID,
|
|
1023
|
+
launchProjectId,
|
|
1024
|
+
personalizeProjectId,
|
|
1025
|
+
lyticsAccessToken,
|
|
1026
|
+
region,
|
|
1027
|
+
useOAuth
|
|
1028
|
+
} = options;
|
|
1029
|
+
const baseUrl = getBaseUrl(region, groupName, subgroupName);
|
|
1030
|
+
const fullUrl = `${baseUrl}${url2.startsWith("/") ? url2 : `/${url2}`}`;
|
|
276
1031
|
const config = {
|
|
277
1032
|
method,
|
|
278
1033
|
url: fullUrl,
|
|
279
1034
|
data: data || void 0,
|
|
280
1035
|
params: Object.keys(queryParams || {}).length > 0 ? queryParams : void 0,
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
1036
|
+
paramsSerializer: function(params) {
|
|
1037
|
+
const searchParams = new URLSearchParams();
|
|
1038
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
1039
|
+
if (Array.isArray(value)) {
|
|
1040
|
+
value.forEach((item) => searchParams.append(`${key}[]`, item));
|
|
1041
|
+
} else if (value !== void 0) {
|
|
1042
|
+
searchParams.append(key, value);
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
return searchParams.toString();
|
|
1046
|
+
},
|
|
1047
|
+
headers: method === "POST" && data === void 0 ? {} : {
|
|
1048
|
+
"Content-Type": "application/json"
|
|
284
1049
|
}
|
|
285
1050
|
};
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
1051
|
+
if (groupName !== GroupEnum.LYTICS) {
|
|
1052
|
+
config.headers = {
|
|
1053
|
+
...config.headers,
|
|
1054
|
+
api_key: apiKey
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
if (groupName === GroupEnum.CDA) {
|
|
1058
|
+
if (!deliveryToken) {
|
|
1059
|
+
throw new Error("Delivery token is required for Delivery API");
|
|
1060
|
+
}
|
|
1061
|
+
config.headers = {
|
|
1062
|
+
...config.headers,
|
|
1063
|
+
access_token: deliveryToken
|
|
1064
|
+
};
|
|
1065
|
+
} else if (groupName === GroupEnum.LYTICS) {
|
|
1066
|
+
if (!lyticsAccessToken) {
|
|
1067
|
+
throw new Error("Lytics access token is required for Lytics API");
|
|
1068
|
+
}
|
|
1069
|
+
config.headers = {
|
|
1070
|
+
...config.headers,
|
|
1071
|
+
Authorization: lyticsAccessToken
|
|
1072
|
+
};
|
|
1073
|
+
} else {
|
|
1074
|
+
if (!useOAuth) {
|
|
1075
|
+
throw new Error(
|
|
1076
|
+
"Please run the command with npx @contentstack/mcp --auth"
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
const oauthHeaders = await oauth_default.getAuthHeaders();
|
|
1080
|
+
if (groupName === GroupEnum.BRANDKIT) {
|
|
1081
|
+
if (!brandKitUID) {
|
|
1082
|
+
throw new Error("Brand Kit UID is required for Brand Kit API");
|
|
1083
|
+
}
|
|
1084
|
+
config.headers = {
|
|
1085
|
+
...config.headers,
|
|
1086
|
+
brand_kit_uid: brandKitUID
|
|
1087
|
+
};
|
|
1088
|
+
} else if (groupName === GroupEnum.PERSONALIZE) {
|
|
1089
|
+
if (!personalizeProjectId) {
|
|
1090
|
+
throw new Error(
|
|
1091
|
+
"Personalize Project ID is required for Personalize API"
|
|
1092
|
+
);
|
|
290
1093
|
}
|
|
291
1094
|
config.headers = {
|
|
292
1095
|
...config.headers,
|
|
293
|
-
|
|
1096
|
+
"x-project-uid": personalizeProjectId
|
|
1097
|
+
};
|
|
1098
|
+
} else if (groupName === GroupEnum.ANALYTICS) {
|
|
1099
|
+
config.params = {
|
|
1100
|
+
...config.params,
|
|
1101
|
+
orgUid: oauthHeaders["organization_uid"]
|
|
294
1102
|
};
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
throw new Error("Delivery token is required for Delivery API");
|
|
1103
|
+
} else if (groupName === GroupEnum.LAUNCH) {
|
|
1104
|
+
if (!launchProjectId) {
|
|
1105
|
+
throw new Error("Launch Project ID is required for Launch API");
|
|
299
1106
|
}
|
|
300
1107
|
config.headers = {
|
|
301
1108
|
...config.headers,
|
|
302
|
-
|
|
1109
|
+
"apollographql-client-name": "contentfly-client",
|
|
1110
|
+
"apollographql-client-version": "1.3",
|
|
1111
|
+
"x-project-uid": launchProjectId
|
|
303
1112
|
};
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
1113
|
+
}
|
|
1114
|
+
config.headers = {
|
|
1115
|
+
...config.headers,
|
|
1116
|
+
...oauthHeaders
|
|
1117
|
+
};
|
|
307
1118
|
}
|
|
308
1119
|
return config;
|
|
309
1120
|
}
|
|
@@ -311,34 +1122,101 @@ function buildReturnValue(groupName, method, url, data, queryParams, options) {
|
|
|
311
1122
|
// src/index.ts
|
|
312
1123
|
dotenv.config();
|
|
313
1124
|
var TOKEN_ARGS = {
|
|
314
|
-
|
|
1125
|
+
OAUTH: "--auth",
|
|
315
1126
|
DELIVERY: "--delivery-token",
|
|
316
1127
|
API_KEY: "--stack-api-key",
|
|
317
|
-
|
|
1128
|
+
BRAND_KIT_ID: "--brand-kit-id",
|
|
1129
|
+
LYTICS_ACCESS_TOKEN: "--lytics-access-token",
|
|
1130
|
+
PERSONALIZE_PROJECT_ID: "--personalize-project-id",
|
|
1131
|
+
LAUNCH_PROJECT_ID: "--launch-project-id",
|
|
1132
|
+
GROUPS: "--groups"
|
|
318
1133
|
};
|
|
319
1134
|
var GROUPS = {
|
|
320
1135
|
ALL: "all",
|
|
321
1136
|
CMA: "cma",
|
|
322
|
-
CDA: "cda"
|
|
1137
|
+
CDA: "cda",
|
|
1138
|
+
BRAND_KIT: "brandkit",
|
|
1139
|
+
LYTICS: "lytics",
|
|
1140
|
+
ANALYTICS: "analytics",
|
|
1141
|
+
PERSONALIZE: "personalize",
|
|
1142
|
+
LAUNCH: "launch"
|
|
323
1143
|
};
|
|
324
1144
|
function getArgValue(argName) {
|
|
325
1145
|
const index = process.argv.findIndex((arg) => arg === argName);
|
|
326
1146
|
return index !== -1 ? process.argv[index + 1] : null;
|
|
327
1147
|
}
|
|
328
|
-
function
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
1148
|
+
function hasNonLyticsGroups(groups) {
|
|
1149
|
+
return groups.some((group) => group !== GROUPS.LYTICS);
|
|
1150
|
+
}
|
|
1151
|
+
function validateGroupRequirements(groups, options) {
|
|
1152
|
+
const groupsToValidate = groups.includes(GROUPS.ALL) ? [
|
|
1153
|
+
GROUPS.CMA,
|
|
1154
|
+
GROUPS.CDA,
|
|
1155
|
+
GROUPS.BRAND_KIT,
|
|
1156
|
+
GROUPS.LYTICS,
|
|
1157
|
+
GROUPS.ANALYTICS,
|
|
1158
|
+
GROUPS.PERSONALIZE,
|
|
1159
|
+
GROUPS.LAUNCH
|
|
1160
|
+
] : groups;
|
|
1161
|
+
for (const group of groupsToValidate) {
|
|
1162
|
+
if (group === GROUPS.CDA && !options.deliveryToken) {
|
|
1163
|
+
throw new Error(
|
|
1164
|
+
"Delivery token is required for Content Delivery API (CDA). Please provide it using --delivery-token or CONTENTSTACK_DELIVERY_TOKEN environment variable."
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
if (group === GROUPS.LYTICS && !options.lyticsAccessToken) {
|
|
1168
|
+
throw new Error(
|
|
1169
|
+
"Lytics access token is required for Lytics API. Please provide it using --lytics-access-token or LYTICS_ACCESS_TOKEN environment variable."
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
if ([
|
|
1173
|
+
GROUPS.CMA,
|
|
1174
|
+
GROUPS.BRAND_KIT,
|
|
1175
|
+
GROUPS.PERSONALIZE,
|
|
1176
|
+
GROUPS.ANALYTICS,
|
|
1177
|
+
GROUPS.LAUNCH
|
|
1178
|
+
].includes(group) && !options.useOAuth) {
|
|
1179
|
+
let groupName;
|
|
1180
|
+
switch (group) {
|
|
1181
|
+
case GROUPS.CMA:
|
|
1182
|
+
groupName = "Content Management API";
|
|
1183
|
+
break;
|
|
1184
|
+
case GROUPS.BRAND_KIT:
|
|
1185
|
+
groupName = "Brand Kit API";
|
|
1186
|
+
break;
|
|
1187
|
+
case GROUPS.PERSONALIZE:
|
|
1188
|
+
groupName = "Personalize API";
|
|
1189
|
+
break;
|
|
1190
|
+
case GROUPS.ANALYTICS:
|
|
1191
|
+
groupName = "Analytics API";
|
|
1192
|
+
break;
|
|
1193
|
+
case GROUPS.LAUNCH:
|
|
1194
|
+
groupName = "Launch API";
|
|
1195
|
+
break;
|
|
1196
|
+
}
|
|
1197
|
+
throw new Error(
|
|
1198
|
+
`OAuth configuration is required for ${groupName}. Please run with npx @contentstack/mcp --auth first.`
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
if (group === GROUPS.BRAND_KIT && !options.brandKitUID) {
|
|
1202
|
+
throw new Error(
|
|
1203
|
+
"Brand Kit ID is required for Brand Kit API. Please provide it using --brand-kit-id or CONTENTSTACK_BRAND_KIT_ID environment variable."
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
if (group === GROUPS.PERSONALIZE && !options.personalizeProjectId) {
|
|
1207
|
+
throw new Error(
|
|
1208
|
+
"Personalize Project ID is required for Personalize API. Please provide it using --personalize-project-id or CONTENTSTACK_PERSONALIZE_PROJECT_ID environment variable."
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
if (group === GROUPS.LAUNCH && !options.launchProjectId) {
|
|
1212
|
+
throw new Error(
|
|
1213
|
+
"Launch Project ID is required for Launch API. Please provide it using --launch-project-id or CONTENTSTACK_LAUNCH_PROJECT_ID environment variable."
|
|
1214
|
+
);
|
|
1215
|
+
}
|
|
337
1216
|
}
|
|
338
|
-
return null;
|
|
339
1217
|
}
|
|
340
1218
|
function createContentstackMCPServer(options) {
|
|
341
|
-
const {
|
|
1219
|
+
const { groups } = options;
|
|
342
1220
|
const server = new Server(
|
|
343
1221
|
{
|
|
344
1222
|
name: "Contentstack MCP",
|
|
@@ -353,8 +1231,8 @@ function createContentstackMCPServer(options) {
|
|
|
353
1231
|
let toolData;
|
|
354
1232
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
355
1233
|
try {
|
|
356
|
-
toolData = await getTools(
|
|
357
|
-
if (!toolData) {
|
|
1234
|
+
toolData = await getTools(groups);
|
|
1235
|
+
if (!toolData || Object.keys(toolData).length === 0) {
|
|
358
1236
|
throw new Error("No tools data received");
|
|
359
1237
|
}
|
|
360
1238
|
return {
|
|
@@ -377,10 +1255,12 @@ function createContentstackMCPServer(options) {
|
|
|
377
1255
|
throw new Error(`Unknown tool: ${name}`);
|
|
378
1256
|
}
|
|
379
1257
|
const groupName = toolData[name].group;
|
|
380
|
-
const
|
|
1258
|
+
const subgroupName = toolData[name].subGroup ?? void 0;
|
|
1259
|
+
const requestConfig = await buildContentstackRequest(
|
|
381
1260
|
mapper,
|
|
382
1261
|
args,
|
|
383
1262
|
groupName,
|
|
1263
|
+
subgroupName,
|
|
384
1264
|
options
|
|
385
1265
|
);
|
|
386
1266
|
if (apiVersionHeaders.includes(name)) {
|
|
@@ -391,7 +1271,7 @@ function createContentstackMCPServer(options) {
|
|
|
391
1271
|
}
|
|
392
1272
|
let response;
|
|
393
1273
|
try {
|
|
394
|
-
response = await
|
|
1274
|
+
response = await axios3(requestConfig);
|
|
395
1275
|
} catch (error) {
|
|
396
1276
|
console.error("API call failed:", error.response.data);
|
|
397
1277
|
throw new Error(
|
|
@@ -412,13 +1292,41 @@ function createContentstackMCPServer(options) {
|
|
|
412
1292
|
});
|
|
413
1293
|
return server;
|
|
414
1294
|
}
|
|
1295
|
+
async function checkOAuthConfig() {
|
|
1296
|
+
try {
|
|
1297
|
+
const handler = new oauth_default();
|
|
1298
|
+
const tokenData = await handler.loadConfig();
|
|
1299
|
+
const isConfigured = Boolean(
|
|
1300
|
+
tokenData && tokenData.access_token && tokenData.refresh_token
|
|
1301
|
+
);
|
|
1302
|
+
return { isConfigured, tokenData };
|
|
1303
|
+
} catch (error) {
|
|
1304
|
+
console.warn("OAuth not configured:", error.message);
|
|
1305
|
+
return { isConfigured: false, tokenData: null };
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
415
1308
|
async function initializeMCP() {
|
|
416
|
-
const
|
|
1309
|
+
const oauthArg = process.argv.includes(TOKEN_ARGS.OAUTH);
|
|
1310
|
+
if (oauthArg) {
|
|
1311
|
+
const handler = new oauth_default();
|
|
1312
|
+
await handler.showMainMenu();
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
417
1315
|
const apiKey = getArgValue(TOKEN_ARGS.API_KEY);
|
|
418
1316
|
const deliveryToken = getArgValue(TOKEN_ARGS.DELIVERY);
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
|
|
1317
|
+
const brandKitID = getArgValue(TOKEN_ARGS.BRAND_KIT_ID);
|
|
1318
|
+
const launchProjectId = getArgValue(TOKEN_ARGS.LAUNCH_PROJECT_ID);
|
|
1319
|
+
const personalizeProjectId = getArgValue(TOKEN_ARGS.PERSONALIZE_PROJECT_ID);
|
|
1320
|
+
const lyticsAccessToken = getArgValue(TOKEN_ARGS.LYTICS_ACCESS_TOKEN);
|
|
1321
|
+
const groupArg = getArgValue(TOKEN_ARGS.GROUPS) || process.env.GROUPS || "cma";
|
|
1322
|
+
const groups = groupArg.split(",");
|
|
1323
|
+
const invalidGroups = groups.filter(
|
|
1324
|
+
(g) => !Object.values(GROUPS).includes(g)
|
|
1325
|
+
);
|
|
1326
|
+
if (invalidGroups.length > 0) {
|
|
1327
|
+
throw new Error(
|
|
1328
|
+
`Invalid groups specified: ${invalidGroups.join(", ")}. Valid options are: ${Object.values(GROUPS).join(", ")}`
|
|
1329
|
+
);
|
|
422
1330
|
}
|
|
423
1331
|
if (apiKey) {
|
|
424
1332
|
process.env.CONTENTSTACK_API_KEY = apiKey;
|
|
@@ -426,23 +1334,42 @@ async function initializeMCP() {
|
|
|
426
1334
|
if (deliveryToken) {
|
|
427
1335
|
process.env.CONTENTSTACK_DELIVERY_TOKEN = deliveryToken;
|
|
428
1336
|
}
|
|
429
|
-
if (
|
|
430
|
-
process.env.
|
|
1337
|
+
if (lyticsAccessToken) {
|
|
1338
|
+
process.env.LYTICS_ACCESS_TOKEN = lyticsAccessToken;
|
|
431
1339
|
}
|
|
432
|
-
if (
|
|
433
|
-
|
|
1340
|
+
if (brandKitID) {
|
|
1341
|
+
process.env.CONTENTSTACK_BRAND_KIT_ID = brandKitID;
|
|
434
1342
|
}
|
|
435
|
-
|
|
436
|
-
process.env.
|
|
437
|
-
|
|
438
|
-
)
|
|
439
|
-
|
|
1343
|
+
if (launchProjectId) {
|
|
1344
|
+
process.env.CONTENTSTACK_LAUNCH_PROJECT_ID = launchProjectId;
|
|
1345
|
+
}
|
|
1346
|
+
if (personalizeProjectId) {
|
|
1347
|
+
process.env.CONTENTSTACK_PERSONALIZE_PROJECT_ID = personalizeProjectId;
|
|
1348
|
+
}
|
|
1349
|
+
const { isConfigured: oauthConfigured, tokenData } = await checkOAuthConfig();
|
|
1350
|
+
if (hasNonLyticsGroups(groups) && !process.env.CONTENTSTACK_API_KEY && !apiKey) {
|
|
1351
|
+
throw new Error(
|
|
1352
|
+
"Please provide the Contentstack API key for non-Lytics groups"
|
|
1353
|
+
);
|
|
1354
|
+
}
|
|
1355
|
+
if (!oauthConfigured && !process.env.CONTENTSTACK_DELIVERY_TOKEN && hasNonLyticsGroups(groups)) {
|
|
1356
|
+
console.warn(
|
|
1357
|
+
"Neither OAuth nor delivery token is configured. Please run with npx @contentstack/mcp --auth to authenticate."
|
|
1358
|
+
);
|
|
1359
|
+
}
|
|
1360
|
+
const options = {
|
|
440
1361
|
apiKey: process.env.CONTENTSTACK_API_KEY || "",
|
|
441
|
-
managementToken: process.env.CONTENTSTACK_MANAGEMENT_TOKEN || "",
|
|
442
1362
|
deliveryToken: process.env.CONTENTSTACK_DELIVERY_TOKEN || "",
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
1363
|
+
brandKitUID: process.env.CONTENTSTACK_BRAND_KIT_ID || "",
|
|
1364
|
+
launchProjectId: process.env.CONTENTSTACK_LAUNCH_PROJECT_ID || "",
|
|
1365
|
+
lyticsAccessToken: process.env.LYTICS_ACCESS_TOKEN || "",
|
|
1366
|
+
personalizeProjectId: process.env.CONTENTSTACK_PERSONALIZE_PROJECT_ID || "",
|
|
1367
|
+
region: tokenData?.region,
|
|
1368
|
+
groups,
|
|
1369
|
+
useOAuth: oauthConfigured
|
|
1370
|
+
};
|
|
1371
|
+
validateGroupRequirements(groups, options);
|
|
1372
|
+
const server = createContentstackMCPServer(options);
|
|
446
1373
|
const transport = new StdioServerTransport();
|
|
447
1374
|
await server.connect(transport);
|
|
448
1375
|
}
|