@kontent-ai/mcp-server 0.26.0 → 0.28.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 +2 -30
- package/build/schemas/bulkGetItemsWithVariantsSchemas.js +14 -0
- package/build/schemas/contentItemSchemas.js +109 -48
- package/build/schemas/filterVariantSchemas.js +15 -4
- package/build/server.js +2 -0
- package/build/test/utils/responseHelper.spec.js +22 -556
- package/build/tools/bulk-get-items-variants-mapi.js +31 -0
- package/build/tools/filter-variants-mapi.js +8 -6
- package/build/tools/get-latest-variant-mapi.js +2 -2
- package/build/tools/get-published-variant-mapi.js +2 -2
- package/build/tools/search-variants-mapi.js +2 -2
- package/build/tools/upsert-language-variant-mapi.js +2 -2
- package/build/utils/responseHelper.js +3 -107
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -23,7 +23,6 @@ Kontent.ai MCP Server implements the Model Context Protocol to connect your Kont
|
|
|
23
23
|
- [🔌 Quickstart](#-quickstart)
|
|
24
24
|
- [🛠️ Available Tools](#️-available-tools)
|
|
25
25
|
- [⚙️ Configuration](#️-configuration)
|
|
26
|
-
- [🔧 Response Optimization](#-response-optimization)
|
|
27
26
|
- [🚀 Transport Options](#-transport-options)
|
|
28
27
|
- [💻 Development](#-development)
|
|
29
28
|
- [🛠 Local Installation](#-local-installation)
|
|
@@ -105,7 +104,8 @@ npx @kontent-ai/mcp-server@latest shttp
|
|
|
105
104
|
* **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
|
|
106
105
|
* **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
|
|
107
106
|
* **delete-language-variant-mapi** – Delete Kontent.ai language variant from Management API
|
|
108
|
-
* **filter-variants-mapi** – Filter Kontent.ai
|
|
107
|
+
* **filter-variants-mapi** – Filter Kontent.ai items with variants returning references (item ID + language ID). Use for exact keyword matching and finding specific terms in content. Supports full filtering capabilities (content types, workflow steps, taxonomies, spaces, collections, publishing states, etc.). Returns paginated results with continuation token for fetching subsequent pages. Use bulk-get-items-variants-mapi to retrieve full content for matched items
|
|
108
|
+
* **bulk-get-items-variants-mapi** – Bulk get Kontent.ai content items with their language variants by item and language reference pairs. Use after filter-variants-mapi to retrieve full content data for specific item+language pairs. Items without a variant in the requested language return the item without the variant property. Returns paginated results with continuation token
|
|
109
109
|
* **search-variants-mapi** – AI-powered semantic search for finding content by meaning and concepts in a specific language variant. Use for: conceptual searches when you don't know exact keywords. Limited filtering options (variant ID only)
|
|
110
110
|
|
|
111
111
|
### Asset Management
|
|
@@ -176,34 +176,6 @@ For multi-tenant mode (Streamable HTTP only), the server accepts:
|
|
|
176
176
|
|
|
177
177
|
This mode allows a single server instance to handle requests for multiple Kontent.ai environments securely without requiring environment variables.
|
|
178
178
|
|
|
179
|
-
## 🔧 Response Optimization
|
|
180
|
-
|
|
181
|
-
The MCP server implements automatic token optimization to reduce AI model costs and improve performance:
|
|
182
|
-
|
|
183
|
-
### Token Reduction Strategy
|
|
184
|
-
|
|
185
|
-
The server automatically removes empty/default values from responses to reduce token usage. This includes:
|
|
186
|
-
|
|
187
|
-
- Null and undefined values
|
|
188
|
-
- Empty strings (`""`)
|
|
189
|
-
- Empty arrays (`[]`)
|
|
190
|
-
- Empty objects (`{}`)
|
|
191
|
-
- Rich text placeholders (`"<p><br/></p>"`)
|
|
192
|
-
- Elements with only an ID after empty value removal
|
|
193
|
-
|
|
194
|
-
### Impact on AI Agents
|
|
195
|
-
|
|
196
|
-
**Important for AI implementations**: When consuming responses from this MCP server:
|
|
197
|
-
|
|
198
|
-
1. **Missing properties indicate default values**, not missing data
|
|
199
|
-
2. Missing elements in variants have their type-specific defaults:
|
|
200
|
-
- Text elements: `""` (empty string)
|
|
201
|
-
- Rich text: `"<p><br/></p>"` (empty placeholder)
|
|
202
|
-
- Number/Date: `null`
|
|
203
|
-
- Custom elements: `null` (for value and searchable_value)
|
|
204
|
-
- Arrays (assets, taxonomy, etc.): `[]`
|
|
205
|
-
3. When creating/updating content, always send complete data
|
|
206
|
-
|
|
207
179
|
## 🚀 Transport Options
|
|
208
180
|
|
|
209
181
|
### 📟 STDIO Transport
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { continuationTokenField } from "./listSchemas.js";
|
|
3
|
+
import { referenceObjectSchema } from "./referenceObjectSchema.js";
|
|
4
|
+
export const bulkGetItemsWithVariantsSchema = z.object({
|
|
5
|
+
variants: z
|
|
6
|
+
.array(z.object({
|
|
7
|
+
item: referenceObjectSchema.describe("Reference to a content item by its id, codename, or external id"),
|
|
8
|
+
language: referenceObjectSchema.describe("Reference to a language by its id, codename, or external id"),
|
|
9
|
+
}))
|
|
10
|
+
.min(1)
|
|
11
|
+
.max(100)
|
|
12
|
+
.describe("Array of item and language reference pairs to retrieve (max 100). Use filter-variants-mapi to get item and language references first."),
|
|
13
|
+
continuation_token: continuationTokenField,
|
|
14
|
+
});
|
|
@@ -1,67 +1,128 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { referenceObjectSchema } from "./referenceObjectSchema.js";
|
|
3
|
-
//
|
|
4
|
-
const
|
|
3
|
+
// Element schemas with descriptions based on Kontent.ai Management API documentation
|
|
4
|
+
const assetInVariantElementSchema = z
|
|
5
|
+
.object({
|
|
5
6
|
element: referenceObjectSchema,
|
|
6
|
-
value: z
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}));
|
|
15
|
-
const assetInVariantElementSchema = z.object({
|
|
7
|
+
value: z
|
|
8
|
+
.array(referenceObjectSchema)
|
|
9
|
+
.nullable()
|
|
10
|
+
.describe("Array of Reference objects, each representing a single asset. Every asset can be referenced only once."),
|
|
11
|
+
})
|
|
12
|
+
.describe("Asset element - references to assets (images, documents). Supports renditions for image-specific editions.");
|
|
13
|
+
const customElementInVariantElementSchema = z
|
|
14
|
+
.object({
|
|
16
15
|
element: referenceObjectSchema,
|
|
17
|
-
value: z
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
value: z
|
|
17
|
+
.string()
|
|
18
|
+
.max(200000)
|
|
19
|
+
.nullable()
|
|
20
|
+
.describe("Custom-formatted data depending on the specific custom element implementation."),
|
|
21
|
+
searchable_value: z
|
|
22
|
+
.string()
|
|
23
|
+
.max(200000)
|
|
24
|
+
.nullable()
|
|
25
|
+
.describe("Plain text for search functionality in content item lists."),
|
|
26
|
+
})
|
|
27
|
+
.describe("Custom element - stores custom-formatted data with optional searchable plain text representation.");
|
|
28
|
+
const dateTimeInVariantElementSchema = z
|
|
29
|
+
.object({
|
|
20
30
|
element: referenceObjectSchema,
|
|
21
|
-
value: z.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
31
|
+
value: z.iso.datetime().nullable(),
|
|
32
|
+
display_timezone: z
|
|
33
|
+
.string()
|
|
34
|
+
.nullable()
|
|
35
|
+
.describe("IANA time zone name affecting UI display without modifying stored value. Defaults to null (local time zone)."),
|
|
36
|
+
})
|
|
37
|
+
.describe("Date & time element - stores date and time values in UTC with optional display timezone.");
|
|
38
|
+
const linkedItemsInVariantElementSchema = z
|
|
39
|
+
.object({
|
|
25
40
|
element: referenceObjectSchema,
|
|
26
|
-
value: z
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
41
|
+
value: z
|
|
42
|
+
.array(referenceObjectSchema)
|
|
43
|
+
.nullable()
|
|
44
|
+
.describe("Array of Reference objects. Each reference represents a single content item (each item referenced only once)."),
|
|
45
|
+
})
|
|
46
|
+
.describe("Linked items element - references to other content items for modular content.");
|
|
47
|
+
const multipleChoiceInVariantElementSchema = z
|
|
48
|
+
.object({
|
|
30
49
|
element: referenceObjectSchema,
|
|
31
|
-
value: z
|
|
32
|
-
|
|
33
|
-
|
|
50
|
+
value: z
|
|
51
|
+
.array(referenceObjectSchema)
|
|
52
|
+
.nullable()
|
|
53
|
+
.describe("Array of Reference objects. Each reference represents one of the multiple choice options defined in content type."),
|
|
54
|
+
})
|
|
55
|
+
.describe("Multiple choice element - references to selected choice options. Single-option mode requires exactly one reference.");
|
|
56
|
+
const numberInVariantElementSchema = z
|
|
57
|
+
.object({
|
|
34
58
|
element: referenceObjectSchema,
|
|
35
|
-
value: z.
|
|
36
|
-
})
|
|
37
|
-
|
|
59
|
+
value: z.number().nullable().describe("Floating-point number."),
|
|
60
|
+
})
|
|
61
|
+
.describe("Number element - stores numeric floating-point values.");
|
|
62
|
+
const taxonomyInVariantElementSchema = z
|
|
63
|
+
.object({
|
|
38
64
|
element: referenceObjectSchema,
|
|
39
|
-
value: z
|
|
40
|
-
|
|
41
|
-
|
|
65
|
+
value: z
|
|
66
|
+
.array(referenceObjectSchema)
|
|
67
|
+
.nullable()
|
|
68
|
+
.describe("Array of Reference objects. Each reference represents a taxonomy term (each term referenced only once)."),
|
|
69
|
+
})
|
|
70
|
+
.describe("Taxonomy element - references to taxonomy terms from a taxonomy group.");
|
|
71
|
+
const textInVariantElementSchema = z
|
|
72
|
+
.object({
|
|
42
73
|
element: referenceObjectSchema,
|
|
43
|
-
value: z.string().nullable(),
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
74
|
+
value: z.string().max(100000).nullable().describe("Plain text content."),
|
|
75
|
+
})
|
|
76
|
+
.describe("Text element - stores plain text content.");
|
|
77
|
+
const urlSlugInVariantElementSchema = z
|
|
78
|
+
.object({
|
|
47
79
|
element: referenceObjectSchema,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
80
|
+
mode: z
|
|
81
|
+
.enum(["autogenerated", "custom"])
|
|
82
|
+
.describe("'autogenerated' (system-generated based on dependent text element) or 'custom' (manual). Switches to custom when value is directly modified."),
|
|
83
|
+
value: z.string().nullable().describe("URL-friendly slug value for SEO."),
|
|
84
|
+
})
|
|
85
|
+
.describe("URL slug element - URL-friendly slug for SEO. Can be auto-generated from dependent text element or custom.");
|
|
86
|
+
// Rich text component schema - uses lazy to handle circular reference
|
|
87
|
+
const richTextComponentSchema = z.lazy(() => z
|
|
88
|
+
.object({
|
|
89
|
+
id: z
|
|
90
|
+
.string()
|
|
91
|
+
.describe("Unique identifier of the component within the rich text element."),
|
|
92
|
+
type: referenceObjectSchema.describe("Reference to the content type defining the component structure."),
|
|
93
|
+
elements: z
|
|
94
|
+
.array(elementInComponentSchema)
|
|
95
|
+
.nullable()
|
|
96
|
+
.describe("Array of element values within the component (supports up to 6 levels deep nesting)."),
|
|
97
|
+
})
|
|
98
|
+
.describe("Component embedded in rich text - a reusable content block defined by a content type."));
|
|
99
|
+
// Rich text element schema - references components which can contain any element type
|
|
100
|
+
const richTextInVariantElementSchema = z
|
|
101
|
+
.object({
|
|
51
102
|
element: referenceObjectSchema,
|
|
52
|
-
value: z
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
103
|
+
value: z
|
|
104
|
+
.string()
|
|
105
|
+
.max(100000)
|
|
106
|
+
.nullable()
|
|
107
|
+
.describe("Valid HTML5 fragment. Defaults to '<p><br/></p>' if empty."),
|
|
108
|
+
components: z
|
|
109
|
+
.array(richTextComponentSchema)
|
|
110
|
+
.nullable()
|
|
111
|
+
.describe("Array of nested component objects that can be embedded in the rich text content."),
|
|
112
|
+
})
|
|
113
|
+
.describe("Rich text element - formatted HTML content that can include embedded assets, components, content items, and links.");
|
|
114
|
+
// Union schema for elements within components
|
|
115
|
+
// Uses z.lazy() to handle circular reference with richTextComponentSchema
|
|
116
|
+
const elementInComponentSchema = z
|
|
117
|
+
.lazy(() => languageVariantElementSchema)
|
|
118
|
+
.describe("Element value within a component - same structure as top-level variant elements.");
|
|
119
|
+
// Top-level language variant element schema - discriminated by unique fields
|
|
59
120
|
export const languageVariantElementSchema = z.union([
|
|
60
121
|
// Most specific schemas first (with unique distinguishing fields)
|
|
61
122
|
urlSlugInVariantElementSchema, // has unique required 'mode' field
|
|
62
|
-
richTextInVariantElementSchema, // has unique
|
|
123
|
+
richTextInVariantElementSchema, // has unique 'components' field
|
|
63
124
|
dateTimeInVariantElementSchema, // has unique nullable 'display_timezone' field
|
|
64
|
-
customElementInVariantElementSchema, // has unique
|
|
125
|
+
customElementInVariantElementSchema, // has unique 'searchable_value' field
|
|
65
126
|
numberInVariantElementSchema, // has unique value type (number)
|
|
66
127
|
// Medium specificity (array value types)
|
|
67
128
|
assetInVariantElementSchema, // value: array of references
|
|
@@ -68,6 +68,21 @@ export const filterVariantsSchema = z.object({
|
|
|
68
68
|
.min(1)
|
|
69
69
|
.optional()
|
|
70
70
|
.describe("Array of taxonomy groups with taxonomy terms"),
|
|
71
|
+
spaces: z
|
|
72
|
+
.array(referenceObjectSchema)
|
|
73
|
+
.min(1)
|
|
74
|
+
.optional()
|
|
75
|
+
.describe("Array of references to spaces by their id or codename (external_id is not supported for spaces)"),
|
|
76
|
+
collections: z
|
|
77
|
+
.array(referenceObjectSchema)
|
|
78
|
+
.min(1)
|
|
79
|
+
.optional()
|
|
80
|
+
.describe("Array of references to collections by their id, codename, or external id"),
|
|
81
|
+
publishing_states: z
|
|
82
|
+
.array(z.enum(["published", "unpublished", "not_published_yet"]))
|
|
83
|
+
.min(1)
|
|
84
|
+
.optional()
|
|
85
|
+
.describe("Array of publishing states to filter by. 'published' - variant is currently published, 'unpublished' - variant was published but is now unpublished, 'not_published_yet' - variant has never been published"),
|
|
71
86
|
order_by: z
|
|
72
87
|
.enum(["name", "due_date", "last_modified"])
|
|
73
88
|
.optional()
|
|
@@ -76,9 +91,5 @@ export const filterVariantsSchema = z.object({
|
|
|
76
91
|
.enum(["asc", "desc"])
|
|
77
92
|
.optional()
|
|
78
93
|
.describe("Order direction"),
|
|
79
|
-
include_content: z
|
|
80
|
-
.boolean()
|
|
81
|
-
.optional()
|
|
82
|
-
.describe("Whether to include the full content of language variants in the response"),
|
|
83
94
|
continuation_token: continuationTokenField,
|
|
84
95
|
});
|
package/build/server.js
CHANGED
|
@@ -7,6 +7,7 @@ import { registerTool as registerAddLanguageMapi } from "./tools/add-language-ma
|
|
|
7
7
|
import { registerTool as registerAddSpaceMapi } from "./tools/add-space-mapi.js";
|
|
8
8
|
import { registerTool as registerAddTaxonomyGroupMapi } from "./tools/add-taxonomy-group-mapi.js";
|
|
9
9
|
import { registerTool as registerAddWorkflowMapi } from "./tools/add-workflow-mapi.js";
|
|
10
|
+
import { registerTool as registerBulkGetItemsVariantsMapi } from "./tools/bulk-get-items-variants-mapi.js";
|
|
10
11
|
import { registerTool as registerChangeVariantWorkflowStepMapi } from "./tools/change-variant-workflow-step-mapi.js";
|
|
11
12
|
import { registerTool as registerCreateVariantVersionMapi } from "./tools/create-variant-version-mapi.js";
|
|
12
13
|
import { registerTool as registerDeleteContentItemMapi } from "./tools/delete-content-item-mapi.js";
|
|
@@ -112,6 +113,7 @@ export const createServer = () => {
|
|
|
112
113
|
registerDeleteWorkflowMapi(server);
|
|
113
114
|
registerChangeVariantWorkflowStepMapi(server);
|
|
114
115
|
registerFilterVariantsMapi(server);
|
|
116
|
+
registerBulkGetItemsVariantsMapi(server);
|
|
115
117
|
registerSearchVariantsMapi(server);
|
|
116
118
|
registerPublishVariantMapi(server);
|
|
117
119
|
registerUnpublishVariantMapi(server);
|
|
@@ -1,320 +1,20 @@
|
|
|
1
1
|
import * as assert from "node:assert";
|
|
2
2
|
import { describe, it } from "mocha";
|
|
3
|
-
import { createMcpToolSuccessResponse
|
|
4
|
-
describe("isEmptyOrDefault", () => {
|
|
5
|
-
describe("should return true for empty/default values", () => {
|
|
6
|
-
it("returns true for null", () => {
|
|
7
|
-
assert.strictEqual(isEmptyOrDefault(null), true);
|
|
8
|
-
});
|
|
9
|
-
it("returns true for undefined", () => {
|
|
10
|
-
assert.strictEqual(isEmptyOrDefault(undefined), true);
|
|
11
|
-
});
|
|
12
|
-
it("returns true for empty string", () => {
|
|
13
|
-
assert.strictEqual(isEmptyOrDefault(""), true);
|
|
14
|
-
});
|
|
15
|
-
it("returns true for rich text empty paragraph", () => {
|
|
16
|
-
assert.strictEqual(isEmptyOrDefault("<p><br/></p>"), true);
|
|
17
|
-
});
|
|
18
|
-
it("returns true for empty array", () => {
|
|
19
|
-
assert.strictEqual(isEmptyOrDefault([]), true);
|
|
20
|
-
});
|
|
21
|
-
it("returns true for empty object", () => {
|
|
22
|
-
assert.strictEqual(isEmptyOrDefault({}), true);
|
|
23
|
-
});
|
|
24
|
-
});
|
|
25
|
-
describe("should return false for non-empty values", () => {
|
|
26
|
-
it("returns false for non-empty string", () => {
|
|
27
|
-
assert.strictEqual(isEmptyOrDefault("hello"), false);
|
|
28
|
-
});
|
|
29
|
-
it("returns false for number zero", () => {
|
|
30
|
-
assert.strictEqual(isEmptyOrDefault(0), false);
|
|
31
|
-
});
|
|
32
|
-
it("returns false for positive number", () => {
|
|
33
|
-
assert.strictEqual(isEmptyOrDefault(42), false);
|
|
34
|
-
});
|
|
35
|
-
it("returns false for boolean false", () => {
|
|
36
|
-
assert.strictEqual(isEmptyOrDefault(false), false);
|
|
37
|
-
});
|
|
38
|
-
it("returns false for boolean true", () => {
|
|
39
|
-
assert.strictEqual(isEmptyOrDefault(true), false);
|
|
40
|
-
});
|
|
41
|
-
it("returns false for non-empty array", () => {
|
|
42
|
-
assert.strictEqual(isEmptyOrDefault([1, 2, 3]), false);
|
|
43
|
-
});
|
|
44
|
-
it("returns false for non-empty object", () => {
|
|
45
|
-
assert.strictEqual(isEmptyOrDefault({ key: "value" }), false);
|
|
46
|
-
});
|
|
47
|
-
it("returns false for Date object", () => {
|
|
48
|
-
assert.strictEqual(isEmptyOrDefault(new Date()), false);
|
|
49
|
-
});
|
|
50
|
-
it("returns false for rich text with actual content", () => {
|
|
51
|
-
assert.strictEqual(isEmptyOrDefault("<p>Hello world</p>"), false);
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
describe("removeEmptyValues", () => {
|
|
56
|
-
describe("primitive values at root level - preserved as-is", () => {
|
|
57
|
-
it("returns null for null", () => {
|
|
58
|
-
assert.strictEqual(removeEmptyValues(null), null);
|
|
59
|
-
});
|
|
60
|
-
it("returns undefined for undefined", () => {
|
|
61
|
-
assert.strictEqual(removeEmptyValues(undefined), undefined);
|
|
62
|
-
});
|
|
63
|
-
it("preserves empty string at root level", () => {
|
|
64
|
-
assert.strictEqual(removeEmptyValues(""), "");
|
|
65
|
-
});
|
|
66
|
-
it("preserves rich text empty paragraph at root level", () => {
|
|
67
|
-
assert.strictEqual(removeEmptyValues("<p><br/></p>"), "<p><br/></p>");
|
|
68
|
-
});
|
|
69
|
-
it("preserves non-empty string", () => {
|
|
70
|
-
assert.strictEqual(removeEmptyValues("hello"), "hello");
|
|
71
|
-
});
|
|
72
|
-
it("preserves number zero", () => {
|
|
73
|
-
assert.strictEqual(removeEmptyValues(0), 0);
|
|
74
|
-
});
|
|
75
|
-
it("preserves boolean false", () => {
|
|
76
|
-
assert.strictEqual(removeEmptyValues(false), false);
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
describe("arrays", () => {
|
|
80
|
-
it("returns empty array for empty array at root level", () => {
|
|
81
|
-
assert.deepStrictEqual(removeEmptyValues([]), []);
|
|
82
|
-
});
|
|
83
|
-
it("removes empty values from array", () => {
|
|
84
|
-
assert.deepStrictEqual(removeEmptyValues([1, null, 2, "", 3]), [1, 2, 3]);
|
|
85
|
-
});
|
|
86
|
-
it("returns empty array when all array items are empty at root level", () => {
|
|
87
|
-
assert.deepStrictEqual(removeEmptyValues([null, "", [], {}]), []);
|
|
88
|
-
});
|
|
89
|
-
it("recursively cleans nested arrays", () => {
|
|
90
|
-
assert.deepStrictEqual(removeEmptyValues([1, [2, null, 3], [null, ""]]), [
|
|
91
|
-
1,
|
|
92
|
-
[2, 3],
|
|
93
|
-
]);
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
describe("objects", () => {
|
|
97
|
-
it("returns empty object for empty object at root level", () => {
|
|
98
|
-
assert.deepStrictEqual(removeEmptyValues({}), {});
|
|
99
|
-
});
|
|
100
|
-
it("removes null properties", () => {
|
|
101
|
-
assert.deepStrictEqual(removeEmptyValues({ a: 1, b: null }), { a: 1 });
|
|
102
|
-
});
|
|
103
|
-
it("removes undefined properties", () => {
|
|
104
|
-
assert.deepStrictEqual(removeEmptyValues({ a: 1, b: undefined }), {
|
|
105
|
-
a: 1,
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
it("removes empty string properties", () => {
|
|
109
|
-
assert.deepStrictEqual(removeEmptyValues({ a: 1, b: "" }), { a: 1 });
|
|
110
|
-
});
|
|
111
|
-
it("removes empty array properties", () => {
|
|
112
|
-
assert.deepStrictEqual(removeEmptyValues({ a: 1, b: [] }), { a: 1 });
|
|
113
|
-
});
|
|
114
|
-
it("removes empty object properties", () => {
|
|
115
|
-
assert.deepStrictEqual(removeEmptyValues({ a: 1, b: {} }), { a: 1 });
|
|
116
|
-
});
|
|
117
|
-
it("removes rich text empty paragraph properties", () => {
|
|
118
|
-
assert.deepStrictEqual(removeEmptyValues({ a: 1, b: "<p><br/></p>" }), {
|
|
119
|
-
a: 1,
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
it("returns empty object when all properties are empty at root level", () => {
|
|
123
|
-
assert.deepStrictEqual(removeEmptyValues({ a: null, b: "", c: [], d: {} }), {});
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
describe("nested structures", () => {
|
|
127
|
-
it("recursively cleans nested objects", () => {
|
|
128
|
-
const input = {
|
|
129
|
-
level1: {
|
|
130
|
-
level2: {
|
|
131
|
-
value: "keep",
|
|
132
|
-
empty: null,
|
|
133
|
-
},
|
|
134
|
-
emptyObj: {},
|
|
135
|
-
},
|
|
136
|
-
};
|
|
137
|
-
const expected = {
|
|
138
|
-
level1: {
|
|
139
|
-
level2: {
|
|
140
|
-
value: "keep",
|
|
141
|
-
},
|
|
142
|
-
},
|
|
143
|
-
};
|
|
144
|
-
assert.deepStrictEqual(removeEmptyValues(input), expected);
|
|
145
|
-
});
|
|
146
|
-
it("removes nested objects that become empty after cleaning", () => {
|
|
147
|
-
const input = {
|
|
148
|
-
keep: "value",
|
|
149
|
-
remove: {
|
|
150
|
-
nested: {
|
|
151
|
-
empty: null,
|
|
152
|
-
},
|
|
153
|
-
},
|
|
154
|
-
};
|
|
155
|
-
const expected = { keep: "value" };
|
|
156
|
-
assert.deepStrictEqual(removeEmptyValues(input), expected);
|
|
157
|
-
});
|
|
158
|
-
it("handles deeply nested structures", () => {
|
|
159
|
-
const input = {
|
|
160
|
-
a: {
|
|
161
|
-
b: {
|
|
162
|
-
c: {
|
|
163
|
-
d: {
|
|
164
|
-
value: "deep",
|
|
165
|
-
empty: "",
|
|
166
|
-
},
|
|
167
|
-
},
|
|
168
|
-
},
|
|
169
|
-
},
|
|
170
|
-
};
|
|
171
|
-
const expected = {
|
|
172
|
-
a: {
|
|
173
|
-
b: {
|
|
174
|
-
c: {
|
|
175
|
-
d: {
|
|
176
|
-
value: "deep",
|
|
177
|
-
},
|
|
178
|
-
},
|
|
179
|
-
},
|
|
180
|
-
},
|
|
181
|
-
};
|
|
182
|
-
assert.deepStrictEqual(removeEmptyValues(input), expected);
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
describe("removeEmptyElementsFromVariant", () => {
|
|
187
|
-
describe("non-object inputs", () => {
|
|
188
|
-
it("returns null for null", () => {
|
|
189
|
-
assert.strictEqual(removeEmptyElementsFromVariant(null), null);
|
|
190
|
-
});
|
|
191
|
-
it("returns undefined for undefined", () => {
|
|
192
|
-
assert.strictEqual(removeEmptyElementsFromVariant(undefined), undefined);
|
|
193
|
-
});
|
|
194
|
-
it("returns primitive values unchanged", () => {
|
|
195
|
-
assert.strictEqual(removeEmptyElementsFromVariant("string"), "string");
|
|
196
|
-
assert.strictEqual(removeEmptyElementsFromVariant(42), 42);
|
|
197
|
-
assert.strictEqual(removeEmptyElementsFromVariant(true), true);
|
|
198
|
-
});
|
|
199
|
-
});
|
|
200
|
-
describe("elements array filtering", () => {
|
|
201
|
-
it("removes elements with only element property", () => {
|
|
202
|
-
const input = {
|
|
203
|
-
elements: [{ element: { id: "id1" } }, { element: { id: "id2" } }],
|
|
204
|
-
};
|
|
205
|
-
const expected = {};
|
|
206
|
-
assert.deepStrictEqual(removeEmptyElementsFromVariant(input), expected);
|
|
207
|
-
});
|
|
208
|
-
it("keeps elements with value property", () => {
|
|
209
|
-
const input = {
|
|
210
|
-
elements: [{ element: { id: "id1" }, value: "content" }],
|
|
211
|
-
};
|
|
212
|
-
const expected = {
|
|
213
|
-
elements: [{ element: { id: "id1" }, value: "content" }],
|
|
214
|
-
};
|
|
215
|
-
assert.deepStrictEqual(removeEmptyElementsFromVariant(input), expected);
|
|
216
|
-
});
|
|
217
|
-
it("filters mixed elements array", () => {
|
|
218
|
-
const input = {
|
|
219
|
-
elements: [
|
|
220
|
-
{ element: { id: "id1" } },
|
|
221
|
-
{ element: { id: "id2" }, value: "keep" },
|
|
222
|
-
{ element: { id: "id3" } },
|
|
223
|
-
{ element: { id: "id4" }, components: [] },
|
|
224
|
-
],
|
|
225
|
-
};
|
|
226
|
-
const expected = {
|
|
227
|
-
elements: [
|
|
228
|
-
{ element: { id: "id2" }, value: "keep" },
|
|
229
|
-
{ element: { id: "id4" }, components: [] },
|
|
230
|
-
],
|
|
231
|
-
};
|
|
232
|
-
assert.deepStrictEqual(removeEmptyElementsFromVariant(input), expected);
|
|
233
|
-
});
|
|
234
|
-
it("keeps non-object elements in elements array", () => {
|
|
235
|
-
const input = {
|
|
236
|
-
elements: ["string", 42, { element: { id: "id1" } }],
|
|
237
|
-
};
|
|
238
|
-
const expected = {
|
|
239
|
-
elements: ["string", 42],
|
|
240
|
-
};
|
|
241
|
-
assert.deepStrictEqual(removeEmptyElementsFromVariant(input), expected);
|
|
242
|
-
});
|
|
243
|
-
});
|
|
244
|
-
describe("nested structures", () => {
|
|
245
|
-
it("processes nested objects with elements arrays", () => {
|
|
246
|
-
const input = {
|
|
247
|
-
item: {
|
|
248
|
-
elements: [{ element: { id: "id1" } }],
|
|
249
|
-
},
|
|
250
|
-
};
|
|
251
|
-
const expected = {
|
|
252
|
-
item: {},
|
|
253
|
-
};
|
|
254
|
-
assert.deepStrictEqual(removeEmptyElementsFromVariant(input), expected);
|
|
255
|
-
});
|
|
256
|
-
it("processes arrays of objects with elements", () => {
|
|
257
|
-
const input = {
|
|
258
|
-
variants: [
|
|
259
|
-
{ elements: [{ element: { id: "id1" } }] },
|
|
260
|
-
{ elements: [{ element: { id: "id2" }, value: "keep" }] },
|
|
261
|
-
],
|
|
262
|
-
};
|
|
263
|
-
const expected = {
|
|
264
|
-
variants: [
|
|
265
|
-
{},
|
|
266
|
-
{ elements: [{ element: { id: "id2" }, value: "keep" }] },
|
|
267
|
-
],
|
|
268
|
-
};
|
|
269
|
-
assert.deepStrictEqual(removeEmptyElementsFromVariant(input), expected);
|
|
270
|
-
});
|
|
271
|
-
});
|
|
272
|
-
describe("preserves other properties", () => {
|
|
273
|
-
it("keeps non-elements properties unchanged", () => {
|
|
274
|
-
const input = {
|
|
275
|
-
id: "123",
|
|
276
|
-
name: "Test",
|
|
277
|
-
elements: [{ element: { id: "id1" } }],
|
|
278
|
-
};
|
|
279
|
-
const expected = {
|
|
280
|
-
id: "123",
|
|
281
|
-
name: "Test",
|
|
282
|
-
};
|
|
283
|
-
assert.deepStrictEqual(removeEmptyElementsFromVariant(input), expected);
|
|
284
|
-
});
|
|
285
|
-
});
|
|
286
|
-
});
|
|
3
|
+
import { createMcpToolSuccessResponse } from "../../utils/responseHelper.js";
|
|
287
4
|
describe("createMcpToolSuccessResponse", () => {
|
|
288
|
-
it("returns correct structure", () => {
|
|
5
|
+
it("returns correct structure with text content", () => {
|
|
289
6
|
const response = createMcpToolSuccessResponse({ key: "value" });
|
|
290
7
|
assert.strictEqual(response.content.length, 1);
|
|
291
8
|
assert.strictEqual(response.content[0].type, "text");
|
|
292
9
|
});
|
|
293
|
-
it("
|
|
294
|
-
const input = {
|
|
295
|
-
id: "123",
|
|
296
|
-
name: null,
|
|
297
|
-
value: "",
|
|
298
|
-
items: [],
|
|
299
|
-
};
|
|
10
|
+
it("serializes object data as JSON string", () => {
|
|
11
|
+
const input = { id: "123", name: "Test" };
|
|
300
12
|
const response = createMcpToolSuccessResponse(input);
|
|
301
13
|
const parsed = JSON.parse(response.content[0].text);
|
|
302
|
-
assert.deepStrictEqual(parsed,
|
|
14
|
+
assert.deepStrictEqual(parsed, input);
|
|
303
15
|
});
|
|
304
|
-
it("
|
|
16
|
+
it("serializes nested data as JSON string", () => {
|
|
305
17
|
const input = {
|
|
306
|
-
contentType: {
|
|
307
|
-
id: "type-1",
|
|
308
|
-
name: "Article",
|
|
309
|
-
elements: [
|
|
310
|
-
{ id: "el-1", name: "Title", codename: "" },
|
|
311
|
-
{ id: "el-2", name: null, codename: "body" },
|
|
312
|
-
],
|
|
313
|
-
},
|
|
314
|
-
};
|
|
315
|
-
const response = createMcpToolSuccessResponse(input);
|
|
316
|
-
const parsed = JSON.parse(response.content[0].text);
|
|
317
|
-
assert.deepStrictEqual(parsed, {
|
|
318
18
|
contentType: {
|
|
319
19
|
id: "type-1",
|
|
320
20
|
name: "Article",
|
|
@@ -323,262 +23,28 @@ describe("createMcpToolSuccessResponse", () => {
|
|
|
323
23
|
{ id: "el-2", codename: "body" },
|
|
324
24
|
],
|
|
325
25
|
},
|
|
326
|
-
});
|
|
327
|
-
});
|
|
328
|
-
});
|
|
329
|
-
describe("createVariantMcpToolSuccessResponse", () => {
|
|
330
|
-
it("returns correct structure", () => {
|
|
331
|
-
const response = createVariantMcpToolSuccessResponse({ key: "value" });
|
|
332
|
-
assert.strictEqual(response.content.length, 1);
|
|
333
|
-
assert.strictEqual(response.content[0].type, "text");
|
|
334
|
-
});
|
|
335
|
-
it("removes empty values and empty elements", () => {
|
|
336
|
-
const input = {
|
|
337
|
-
item: { id: "item-1" },
|
|
338
|
-
language: { id: "lang-1" },
|
|
339
|
-
elements: [
|
|
340
|
-
{ element: { id: "el-1" }, value: "" },
|
|
341
|
-
{ element: { id: "el-2" }, value: "content" },
|
|
342
|
-
{ element: { id: "el-3" }, value: null },
|
|
343
|
-
],
|
|
344
|
-
};
|
|
345
|
-
const response = createVariantMcpToolSuccessResponse(input);
|
|
346
|
-
const parsed = JSON.parse(response.content[0].text);
|
|
347
|
-
assert.deepStrictEqual(parsed, {
|
|
348
|
-
item: { id: "item-1" },
|
|
349
|
-
language: { id: "lang-1" },
|
|
350
|
-
elements: [{ element: { id: "el-2" }, value: "content" }],
|
|
351
|
-
});
|
|
352
|
-
});
|
|
353
|
-
it("removes elements array when all elements become empty", () => {
|
|
354
|
-
const input = {
|
|
355
|
-
item: { id: "item-1" },
|
|
356
|
-
elements: [
|
|
357
|
-
{ element: { id: "el-1" }, value: "" },
|
|
358
|
-
{ element: { id: "el-2" }, value: null },
|
|
359
|
-
{ element: { id: "el-3" }, value: "<p><br/></p>" },
|
|
360
|
-
],
|
|
361
|
-
};
|
|
362
|
-
const response = createVariantMcpToolSuccessResponse(input);
|
|
363
|
-
const parsed = JSON.parse(response.content[0].text);
|
|
364
|
-
assert.deepStrictEqual(parsed, {
|
|
365
|
-
item: { id: "item-1" },
|
|
366
|
-
});
|
|
367
|
-
});
|
|
368
|
-
it("handles real-world variant response", () => {
|
|
369
|
-
const input = {
|
|
370
|
-
item: { id: "f4b3fc05-e988-4dae-9ac1-a94aba566474" },
|
|
371
|
-
language: { id: "d1f95fde-af02-b3b5-bd9e-f232311ccab8" },
|
|
372
|
-
last_modified: "2018-02-27T19:08:25.404Z",
|
|
373
|
-
workflow: {
|
|
374
|
-
workflow_identifier: { id: "00000000-0000-0000-0000-000000000000" },
|
|
375
|
-
step_identifier: { id: "c199950d-99f0-4983-b711-6c4c91624b22" },
|
|
376
|
-
},
|
|
377
|
-
elements: [
|
|
378
|
-
{ element: { id: "text-id" }, value: "Article Title" },
|
|
379
|
-
{ element: { id: "rich-id" }, value: "<p><br/></p>", components: [] },
|
|
380
|
-
{ element: { id: "number-id" }, value: null },
|
|
381
|
-
{ element: { id: "assets-id" }, value: [] },
|
|
382
|
-
{ element: { id: "taxonomy-id" }, value: [] },
|
|
383
|
-
],
|
|
384
26
|
};
|
|
385
|
-
const response =
|
|
27
|
+
const response = createMcpToolSuccessResponse(input);
|
|
386
28
|
const parsed = JSON.parse(response.content[0].text);
|
|
387
|
-
assert.deepStrictEqual(parsed,
|
|
388
|
-
item: { id: "f4b3fc05-e988-4dae-9ac1-a94aba566474" },
|
|
389
|
-
language: { id: "d1f95fde-af02-b3b5-bd9e-f232311ccab8" },
|
|
390
|
-
last_modified: "2018-02-27T19:08:25.404Z",
|
|
391
|
-
workflow: {
|
|
392
|
-
workflow_identifier: { id: "00000000-0000-0000-0000-000000000000" },
|
|
393
|
-
step_identifier: { id: "c199950d-99f0-4983-b711-6c4c91624b22" },
|
|
394
|
-
},
|
|
395
|
-
elements: [{ element: { id: "text-id" }, value: "Article Title" }],
|
|
396
|
-
});
|
|
29
|
+
assert.deepStrictEqual(parsed, input);
|
|
397
30
|
});
|
|
398
|
-
it("
|
|
399
|
-
const input = {
|
|
400
|
-
|
|
401
|
-
{
|
|
402
|
-
item: { id: "item-1" },
|
|
403
|
-
elements: [
|
|
404
|
-
{ element: { id: "el-1" }, value: "" },
|
|
405
|
-
{ element: { id: "el-2" }, value: "content" },
|
|
406
|
-
],
|
|
407
|
-
},
|
|
408
|
-
{
|
|
409
|
-
item: { id: "item-2" },
|
|
410
|
-
elements: [
|
|
411
|
-
{ element: { id: "el-1" }, value: null },
|
|
412
|
-
{ element: { id: "el-2" }, value: [] },
|
|
413
|
-
],
|
|
414
|
-
},
|
|
415
|
-
],
|
|
416
|
-
pagination: {
|
|
417
|
-
continuation_token: null,
|
|
418
|
-
next_page: "",
|
|
419
|
-
},
|
|
420
|
-
};
|
|
421
|
-
const response = createVariantMcpToolSuccessResponse(input);
|
|
31
|
+
it("serializes arrays as JSON string", () => {
|
|
32
|
+
const input = [{ id: "1" }, { id: "2" }];
|
|
33
|
+
const response = createMcpToolSuccessResponse(input);
|
|
422
34
|
const parsed = JSON.parse(response.content[0].text);
|
|
423
|
-
assert.deepStrictEqual(parsed,
|
|
424
|
-
variants: [
|
|
425
|
-
{
|
|
426
|
-
item: { id: "item-1" },
|
|
427
|
-
elements: [{ element: { id: "el-2" }, value: "content" }],
|
|
428
|
-
},
|
|
429
|
-
{
|
|
430
|
-
item: { id: "item-2" },
|
|
431
|
-
},
|
|
432
|
-
],
|
|
433
|
-
});
|
|
434
|
-
});
|
|
435
|
-
});
|
|
436
|
-
describe("variant with all empty elements", () => {
|
|
437
|
-
it("removes elements array entirely when all elements become empty", () => {
|
|
438
|
-
const input = {
|
|
439
|
-
elements: [
|
|
440
|
-
{ element: { id: "text-id" }, value: "" },
|
|
441
|
-
{ element: { id: "rich-id" }, value: "<p><br/></p>", components: [] },
|
|
442
|
-
{ element: { id: "number-id" }, value: null },
|
|
443
|
-
{ element: { id: "assets-id" }, value: [] },
|
|
444
|
-
],
|
|
445
|
-
};
|
|
446
|
-
const response = createVariantMcpToolSuccessResponse(input);
|
|
447
|
-
const result = JSON.parse(response.content[0].text);
|
|
448
|
-
assert.deepStrictEqual(result, {}, "All empty elements should be removed, resulting in an empty object");
|
|
35
|
+
assert.deepStrictEqual(parsed, input);
|
|
449
36
|
});
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
});
|
|
37
|
+
it("passes through string values without JSON encoding", () => {
|
|
38
|
+
const response = createMcpToolSuccessResponse("plain text");
|
|
39
|
+
assert.strictEqual(response.content[0].text, "plain text");
|
|
491
40
|
});
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
-
});
|
|
41
|
+
it("returns 'undefined' text when input is undefined", () => {
|
|
42
|
+
const response = createMcpToolSuccessResponse(undefined);
|
|
43
|
+
assert.strictEqual(response.content[0].text, "undefined");
|
|
568
44
|
});
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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
|
-
});
|
|
45
|
+
it("returns valid JSON string when input is null", () => {
|
|
46
|
+
const response = createMcpToolSuccessResponse(null);
|
|
47
|
+
assert.strictEqual(typeof response.content[0].text, "string");
|
|
48
|
+
assert.doesNotThrow(() => JSON.parse(response.content[0].text));
|
|
583
49
|
});
|
|
584
50
|
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createMapiClient } from "../clients/kontentClients.js";
|
|
2
|
+
import { bulkGetItemsWithVariantsSchema } from "../schemas/bulkGetItemsWithVariantsSchemas.js";
|
|
3
|
+
import { handleMcpToolError } from "../utils/errorHandler.js";
|
|
4
|
+
import { createMcpToolSuccessResponse } from "../utils/responseHelper.js";
|
|
5
|
+
import { throwError } from "../utils/throwError.js";
|
|
6
|
+
export const registerTool = (server) => {
|
|
7
|
+
server.tool("bulk-get-items-variants-mapi", "Bulk get Kontent.ai content items with their language variants by item and language reference pairs. Items without a variant in the requested language return the item without the variant property.", bulkGetItemsWithVariantsSchema.shape, async ({ variants, continuation_token }, { authInfo: { token, clientId } = {} }) => {
|
|
8
|
+
try {
|
|
9
|
+
const environmentId = clientId ?? process.env.KONTENT_ENVIRONMENT_ID;
|
|
10
|
+
if (!environmentId) {
|
|
11
|
+
throwError("Missing required environment ID");
|
|
12
|
+
}
|
|
13
|
+
const client = createMapiClient(environmentId, token);
|
|
14
|
+
const query = client.bulkGetItemsWithVariants().withData({
|
|
15
|
+
variants,
|
|
16
|
+
});
|
|
17
|
+
const response = await (continuation_token
|
|
18
|
+
? query.xContinuationToken(continuation_token)
|
|
19
|
+
: query).toPromise();
|
|
20
|
+
return createMcpToolSuccessResponse({
|
|
21
|
+
data: response.rawData.data,
|
|
22
|
+
pagination: {
|
|
23
|
+
continuation_token: response.data.pagination.continuationToken,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
return handleMcpToolError(error, "Bulk Get Items With Variants");
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
};
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { createMapiClient } from "../clients/kontentClients.js";
|
|
2
2
|
import { filterVariantsSchema } from "../schemas/filterVariantSchemas.js";
|
|
3
3
|
import { handleMcpToolError } from "../utils/errorHandler.js";
|
|
4
|
-
import {
|
|
4
|
+
import { createMcpToolSuccessResponse } from "../utils/responseHelper.js";
|
|
5
5
|
import { throwError } from "../utils/throwError.js";
|
|
6
6
|
export const registerTool = (server) => {
|
|
7
|
-
server.tool("filter-variants-mapi", "Filter Kontent.ai variants. For EXACT keyword matching and compliance (terms use OR). Use search-variants-mapi for semantic/topic search.", filterVariantsSchema.shape, async ({ search_phrase, content_types, contributors, has_no_contributors, completion_statuses, language, workflow_steps, taxonomy_groups, order_by, order_direction,
|
|
7
|
+
server.tool("filter-variants-mapi", "Filter Kontent.ai items with variants returning references (item ID + language ID). For EXACT keyword matching and compliance (terms use OR). Use bulk-get-items-variants-mapi to retrieve full content for matched items. Use search-variants-mapi for semantic/topic search.", filterVariantsSchema.shape, async ({ search_phrase, content_types, contributors, has_no_contributors, completion_statuses, language, workflow_steps, taxonomy_groups, spaces, collections, publishing_states, order_by, order_direction, continuation_token, }, { authInfo: { token, clientId } = {} }) => {
|
|
8
8
|
try {
|
|
9
9
|
const environmentId = clientId ?? process.env.KONTENT_ENVIRONMENT_ID;
|
|
10
10
|
if (!environmentId) {
|
|
11
11
|
throwError("Missing required environment ID");
|
|
12
12
|
}
|
|
13
13
|
const client = createMapiClient(environmentId, token);
|
|
14
|
-
const query = client.
|
|
14
|
+
const query = client.filterItemsWithVariants().withData({
|
|
15
15
|
filters: {
|
|
16
16
|
search_phrase,
|
|
17
17
|
content_types,
|
|
@@ -21,6 +21,9 @@ export const registerTool = (server) => {
|
|
|
21
21
|
language,
|
|
22
22
|
workflow_steps,
|
|
23
23
|
taxonomy_groups,
|
|
24
|
+
spaces,
|
|
25
|
+
collections,
|
|
26
|
+
publishing_states,
|
|
24
27
|
},
|
|
25
28
|
order: order_by
|
|
26
29
|
? {
|
|
@@ -28,13 +31,12 @@ export const registerTool = (server) => {
|
|
|
28
31
|
direction: order_direction || "asc",
|
|
29
32
|
}
|
|
30
33
|
: undefined,
|
|
31
|
-
include_content: include_content ?? false,
|
|
32
34
|
});
|
|
33
35
|
const response = await (continuation_token
|
|
34
36
|
? query.xContinuationToken(continuation_token)
|
|
35
37
|
: query).toPromise();
|
|
36
|
-
return
|
|
37
|
-
|
|
38
|
+
return createMcpToolSuccessResponse({
|
|
39
|
+
variants: response.rawData.variants,
|
|
38
40
|
pagination: {
|
|
39
41
|
continuation_token: response.data.pagination.continuationToken,
|
|
40
42
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { createMapiClient } from "../clients/kontentClients.js";
|
|
3
3
|
import { handleMcpToolError } from "../utils/errorHandler.js";
|
|
4
|
-
import {
|
|
4
|
+
import { createMcpToolSuccessResponse } from "../utils/responseHelper.js";
|
|
5
5
|
export const registerTool = (server) => {
|
|
6
6
|
server.tool("get-latest-variant-mapi", "Get latest Kontent.ai language variant. Variants hold language-specific content; structure defined by content type and its snippets.", {
|
|
7
7
|
itemId: z.string().describe("Item ID"),
|
|
@@ -14,7 +14,7 @@ export const registerTool = (server) => {
|
|
|
14
14
|
.byItemId(itemId)
|
|
15
15
|
.byLanguageId(languageId)
|
|
16
16
|
.toPromise();
|
|
17
|
-
return
|
|
17
|
+
return createMcpToolSuccessResponse(response.rawData);
|
|
18
18
|
}
|
|
19
19
|
catch (error) {
|
|
20
20
|
return handleMcpToolError(error, "Latest Language Variant Retrieval");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { createMapiClient } from "../clients/kontentClients.js";
|
|
3
3
|
import { handleMcpToolError } from "../utils/errorHandler.js";
|
|
4
|
-
import {
|
|
4
|
+
import { createMcpToolSuccessResponse } from "../utils/responseHelper.js";
|
|
5
5
|
export const registerTool = (server) => {
|
|
6
6
|
server.tool("get-published-variant-mapi", "Get published Kontent.ai language variant. Variants hold language-specific content; structure defined by content type and its snippets.", {
|
|
7
7
|
itemId: z.string().describe("Item ID"),
|
|
@@ -15,7 +15,7 @@ export const registerTool = (server) => {
|
|
|
15
15
|
.byLanguageId(languageId)
|
|
16
16
|
.published()
|
|
17
17
|
.toPromise();
|
|
18
|
-
return
|
|
18
|
+
return createMcpToolSuccessResponse(response.rawData);
|
|
19
19
|
}
|
|
20
20
|
catch (error) {
|
|
21
21
|
return handleMcpToolError(error, "Published Language Variant Retrieval");
|
|
@@ -2,7 +2,7 @@ import pRetry, { AbortError } from "p-retry";
|
|
|
2
2
|
import { createMapiClient } from "../clients/kontentClients.js";
|
|
3
3
|
import { searchOperationSchema } from "../schemas/searchOperationSchemas.js";
|
|
4
4
|
import { handleMcpToolError } from "../utils/errorHandler.js";
|
|
5
|
-
import { createMcpToolSuccessResponse
|
|
5
|
+
import { createMcpToolSuccessResponse } from "../utils/responseHelper.js";
|
|
6
6
|
import { throwError } from "../utils/throwError.js";
|
|
7
7
|
class OperationResultIncompleteError extends Error {
|
|
8
8
|
constructor() {
|
|
@@ -84,7 +84,7 @@ export const registerTool = (server) => {
|
|
|
84
84
|
maxTimeout: 10000,
|
|
85
85
|
factor: 1.5,
|
|
86
86
|
});
|
|
87
|
-
return
|
|
87
|
+
return createMcpToolSuccessResponse({
|
|
88
88
|
result: resultData,
|
|
89
89
|
});
|
|
90
90
|
}
|
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { createMapiClient } from "../clients/kontentClients.js";
|
|
3
3
|
import { languageVariantElementSchema } from "../schemas/contentItemSchemas.js";
|
|
4
4
|
import { handleMcpToolError } from "../utils/errorHandler.js";
|
|
5
|
-
import {
|
|
5
|
+
import { createMcpToolSuccessResponse } from "../utils/responseHelper.js";
|
|
6
6
|
export const registerTool = (server) => {
|
|
7
7
|
server.tool("upsert-language-variant-mapi", "Create or update Kontent.ai variant. Element values must fulfill limitations and guidelines defined in content type.", {
|
|
8
8
|
itemId: z.string().describe("Content item ID"),
|
|
@@ -28,7 +28,7 @@ export const registerTool = (server) => {
|
|
|
28
28
|
.byLanguageId(languageId)
|
|
29
29
|
.withData(() => data)
|
|
30
30
|
.toPromise();
|
|
31
|
-
return
|
|
31
|
+
return createMcpToolSuccessResponse(response.rawData);
|
|
32
32
|
}
|
|
33
33
|
catch (error) {
|
|
34
34
|
return handleMcpToolError(error, "Language Variant Upsert");
|
|
@@ -1,108 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Utility for creating standardized MCP tool success responses
|
|
2
|
+
* Utility for creating standardized MCP tool success responses.
|
|
3
|
+
* Passes API data through to MCP response format without transformation.
|
|
3
4
|
*/
|
|
4
|
-
export function isEmptyOrDefault(value) {
|
|
5
|
-
if (value === null || value === undefined) {
|
|
6
|
-
return true;
|
|
7
|
-
}
|
|
8
|
-
if (typeof value === "string" && value === "") {
|
|
9
|
-
return true;
|
|
10
|
-
}
|
|
11
|
-
if (typeof value === "string" && value === "<p><br/></p>") {
|
|
12
|
-
return true;
|
|
13
|
-
}
|
|
14
|
-
if (Array.isArray(value) && value.length === 0) {
|
|
15
|
-
return true;
|
|
16
|
-
}
|
|
17
|
-
if (typeof value === "object" &&
|
|
18
|
-
!Array.isArray(value) &&
|
|
19
|
-
!(value instanceof Date) &&
|
|
20
|
-
!(value instanceof Function) &&
|
|
21
|
-
Object.keys(value).length === 0) {
|
|
22
|
-
return true;
|
|
23
|
-
}
|
|
24
|
-
return false;
|
|
25
|
-
}
|
|
26
|
-
export function removeEmptyElementsFromVariant(obj) {
|
|
27
|
-
if (obj === null || obj === undefined || typeof obj !== "object") {
|
|
28
|
-
return obj;
|
|
29
|
-
}
|
|
30
|
-
if (Array.isArray(obj)) {
|
|
31
|
-
return obj.map((item) => removeEmptyElementsFromVariant(item));
|
|
32
|
-
}
|
|
33
|
-
const result = {};
|
|
34
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
35
|
-
if (key === "elements" && Array.isArray(value)) {
|
|
36
|
-
const filteredElements = value
|
|
37
|
-
.filter((element) => {
|
|
38
|
-
if (typeof element !== "object" || element === null) {
|
|
39
|
-
return true;
|
|
40
|
-
}
|
|
41
|
-
const keys = Object.keys(element);
|
|
42
|
-
return !(keys.length === 1 && keys[0] === "element");
|
|
43
|
-
})
|
|
44
|
-
.map((element) => removeEmptyElementsFromVariant(element));
|
|
45
|
-
if (filteredElements.length > 0) {
|
|
46
|
-
result[key] = filteredElements;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
const processedValue = removeEmptyElementsFromVariant(value);
|
|
51
|
-
if (processedValue !== undefined) {
|
|
52
|
-
result[key] = processedValue;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return result;
|
|
57
|
-
}
|
|
58
|
-
function removeEmptyValuesRecursive(obj) {
|
|
59
|
-
if (obj === null || obj === undefined) {
|
|
60
|
-
return undefined;
|
|
61
|
-
}
|
|
62
|
-
if (typeof obj !== "object") {
|
|
63
|
-
return isEmptyOrDefault(obj) ? undefined : obj;
|
|
64
|
-
}
|
|
65
|
-
if (Array.isArray(obj)) {
|
|
66
|
-
const cleaned = obj
|
|
67
|
-
.map((item) => removeEmptyValuesRecursive(item))
|
|
68
|
-
.filter((item) => item !== undefined);
|
|
69
|
-
return cleaned.length === 0 ? undefined : cleaned;
|
|
70
|
-
}
|
|
71
|
-
const cleaned = {};
|
|
72
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
73
|
-
const cleanedValue = removeEmptyValuesRecursive(value);
|
|
74
|
-
if (cleanedValue !== undefined) {
|
|
75
|
-
cleaned[key] = cleanedValue;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
const keys = Object.keys(cleaned);
|
|
79
|
-
return keys.length === 0 ? undefined : cleaned;
|
|
80
|
-
}
|
|
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
5
|
/**
|
|
101
6
|
* Converts data to MCP tool success response format.
|
|
102
7
|
* Handles undefined separately as JSON.stringify(undefined) returns undefined (not a string).
|
|
103
8
|
* Skips stringify for strings as they don't need JSON encoding for MCP text response.
|
|
104
9
|
*/
|
|
105
|
-
const
|
|
10
|
+
export const createMcpToolSuccessResponse = (data) => {
|
|
106
11
|
const text = data === undefined
|
|
107
12
|
? "undefined"
|
|
108
13
|
: typeof data === "string"
|
|
@@ -117,12 +22,3 @@ const toMcpSuccessResponse = (data) => {
|
|
|
117
22
|
],
|
|
118
23
|
};
|
|
119
24
|
};
|
|
120
|
-
export const createMcpToolSuccessResponse = (data) => {
|
|
121
|
-
const cleaned = removeEmptyValues(data);
|
|
122
|
-
return toMcpSuccessResponse(cleaned);
|
|
123
|
-
};
|
|
124
|
-
export const createVariantMcpToolSuccessResponse = (data) => {
|
|
125
|
-
const cleaned = removeEmptyValues(data);
|
|
126
|
-
const optimized = removeEmptyElementsFromVariant(cleaned);
|
|
127
|
-
return toMcpSuccessResponse(optimized);
|
|
128
|
-
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kontent-ai/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"mcpName": "io.github.kontent-ai/mcp-server",
|
|
6
6
|
"repository": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"author": "Jiri Lojda",
|
|
30
30
|
"license": "MIT",
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@kontent-ai/management-sdk": "^8.
|
|
32
|
+
"@kontent-ai/management-sdk": "^8.3.0",
|
|
33
33
|
"@modelcontextprotocol/sdk": "^1.25.2",
|
|
34
34
|
"applicationinsights": "^2.9.8",
|
|
35
35
|
"dotenv": "^17.2.3",
|