@kontent-ai/mcp-server 0.16.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 (34) hide show
  1. package/README.md +112 -24
  2. package/build/bin.js +73 -24
  3. package/build/tools/add-content-item-mapi.js +2 -2
  4. package/build/tools/add-content-type-mapi.js +2 -2
  5. package/build/tools/add-content-type-snippet-mapi.js +2 -2
  6. package/build/tools/add-taxonomy-group-mapi.js +2 -2
  7. package/build/tools/change-variant-workflow-step-mapi.js +2 -2
  8. package/build/tools/create-variant-version-mapi.js +2 -2
  9. package/build/tools/delete-content-item-mapi.js +2 -2
  10. package/build/tools/delete-content-type-mapi.js +2 -2
  11. package/build/tools/delete-language-variant-mapi.js +2 -2
  12. package/build/tools/filter-variants-mapi.js +8 -5
  13. package/build/tools/get-asset-mapi.js +2 -2
  14. package/build/tools/get-item-mapi.js +2 -2
  15. package/build/tools/get-taxonomy-group-mapi.js +2 -2
  16. package/build/tools/get-type-mapi.js +2 -2
  17. package/build/tools/get-type-snippet-mapi.js +2 -2
  18. package/build/tools/get-variant-mapi.js +2 -2
  19. package/build/tools/list-assets-mapi.js +2 -2
  20. package/build/tools/list-content-type-snippets-mapi.js +2 -2
  21. package/build/tools/list-content-types-mapi.js +2 -2
  22. package/build/tools/list-languages-mapi.js +2 -2
  23. package/build/tools/list-taxonomy-groups-mapi.js +2 -2
  24. package/build/tools/list-workflows-mapi.js +2 -2
  25. package/build/tools/patch-content-type-mapi.js +2 -2
  26. package/build/tools/publish-variant-mapi.js +2 -2
  27. package/build/tools/unpublish-variant-mapi.js +2 -2
  28. package/build/tools/update-content-item-mapi.js +2 -2
  29. package/build/tools/upsert-language-variant-mapi.js +2 -2
  30. package/build/utils/errorHandler.js +2 -2
  31. package/build/utils/extractBearerToken.js +12 -0
  32. package/build/utils/isValidGuid.js +9 -0
  33. package/build/utils/responseHelper.js +1 -1
  34. 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
@@ -111,13 +111,25 @@ npx @kontent-ai/mcp-server@latest sse
111
111
 
112
112
  ## ⚙️ Configuration
113
113
 
114
- 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:
115
119
 
116
120
  | Variable | Description | Required |
117
121
  |----------|-------------|----------|
118
122
  | KONTENT_API_KEY | Your Kontent.ai Management API key | ✅ |
119
123
  | KONTENT_ENVIRONMENT_ID | Your environment ID | ✅ |
120
- | 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.
121
133
 
122
134
  ## 🚀 Transport Options
123
135
 
@@ -138,14 +150,16 @@ To run the server with STDIO transport, configure your MCP client with:
138
150
  }
139
151
  ```
140
152
 
141
- ### 🌐 SSE Transport
153
+ ### 🌊 Streamable HTTP Transport
142
154
 
143
- For SSE transport, first start the server:
155
+ For Streamable HTTP transport, first start the server:
144
156
 
145
157
  ```bash
146
- npx @kontent-ai/mcp-server@latest sse
158
+ npx @kontent-ai/mcp-server@latest shttp
147
159
  ```
148
160
 
161
+ #### Single-Tenant Mode
162
+
149
163
  With environment variables in a `.env` file, or otherwise accessible to the process:
150
164
  ```env
151
165
  KONTENT_API_KEY=<management-api-key>
@@ -156,36 +170,112 @@ PORT=3001 # optional, defaults to 3001
156
170
  Then configure your MCP client:
157
171
  ```json
158
172
  {
159
- "kontent-ai-sse": {
160
- "url": "http://localhost:3001/sse"
173
+ "kontent-ai-http": {
174
+ "url": "http://localhost:3001/mcp"
161
175
  }
162
176
  }
163
177
  ```
164
178
 
165
- ### 🌊 Streamable HTTP Transport
179
+ #### Multi-Tenant Mode
166
180
 
167
- 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.
168
182
 
169
- ```bash
170
- 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
+ }
171
198
  ```
172
199
 
173
- With environment variables in a `.env` file, or otherwise accessible to the process:
174
- ```env
175
- KONTENT_API_KEY=<management-api-key>
176
- KONTENT_ENVIRONMENT_ID=<environment-id>
177
- 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
+ }
178
225
  ```
179
226
 
180
- 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
+
181
236
  ```json
182
237
  {
183
- "kontent-ai-http": {
184
- "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
+ }
185
248
  }
186
249
  }
187
250
  ```
188
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
+
189
279
  ## 💻 Development
190
280
 
191
281
  ### 🛠 Local Installation
@@ -202,12 +292,10 @@ npm ci
202
292
  npm run build
203
293
 
204
294
  # Start the server
205
- npm run start:sse # For SSE transport
206
295
  npm run start:stdio # For STDIO transport
207
296
  npm run start:shttp # For Streamable HTTP transport
208
297
 
209
298
  # Start the server with automatic reloading (no need to build first)
210
- npm run dev:sse # For SSE transport
211
299
  npm run dev:stdio # For STDIO transport
212
300
  npm run dev:shttp # For Streamable HTTP transport
213
301
  ```
@@ -232,7 +320,7 @@ For debugging, you can use the MCP inspector:
232
320
  npx @modelcontextprotocol/inspector -e KONTENT_API_KEY=<key> -e KONTENT_ENVIRONMENT_ID=<env-id> node path/to/build/bin.js
233
321
  ```
234
322
 
235
- Or use the MCP inspector on a running sse server:
323
+ Or use the MCP inspector on a running streamable HTTP server:
236
324
 
237
325
  ```bash
238
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
  }
@@ -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()
@@ -12,8 +12,8 @@ export const registerTool = (server) => {
12
12
  .string()
13
13
  .uuid()
14
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 }) => {
16
- const client = createMapiClient();
15
+ }, async ({ itemId, languageId }, { authInfo: { token, clientId } = {} }) => {
16
+ const client = createMapiClient(clientId, token);
17
17
  try {
18
18
  const response = await client
19
19
  .createNewVersionOfLanguageVariant()
@@ -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();
@@ -16,8 +16,8 @@ export const registerTool = (server) => {
16
16
  .string()
17
17
  .optional()
18
18
  .describe("Internal ID of the workflow step (optional)"),
19
- }, async ({ itemId, languageId, elements, workflow_step_id }) => {
20
- const client = createMapiClient();
19
+ }, async ({ itemId, languageId, elements, workflow_step_id }, { authInfo: { token, clientId } = {} }) => {
20
+ const client = createMapiClient(clientId, token);
21
21
  const data = {
22
22
  elements,
23
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.16.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"