@kontent-ai/mcp-server 0.21.11 → 0.22.1

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 CHANGED
@@ -89,7 +89,13 @@ npx @kontent-ai/mcp-server@latest shttp
89
89
 
90
90
  * **get-item-mapi** – Get Kontent.ai item by internal ID from Management API
91
91
  * **get-item-dapi** – Get Kontent.ai item by codename from Delivery API
92
- * **get-variant-mapi** – Get Kontent.ai language variant of content item from Management API
92
+ * **get-latest-variant-mapi** – Get latest version of Kontent.ai language variant from Management API
93
+ * **get-published-variant-mapi** – Get published version of Kontent.ai language variant from Management API
94
+ * **list-variants-item-mapi** – List all Kontent.ai language variants of a content item from Management API
95
+ * **list-variants-collection-mapi** – List Kontent.ai language variants by collection from Management API (paginated)
96
+ * **list-variants-type-mapi** – List Kontent.ai language variants by content type from Management API (paginated)
97
+ * **list-variants-components-type-mapi** – List Kontent.ai language variants containing components of a specific content type from Management API (paginated)
98
+ * **list-variants-space-mapi** – List Kontent.ai language variants by space from Management API (paginated)
93
99
  * **add-content-item-mapi** – Add new Kontent.ai content item via Management API. This creates the content item structure but does not add content to language variants. Use upsert-language-variant-mapi to add content to the item
94
100
  * **update-content-item-mapi** – Update existing Kontent.ai content item by internal ID via Management API. The content item must already exist - this tool will not create new items
95
101
  * **delete-content-item-mapi** – Delete Kontent.ai content item by internal ID from Management API
@@ -107,6 +113,17 @@ npx @kontent-ai/mcp-server@latest shttp
107
113
  ### Language Management
108
114
 
109
115
  * **list-languages-mapi** – Get all Kontent.ai languages from Management API
116
+ * **add-language-mapi** – Add new Kontent.ai language via Management API
117
+ * **patch-language-mapi** – Update Kontent.ai language using replace operations via Management API
118
+
119
+ ### Collection Management
120
+
121
+ * **list-collections-mapi** – Get all Kontent.ai collections from Management API. Collections set boundaries for content items in your environment and help organize content by team, brand, or project
122
+ * **patch-collections-mapi** – Update Kontent.ai collections using patch operations (addInto to add new collections, move to reorder, remove to delete empty collections, replace to rename)
123
+
124
+ ### Space Management
125
+
126
+ * **list-spaces-mapi** – Get all Kontent.ai spaces from Management API
110
127
 
111
128
  ### Workflow Management
112
129
 
@@ -217,7 +234,8 @@ Then configure your MCP client:
217
234
 
218
235
  No environment variables required. The server accepts requests for multiple environments using URL path parameters and Bearer authentication.
219
236
 
220
- ##### VS Code Configuration
237
+ <details>
238
+ <summary><strong>VS Code</strong></summary>
221
239
 
222
240
  Create a `.vscode/mcp.json` file in your workspace:
223
241
 
@@ -261,7 +279,10 @@ For secure configuration with input prompts:
261
279
  }
262
280
  ```
263
281
 
264
- ##### Claude Desktop Configuration
282
+ </details>
283
+
284
+ <details>
285
+ <summary><strong>Claude Desktop</strong></summary>
265
286
 
266
287
  Update your Claude Desktop configuration file:
267
288
  - **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
@@ -286,32 +307,25 @@ Use `mcp-remote` as a proxy to add authentication headers:
286
307
  }
287
308
  ```
288
309
 
289
- ##### Claude Code Configuration
310
+ </details>
311
+
312
+ <details>
313
+ <summary><strong>Claude Code</strong></summary>
290
314
 
291
- For Claude Code (claude.ai/code), add the server configuration:
315
+ Add the server using the CLI:
292
316
 
293
317
  ```bash
294
- # Add the multi-tenant server
295
- claude mcp add \
296
- --url "http://localhost:3001/<environment-id>/mcp" \
297
- --header "Authorization: Bearer <management-api-key>" \
298
- kontent-ai-multi
318
+ claude mcp add --transport http kontent-ai-multi \
319
+ "http://localhost:3001/<environment-id>/mcp" \
320
+ --header "Authorization: Bearer <management-api-key>"
299
321
  ```
300
322
 
301
- Or configure directly in the settings:
323
+ > **Note**: You can also configure this in your Claude Code settings JSON with the `url` and `headers` properties.
302
324
 
303
- ```json
304
- {
305
- "kontent-ai-multi": {
306
- "url": "http://localhost:3001/<environment-id>/mcp",
307
- "headers": {
308
- "Authorization": "Bearer <management-api-key>"
309
- }
310
- }
311
- }
312
- ```
325
+ </details>
313
326
 
314
- **Important**: Replace `<environment-id>` with your actual Kontent.ai environment ID (GUID format) and `<management-api-key>` with your Management API key.
327
+ > [!IMPORTANT]
328
+ > Replace `<environment-id>` with your Kontent.ai environment ID (GUID) and `<management-api-key>` with your Management API key.
315
329
 
316
330
  ## 💻 Development
317
331
 
@@ -0,0 +1,37 @@
1
+ import { z } from "zod";
2
+ import { referenceObjectSchema } from "./referenceObjectSchema.js";
3
+ const addIntoOperationSchema = z.object({
4
+ op: z.literal("addInto"),
5
+ value: z.object({
6
+ name: z.string(),
7
+ codename: z.string().optional(),
8
+ external_id: z.string().optional(),
9
+ }),
10
+ before: referenceObjectSchema.optional(),
11
+ after: referenceObjectSchema.optional(),
12
+ });
13
+ const moveOperationSchema = z.object({
14
+ op: z.literal("move"),
15
+ reference: referenceObjectSchema,
16
+ before: referenceObjectSchema.optional(),
17
+ after: referenceObjectSchema.optional(),
18
+ });
19
+ const removeOperationSchema = z.object({
20
+ op: z.literal("remove"),
21
+ reference: referenceObjectSchema,
22
+ });
23
+ const replaceOperationSchema = z.object({
24
+ op: z.literal("replace"),
25
+ reference: referenceObjectSchema,
26
+ property_name: z.enum(["name"]),
27
+ value: z.string(),
28
+ });
29
+ export const collectionPatchOperationSchema = z.discriminatedUnion("op", [
30
+ addIntoOperationSchema,
31
+ moveOperationSchema,
32
+ removeOperationSchema,
33
+ replaceOperationSchema,
34
+ ]);
35
+ export const collectionPatchOperationsSchema = z
36
+ .array(collectionPatchOperationSchema)
37
+ .min(1);
@@ -0,0 +1,43 @@
1
+ import { z } from "zod";
2
+ import { referenceObjectSchema } from "./referenceObjectSchema.js";
3
+ export const addLanguageSchema = z.object({
4
+ name: z.string().describe("Display name of the language"),
5
+ codename: z.string().describe("Codename identifier for the language"),
6
+ is_active: z
7
+ .boolean()
8
+ .optional()
9
+ .describe("Whether the language is active (defaults to true)"),
10
+ fallback_language: referenceObjectSchema
11
+ .optional()
12
+ .describe("Reference to fallback language (by id, codename, or external_id)"),
13
+ external_id: z.string().optional().describe("External ID for the language"),
14
+ });
15
+ const languageReplaceOperationSchema = z.discriminatedUnion("property_name", [
16
+ z.object({
17
+ op: z.literal("replace"),
18
+ property_name: z.literal("codename"),
19
+ value: z.string(),
20
+ }),
21
+ z.object({
22
+ op: z.literal("replace"),
23
+ property_name: z.literal("name"),
24
+ value: z.string(),
25
+ }),
26
+ z.object({
27
+ op: z.literal("replace"),
28
+ property_name: z.literal("is_active"),
29
+ value: z.boolean(),
30
+ }),
31
+ z.object({
32
+ op: z.literal("replace"),
33
+ property_name: z.literal("fallback_language"),
34
+ value: referenceObjectSchema,
35
+ }),
36
+ ]);
37
+ export const patchLanguageSchema = z.object({
38
+ languageId: z.string().describe("Language ID to modify"),
39
+ operations: z
40
+ .array(languageReplaceOperationSchema)
41
+ .min(1)
42
+ .describe("Array of replace operations for codename, name, is_active, or fallback_language. Note: Only active languages can be modified - if language is deactivated, is_active: true must be first operation."),
43
+ });
@@ -24,3 +24,19 @@ export const listContentTypeSnippetsSchema = z.object({
24
24
  export const listAssetsSchema = z.object({
25
25
  continuation_token: continuationTokenField,
26
26
  });
27
+ export const listVariantsCollectionSchema = z.object({
28
+ collectionId: z.string().describe("Collection ID"),
29
+ continuation_token: continuationTokenField,
30
+ });
31
+ export const listVariantsTypeSchema = z.object({
32
+ contentTypeId: z.string().describe("Content type ID"),
33
+ continuation_token: continuationTokenField,
34
+ });
35
+ export const listVariantsComponentsTypeSchema = z.object({
36
+ contentTypeId: z.string().describe("Content type ID"),
37
+ continuation_token: continuationTokenField,
38
+ });
39
+ export const listVariantsSpaceSchema = z.object({
40
+ spaceId: z.string().describe("Space ID"),
41
+ continuation_token: continuationTokenField,
42
+ });
@@ -4,7 +4,7 @@ const taxonomyTermSchema = z.object({
4
4
  name: z.string(),
5
5
  codename: z.string().optional(),
6
6
  external_id: z.string().optional(),
7
- terms: z.lazy(() => z.array(taxonomyTermSchema)),
7
+ terms: z.lazy(() => z.array(taxonomyTermSchema)).optional(),
8
8
  });
9
9
  // Schema for a taxonomy group
10
10
  export const taxonomyGroupSchemas = {
@@ -14,5 +14,8 @@ export const taxonomyGroupSchemas = {
14
14
  .optional()
15
15
  .describe("Codename (auto-generated if omitted)"),
16
16
  external_id: z.string().optional().describe("External ID"),
17
- terms: z.array(taxonomyTermSchema).describe("Taxonomy terms hierarchy"),
17
+ terms: z
18
+ .array(taxonomyTermSchema)
19
+ .optional()
20
+ .describe("Taxonomy terms hierarchy"),
18
21
  };
package/build/server.js CHANGED
@@ -3,6 +3,7 @@ import packageJson from "../package.json" with { type: "json" };
3
3
  import { registerTool as registerAddContentItemMapi } from "./tools/add-content-item-mapi.js";
4
4
  import { registerTool as registerAddContentTypeMapi } from "./tools/add-content-type-mapi.js";
5
5
  import { registerTool as registerAddContentTypeSnippetMapi } from "./tools/add-content-type-snippet-mapi.js";
6
+ import { registerTool as registerAddLanguageMapi } from "./tools/add-language-mapi.js";
6
7
  import { registerTool as registerAddTaxonomyGroupMapi } from "./tools/add-taxonomy-group-mapi.js";
7
8
  import { registerTool as registerChangeVariantWorkflowStepMapi } from "./tools/change-variant-workflow-step-mapi.js";
8
9
  import { registerTool as registerCreateVariantVersionMapi } from "./tools/create-variant-version-mapi.js";
@@ -13,17 +14,27 @@ import { registerTool as registerFilterVariantsMapi } from "./tools/filter-varia
13
14
  import { registerTool as registerGetAssetMapi } from "./tools/get-asset-mapi.js";
14
15
  import { registerTool as registerGetInitialContext } from "./tools/get-initial-context.js";
15
16
  import { registerTool as registerGetItemMapi } from "./tools/get-item-mapi.js";
17
+ import { registerTool as registerGetLatestVariantMapi } from "./tools/get-latest-variant-mapi.js";
18
+ import { registerTool as registerGetPublishedVariantMapi } from "./tools/get-published-variant-mapi.js";
16
19
  import { registerTool as registerGetTaxonomyGroupMapi } from "./tools/get-taxonomy-group-mapi.js";
17
20
  import { registerTool as registerGetTypeMapi } from "./tools/get-type-mapi.js";
18
21
  import { registerTool as registerGetTypeSnippetMapi } from "./tools/get-type-snippet-mapi.js";
19
- import { registerTool as registerGetVariantMapi } from "./tools/get-variant-mapi.js";
20
22
  import { registerTool as registerListAssetsMapi } from "./tools/list-assets-mapi.js";
23
+ import { registerTool as registerListCollectionsMapi } from "./tools/list-collections-mapi.js";
21
24
  import { registerTool as registerListContentTypeSnippetsMapi } from "./tools/list-content-type-snippets-mapi.js";
22
25
  import { registerTool as registerListContentTypesMapi } from "./tools/list-content-types-mapi.js";
23
26
  import { registerTool as registerListLanguagesMapi } from "./tools/list-languages-mapi.js";
27
+ import { registerTool as registerListSpacesMapi } from "./tools/list-spaces-mapi.js";
24
28
  import { registerTool as registerListTaxonomyGroupsMapi } from "./tools/list-taxonomy-groups-mapi.js";
29
+ import { registerTool as registerListVariantsCollectionMapi } from "./tools/list-variants-collection-mapi.js";
30
+ import { registerTool as registerListVariantsComponentsTypeMapi } from "./tools/list-variants-components-type-mapi.js";
31
+ import { registerTool as registerListVariantsItemMapi } from "./tools/list-variants-item-mapi.js";
32
+ import { registerTool as registerListVariantsSpaceMapi } from "./tools/list-variants-space-mapi.js";
33
+ import { registerTool as registerListVariantsTypeMapi } from "./tools/list-variants-type-mapi.js";
25
34
  import { registerTool as registerListWorkflowsMapi } from "./tools/list-workflows-mapi.js";
35
+ import { registerTool as registerPatchCollectionsMapi } from "./tools/patch-collections-mapi.js";
26
36
  import { registerTool as registerPatchContentTypeMapi } from "./tools/patch-content-type-mapi.js";
37
+ import { registerTool as registerPatchLanguageMapi } from "./tools/patch-language-mapi.js";
27
38
  import { registerTool as registerPublishVariantMapi } from "./tools/publish-variant-mapi.js";
28
39
  import { registerTool as registerSearchVariantsMapi } from "./tools/search-variants-mapi.js";
29
40
  import { registerTool as registerUnpublishVariantMapi } from "./tools/unpublish-variant-mapi.js";
@@ -42,11 +53,22 @@ export const createServer = () => {
42
53
  // Register all tools
43
54
  registerGetInitialContext(server);
44
55
  registerGetItemMapi(server);
45
- registerGetVariantMapi(server);
56
+ registerGetLatestVariantMapi(server);
57
+ registerGetPublishedVariantMapi(server);
58
+ registerListVariantsItemMapi(server);
59
+ registerListVariantsCollectionMapi(server);
60
+ registerListVariantsTypeMapi(server);
61
+ registerListVariantsComponentsTypeMapi(server);
62
+ registerListVariantsSpaceMapi(server);
46
63
  registerGetTypeMapi(server);
47
64
  registerListContentTypesMapi(server);
48
65
  registerDeleteContentTypeMapi(server);
49
66
  registerListLanguagesMapi(server);
67
+ registerAddLanguageMapi(server);
68
+ registerPatchLanguageMapi(server);
69
+ registerListCollectionsMapi(server);
70
+ registerPatchCollectionsMapi(server);
71
+ registerListSpacesMapi(server);
50
72
  registerGetAssetMapi(server);
51
73
  registerListAssetsMapi(server);
52
74
  registerAddContentTypeMapi(server);
@@ -53,18 +53,18 @@ describe("isEmptyOrDefault", () => {
53
53
  });
54
54
  });
55
55
  describe("removeEmptyValues", () => {
56
- describe("primitive values", () => {
57
- it("returns undefined for null", () => {
58
- assert.strictEqual(removeEmptyValues(null), undefined);
56
+ describe("primitive values at root level - preserved as-is", () => {
57
+ it("returns null for null", () => {
58
+ assert.strictEqual(removeEmptyValues(null), null);
59
59
  });
60
60
  it("returns undefined for undefined", () => {
61
61
  assert.strictEqual(removeEmptyValues(undefined), undefined);
62
62
  });
63
- it("returns undefined for empty string", () => {
64
- assert.strictEqual(removeEmptyValues(""), undefined);
63
+ it("preserves empty string at root level", () => {
64
+ assert.strictEqual(removeEmptyValues(""), "");
65
65
  });
66
- it("returns undefined for rich text empty paragraph", () => {
67
- assert.strictEqual(removeEmptyValues("<p><br/></p>"), undefined);
66
+ it("preserves rich text empty paragraph at root level", () => {
67
+ assert.strictEqual(removeEmptyValues("<p><br/></p>"), "<p><br/></p>");
68
68
  });
69
69
  it("preserves non-empty string", () => {
70
70
  assert.strictEqual(removeEmptyValues("hello"), "hello");
@@ -77,14 +77,14 @@ describe("removeEmptyValues", () => {
77
77
  });
78
78
  });
79
79
  describe("arrays", () => {
80
- it("returns undefined for empty array", () => {
81
- assert.strictEqual(removeEmptyValues([]), undefined);
80
+ it("returns empty array for empty array at root level", () => {
81
+ assert.deepStrictEqual(removeEmptyValues([]), []);
82
82
  });
83
83
  it("removes empty values from array", () => {
84
84
  assert.deepStrictEqual(removeEmptyValues([1, null, 2, "", 3]), [1, 2, 3]);
85
85
  });
86
- it("returns undefined when all array items are empty", () => {
87
- assert.strictEqual(removeEmptyValues([null, "", [], {}]), undefined);
86
+ it("returns empty array when all array items are empty at root level", () => {
87
+ assert.deepStrictEqual(removeEmptyValues([null, "", [], {}]), []);
88
88
  });
89
89
  it("recursively cleans nested arrays", () => {
90
90
  assert.deepStrictEqual(removeEmptyValues([1, [2, null, 3], [null, ""]]), [
@@ -94,8 +94,8 @@ describe("removeEmptyValues", () => {
94
94
  });
95
95
  });
96
96
  describe("objects", () => {
97
- it("returns undefined for empty object", () => {
98
- assert.strictEqual(removeEmptyValues({}), undefined);
97
+ it("returns empty object for empty object at root level", () => {
98
+ assert.deepStrictEqual(removeEmptyValues({}), {});
99
99
  });
100
100
  it("removes null properties", () => {
101
101
  assert.deepStrictEqual(removeEmptyValues({ a: 1, b: null }), { a: 1 });
@@ -119,8 +119,8 @@ describe("removeEmptyValues", () => {
119
119
  a: 1,
120
120
  });
121
121
  });
122
- it("returns undefined when all properties are empty", () => {
123
- assert.strictEqual(removeEmptyValues({ a: null, b: "", c: [], d: {} }), undefined);
122
+ it("returns empty object when all properties are empty at root level", () => {
123
+ assert.deepStrictEqual(removeEmptyValues({ a: null, b: "", c: [], d: {} }), {});
124
124
  });
125
125
  });
126
126
  describe("nested structures", () => {
@@ -448,3 +448,137 @@ describe("variant with all empty elements", () => {
448
448
  assert.deepStrictEqual(result, {}, "All empty elements should be removed, resulting in an empty object");
449
449
  });
450
450
  });
451
+ describe("top-level empty value preservation", () => {
452
+ describe("createMcpToolSuccessResponse", () => {
453
+ it("returns empty array when input is empty array", () => {
454
+ const response = createMcpToolSuccessResponse([]);
455
+ const parsed = JSON.parse(response.content[0].text);
456
+ assert.deepStrictEqual(parsed, []);
457
+ });
458
+ it("returns empty object when input is empty object", () => {
459
+ const response = createMcpToolSuccessResponse({});
460
+ const parsed = JSON.parse(response.content[0].text);
461
+ assert.deepStrictEqual(parsed, {});
462
+ });
463
+ it("returns empty object when all properties are removed", () => {
464
+ const input = { a: null, b: "", c: [] };
465
+ const response = createMcpToolSuccessResponse(input);
466
+ const parsed = JSON.parse(response.content[0].text);
467
+ assert.deepStrictEqual(parsed, {});
468
+ });
469
+ it("returns empty array when all array items are removed", () => {
470
+ const input = [null, "", [], {}];
471
+ const response = createMcpToolSuccessResponse(input);
472
+ const parsed = JSON.parse(response.content[0].text);
473
+ assert.deepStrictEqual(parsed, []);
474
+ });
475
+ it("returns valid JSON string (not undefined)", () => {
476
+ const response = createMcpToolSuccessResponse([]);
477
+ assert.strictEqual(typeof response.content[0].text, "string");
478
+ assert.doesNotThrow(() => JSON.parse(response.content[0].text));
479
+ });
480
+ it("handles filter-variants-like response with empty data array", () => {
481
+ const input = {
482
+ data: [],
483
+ pagination: {
484
+ continuation_token: null,
485
+ },
486
+ };
487
+ const response = createMcpToolSuccessResponse(input);
488
+ const parsed = JSON.parse(response.content[0].text);
489
+ assert.deepStrictEqual(parsed, {});
490
+ });
491
+ });
492
+ describe("createVariantMcpToolSuccessResponse", () => {
493
+ it("returns empty array when input is empty array", () => {
494
+ const response = createVariantMcpToolSuccessResponse([]);
495
+ const parsed = JSON.parse(response.content[0].text);
496
+ assert.deepStrictEqual(parsed, []);
497
+ });
498
+ it("returns empty object when input is empty object", () => {
499
+ const response = createVariantMcpToolSuccessResponse({});
500
+ const parsed = JSON.parse(response.content[0].text);
501
+ assert.deepStrictEqual(parsed, {});
502
+ });
503
+ it("returns empty object when all properties are removed", () => {
504
+ const input = { a: null, b: "", c: [] };
505
+ const response = createVariantMcpToolSuccessResponse(input);
506
+ const parsed = JSON.parse(response.content[0].text);
507
+ assert.deepStrictEqual(parsed, {});
508
+ });
509
+ it("returns empty array when all array items are removed", () => {
510
+ const input = [null, "", [], {}];
511
+ const response = createVariantMcpToolSuccessResponse(input);
512
+ const parsed = JSON.parse(response.content[0].text);
513
+ assert.deepStrictEqual(parsed, []);
514
+ });
515
+ it("returns valid JSON string (not undefined)", () => {
516
+ const response = createVariantMcpToolSuccessResponse([]);
517
+ assert.strictEqual(typeof response.content[0].text, "string");
518
+ assert.doesNotThrow(() => JSON.parse(response.content[0].text));
519
+ });
520
+ it("handles filter-variants-like response with empty data array", () => {
521
+ const input = {
522
+ data: [],
523
+ pagination: {
524
+ continuation_token: null,
525
+ },
526
+ };
527
+ const response = createVariantMcpToolSuccessResponse(input);
528
+ const parsed = JSON.parse(response.content[0].text);
529
+ assert.deepStrictEqual(parsed, {});
530
+ });
531
+ it("handles filter-variants response with variants array becoming empty", () => {
532
+ const input = {
533
+ variants: [
534
+ {
535
+ elements: [
536
+ { element: { id: "el-1" }, value: null },
537
+ { element: { id: "el-2" }, value: "" },
538
+ ],
539
+ },
540
+ ],
541
+ pagination: {
542
+ continuation_token: null,
543
+ },
544
+ };
545
+ const response = createVariantMcpToolSuccessResponse(input);
546
+ const parsed = JSON.parse(response.content[0].text);
547
+ // variants array should still be present (with empty variant objects)
548
+ assert.ok(parsed.variants !== undefined);
549
+ assert.strictEqual(parsed.variants.length, 1);
550
+ });
551
+ });
552
+ });
553
+ describe("undefined input handling - MCP protocol compliance", () => {
554
+ describe("createMcpToolSuccessResponse", () => {
555
+ it("returns string when input is undefined", () => {
556
+ const response = createMcpToolSuccessResponse(undefined);
557
+ assert.strictEqual(typeof response.content[0].text, "string");
558
+ });
559
+ it("returns 'undefined' text when input is undefined", () => {
560
+ const response = createMcpToolSuccessResponse(undefined);
561
+ assert.strictEqual(response.content[0].text, "undefined");
562
+ });
563
+ it("returns valid JSON string when input is null", () => {
564
+ const response = createMcpToolSuccessResponse(null);
565
+ assert.strictEqual(typeof response.content[0].text, "string");
566
+ assert.doesNotThrow(() => JSON.parse(response.content[0].text));
567
+ });
568
+ });
569
+ describe("createVariantMcpToolSuccessResponse", () => {
570
+ it("returns string when input is undefined", () => {
571
+ const response = createVariantMcpToolSuccessResponse(undefined);
572
+ assert.strictEqual(typeof response.content[0].text, "string");
573
+ });
574
+ it("returns 'undefined' text when input is undefined", () => {
575
+ const response = createVariantMcpToolSuccessResponse(undefined);
576
+ assert.strictEqual(response.content[0].text, "undefined");
577
+ });
578
+ it("returns valid JSON string when input is null", () => {
579
+ const response = createVariantMcpToolSuccessResponse(null);
580
+ assert.strictEqual(typeof response.content[0].text, "string");
581
+ assert.doesNotThrow(() => JSON.parse(response.content[0].text));
582
+ });
583
+ });
584
+ });
@@ -0,0 +1,25 @@
1
+ import { createMapiClient } from "../clients/kontentClients.js";
2
+ import { addLanguageSchema } from "../schemas/languageSchemas.js";
3
+ import { handleMcpToolError } from "../utils/errorHandler.js";
4
+ import { createMcpToolSuccessResponse } from "../utils/responseHelper.js";
5
+ export const registerTool = (server) => {
6
+ server.tool("add-language-mapi", "Add new Kontent.ai language via Management API", addLanguageSchema.shape, async ({ name, codename, is_active, fallback_language, external_id }, { authInfo: { token, clientId } = {} }) => {
7
+ const client = createMapiClient(clientId, token);
8
+ try {
9
+ const response = await client
10
+ .addLanguage()
11
+ .withData({
12
+ name,
13
+ codename,
14
+ is_active,
15
+ fallback_language,
16
+ external_id,
17
+ })
18
+ .toPromise();
19
+ return createMcpToolSuccessResponse(response.rawData);
20
+ }
21
+ catch (error) {
22
+ return handleMcpToolError(error, "Language Creation");
23
+ }
24
+ });
25
+ };
@@ -17,6 +17,12 @@ Content types define the structure and blueprint for language variants. They spe
17
17
  ### Content Type Snippets
18
18
  Content type snippets are reusable field groups that promote consistency across multiple content types. Following the DRY principle (define once, use everywhere), one snippet can be used across multiple content types. This prevents duplication and ensures consistency when you need the same fields across different content types.
19
19
 
20
+ ### Collections
21
+ Collections organize content items into logical groups by team, brand, or project. Each content item belongs to exactly one collection.
22
+
23
+ ### Languages
24
+ Languages define available locales for content. Each language can have a fallback language for content inheritance and can be activated or deactivated.
25
+
20
26
  ## Understanding Key Relationships
21
27
 
22
28
  The content structure flows from Content Type → Content Item → Language Variant(s). For reusability, Content Type Snippets can be included in multiple Content Types. For localization, each Content Item can have one Language Variant per language.
@@ -3,7 +3,7 @@ import { createMapiClient } from "../clients/kontentClients.js";
3
3
  import { handleMcpToolError } from "../utils/errorHandler.js";
4
4
  import { createVariantMcpToolSuccessResponse } from "../utils/responseHelper.js";
5
5
  export const registerTool = (server) => {
6
- server.tool("get-variant-mapi", "Get Kontent.ai variant", {
6
+ server.tool("get-latest-variant-mapi", "Get latest version of Kontent.ai language variant from Management API", {
7
7
  itemId: z.string().describe("Item ID"),
8
8
  languageId: z.string().describe("Language variant ID"),
9
9
  }, async ({ itemId, languageId }, { authInfo: { token, clientId } = {} }) => {
@@ -17,7 +17,7 @@ export const registerTool = (server) => {
17
17
  return createVariantMcpToolSuccessResponse(response.rawData);
18
18
  }
19
19
  catch (error) {
20
- return handleMcpToolError(error, "Language Variant Retrieval");
20
+ return handleMcpToolError(error, "Latest Language Variant Retrieval");
21
21
  }
22
22
  });
23
23
  };
@@ -0,0 +1,24 @@
1
+ import { z } from "zod";
2
+ import { createMapiClient } from "../clients/kontentClients.js";
3
+ import { handleMcpToolError } from "../utils/errorHandler.js";
4
+ import { createVariantMcpToolSuccessResponse } from "../utils/responseHelper.js";
5
+ export const registerTool = (server) => {
6
+ server.tool("get-published-variant-mapi", "Get published version of Kontent.ai language variant from Management API", {
7
+ itemId: z.string().describe("Item ID"),
8
+ languageId: z.string().describe("Language variant ID"),
9
+ }, async ({ itemId, languageId }, { authInfo: { token, clientId } = {} }) => {
10
+ const client = createMapiClient(clientId, token);
11
+ try {
12
+ const response = await client
13
+ .viewLanguageVariant()
14
+ .byItemId(itemId)
15
+ .byLanguageId(languageId)
16
+ .published()
17
+ .toPromise();
18
+ return createVariantMcpToolSuccessResponse(response.rawData);
19
+ }
20
+ catch (error) {
21
+ return handleMcpToolError(error, "Published Language Variant Retrieval");
22
+ }
23
+ });
24
+ };
@@ -0,0 +1,15 @@
1
+ import { createMapiClient } from "../clients/kontentClients.js";
2
+ import { handleMcpToolError } from "../utils/errorHandler.js";
3
+ import { createMcpToolSuccessResponse } from "../utils/responseHelper.js";
4
+ export const registerTool = (server) => {
5
+ server.tool("list-collections-mapi", "Get all Kontent.ai collections", {}, async (_, { authInfo: { token, clientId } = {} }) => {
6
+ const client = createMapiClient(clientId, token);
7
+ try {
8
+ const response = await client.listCollections().toPromise();
9
+ return createMcpToolSuccessResponse(response.rawData);
10
+ }
11
+ catch (error) {
12
+ return handleMcpToolError(error, "Collections Listing");
13
+ }
14
+ });
15
+ };
@@ -0,0 +1,15 @@
1
+ import { createMapiClient } from "../clients/kontentClients.js";
2
+ import { handleMcpToolError } from "../utils/errorHandler.js";
3
+ import { createMcpToolSuccessResponse } from "../utils/responseHelper.js";
4
+ export const registerTool = (server) => {
5
+ server.tool("list-spaces-mapi", "Get all Kontent.ai spaces from Management API", {}, async (_, { authInfo: { token, clientId } = {} }) => {
6
+ const client = createMapiClient(clientId, token);
7
+ try {
8
+ const response = await client.listSpaces().toPromise();
9
+ return createMcpToolSuccessResponse(response.rawData);
10
+ }
11
+ catch (error) {
12
+ return handleMcpToolError(error, "Spaces Listing");
13
+ }
14
+ });
15
+ };
@@ -0,0 +1,26 @@
1
+ import { createMapiClient } from "../clients/kontentClients.js";
2
+ import { listVariantsCollectionSchema } from "../schemas/listSchemas.js";
3
+ import { handleMcpToolError } from "../utils/errorHandler.js";
4
+ import { createMcpToolSuccessResponse } from "../utils/responseHelper.js";
5
+ export const registerTool = (server) => {
6
+ server.tool("list-variants-collection-mapi", "List Kontent.ai language variants by collection from Management API (paginated)", listVariantsCollectionSchema.describe("Use list-collections-mapi to get collection ID if not provided").shape, async ({ collectionId, continuation_token }, { authInfo: { token, clientId } = {} }) => {
7
+ const client = createMapiClient(clientId, token);
8
+ try {
9
+ const query = client
10
+ .listLanguageVariantsByCollection()
11
+ .byCollectionId(collectionId);
12
+ const response = await (continuation_token
13
+ ? query.xContinuationToken(continuation_token)
14
+ : query).toPromise();
15
+ return createMcpToolSuccessResponse({
16
+ variants: response.rawData.variants,
17
+ pagination: {
18
+ continuation_token: response.data.pagination.continuationToken,
19
+ },
20
+ });
21
+ }
22
+ catch (error) {
23
+ return handleMcpToolError(error, "Collection Variants Listing");
24
+ }
25
+ });
26
+ };
@@ -0,0 +1,26 @@
1
+ import { createMapiClient } from "../clients/kontentClients.js";
2
+ import { listVariantsComponentsTypeSchema } from "../schemas/listSchemas.js";
3
+ import { handleMcpToolError } from "../utils/errorHandler.js";
4
+ import { createMcpToolSuccessResponse } from "../utils/responseHelper.js";
5
+ export const registerTool = (server) => {
6
+ server.tool("list-variants-components-type-mapi", "List Kontent.ai language variants containing components of a specific content type from Management API (paginated)", listVariantsComponentsTypeSchema.shape, async ({ contentTypeId, continuation_token }, { authInfo: { token, clientId } = {} }) => {
7
+ const client = createMapiClient(clientId, token);
8
+ try {
9
+ const query = client
10
+ .listLanguageVariantsOfContentTypeWithComponents()
11
+ .byTypeId(contentTypeId);
12
+ const response = await (continuation_token
13
+ ? query.xContinuationToken(continuation_token)
14
+ : query).toPromise();
15
+ return createMcpToolSuccessResponse({
16
+ variants: response.rawData.variants,
17
+ pagination: {
18
+ continuation_token: response.data.pagination.continuationToken,
19
+ },
20
+ });
21
+ }
22
+ catch (error) {
23
+ return handleMcpToolError(error, "Content Type Variants With Components Listing");
24
+ }
25
+ });
26
+ };
@@ -0,0 +1,21 @@
1
+ import { z } from "zod";
2
+ import { createMapiClient } from "../clients/kontentClients.js";
3
+ import { handleMcpToolError } from "../utils/errorHandler.js";
4
+ import { createMcpToolSuccessResponse } from "../utils/responseHelper.js";
5
+ export const registerTool = (server) => {
6
+ server.tool("list-variants-item-mapi", "List all Kontent.ai language variants of a content item from Management API", {
7
+ itemId: z.string().describe("Content item ID"),
8
+ }, async ({ itemId }, { authInfo: { token, clientId } = {} }) => {
9
+ const client = createMapiClient(clientId, token);
10
+ try {
11
+ const response = await client
12
+ .listLanguageVariantsOfItem()
13
+ .byItemId(itemId)
14
+ .toPromise();
15
+ return createMcpToolSuccessResponse(response.rawData);
16
+ }
17
+ catch (error) {
18
+ return handleMcpToolError(error, "Item Variants Listing");
19
+ }
20
+ });
21
+ };
@@ -0,0 +1,24 @@
1
+ import { createMapiClient } from "../clients/kontentClients.js";
2
+ import { listVariantsSpaceSchema } from "../schemas/listSchemas.js";
3
+ import { handleMcpToolError } from "../utils/errorHandler.js";
4
+ import { createMcpToolSuccessResponse } from "../utils/responseHelper.js";
5
+ export const registerTool = (server) => {
6
+ server.tool("list-variants-space-mapi", "List Kontent.ai language variants by space from Management API (paginated)", listVariantsSpaceSchema.shape, async ({ spaceId, continuation_token }, { authInfo: { token, clientId } = {} }) => {
7
+ const client = createMapiClient(clientId, token);
8
+ try {
9
+ const query = client.listLanguageVariantsBySpace().bySpaceId(spaceId);
10
+ const response = await (continuation_token
11
+ ? query.xContinuationToken(continuation_token)
12
+ : query).toPromise();
13
+ return createMcpToolSuccessResponse({
14
+ variants: response.rawData.variants,
15
+ pagination: {
16
+ continuation_token: response.data.pagination.continuationToken,
17
+ },
18
+ });
19
+ }
20
+ catch (error) {
21
+ return handleMcpToolError(error, "Space Variants Listing");
22
+ }
23
+ });
24
+ };
@@ -0,0 +1,26 @@
1
+ import { createMapiClient } from "../clients/kontentClients.js";
2
+ import { listVariantsTypeSchema } from "../schemas/listSchemas.js";
3
+ import { handleMcpToolError } from "../utils/errorHandler.js";
4
+ import { createMcpToolSuccessResponse } from "../utils/responseHelper.js";
5
+ export const registerTool = (server) => {
6
+ server.tool("list-variants-type-mapi", "List Kontent.ai language variants by content type from Management API (paginated)", listVariantsTypeSchema.shape, async ({ contentTypeId, continuation_token }, { authInfo: { token, clientId } = {} }) => {
7
+ const client = createMapiClient(clientId, token);
8
+ try {
9
+ const query = client
10
+ .listLanguageVariantsOfContentType()
11
+ .byTypeId(contentTypeId);
12
+ const response = await (continuation_token
13
+ ? query.xContinuationToken(continuation_token)
14
+ : query).toPromise();
15
+ return createMcpToolSuccessResponse({
16
+ variants: response.rawData.variants,
17
+ pagination: {
18
+ continuation_token: response.data.pagination.continuationToken,
19
+ },
20
+ });
21
+ }
22
+ catch (error) {
23
+ return handleMcpToolError(error, "Content Type Variants Listing");
24
+ }
25
+ });
26
+ };
@@ -0,0 +1,25 @@
1
+ import { createMapiClient } from "../clients/kontentClients.js";
2
+ import { collectionPatchOperationsSchema } from "../schemas/collectionSchemas.js";
3
+ import { handleMcpToolError } from "../utils/errorHandler.js";
4
+ import { createMcpToolSuccessResponse } from "../utils/responseHelper.js";
5
+ export const registerTool = (server) => {
6
+ server.tool("patch-collections-mapi", "Update Kontent.ai collections using patch operations (addInto, move, remove, replace)", {
7
+ operations: collectionPatchOperationsSchema.describe("Patch operations array. Call list-collections-mapi first. Use addInto to add new collections, move to reorder, remove to delete empty collections, replace to rename."),
8
+ }, async ({ operations }, { authInfo: { token, clientId } = {} }) => {
9
+ const client = createMapiClient(clientId, token);
10
+ try {
11
+ const response = await client
12
+ .setCollections()
13
+ .withData(operations)
14
+ .toPromise();
15
+ return createMcpToolSuccessResponse({
16
+ message: `Collections updated successfully with ${operations.length} operation(s)`,
17
+ collections: response.rawData,
18
+ appliedOperations: operations,
19
+ });
20
+ }
21
+ catch (error) {
22
+ return handleMcpToolError(error, "Collections Patch");
23
+ }
24
+ });
25
+ };
@@ -0,0 +1,24 @@
1
+ import { createMapiClient } from "../clients/kontentClients.js";
2
+ import { patchLanguageSchema } from "../schemas/languageSchemas.js";
3
+ import { handleMcpToolError } from "../utils/errorHandler.js";
4
+ import { createMcpToolSuccessResponse } from "../utils/responseHelper.js";
5
+ export const registerTool = (server) => {
6
+ server.tool("patch-language-mapi", "Update Kontent.ai language using replace operations via Management API. Only active languages can be modified - if deactivated, is_active: true must be first operation.", patchLanguageSchema.shape, async ({ languageId, operations }, { authInfo: { token, clientId } = {} }) => {
7
+ const client = createMapiClient(clientId, token);
8
+ try {
9
+ const response = await client
10
+ .modifyLanguage()
11
+ .byLanguageId(languageId)
12
+ .withData(operations)
13
+ .toPromise();
14
+ return createMcpToolSuccessResponse({
15
+ message: `Language updated with ${operations.length} operation(s)`,
16
+ language: response.rawData,
17
+ appliedOperations: operations,
18
+ });
19
+ }
20
+ catch (error) {
21
+ return handleMcpToolError(error, "Language Patch");
22
+ }
23
+ });
24
+ };
@@ -55,7 +55,7 @@ export function removeEmptyElementsFromVariant(obj) {
55
55
  }
56
56
  return result;
57
57
  }
58
- export function removeEmptyValues(obj) {
58
+ function removeEmptyValuesRecursive(obj) {
59
59
  if (obj === null || obj === undefined) {
60
60
  return undefined;
61
61
  }
@@ -64,13 +64,13 @@ export function removeEmptyValues(obj) {
64
64
  }
65
65
  if (Array.isArray(obj)) {
66
66
  const cleaned = obj
67
- .map((item) => removeEmptyValues(item))
67
+ .map((item) => removeEmptyValuesRecursive(item))
68
68
  .filter((item) => item !== undefined);
69
69
  return cleaned.length === 0 ? undefined : cleaned;
70
70
  }
71
71
  const cleaned = {};
72
72
  for (const [key, value] of Object.entries(obj)) {
73
- const cleanedValue = removeEmptyValues(value);
73
+ const cleanedValue = removeEmptyValuesRecursive(value);
74
74
  if (cleanedValue !== undefined) {
75
75
  cleaned[key] = cleanedValue;
76
76
  }
@@ -78,25 +78,51 @@ export function removeEmptyValues(obj) {
78
78
  const keys = Object.keys(cleaned);
79
79
  return keys.length === 0 ? undefined : cleaned;
80
80
  }
81
- export const createMcpToolSuccessResponse = (data) => {
81
+ export function removeEmptyValues(obj) {
82
+ // At root level, preserve the structure even if empty
83
+ if (typeof obj !== "object" || obj === null) {
84
+ return obj;
85
+ }
86
+ if (Array.isArray(obj)) {
87
+ return obj
88
+ .map((item) => removeEmptyValuesRecursive(item))
89
+ .filter((item) => item !== undefined);
90
+ }
91
+ const cleaned = {};
92
+ for (const [key, value] of Object.entries(obj)) {
93
+ const cleanedValue = removeEmptyValuesRecursive(value);
94
+ if (cleanedValue !== undefined) {
95
+ cleaned[key] = cleanedValue;
96
+ }
97
+ }
98
+ return cleaned;
99
+ }
100
+ /**
101
+ * Converts data to MCP tool success response format.
102
+ * Handles undefined separately as JSON.stringify(undefined) returns undefined (not a string).
103
+ * Skips stringify for strings as they don't need JSON encoding for MCP text response.
104
+ */
105
+ const toMcpSuccessResponse = (data) => {
106
+ const text = data === undefined
107
+ ? "undefined"
108
+ : typeof data === "string"
109
+ ? data
110
+ : JSON.stringify(data);
82
111
  return {
83
112
  content: [
84
113
  {
85
114
  type: "text",
86
- text: JSON.stringify(removeEmptyValues(data)),
115
+ text,
87
116
  },
88
117
  ],
89
118
  };
90
119
  };
120
+ export const createMcpToolSuccessResponse = (data) => {
121
+ const cleaned = removeEmptyValues(data);
122
+ return toMcpSuccessResponse(cleaned);
123
+ };
91
124
  export const createVariantMcpToolSuccessResponse = (data) => {
92
125
  const cleaned = removeEmptyValues(data);
93
126
  const optimized = removeEmptyElementsFromVariant(cleaned);
94
- return {
95
- content: [
96
- {
97
- type: "text",
98
- text: JSON.stringify(optimized),
99
- },
100
- ],
101
- };
127
+ return toMcpSuccessResponse(optimized);
102
128
  };
package/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "@kontent-ai/mcp-server",
3
- "version": "0.21.11",
3
+ "version": "0.22.1",
4
4
  "type": "module",
5
5
  "mcpName": "io.github.kontent-ai/mcp-server",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/kontent-ai/mcp-server"
9
+ },
6
10
  "scripts": {
7
11
  "build": "rimraf build && tsc --project scripts/tsconfig.json && tsc",
8
12
  "start:stdio": "node build/bin.js stdio",
@@ -25,7 +29,7 @@
25
29
  "author": "Jiri Lojda",
26
30
  "license": "MIT",
27
31
  "dependencies": {
28
- "@kontent-ai/management-sdk": "^7.11.0",
32
+ "@kontent-ai/management-sdk": "^8.1.0",
29
33
  "@modelcontextprotocol/sdk": "^1.12.0",
30
34
  "applicationinsights": "^2.9.8",
31
35
  "dotenv": "^16.5.0",