@kontent-ai/mcp-server 0.21.11 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +39 -25
  2. package/build/schemas/collectionSchemas.js +37 -0
  3. package/build/schemas/filterVariantSchemas.js +1 -1
  4. package/build/schemas/languageSchemas.js +43 -0
  5. package/build/schemas/listSchemas.js +16 -0
  6. package/build/schemas/searchOperationSchemas.js +1 -4
  7. package/build/schemas/taxonomySchemas.js +5 -2
  8. package/build/schemas/workflowSchemas.js +6 -14
  9. package/build/server.js +26 -8
  10. package/build/telemetry/applicationInsights.js +1 -1
  11. package/build/test/utils/responseHelper.spec.js +149 -15
  12. package/build/tools/add-language-mapi.js +25 -0
  13. package/build/tools/change-variant-workflow-step-mapi.js +3 -4
  14. package/build/tools/create-variant-version-mapi.js +1 -2
  15. package/build/tools/get-asset-mapi.js +1 -1
  16. package/build/tools/get-item-mapi.js +1 -1
  17. package/build/tools/{get-variant-mapi.js → get-latest-variant-mapi.js} +2 -2
  18. package/build/tools/get-patch-guide.js +12 -0
  19. package/build/tools/get-published-variant-mapi.js +24 -0
  20. package/build/tools/get-taxonomy-group-mapi.js +1 -1
  21. package/build/tools/get-type-mapi.js +1 -1
  22. package/build/tools/get-type-snippet-mapi.js +1 -1
  23. package/build/tools/list-assets-mapi.js +1 -1
  24. package/build/tools/list-collections-mapi.js +15 -0
  25. package/build/tools/list-content-type-snippets-mapi.js +1 -1
  26. package/build/tools/list-content-types-mapi.js +1 -1
  27. package/build/tools/list-languages-mapi.js +1 -1
  28. package/build/tools/list-spaces-mapi.js +15 -0
  29. package/build/tools/list-taxonomy-groups-mapi.js +1 -1
  30. package/build/tools/list-variants-collection-mapi.js +26 -0
  31. package/build/tools/list-variants-components-type-mapi.js +26 -0
  32. package/build/tools/list-variants-item-mapi.js +21 -0
  33. package/build/tools/list-variants-space-mapi.js +24 -0
  34. package/build/tools/list-variants-type-mapi.js +26 -0
  35. package/build/tools/list-workflows-mapi.js +1 -1
  36. package/build/tools/patch-collections-mapi.js +25 -0
  37. package/build/tools/patch-content-type-mapi.js +1 -1
  38. package/build/tools/patch-language-mapi.js +24 -0
  39. package/build/tools/publish-variant-mapi.js +3 -5
  40. package/build/tools/unpublish-variant-mapi.js +3 -5
  41. package/build/tools/upsert-language-variant-mapi.js +1 -1
  42. package/build/utils/responseHelper.js +39 -13
  43. package/package.json +19 -15
  44. package/build/tools/context/initial-context.js +0 -195
  45. package/build/tools/get-initial-context.js +0 -13
package/README.md CHANGED
@@ -61,9 +61,9 @@ npx @kontent-ai/mcp-server@latest shttp
61
61
 
62
62
  ## 🛠️ Available Tools
63
63
 
64
- ### Context and Setup
64
+ ### Patch Operations Guide
65
65
 
66
- * **get-initial-context** – 🚨 **MANDATORY FIRST STEP**: This tool MUST be called before using ANY other tools. It provides essential context, configuration, and operational guidelines for Kontent.ai
66
+ * **get-patch-guide** – 🚨 **REQUIRED before any patch operation**. Get JSON Patch operations guide for Kontent.ai Management API
67
67
 
68
68
  ### Content Type Management
69
69
 
@@ -89,11 +89,17 @@ 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
96
- * **upsert-language-variant-mapi** – Create or update Kontent.ai language variant of a content item via Management API. This adds actual content to the content item elements. When updating an existing variant, only the provided elements will be modified
102
+ * **upsert-language-variant-mapi** – Create or update Kontent.ai language variant of a content item via Management API. Element values must fulfill limitations and guidelines defined in content type. When updating, only provided elements will be modified
97
103
  * **create-variant-version-mapi** – Create new version of Kontent.ai language variant via Management API. This operation creates a new version of an existing language variant, useful for content versioning and creating new drafts from published content
98
104
  * **delete-language-variant-mapi** – Delete Kontent.ai language variant from Management API
99
105
  * **filter-variants-mapi** – Filter Kontent.ai language variants of content items using Management API. Use for exact keyword matching and finding specific terms in content. Supports full filtering capabilities (content types, workflow steps, taxonomies, etc.) and optionally includes full content of variants. Returns paginated results with continuation token for fetching subsequent pages
@@ -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);
@@ -10,7 +10,7 @@ const userReferenceSchema = z
10
10
  }),
11
11
  z.object({
12
12
  id: z.never().optional(),
13
- email: z.string().email().describe("User email address"),
13
+ email: z.email().describe("User email address"),
14
14
  }),
15
15
  ])
16
16
  .describe("Reference to a user by either their id or email (but not both)");
@@ -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
+ });
@@ -5,10 +5,7 @@ export const searchOperationSchema = z.object({
5
5
  .describe("Search phrase for AI-powered semantic search. Uses vector database to find content by meaning and similarity, not just exact keyword matching"),
6
6
  filter: z
7
7
  .object({
8
- variantId: z
9
- .string()
10
- .uuid()
11
- .describe("UUID of the language variant to filter by"),
8
+ variantId: z.uuid().describe("UUID of the language variant to filter by"),
12
9
  })
13
10
  .describe("Mandatory content item variant filter to restrict search scope. Must specify a valid variant UUID"),
14
11
  });
@@ -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
  };
@@ -2,7 +2,6 @@ import { z } from "zod";
2
2
  // Schema for a workflow step
3
3
  const workflowStepSchema = z.object({
4
4
  id: z
5
- .string()
6
5
  .uuid()
7
6
  .describe("The unique identifier of the workflow step in UUID format"),
8
7
  name: z.string().describe("The human-readable name of the workflow step"),
@@ -10,18 +9,17 @@ const workflowStepSchema = z.object({
10
9
  .string()
11
10
  .describe("The codename of the workflow step used for API operations"),
12
11
  transitions_to: z
13
- .array(z.string().uuid())
12
+ .array(z.uuid())
14
13
  .describe("Array of workflow step IDs that this step can transition to")
15
14
  .optional(),
16
15
  role_ids: z
17
- .array(z.string().uuid())
16
+ .array(z.uuid())
18
17
  .describe("Array of role IDs that have permissions for this workflow step")
19
18
  .optional(),
20
19
  });
21
20
  // Schema for the published step
22
21
  const publishedStepSchema = z.object({
23
22
  id: z
24
- .string()
25
23
  .uuid()
26
24
  .describe("The unique identifier of the published step in UUID format"),
27
25
  name: z
@@ -31,18 +29,17 @@ const publishedStepSchema = z.object({
31
29
  .string()
32
30
  .describe("The codename of the published step - typically 'published'"),
33
31
  unpublish_role_ids: z
34
- .array(z.string().uuid())
32
+ .array(z.uuid())
35
33
  .describe("Array of role IDs that can unpublish content from this step")
36
34
  .optional(),
37
35
  create_new_version_role_ids: z
38
- .array(z.string().uuid())
36
+ .array(z.uuid())
39
37
  .describe("Array of role IDs that can create new versions of content in this step")
40
38
  .optional(),
41
39
  });
42
40
  // Schema for the scheduled step
43
41
  const scheduledStepSchema = z.object({
44
42
  id: z
45
- .string()
46
43
  .uuid()
47
44
  .describe("The unique identifier of the scheduled step in UUID format"),
48
45
  name: z
@@ -55,7 +52,6 @@ const scheduledStepSchema = z.object({
55
52
  // Schema for the archived step
56
53
  const archivedStepSchema = z.object({
57
54
  id: z
58
- .string()
59
55
  .uuid()
60
56
  .describe("The unique identifier of the archived step in UUID format"),
61
57
  name: z
@@ -65,7 +61,7 @@ const archivedStepSchema = z.object({
65
61
  .string()
66
62
  .describe("The codename of the archived step - typically 'archived'"),
67
63
  role_ids: z
68
- .array(z.string().uuid())
64
+ .array(z.uuid())
69
65
  .describe("Array of role IDs that can unarchive content from this step")
70
66
  .optional(),
71
67
  });
@@ -74,7 +70,6 @@ const workflowScopeSchema = z.object({
74
70
  content_types: z
75
71
  .array(z.object({
76
72
  id: z
77
- .string()
78
73
  .uuid()
79
74
  .describe("The unique identifier of the content type in UUID format"),
80
75
  }))
@@ -82,10 +77,7 @@ const workflowScopeSchema = z.object({
82
77
  });
83
78
  // Main workflow schema
84
79
  export const workflowSchema = z.object({
85
- id: z
86
- .string()
87
- .uuid()
88
- .describe("The unique identifier of the workflow in UUID format"),
80
+ id: z.uuid().describe("The unique identifier of the workflow in UUID format"),
89
81
  name: z.string().describe("The human-readable name of the workflow"),
90
82
  codename: z
91
83
  .string()
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";
@@ -11,19 +12,29 @@ import { registerTool as registerDeleteContentTypeMapi } from "./tools/delete-co
11
12
  import { registerTool as registerDeleteLanguageVariantMapi } from "./tools/delete-language-variant-mapi.js";
12
13
  import { registerTool as registerFilterVariantsMapi } from "./tools/filter-variants-mapi.js";
13
14
  import { registerTool as registerGetAssetMapi } from "./tools/get-asset-mapi.js";
14
- import { registerTool as registerGetInitialContext } from "./tools/get-initial-context.js";
15
15
  import { registerTool as registerGetItemMapi } from "./tools/get-item-mapi.js";
16
+ import { registerTool as registerGetLatestVariantMapi } from "./tools/get-latest-variant-mapi.js";
17
+ import { registerTool as registerGetPatchGuide } from "./tools/get-patch-guide.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";
@@ -34,19 +45,26 @@ export const createServer = () => {
34
45
  const server = new McpServer({
35
46
  name: "kontent-ai",
36
47
  version: packageJson.version,
37
- capabilities: {
38
- resources: {},
39
- tools: {},
40
- },
41
48
  });
42
49
  // Register all tools
43
- registerGetInitialContext(server);
50
+ registerGetPatchGuide(server);
44
51
  registerGetItemMapi(server);
45
- registerGetVariantMapi(server);
52
+ registerGetLatestVariantMapi(server);
53
+ registerGetPublishedVariantMapi(server);
54
+ registerListVariantsItemMapi(server);
55
+ registerListVariantsCollectionMapi(server);
56
+ registerListVariantsTypeMapi(server);
57
+ registerListVariantsComponentsTypeMapi(server);
58
+ registerListVariantsSpaceMapi(server);
46
59
  registerGetTypeMapi(server);
47
60
  registerListContentTypesMapi(server);
48
61
  registerDeleteContentTypeMapi(server);
49
62
  registerListLanguagesMapi(server);
63
+ registerAddLanguageMapi(server);
64
+ registerPatchLanguageMapi(server);
65
+ registerListCollectionsMapi(server);
66
+ registerPatchCollectionsMapi(server);
67
+ registerListSpacesMapi(server);
50
68
  registerGetAssetMapi(server);
51
69
  registerListAssetsMapi(server);
52
70
  registerAddContentTypeMapi(server);
@@ -148,7 +148,7 @@ export function initializeApplicationInsights() {
148
148
  .setAutoCollectDependencies(false)
149
149
  .setAutoDependencyCorrelation(false)
150
150
  .setAutoCollectHeartbeat(false)
151
- .setAutoCollectPerformance(false)
151
+ .setAutoCollectPerformance(false, false)
152
152
  .setAutoCollectIncomingRequestAzureFunctions(false)
153
153
  .setAutoCollectPreAggregatedMetrics(false)
154
154
  .start();
@@ -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
+ };