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