@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.
- package/README.md +267 -36
- package/dist/index.js +1058 -103
- 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 {
|
|
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
|
|
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.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.
|
|
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: "
|
|
113
|
-
cda: "
|
|
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
|
|
118
|
-
|
|
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 (
|
|
811
|
+
var getTools = async (groups) => {
|
|
128
812
|
try {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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(
|
|
867
|
+
throw new Error(
|
|
868
|
+
`Failed to fetch tools: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
869
|
+
);
|
|
153
870
|
}
|
|
154
871
|
};
|
|
155
|
-
function
|
|
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
|
-
|
|
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(([
|
|
162
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
267
|
-
if (!
|
|
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 {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
1113
|
+
brand_kit_uid: brandKitUID
|
|
294
1114
|
};
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
1123
|
+
"x-project-uid": personalizeProjectId
|
|
303
1124
|
};
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
1152
|
+
OAUTH: "--auth",
|
|
315
1153
|
DELIVERY: "--delivery-token",
|
|
316
1154
|
API_KEY: "--stack-api-key",
|
|
317
|
-
|
|
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
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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 {
|
|
342
|
-
const
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
420
|
-
|
|
421
|
-
|
|
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 (
|
|
430
|
-
process.env.
|
|
1365
|
+
if (lyticsAccessToken) {
|
|
1366
|
+
process.env.LYTICS_ACCESS_TOKEN = lyticsAccessToken;
|
|
431
1367
|
}
|
|
432
|
-
if (
|
|
433
|
-
|
|
1368
|
+
if (brandKitID) {
|
|
1369
|
+
process.env.CONTENTSTACK_BRAND_KIT_ID = brandKitID;
|
|
434
1370
|
}
|
|
435
|
-
|
|
436
|
-
process.env.
|
|
437
|
-
|
|
438
|
-
)
|
|
439
|
-
|
|
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
|
-
|
|
444
|
-
|
|
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
|
}
|