@kontent-ai/mcp-server 0.15.0 → 0.17.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 (35) hide show
  1. package/README.md +114 -25
  2. package/build/bin.js +73 -24
  3. package/build/server.js +2 -0
  4. package/build/tools/add-content-item-mapi.js +2 -2
  5. package/build/tools/add-content-type-mapi.js +2 -2
  6. package/build/tools/add-content-type-snippet-mapi.js +2 -2
  7. package/build/tools/add-taxonomy-group-mapi.js +2 -2
  8. package/build/tools/change-variant-workflow-step-mapi.js +2 -2
  9. package/build/tools/create-variant-version-mapi.js +32 -0
  10. package/build/tools/delete-content-item-mapi.js +2 -2
  11. package/build/tools/delete-content-type-mapi.js +2 -2
  12. package/build/tools/delete-language-variant-mapi.js +2 -2
  13. package/build/tools/filter-variants-mapi.js +8 -5
  14. package/build/tools/get-asset-mapi.js +2 -2
  15. package/build/tools/get-item-mapi.js +2 -2
  16. package/build/tools/get-taxonomy-group-mapi.js +2 -2
  17. package/build/tools/get-type-mapi.js +2 -2
  18. package/build/tools/get-type-snippet-mapi.js +2 -2
  19. package/build/tools/get-variant-mapi.js +2 -2
  20. package/build/tools/list-assets-mapi.js +2 -2
  21. package/build/tools/list-content-type-snippets-mapi.js +2 -2
  22. package/build/tools/list-content-types-mapi.js +2 -2
  23. package/build/tools/list-languages-mapi.js +2 -2
  24. package/build/tools/list-taxonomy-groups-mapi.js +2 -2
  25. package/build/tools/list-workflows-mapi.js +2 -2
  26. package/build/tools/patch-content-type-mapi.js +2 -2
  27. package/build/tools/publish-variant-mapi.js +2 -2
  28. package/build/tools/unpublish-variant-mapi.js +2 -2
  29. package/build/tools/update-content-item-mapi.js +2 -2
  30. package/build/tools/upsert-language-variant-mapi.js +6 -4
  31. package/build/utils/errorHandler.js +2 -2
  32. package/build/utils/extractBearerToken.js +12 -0
  33. package/build/utils/isValidGuid.js +9 -0
  34. package/build/utils/responseHelper.js +1 -1
  35. package/package.json +1 -3
package/README.md CHANGED
@@ -48,10 +48,10 @@ You can run the Kontent.ai MCP Server with npx:
48
48
  npx @kontent-ai/mcp-server@latest stdio
49
49
  ```
50
50
 
51
- #### SSE Transport
51
+ #### Streamable HTTP Transport
52
52
 
53
53
  ```bash
54
- npx @kontent-ai/mcp-server@latest sse
54
+ npx @kontent-ai/mcp-server@latest shttp
55
55
  ```
56
56
 
57
57
  ## 🛠️ Available Tools
@@ -88,7 +88,8 @@ npx @kontent-ai/mcp-server@latest sse
88
88
  * **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
89
89
  * **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
90
90
  * **delete-content-item-mapi** – Delete Kontent.ai content item by internal ID from Management API
91
- * **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. Elements should be provided as JSON string in the format expected by the SDK
91
+ * **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
92
+ * **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
92
93
  * **delete-language-variant-mapi** – Delete Kontent.ai language variant from Management API
93
94
  * **filter-variants-mapi** – Search and filter Kontent.ai language variants of content items using Management API
94
95
 
@@ -110,13 +111,25 @@ npx @kontent-ai/mcp-server@latest sse
110
111
 
111
112
  ## ⚙️ Configuration
112
113
 
113
- The server requires the following environment variables:
114
+ The server supports two configuration modes:
115
+
116
+ ### Single-Tenant Mode (Default)
117
+
118
+ For single-tenant mode, configure environment variables:
114
119
 
115
120
  | Variable | Description | Required |
116
121
  |----------|-------------|----------|
117
122
  | KONTENT_API_KEY | Your Kontent.ai Management API key | ✅ |
118
123
  | KONTENT_ENVIRONMENT_ID | Your environment ID | ✅ |
119
- | PORT | Port for SSE transport (defaults to 3001) | ❌ |
124
+ | PORT | Port for HTTP transport (defaults to 3001) | ❌ |
125
+
126
+ ### Multi-Tenant Mode
127
+
128
+ For multi-tenant mode (Streamable HTTP only), the server accepts:
129
+ - **Environment ID** as a URL path parameter: `/{environmentId}/mcp`
130
+ - **API Key** via Bearer token in the Authorization header: `Authorization: Bearer <api-key>`
131
+
132
+ This mode allows a single server instance to handle requests for multiple Kontent.ai environments securely without requiring environment variables.
120
133
 
121
134
  ## 🚀 Transport Options
122
135
 
@@ -137,14 +150,16 @@ To run the server with STDIO transport, configure your MCP client with:
137
150
  }
138
151
  ```
139
152
 
140
- ### 🌐 SSE Transport
153
+ ### 🌊 Streamable HTTP Transport
141
154
 
142
- For SSE transport, first start the server:
155
+ For Streamable HTTP transport, first start the server:
143
156
 
144
157
  ```bash
145
- npx @kontent-ai/mcp-server@latest sse
158
+ npx @kontent-ai/mcp-server@latest shttp
146
159
  ```
147
160
 
161
+ #### Single-Tenant Mode
162
+
148
163
  With environment variables in a `.env` file, or otherwise accessible to the process:
149
164
  ```env
150
165
  KONTENT_API_KEY=<management-api-key>
@@ -155,36 +170,112 @@ PORT=3001 # optional, defaults to 3001
155
170
  Then configure your MCP client:
156
171
  ```json
157
172
  {
158
- "kontent-ai-sse": {
159
- "url": "http://localhost:3001/sse"
173
+ "kontent-ai-http": {
174
+ "url": "http://localhost:3001/mcp"
160
175
  }
161
176
  }
162
177
  ```
163
178
 
164
- ### 🌊 Streamable HTTP Transport
179
+ #### Multi-Tenant Mode
165
180
 
166
- For Streamable HTTP transport, first start the server:
181
+ No environment variables required. The server accepts requests for multiple environments using URL path parameters and Bearer authentication.
167
182
 
168
- ```bash
169
- npx @kontent-ai/mcp-server@latest shttp
183
+ ##### VS Code Configuration
184
+
185
+ Create a `.vscode/mcp.json` file in your workspace:
186
+
187
+ ```json
188
+ {
189
+ "servers": {
190
+ "kontent-ai-multi": {
191
+ "uri": "http://localhost:3001/<environment-id>/mcp",
192
+ "headers": {
193
+ "Authorization": "Bearer <management-api-key>"
194
+ }
195
+ }
196
+ }
197
+ }
170
198
  ```
171
199
 
172
- With environment variables in a `.env` file, or otherwise accessible to the process:
173
- ```env
174
- KONTENT_API_KEY=<management-api-key>
175
- KONTENT_ENVIRONMENT_ID=<environment-id>
176
- PORT=3001 # optional, defaults to 3001
200
+ For secure configuration with input prompts:
201
+
202
+ ```json
203
+ {
204
+ "inputs": [
205
+ {
206
+ "id": "apiKey",
207
+ "type": "password",
208
+ "description": "Kontent.ai API Key"
209
+ },
210
+ {
211
+ "id": "environmentId",
212
+ "type": "text",
213
+ "description": "Environment ID"
214
+ }
215
+ ],
216
+ "servers": {
217
+ "kontent-ai-multi": {
218
+ "uri": "http://localhost:3001/${inputs.environmentId}/mcp",
219
+ "headers": {
220
+ "Authorization": "Bearer ${inputs.apiKey}"
221
+ }
222
+ }
223
+ }
224
+ }
177
225
  ```
178
226
 
179
- Then configure your MCP client:
227
+ ##### Claude Desktop Configuration
228
+
229
+ Update your Claude Desktop configuration file:
230
+ - **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
231
+ - **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
232
+ - **Linux**: `~/.config/Claude/claude_desktop_config.json`
233
+
234
+ Use `mcp-remote` as a proxy to add authentication headers:
235
+
180
236
  ```json
181
237
  {
182
- "kontent-ai-http": {
183
- "url": "http://localhost:3001/mcp"
238
+ "mcpServers": {
239
+ "kontent-ai-multi": {
240
+ "command": "npx",
241
+ "args": [
242
+ "mcp-remote",
243
+ "http://localhost:3001/<environment-id>/mcp",
244
+ "--header",
245
+ "Authorization: Bearer <management-api-key>"
246
+ ]
247
+ }
184
248
  }
185
249
  }
186
250
  ```
187
251
 
252
+ ##### Claude Code Configuration
253
+
254
+ For Claude Code (claude.ai/code), add the server configuration:
255
+
256
+ ```bash
257
+ # Add the multi-tenant server
258
+ claude mcp add \
259
+ --url "http://localhost:3001/<environment-id>/mcp" \
260
+ --header "Authorization: Bearer <management-api-key>" \
261
+ kontent-ai-multi
262
+ ```
263
+
264
+ Or configure directly in the settings:
265
+
266
+ ```json
267
+ {
268
+ "kontent-ai-multi": {
269
+ "url": "http://localhost:3001/<environment-id>/mcp",
270
+ "headers": {
271
+ "Authorization": "Bearer <management-api-key>"
272
+ }
273
+ }
274
+ }
275
+ ```
276
+
277
+ **Important**: Replace `<environment-id>` with your actual Kontent.ai environment ID (GUID format) and `<management-api-key>` with your Management API key.
278
+
188
279
  ## 💻 Development
189
280
 
190
281
  ### 🛠 Local Installation
@@ -201,12 +292,10 @@ npm ci
201
292
  npm run build
202
293
 
203
294
  # Start the server
204
- npm run start:sse # For SSE transport
205
295
  npm run start:stdio # For STDIO transport
206
296
  npm run start:shttp # For Streamable HTTP transport
207
297
 
208
298
  # Start the server with automatic reloading (no need to build first)
209
- npm run dev:sse # For SSE transport
210
299
  npm run dev:stdio # For STDIO transport
211
300
  npm run dev:shttp # For Streamable HTTP transport
212
301
  ```
@@ -231,7 +320,7 @@ For debugging, you can use the MCP inspector:
231
320
  npx @modelcontextprotocol/inspector -e KONTENT_API_KEY=<key> -e KONTENT_ENVIRONMENT_ID=<env-id> node path/to/build/bin.js
232
321
  ```
233
322
 
234
- Or use the MCP inspector on a running sse server:
323
+ Or use the MCP inspector on a running streamable HTTP server:
235
324
 
236
325
  ```bash
237
326
  npx @modelcontextprotocol/inspector
package/build/bin.js CHANGED
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
3
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
3
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
5
4
  import "dotenv/config";
6
5
  import express from "express";
7
6
  import packageJson from "../package.json" with { type: "json" };
8
7
  import { createServer } from "./server.js";
8
+ import { extractBearerToken } from "./utils/extractBearerToken.js";
9
+ import { isValidGuid } from "./utils/isValidGuid.js";
9
10
  const version = packageJson.version;
10
11
  async function startStreamableHTTP() {
11
12
  const app = express();
@@ -39,7 +40,6 @@ async function startStreamableHTTP() {
39
40
  }
40
41
  });
41
42
  app.get("/mcp", async (_, res) => {
42
- console.log("Received GET MCP request");
43
43
  res.writeHead(405).end(JSON.stringify({
44
44
  jsonrpc: "2.0",
45
45
  error: {
@@ -50,7 +50,6 @@ async function startStreamableHTTP() {
50
50
  }));
51
51
  });
52
52
  app.delete("/mcp", async (_, res) => {
53
- console.log("Received DELETE MCP request");
54
53
  res.writeHead(405).end(JSON.stringify({
55
54
  jsonrpc: "2.0",
56
55
  error: {
@@ -60,25 +59,80 @@ async function startStreamableHTTP() {
60
59
  id: null,
61
60
  }));
62
61
  });
63
- const PORT = process.env.PORT || 3001;
64
- app.listen(PORT, () => {
65
- console.log(`Kontent.ai MCP Server v${version} (Streamable HTTP) running on port ${PORT}`);
62
+ app.post("/:environmentId/mcp", async (req, res) => {
63
+ try {
64
+ const { environmentId } = req.params;
65
+ if (!isValidGuid(environmentId)) {
66
+ res.status(400).json({
67
+ error: "Invalid environment ID format. Must be a valid GUID.",
68
+ });
69
+ return;
70
+ }
71
+ const { server } = createServer();
72
+ const transport = new StreamableHTTPServerTransport({
73
+ sessionIdGenerator: undefined,
74
+ });
75
+ res.on("close", () => {
76
+ console.log("Request closed");
77
+ transport.close();
78
+ server.close();
79
+ });
80
+ const authToken = extractBearerToken(req);
81
+ if (!authToken) {
82
+ res.status(401).json({
83
+ error: "Authorization header with Bearer token is required.",
84
+ });
85
+ return;
86
+ }
87
+ await server.connect(transport);
88
+ await transport.handleRequest(Object.assign(req, {
89
+ auth: {
90
+ clientId: environmentId,
91
+ token: authToken,
92
+ scopes: [],
93
+ },
94
+ }), res, req.body);
95
+ }
96
+ catch (error) {
97
+ console.error("Error handling MCP request:", error);
98
+ if (!res.headersSent) {
99
+ res.status(500).json({
100
+ jsonrpc: "2.0",
101
+ error: {
102
+ code: -32603,
103
+ message: "Internal server error",
104
+ },
105
+ id: null,
106
+ });
107
+ }
108
+ }
66
109
  });
67
- }
68
- async function startSSE() {
69
- const app = express();
70
- const { server } = createServer();
71
- let transport;
72
- app.get("/sse", async (_req, res) => {
73
- transport = new SSEServerTransport("/message", res);
74
- await server.connect(transport);
110
+ app.get("/:environmentId/mcp", async (_, res) => {
111
+ res.writeHead(405).end(JSON.stringify({
112
+ jsonrpc: "2.0",
113
+ error: {
114
+ code: -32000,
115
+ message: "Method not allowed.",
116
+ },
117
+ id: null,
118
+ }));
75
119
  });
76
- app.post("/message", async (req, res) => {
77
- await transport.handlePostMessage(req, res);
120
+ app.delete("/:environmentId/mcp", async (_, res) => {
121
+ res.writeHead(405).end(JSON.stringify({
122
+ jsonrpc: "2.0",
123
+ error: {
124
+ code: -32000,
125
+ message: "Method not allowed.",
126
+ },
127
+ id: null,
128
+ }));
78
129
  });
79
130
  const PORT = process.env.PORT || 3001;
80
131
  app.listen(PORT, () => {
81
- console.log(`Kontent.ai MCP Server v${version} (SSE) running on port ${PORT}`);
132
+ console.log(`Kontent.ai MCP Server v${version} (Streamable HTTP) running on port ${PORT}.
133
+ Available endpoints:
134
+ /mcp
135
+ /{environmentId}/mcp (requires Bearer authentication)`);
82
136
  });
83
137
  }
84
138
  async function startStdio() {
@@ -91,18 +145,13 @@ async function main() {
91
145
  const args = process.argv.slice(2);
92
146
  const transportType = args[0]?.toLowerCase();
93
147
  if (!transportType ||
94
- (transportType !== "stdio" &&
95
- transportType !== "sse" &&
96
- transportType !== "shttp")) {
97
- console.error("Please specify a valid transport type: stdio, sse, or shttp");
148
+ (transportType !== "stdio" && transportType !== "shttp")) {
149
+ console.error("Please specify a valid transport type: stdio or shttp");
98
150
  process.exit(1);
99
151
  }
100
152
  if (transportType === "stdio") {
101
153
  await startStdio();
102
154
  }
103
- else if (transportType === "sse") {
104
- await startSSE();
105
- }
106
155
  else if (transportType === "shttp") {
107
156
  await startStreamableHTTP();
108
157
  }
package/build/server.js CHANGED
@@ -5,6 +5,7 @@ import { registerTool as registerAddContentTypeMapi } from "./tools/add-content-
5
5
  import { registerTool as registerAddContentTypeSnippetMapi } from "./tools/add-content-type-snippet-mapi.js";
6
6
  import { registerTool as registerAddTaxonomyGroupMapi } from "./tools/add-taxonomy-group-mapi.js";
7
7
  import { registerTool as registerChangeVariantWorkflowStepMapi } from "./tools/change-variant-workflow-step-mapi.js";
8
+ import { registerTool as registerCreateVariantVersionMapi } from "./tools/create-variant-version-mapi.js";
8
9
  import { registerTool as registerDeleteContentItemMapi } from "./tools/delete-content-item-mapi.js";
9
10
  import { registerTool as registerDeleteContentTypeMapi } from "./tools/delete-content-type-mapi.js";
10
11
  import { registerTool as registerDeleteLanguageVariantMapi } from "./tools/delete-language-variant-mapi.js";
@@ -61,6 +62,7 @@ export const createServer = () => {
61
62
  registerUpdateContentItemMapi(server);
62
63
  registerDeleteContentItemMapi(server);
63
64
  registerUpsertLanguageVariantMapi(server);
65
+ registerCreateVariantVersionMapi(server);
64
66
  registerDeleteLanguageVariantMapi(server);
65
67
  registerListWorkflowsMapi(server);
66
68
  registerChangeVariantWorkflowStepMapi(server);
@@ -32,8 +32,8 @@ export const registerTool = (server) => {
32
32
  })
33
33
  .optional()
34
34
  .describe("Reference to a collection by id, codename, or external_id (optional)"),
35
- }, async ({ name, type, codename, external_id, collection }) => {
36
- const client = createMapiClient();
35
+ }, async ({ name, type, codename, external_id, collection }, { authInfo: { token, clientId } = {} }) => {
36
+ const client = createMapiClient(clientId, token);
37
37
  try {
38
38
  const response = await client
39
39
  .addContentItem()
@@ -21,8 +21,8 @@ export const registerTool = (server) => {
21
21
  .array(contentGroupSchema)
22
22
  .optional()
23
23
  .describe("Array of content groups (optional)"),
24
- }, async ({ name, codename, external_id, elements, content_groups }) => {
25
- const client = createMapiClient();
24
+ }, async ({ name, codename, external_id, elements, content_groups }, { authInfo: { token, clientId } = {} }) => {
25
+ const client = createMapiClient(clientId, token);
26
26
  try {
27
27
  const response = await client
28
28
  .addContentType()
@@ -17,8 +17,8 @@ export const registerTool = (server) => {
17
17
  elements: z
18
18
  .array(snippetElementSchema)
19
19
  .describe("Array of elements that define the structure of the content type snippet"),
20
- }, async ({ name, codename, external_id, elements }) => {
21
- const client = createMapiClient();
20
+ }, async ({ name, codename, external_id, elements }, { authInfo: { token, clientId } = {} }) => {
21
+ const client = createMapiClient(clientId, token);
22
22
  try {
23
23
  const response = await client
24
24
  .addContentTypeSnippet()
@@ -3,8 +3,8 @@ import { taxonomyGroupSchemas } from "../schemas/taxonomySchemas.js";
3
3
  import { handleMcpToolError } from "../utils/errorHandler.js";
4
4
  import { createMcpToolSuccessResponse } from "../utils/responseHelper.js";
5
5
  export const registerTool = (server) => {
6
- server.tool("add-taxonomy-group-mapi", "Add new Kontent.ai taxonomy group via Management API", taxonomyGroupSchemas, async (taxonomyGroup) => {
7
- const client = createMapiClient();
6
+ server.tool("add-taxonomy-group-mapi", "Add new Kontent.ai taxonomy group via Management API", taxonomyGroupSchemas, async (taxonomyGroup, { authInfo: { token, clientId } = {} }) => {
7
+ const client = createMapiClient(clientId, token);
8
8
  try {
9
9
  const response = await client
10
10
  .addTaxonomy()
@@ -20,8 +20,8 @@ export const registerTool = (server) => {
20
20
  .string()
21
21
  .uuid()
22
22
  .describe("Internal ID (UUID) of the target workflow step. This must be a valid step ID from the specified workflow. Common steps include Draft, Review, Published, and Archived, but the actual IDs depend on your specific workflow configuration"),
23
- }, async ({ itemId, languageId, workflowId, workflowStepId }) => {
24
- const client = createMapiClient();
23
+ }, async ({ itemId, languageId, workflowId, workflowStepId }, { authInfo: { token, clientId } = {} }) => {
24
+ const client = createMapiClient(clientId, token);
25
25
  try {
26
26
  const response = await client
27
27
  .changeWorkflowOfLanguageVariant()
@@ -0,0 +1,32 @@
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("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.", {
7
+ itemId: z
8
+ .string()
9
+ .uuid()
10
+ .describe("Internal ID (UUID) of the content item whose language variant you want to create a new version of"),
11
+ languageId: z
12
+ .string()
13
+ .uuid()
14
+ .describe("Internal ID (UUID) of the language variant to create a new version of. Use '00000000-0000-0000-0000-000000000000' for the default language"),
15
+ }, async ({ itemId, languageId }, { authInfo: { token, clientId } = {} }) => {
16
+ const client = createMapiClient(clientId, token);
17
+ try {
18
+ const response = await client
19
+ .createNewVersionOfLanguageVariant()
20
+ .byItemId(itemId)
21
+ .byLanguageId(languageId)
22
+ .toPromise();
23
+ return createMcpToolSuccessResponse({
24
+ message: `Successfully created new version of language variant '${languageId}' for content item '${itemId}'`,
25
+ result: response.data,
26
+ });
27
+ }
28
+ catch (error) {
29
+ return handleMcpToolError(error, "Variant Version Creation");
30
+ }
31
+ });
32
+ };
@@ -5,8 +5,8 @@ import { createMcpToolSuccessResponse } from "../utils/responseHelper.js";
5
5
  export const registerTool = (server) => {
6
6
  server.tool("delete-content-item-mapi", "Delete Kontent.ai content item by internal ID from Management API", {
7
7
  id: z.string().describe("Internal ID of the content item to delete"),
8
- }, async ({ id }) => {
9
- const client = createMapiClient();
8
+ }, async ({ id }, { authInfo: { token, clientId } = {} }) => {
9
+ const client = createMapiClient(clientId, token);
10
10
  try {
11
11
  const response = await client
12
12
  .deleteContentItem()
@@ -5,8 +5,8 @@ import { createMcpToolSuccessResponse } from "../utils/responseHelper.js";
5
5
  export const registerTool = (server) => {
6
6
  server.tool("delete-content-type-mapi", "Delete a content type by codename from Management API", {
7
7
  codename: z.string().describe("Codename of the content type to delete"),
8
- }, async ({ codename }) => {
9
- const client = createMapiClient();
8
+ }, async ({ codename }, { authInfo: { token, clientId } = {} }) => {
9
+ const client = createMapiClient(clientId, token);
10
10
  try {
11
11
  const response = await client
12
12
  .deleteContentType()
@@ -8,8 +8,8 @@ export const registerTool = (server) => {
8
8
  languageId: z
9
9
  .string()
10
10
  .describe("Internal ID of the language variant to delete"),
11
- }, async ({ itemId, languageId }) => {
12
- const client = createMapiClient();
11
+ }, async ({ itemId, languageId }, { authInfo: { token, clientId } = {} }) => {
12
+ const client = createMapiClient(clientId, token);
13
13
  try {
14
14
  const response = await client
15
15
  .deleteLanguageVariant()
@@ -3,7 +3,7 @@ import { handleMcpToolError } from "../utils/errorHandler.js";
3
3
  import { createMcpToolSuccessResponse } from "../utils/responseHelper.js";
4
4
  import { throwError } from "../utils/throwError.js";
5
5
  export const registerTool = (server) => {
6
- server.tool("filter-variants-mapi", "Search and filter Kontent.ai language variants of content items using Management API", filterVariantsSchema.shape, async ({ search_phrase, content_types, contributors, has_no_contributors, completion_statuses, language, workflow_steps, taxonomy_groups, order_by, order_direction, continuation_token, }) => {
6
+ server.tool("filter-variants-mapi", "Search and filter Kontent.ai language variants of content items using Management API", filterVariantsSchema.shape, async ({ search_phrase, content_types, contributors, has_no_contributors, completion_statuses, language, workflow_steps, taxonomy_groups, order_by, order_direction, continuation_token, }, { authInfo: { token, clientId } = {} }) => {
7
7
  try {
8
8
  const requestPayload = {
9
9
  filters: {
@@ -23,10 +23,13 @@ export const registerTool = (server) => {
23
23
  }
24
24
  : null,
25
25
  };
26
- const environmentId = process.env.KONTENT_ENVIRONMENT_ID;
27
- const apiKey = process.env.KONTENT_API_KEY;
28
- if (!environmentId || !apiKey) {
29
- throwError("Missing required environment variables");
26
+ const environmentId = clientId ?? process.env.KONTENT_ENVIRONMENT_ID;
27
+ if (!environmentId) {
28
+ throwError("Missing required environment ID");
29
+ }
30
+ const apiKey = token ?? process.env.KONTENT_API_KEY;
31
+ if (!apiKey) {
32
+ throwError("Missing required API key");
30
33
  }
31
34
  const url = `https://manage.kontent.ai/v2/projects/${environmentId}/early-access/variants/filter`;
32
35
  const headers = {
@@ -5,8 +5,8 @@ import { createMcpToolSuccessResponse } from "../utils/responseHelper.js";
5
5
  export const registerTool = (server) => {
6
6
  server.tool("get-asset-mapi", "Get a specific Kontent.ai asset by internal ID from Management API", {
7
7
  assetId: z.string().describe("Internal ID of the asset to retrieve"),
8
- }, async ({ assetId }) => {
9
- const client = createMapiClient();
8
+ }, async ({ assetId }, { authInfo: { token, clientId } = {} }) => {
9
+ const client = createMapiClient(clientId, token);
10
10
  try {
11
11
  const response = await client
12
12
  .viewAsset()
@@ -5,8 +5,8 @@ import { createMcpToolSuccessResponse } from "../utils/responseHelper.js";
5
5
  export const registerTool = (server) => {
6
6
  server.tool("get-item-mapi", "Get Kontent.ai item by internal ID from Management API", {
7
7
  id: z.string().describe("Internal ID of the item to get"),
8
- }, async ({ id }) => {
9
- const client = createMapiClient();
8
+ }, async ({ id }, { authInfo: { token, clientId } = {} }) => {
9
+ const client = createMapiClient(clientId, token);
10
10
  try {
11
11
  const response = await client
12
12
  .viewContentItem()
@@ -5,8 +5,8 @@ import { createMcpToolSuccessResponse } from "../utils/responseHelper.js";
5
5
  export const registerTool = (server) => {
6
6
  server.tool("get-taxonomy-group-mapi", "Get Kontent.ai taxonomy group by internal ID from Management API", {
7
7
  id: z.string().describe("Internal ID of the taxonomy group to get"),
8
- }, async ({ id }) => {
9
- const client = createMapiClient();
8
+ }, async ({ id }, { authInfo: { token, clientId } = {} }) => {
9
+ const client = createMapiClient(clientId, token);
10
10
  try {
11
11
  const response = await client
12
12
  .getTaxonomy()
@@ -5,8 +5,8 @@ import { createMcpToolSuccessResponse } from "../utils/responseHelper.js";
5
5
  export const registerTool = (server) => {
6
6
  server.tool("get-type-mapi", "Get Kontent.ai content type by internal ID from Management API", {
7
7
  id: z.string().describe("Internal ID of the content type to get"),
8
- }, async ({ id }) => {
9
- const client = createMapiClient();
8
+ }, async ({ id }, { authInfo: { token, clientId } = {} }) => {
9
+ const client = createMapiClient(clientId, token);
10
10
  try {
11
11
  const response = await client
12
12
  .viewContentType()
@@ -5,8 +5,8 @@ import { createMcpToolSuccessResponse } from "../utils/responseHelper.js";
5
5
  export const registerTool = (server) => {
6
6
  server.tool("get-type-snippet-mapi", "Get Kontent.ai content type snippet by internal ID from Management API", {
7
7
  id: z.string().describe("Internal ID of the content type snippet to get"),
8
- }, async ({ id }) => {
9
- const client = createMapiClient();
8
+ }, async ({ id }, { authInfo: { token, clientId } = {} }) => {
9
+ const client = createMapiClient(clientId, token);
10
10
  try {
11
11
  const response = await client
12
12
  .viewContentTypeSnippet()
@@ -8,8 +8,8 @@ export const registerTool = (server) => {
8
8
  languageId: z
9
9
  .string()
10
10
  .describe("Internal ID of the language variant to get"),
11
- }, async ({ itemId, languageId }) => {
12
- const client = createMapiClient();
11
+ }, async ({ itemId, languageId }, { authInfo: { token, clientId } = {} }) => {
12
+ const client = createMapiClient(clientId, token);
13
13
  try {
14
14
  const response = await client
15
15
  .viewLanguageVariant()
@@ -2,8 +2,8 @@ import { createMapiClient } from "../clients/kontentClients.js";
2
2
  import { handleMcpToolError } from "../utils/errorHandler.js";
3
3
  import { createMcpToolSuccessResponse } from "../utils/responseHelper.js";
4
4
  export const registerTool = (server) => {
5
- server.tool("list-assets-mapi", "Get all Kontent.ai assets from Management API", {}, async () => {
6
- const client = createMapiClient();
5
+ server.tool("list-assets-mapi", "Get all Kontent.ai assets from Management API", {}, async (_, { authInfo: { token, clientId } = {} }) => {
6
+ const client = createMapiClient(clientId, token);
7
7
  try {
8
8
  const response = await client.listAssets().toAllPromise();
9
9
  return createMcpToolSuccessResponse(response.data);
@@ -2,8 +2,8 @@ import { createMapiClient } from "../clients/kontentClients.js";
2
2
  import { handleMcpToolError } from "../utils/errorHandler.js";
3
3
  import { createMcpToolSuccessResponse } from "../utils/responseHelper.js";
4
4
  export const registerTool = (server) => {
5
- server.tool("list-content-type-snippets-mapi", "Get all Kontent.ai content type snippets from Management API", {}, async () => {
6
- const client = createMapiClient();
5
+ server.tool("list-content-type-snippets-mapi", "Get all Kontent.ai content type snippets from Management API", {}, async (_, { authInfo: { token, clientId } = {} }) => {
6
+ const client = createMapiClient(clientId, token);
7
7
  try {
8
8
  const response = await client.listContentTypeSnippets().toAllPromise();
9
9
  return createMcpToolSuccessResponse(response.data);
@@ -2,8 +2,8 @@ import { createMapiClient } from "../clients/kontentClients.js";
2
2
  import { handleMcpToolError } from "../utils/errorHandler.js";
3
3
  import { createMcpToolSuccessResponse } from "../utils/responseHelper.js";
4
4
  export const registerTool = (server) => {
5
- server.tool("list-content-types-mapi", "Get all Kontent.ai content types from Management API", {}, async () => {
6
- const client = createMapiClient();
5
+ server.tool("list-content-types-mapi", "Get all Kontent.ai content types from Management API", {}, async (_, { authInfo: { token, clientId } = {} }) => {
6
+ const client = createMapiClient(clientId, token);
7
7
  try {
8
8
  const response = await client.listContentTypes().toAllPromise();
9
9
  return createMcpToolSuccessResponse(response.data);
@@ -2,8 +2,8 @@ import { createMapiClient } from "../clients/kontentClients.js";
2
2
  import { handleMcpToolError } from "../utils/errorHandler.js";
3
3
  import { createMcpToolSuccessResponse } from "../utils/responseHelper.js";
4
4
  export const registerTool = (server) => {
5
- server.tool("list-languages-mapi", "Get all Kontent.ai languages from Management API", {}, async () => {
6
- const client = createMapiClient();
5
+ server.tool("list-languages-mapi", "Get all Kontent.ai languages from Management API", {}, async (_, { authInfo: { token, clientId } = {} }) => {
6
+ const client = createMapiClient(clientId, token);
7
7
  try {
8
8
  const response = await client.listLanguages().toAllPromise();
9
9
  return createMcpToolSuccessResponse(response.data);
@@ -2,8 +2,8 @@ import { createMapiClient } from "../clients/kontentClients.js";
2
2
  import { handleMcpToolError } from "../utils/errorHandler.js";
3
3
  import { createMcpToolSuccessResponse } from "../utils/responseHelper.js";
4
4
  export const registerTool = (server) => {
5
- server.tool("list-taxonomy-groups-mapi", "Get all Kontent.ai taxonomy groups from Management API", {}, async () => {
6
- const client = createMapiClient();
5
+ server.tool("list-taxonomy-groups-mapi", "Get all Kontent.ai taxonomy groups from Management API", {}, async (_, { authInfo: { token, clientId } = {} }) => {
6
+ const client = createMapiClient(clientId, token);
7
7
  try {
8
8
  const response = await client.listTaxonomies().toAllPromise();
9
9
  return createMcpToolSuccessResponse(response.data);
@@ -2,8 +2,8 @@ import { createMapiClient } from "../clients/kontentClients.js";
2
2
  import { handleMcpToolError } from "../utils/errorHandler.js";
3
3
  import { createMcpToolSuccessResponse } from "../utils/responseHelper.js";
4
4
  export const registerTool = (server) => {
5
- server.tool("list-workflows-mapi", "Get all Kontent.ai workflows from Management API. Workflows define the content lifecycle stages and transitions between them.", {}, async () => {
6
- const client = createMapiClient();
5
+ server.tool("list-workflows-mapi", "Get all Kontent.ai workflows from Management API. Workflows define the content lifecycle stages and transitions between them.", {}, async (_, { authInfo: { token, clientId } = {} }) => {
6
+ const client = createMapiClient(clientId, token);
7
7
  try {
8
8
  const response = await client.listWorkflows().toPromise();
9
9
  return createMcpToolSuccessResponse(response.data);
@@ -54,8 +54,8 @@ export const registerTool = (server) => {
54
54
  - Consider element ordering when using move operations
55
55
  - Use atomic operations for complex changes like removing content groups
56
56
  - When adding to allowed_formatting or allowed_table_formatting, always ensure 'unstyled' is the first item in the array`),
57
- }, async ({ codename, operations }) => {
58
- const client = createMapiClient();
57
+ }, async ({ codename, operations }, { authInfo: { token, clientId } = {} }) => {
58
+ const client = createMapiClient(clientId, token);
59
59
  try {
60
60
  // Apply patch operations using the modifyContentType method
61
61
  const response = await client
@@ -21,8 +21,8 @@ export const registerTool = (server) => {
21
21
  .string()
22
22
  .optional()
23
23
  .describe("The timezone identifier for displaying the scheduled time in the Kontent.ai UI (e.g., 'America/New_York', 'Europe/London', 'UTC'). This parameter is used for scheduled publishing to specify the timezone context for the scheduled_to parameter. If not provided, the system will use the default timezone. This helps content creators understand when content will be published in their local context."),
24
- }, async ({ itemId, languageId, scheduledTo, displayTimezone }) => {
25
- const client = createMapiClient();
24
+ }, async ({ itemId, languageId, scheduledTo, displayTimezone }, { authInfo: { token, clientId } = {} }) => {
25
+ const client = createMapiClient(clientId, token);
26
26
  try {
27
27
  // Validate that displayTimezone can only be used with scheduledTo
28
28
  if (displayTimezone && !scheduledTo) {
@@ -21,8 +21,8 @@ export const registerTool = (server) => {
21
21
  .string()
22
22
  .optional()
23
23
  .describe("The timezone identifier for displaying the scheduled time in the Kontent.ai UI (e.g., 'America/New_York', 'Europe/London', 'UTC'). This parameter is used for scheduled unpublishing to specify the timezone context for the scheduled_to parameter. If not provided, the system will use the default timezone. This helps content creators understand when content will be unpublished in their local context."),
24
- }, async ({ itemId, languageId, scheduledTo, displayTimezone }) => {
25
- const client = createMapiClient();
24
+ }, async ({ itemId, languageId, scheduledTo, displayTimezone }, { authInfo: { token, clientId } = {} }) => {
25
+ const client = createMapiClient(clientId, token);
26
26
  try {
27
27
  // Validate that displayTimezone can only be used with scheduledTo
28
28
  if (displayTimezone && !scheduledTo) {
@@ -19,8 +19,8 @@ export const registerTool = (server) => {
19
19
  })
20
20
  .optional()
21
21
  .describe("Reference to a collection by id, codename, or external_id (optional)"),
22
- }, async ({ id, name, collection }) => {
23
- const client = createMapiClient();
22
+ }, async ({ id, name, collection }, { authInfo: { token, clientId } = {} }) => {
23
+ const client = createMapiClient(clientId, token);
24
24
  try {
25
25
  // First, verify the item exists by trying to get it
26
26
  await client.viewContentItem().byItemId(id).toPromise();
@@ -4,18 +4,20 @@ import { languageVariantElementSchema } from "../schemas/contentItemSchemas.js";
4
4
  import { handleMcpToolError } from "../utils/errorHandler.js";
5
5
  import { createMcpToolSuccessResponse } from "../utils/responseHelper.js";
6
6
  export const registerTool = (server) => {
7
- server.tool("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. Elements should be provided as JSON string in the format expected by the SDK.", {
7
+ server.tool("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.", {
8
8
  itemId: z.string().describe("Internal ID of the content item"),
9
9
  languageId: z
10
10
  .string()
11
11
  .describe("Internal ID of the language variant (e.g., '00000000-0000-0000-0000-000000000000' for default language)"),
12
- elements: z.array(languageVariantElementSchema),
12
+ elements: z
13
+ .array(languageVariantElementSchema)
14
+ .describe("Array of content elements, each with 'element' (reference object with id/codename/external_id) and 'value' properties. Additional properties may be required depending on element type (e.g., 'mode' for URL slugs)."),
13
15
  workflow_step_id: z
14
16
  .string()
15
17
  .optional()
16
18
  .describe("Internal ID of the workflow step (optional)"),
17
- }, async ({ itemId, languageId, elements, workflow_step_id }) => {
18
- const client = createMapiClient();
19
+ }, async ({ itemId, languageId, elements, workflow_step_id }, { authInfo: { token, clientId } = {} }) => {
20
+ const client = createMapiClient(clientId, token);
19
21
  const data = {
20
22
  elements,
21
23
  };
@@ -50,7 +50,7 @@ export const handleMcpToolError = (error, context) => {
50
50
  content: [
51
51
  {
52
52
  type: "text",
53
- text: `${contextPrefix}HTTP Error ${error.response.status}: ${error.response.statusText || "Unknown HTTP error"}\n\nResponse: ${JSON.stringify(error.response.data, null, 2)}`,
53
+ text: `${contextPrefix}HTTP Error ${error.response.status}: ${error.response.statusText || "Unknown HTTP error"}\n\nResponse: ${JSON.stringify(error.response.data)}`,
54
54
  },
55
55
  ],
56
56
  isError: true,
@@ -61,7 +61,7 @@ export const handleMcpToolError = (error, context) => {
61
61
  content: [
62
62
  {
63
63
  type: "text",
64
- text: `${contextPrefix}Unexpected error: ${error instanceof Error ? error.message : "Unknown error occurred"}\n\nFull error: ${JSON.stringify(error, null, 2)}`,
64
+ text: `${contextPrefix}Unexpected error: ${error instanceof Error ? error.message : "Unknown error occurred"}\n\nFull error: ${JSON.stringify(error)}`,
65
65
  },
66
66
  ],
67
67
  isError: true,
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Extracts Bearer token from request authorization header
3
+ * @param req Express request object
4
+ * @returns Bearer token string or null if not found
5
+ */
6
+ export const extractBearerToken = (req) => {
7
+ const authHeader = req.headers.authorization;
8
+ if (authHeader?.startsWith("Bearer ")) {
9
+ return authHeader.substring(7); // Remove 'Bearer ' prefix
10
+ }
11
+ return null;
12
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Validates if a string is a valid GUID format
3
+ * @param guid String to validate
4
+ * @returns true if valid GUID format, false otherwise
5
+ */
6
+ export const isValidGuid = (guid) => {
7
+ const guidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
8
+ return guidRegex.test(guid);
9
+ };
@@ -11,7 +11,7 @@ export const createMcpToolSuccessResponse = (data) => {
11
11
  content: [
12
12
  {
13
13
  type: "text",
14
- text: JSON.stringify(data, null, 2),
14
+ text: JSON.stringify(data),
15
15
  },
16
16
  ],
17
17
  };
package/package.json CHANGED
@@ -1,14 +1,12 @@
1
1
  {
2
2
  "name": "@kontent-ai/mcp-server",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "build": "rimraf build && tsc",
7
7
  "start:stdio": "node build/bin.js stdio",
8
- "start:sse": "node build/bin.js sse",
9
8
  "start:shttp": "node build/bin.js shttp",
10
9
  "dev:stdio": "tsx watch src/bin.ts stdio",
11
- "dev:sse": "tsx watch src/bin.ts sse",
12
10
  "dev:shttp": "tsx watch src/bin.ts shttp",
13
11
  "format": "cross-env node node_modules/@biomejs/biome/bin/biome ci ./ --config-path=./biome.json",
14
12
  "format:fix": "cross-env node node_modules/@biomejs/biome/bin/biome check ./ --fix --unsafe --config-path=./biome.json"