@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.
Files changed (3) hide show
  1. package/README.md +241 -36
  2. package/dist/index.js +1024 -97
  3. 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 axios2 from "axios";
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.1.1",
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.11.0",
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: "http://mcp.contentstack.com/cma/tools",
113
- cda: "http://mcp.contentstack.com/cda/tools"
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
- var getBaseUrl = (region, group) => {
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 (group) => {
788
+ var getTools = async (groups) => {
128
789
  try {
129
- switch (group) {
130
- case GroupEnum.CMA: {
131
- const response = await axios.get(TOOL_URLS.cma);
132
- return { ...response.data };
133
- }
134
- case GroupEnum.CDA: {
135
- const response = await axios.get(TOOL_URLS.cda);
136
- return { ...response.data };
137
- }
138
- case GroupEnum.ALL: {
139
- const [contentstackRes, deliveryRes] = await Promise.all([
140
- axios.get(TOOL_URLS.cma),
141
- axios.get(TOOL_URLS.cda)
142
- ]);
143
- return {
144
- ...contentstackRes.data,
145
- ...deliveryRes.data
146
- };
147
- }
148
- default:
149
- throw new Error(`Invalid group: ${group}`);
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(`Failed to fetch tools for group ${group}`);
840
+ throw new Error(
841
+ `Failed to fetch tools: ${error instanceof Error ? error.message : "Unknown error"}`
842
+ );
153
843
  }
154
844
  };
155
- function buildContentstackRequest(actionMapper, args, groupName, options) {
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
- let url = actionMapper.apiUrl;
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(([paramName, argName]) => {
162
- url = url.replace(new RegExp(paramName, "g"), args[argName] ?? "");
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
- queryParams[paramName] = args[argName];
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
- url,
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, url, data, queryParams, options) {
267
- if (!url) {
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 { apiKey, managementToken, deliveryToken, group, region } = options;
271
- const baseUrl = getBaseUrl(region, groupName);
272
- if (!baseUrl) {
273
- throw new Error(`Invalid group: ${group}`);
274
- }
275
- const fullUrl = `${baseUrl}${url.startsWith("/") ? url : `/${url}`}`;
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
- headers: {
282
- "Content-Type": "application/json",
283
- api_key: apiKey
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
- switch (groupName) {
287
- case GroupEnum.CMA:
288
- if (!managementToken) {
289
- throw new Error("Management token is required for Contentstack API");
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
- authorization: managementToken
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
- break;
296
- case GroupEnum.CDA:
297
- if (!deliveryToken) {
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
- access_token: deliveryToken
1109
+ "apollographql-client-name": "contentfly-client",
1110
+ "apollographql-client-version": "1.3",
1111
+ "x-project-uid": launchProjectId
303
1112
  };
304
- break;
305
- default:
306
- throw new Error(`Unknown tool group: ${group}`);
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
- MANAGEMENT: "--management-token",
1125
+ OAUTH: "--auth",
315
1126
  DELIVERY: "--delivery-token",
316
1127
  API_KEY: "--stack-api-key",
317
- REGION: "--region"
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 determineGroup(managementToken, deliveryToken) {
329
- if (managementToken?.trim() && deliveryToken?.trim()) {
330
- return GROUPS.ALL;
331
- }
332
- if (managementToken?.trim()) {
333
- return GROUPS.CMA;
334
- }
335
- if (deliveryToken?.trim()) {
336
- return GROUPS.CDA;
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 { group } = options;
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(group);
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 requestConfig = buildContentstackRequest(
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 axios2(requestConfig);
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 managementToken = getArgValue(TOKEN_ARGS.MANAGEMENT);
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 region = getArgValue(TOKEN_ARGS.REGION);
420
- if (managementToken) {
421
- process.env.CONTENTSTACK_MANAGEMENT_TOKEN = managementToken;
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 (region) {
430
- process.env.CONTENTSTACK_REGION = region;
1337
+ if (lyticsAccessToken) {
1338
+ process.env.LYTICS_ACCESS_TOKEN = lyticsAccessToken;
431
1339
  }
432
- if (!process.env.CONTENTSTACK_API_KEY && !apiKey) {
433
- throw new Error("Please provide the Contentstack API key");
1340
+ if (brandKitID) {
1341
+ process.env.CONTENTSTACK_BRAND_KIT_ID = brandKitID;
434
1342
  }
435
- const group = determineGroup(
436
- process.env.CONTENTSTACK_MANAGEMENT_TOKEN,
437
- process.env.CONTENTSTACK_DELIVERY_TOKEN
438
- );
439
- const server = createContentstackMCPServer({
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
- region: process.env.CONTENTSTACK_REGION || "NA",
444
- group
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
  }