@commercetools/commerce-mcp 4.0.0 → 4.2.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 CHANGED
@@ -12,11 +12,14 @@ pnpm run build
12
12
  # link the local package
13
13
  pnpm add link:../typescript
14
14
 
15
+ # build this package: the executable is dist/cli.js
16
+ pnpm run build
17
+
15
18
  # run the server in terminal
16
- npx ts-node src/index.ts --tools=read_products --clientId=CLIENT_ID --clientSecret=CLIENT_SECRET --authUrl=AUTH_URL --projectKey=PROJECT_KEY --apiUrl=API_URL
19
+ node dist/cli.js --tools=read_products --clientId=CLIENT_ID --clientSecret=CLIENT_SECRET --authUrl=AUTH_URL --projectKey=PROJECT_KEY --apiUrl=API_URL
17
20
 
18
21
  # test using mcptools : Install mcptools from https://github.com/f/mcptools
19
- mcp call read_products --params '{"limit": 2}' npx ts-node /<absolute-path>/commerce-agent/modelcontextprotocol/src/index.ts --tools=all \
22
+ mcp call read_products --params '{"limit": 2}' node /<absolute-path>/commerce-agent/modelcontextprotocol/dist/cli.js --tools=all \
20
23
  --projectKey="PROJECT_KEY" \
21
24
  --clientSecret="CLIENT_SECRET" \
22
25
  --clientId="CLIENT_ID" \
@@ -24,11 +27,14 @@ mcp call read_products --params '{"limit": 2}' npx ts-node /<absolute-path>/comm
24
27
  --apiUrl="API_URL"
25
28
  ```
26
29
 
30
+ `src/index.ts` exports `main()` but does not call it, so running that file
31
+ directly starts nothing. `dist/cli.js` is the entry point that does.
32
+
27
33
  **_Do not commit the linked package in package.json to the repo_**
28
34
 
29
35
  ## Testing Using Claude Desktop
30
36
 
31
- NOTE: This will not work with Claude Desktop unless you uninstall node v16 from your machine!
37
+ NOTE: This package requires Node.js 20 or newer. Make sure the `node` on Claude Desktop's PATH is a v20+ install.
32
38
 
33
39
  ```bash
34
40
  # navigate to ../typescript
@@ -38,6 +44,9 @@ pnpm run build
38
44
 
39
45
  # link the local package
40
46
  pnpm add link:../typescript
47
+
48
+ # build this package: the executable is dist/cli.js
49
+ pnpm run build
41
50
  ```
42
51
 
43
52
  Configure MCP servers in Claude Desktop
@@ -46,10 +55,9 @@ Configure MCP servers in Claude Desktop
46
55
  {
47
56
  "mcpServers": {
48
57
  "commercetools": {
49
- "command": "npx",
58
+ "command": "node",
50
59
  "args": [
51
- "ts-node",
52
- "/<absolute-path>/commerce-agent/modelcontextprotocol/src/index.ts",
60
+ "/<absolute-path>/commerce-agent/modelcontextprotocol/dist/cli.js",
53
61
  "--tools=all",
54
62
  "--projectKey=PROJECT_KEY",
55
63
  "--clientSecret=CLIENT_SECRET",
@@ -76,7 +84,7 @@ Run the following command in your terminal:
76
84
 
77
85
  ```bash
78
86
  # Start MCP Inspector and server with all tools
79
- npx @modelcontextprotocol/inspector node dist/index.js --tools=all --clientId=CLIENT_ID --clientSecret=CLIENT_SECRET --projectKey=PROJECT_KEY --authUrl=AUTH_URL --apiUrl=API_URL
87
+ npx @modelcontextprotocol/inspector node dist/cli.js --tools=all --clientId=CLIENT_ID --clientSecret=CLIENT_SECRET --projectKey=PROJECT_KEY --authUrl=AUTH_URL --apiUrl=API_URL
80
88
  ```
81
89
 
82
90
  ### Instructions
package/dist/cli.js ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Executable entry point. Kept separate from `index.ts` so that module's
4
+ * exports stay importable from tests and from other packages without running
5
+ * the server as a side effect.
6
+ */
7
+ import { handleError, main } from './index.js';
8
+ main().catch((error) => {
9
+ handleError(error);
10
+ process.exitCode = 1;
11
+ });
package/dist/index.js CHANGED
@@ -1,13 +1,11 @@
1
1
  #!/usr/bin/env node
2
- "use strict";
3
- Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.resolveToolsForConfiguration = exports.applyResolvedToolsToConfiguration = exports.ACCEPTED_TOOLS = void 0;
5
- exports.parseArgs = parseArgs;
6
- exports.main = main;
7
- const modelcontextprotocol_1 = require("@commercetools/commerce-agent/modelcontextprotocol");
8
- const processors_1 = require("@commercetools/processors");
9
- const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
10
- const colors_1 = require("colors");
2
+ import { ACCEPTED_TOOLS, applyResolvedToolsToConfiguration, CommercetoolsCommerceAgent, CommercetoolsCommerceAgentStreamable, DEFAULT_HOST, LOOPBACK_HOSTNAMES, normalizeBindHost, resolveToolsForConfiguration, } from '@commercetools/commerce-agent/modelcontextprotocol';
3
+ import { FieldFilteringHandler, } from '@commercetools/processors';
4
+ import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
5
+ import dotenv from 'dotenv';
6
+ import colors from 'colors';
7
+ const red = colors.red;
8
+ const yellow = colors.yellow;
11
9
  const HIDDEN_ARGS = ['customerId', 'isAdmin', 'storeKey', 'businessUnitKey'];
12
10
  const PUBLIC_ARGS = [
13
11
  'tools',
@@ -21,13 +19,14 @@ const PUBLIC_ARGS = [
21
19
  'dynamicToolLoadingThreshold',
22
20
  'toolOutputFormat',
23
21
  'logging',
22
+ 'host',
23
+ 'port',
24
+ 'allowedHosts',
25
+ 'allowedOrigins',
24
26
  ];
25
27
  const ACCEPTED_ARGS = [...PUBLIC_ARGS, ...HIDDEN_ARGS];
26
- var modelcontextprotocol_2 = require("@commercetools/commerce-agent/modelcontextprotocol");
27
- Object.defineProperty(exports, "ACCEPTED_TOOLS", { enumerable: true, get: function () { return modelcontextprotocol_2.ACCEPTED_TOOLS; } });
28
- Object.defineProperty(exports, "applyResolvedToolsToConfiguration", { enumerable: true, get: function () { return modelcontextprotocol_2.applyResolvedToolsToConfiguration; } });
29
- Object.defineProperty(exports, "resolveToolsForConfiguration", { enumerable: true, get: function () { return modelcontextprotocol_2.resolveToolsForConfiguration; } });
30
- function parseArgs(args) {
28
+ export { ACCEPTED_TOOLS, applyResolvedToolsToConfiguration, resolveToolsForConfiguration, } from '@commercetools/commerce-agent/modelcontextprotocol';
29
+ export function parseArgs(args) {
31
30
  const options = {};
32
31
  const env = {};
33
32
  args.forEach((arg) => {
@@ -63,6 +62,15 @@ function parseArgs(args) {
63
62
  else if (key == 'stateless') {
64
63
  env.stateless = value == 'true';
65
64
  }
65
+ else if (key == 'host') {
66
+ env.host = value;
67
+ }
68
+ else if (key == 'allowedHosts') {
69
+ env.allowedHosts = splitList(value);
70
+ }
71
+ else if (key == 'allowedOrigins') {
72
+ env.allowedOrigins = splitList(value);
73
+ }
66
74
  else if (key == 'port') {
67
75
  env.port = Number(value);
68
76
  }
@@ -112,8 +120,8 @@ function parseArgs(args) {
112
120
  if (tool == 'all' || tool == 'read_all') {
113
121
  return;
114
122
  }
115
- if (!modelcontextprotocol_1.ACCEPTED_TOOLS.includes(tool.trim())) {
116
- throw new Error(`Invalid tool: ${tool}. Accepted tools are: ${modelcontextprotocol_1.ACCEPTED_TOOLS.join(', ')}`);
123
+ if (!ACCEPTED_TOOLS.includes(tool.trim())) {
124
+ throw new Error(`Invalid tool: ${tool}. Accepted tools are: ${ACCEPTED_TOOLS.join(', ')}`);
117
125
  }
118
126
  });
119
127
  // Check for commercetools env vars
@@ -130,6 +138,11 @@ function parseArgs(args) {
130
138
  env.remote = env.remote || process.env.REMOTE == 'true';
131
139
  env.logging = env.logging || process.env.LOGGING == 'true';
132
140
  env.stateless = env.stateless || process.env.STATELESS == 'true';
141
+ env.host = env.host || process.env.HOST || DEFAULT_HOST;
142
+ env.allowedHosts =
143
+ env.allowedHosts || splitList(process.env.ALLOWED_HOSTS) || undefined;
144
+ env.allowedOrigins =
145
+ env.allowedOrigins || splitList(process.env.ALLOWED_ORIGINS) || undefined;
133
146
  env.port = env.port || Number(process.env.PORT);
134
147
  options.businessUnitKey =
135
148
  options.businessUnitKey || process.env.BUSINESS_UNIT_KEY;
@@ -160,7 +173,7 @@ function parseArgs(args) {
160
173
  }
161
174
  break;
162
175
  case 'auth_token':
163
- if (!env.accessToken) {
176
+ if (!env.remote && !env.accessToken) {
164
177
  throw new Error('Missing required access token when "authType" is "auth_token". Please make sure to provide the value for "accessToken" or via environment variable (ACCESS_TOKEN).');
165
178
  }
166
179
  break;
@@ -260,14 +273,92 @@ function createAuthConfig(env) {
260
273
  throw new Error(`Unsupported auth type: ${env.authType}`);
261
274
  }
262
275
  }
263
- function handleError(error) {
264
- console.error((0, colors_1.red)('\n🚨 Error initializing commercetools MCP server:\n'));
265
- console.error((0, colors_1.yellow)(` ${error.message}\n`));
276
+ function splitList(value) {
277
+ if (!value)
278
+ return undefined;
279
+ const entries = value
280
+ .split(',')
281
+ .map((entry) => entry.trim())
282
+ .filter(Boolean);
283
+ return entries.length > 0 ? entries : undefined;
266
284
  }
267
- async function main() {
268
- require('dotenv').config({
269
- quiet: true,
270
- });
285
+ /**
286
+ * Hostnames the server will answer for. Loopback always works; the interface
287
+ * it was told to bind is added so addressing the server by that address works
288
+ * without extra configuration, and `--allowedHosts` covers the rest (a domain
289
+ * in front of a proxy, a container hostname, ...).
290
+ */
291
+ function resolveAllowedHosts(env) {
292
+ const hosts = new Set([...LOOPBACK_HOSTNAMES, ...(env.allowedHosts ?? [])]);
293
+ const host = env.host;
294
+ if (host && !isWildcardHost(host)) {
295
+ hosts.add(host);
296
+ }
297
+ return [...hosts];
298
+ }
299
+ const LOOPBACK_HOSTS = ['127.0.0.1', 'localhost', '::1', '::ffff:127.0.0.1'];
300
+ /** Hosts that mean "listen on every interface" rather than a single address. */
301
+ const WILDCARD_HOSTS = ['0.0.0.0', '::', '::0', '*', ''];
302
+ function isLoopbackHost(host) {
303
+ return LOOPBACK_HOSTS.includes(host) || host.startsWith('127.');
304
+ }
305
+ function isWildcardHost(host) {
306
+ return WILDCARD_HOSTS.includes(host.trim());
307
+ }
308
+ /**
309
+ * Binding beyond loopback puts the server on the network, where anything that
310
+ * can reach the port can talk to it. That is a legitimate choice behind a
311
+ * gateway, but it should never happen without the operator noticing — and a
312
+ * wildcard bind deserves a louder warning than a single chosen interface,
313
+ * since it also covers interfaces the operator may not have had in mind.
314
+ */
315
+ function warnIfPubliclyBound(host) {
316
+ if (isLoopbackHost(host))
317
+ return;
318
+ const exposure = isWildcardHost(host)
319
+ ? `bound to ${host || '0.0.0.0'} — every network interface on this machine, including\n` +
320
+ ` any that is publicly routable`
321
+ : `bound to ${host}, so it is reachable from outside this machine`;
322
+ console.error(yellow(`\n\u26a0\ufe0f The MCP server is ${exposure}.\n` +
323
+ ` Callers still need a valid "Authorization: Bearer <token>" header, but make sure the port\n` +
324
+ ` is only exposed to networks you trust. Use --host=127.0.0.1 to keep it local.\n`));
325
+ }
326
+ /**
327
+ * Stateful mode is on its way out (DEVX-888). The 2026-07-28 spec removes
328
+ * protocol sessions and `Mcp-Session-Id` entirely, and the v2 handler answers
329
+ * the session operations (GET/DELETE) with 405.
330
+ *
331
+ * An audit found nothing kept across calls but the transport instance and the
332
+ * token fingerprint that binds a session to its opener, so per-request auth
333
+ * already covers what sessions were carrying. Nothing changes yet — this only
334
+ * warns, because remote currently defaults to stateful and silently flipping
335
+ * that would break clients mid-release.
336
+ */
337
+ function warnIfStateful(stateless) {
338
+ if (stateless)
339
+ return;
340
+ console.error(yellow(`\n\u26a0\ufe0f Stateful session mode is deprecated and will be removed.\n` +
341
+ ` The 2026-07-28 MCP spec has no protocol sessions, so this mode cannot carry\n` +
342
+ ` forward. Nothing is stored across calls today beyond the session's own auth\n` +
343
+ ` binding, which per-request credentials already provide.\n` +
344
+ ` Pass --stateless=true to adopt the future default now.\n`));
345
+ }
346
+ /** True once the server actually holds an address. */
347
+ function isBound(server) {
348
+ const address = server?.address;
349
+ return typeof address === 'function' ? address.call(server) !== null : true;
350
+ }
351
+ function onServerError(server, listener) {
352
+ const on = server?.on;
353
+ if (typeof on === 'function')
354
+ on.call(server, 'error', listener);
355
+ }
356
+ export function handleError(error) {
357
+ console.error(red('\n🚨 Error initializing commercetools MCP server:\n'));
358
+ console.error(yellow(` ${error.message}\n`));
359
+ }
360
+ export async function main() {
361
+ dotenv.config({ quiet: true });
271
362
  const { options, env } = parseArgs(process.argv.slice(2));
272
363
  // Create the CommercetoolsCommerceAgent instance
273
364
  const selectedTools = options.tools;
@@ -283,43 +374,63 @@ async function main() {
283
374
  businessUnitKey: options.businessUnitKey,
284
375
  logging: options.logging,
285
376
  fieldFiltering: options.fieldFiltering
286
- ? new processors_1.FieldFilteringHandler(options.fieldFiltering)
377
+ ? new FieldFilteringHandler(options.fieldFiltering)
287
378
  : undefined,
288
379
  },
289
380
  };
290
- const resolution = (0, modelcontextprotocol_1.resolveToolsForConfiguration)(selectedTools, Boolean(options.isAdmin));
291
- (0, modelcontextprotocol_1.applyResolvedToolsToConfiguration)(configuration, resolution);
381
+ const resolution = resolveToolsForConfiguration(selectedTools, Boolean(options.isAdmin));
382
+ applyResolvedToolsToConfiguration(configuration, resolution);
292
383
  const authConfig = createAuthConfig(env);
293
384
  // eslint-disable-next-line require-await
294
385
  async function getServer() {
295
- return modelcontextprotocol_1.CommercetoolsCommerceAgent.create({
386
+ return CommercetoolsCommerceAgent.create({
296
387
  authConfig,
297
388
  configuration,
298
389
  });
299
390
  }
300
391
  if (env.remote) {
301
- const streamServer = new modelcontextprotocol_1.CommercetoolsCommerceAgentStreamable({
392
+ warnIfStateful(Boolean(env.stateless));
393
+ const streamServer = new CommercetoolsCommerceAgentStreamable({
302
394
  authConfig,
303
395
  configuration,
304
396
  stateless: env.stateless,
305
- streamableHttpOptions: {
306
- sessionIdGenerator: undefined,
307
- },
397
+ allowedHosts: resolveAllowedHosts(env),
398
+ allowedOrigins: env.allowedOrigins,
308
399
  });
309
400
  const port = env.port || 8080;
310
- streamServer.listen(port, function () {
311
- console.error(`Stream server listening on`, port);
401
+ // `*` and an empty value mean "every interface"; the OS needs 0.0.0.0.
402
+ const host = normalizeBindHost(env.host);
403
+ warnIfPubliclyBound(host);
404
+ /**
405
+ * Kept in a holder so the listen callback can read the server without a
406
+ * binding that is still in its temporal dead zone should an app
407
+ * implementation invoke the callback synchronously.
408
+ */
409
+ const bound = {};
410
+ bound.server = streamServer.listen(port, host, function () {
411
+ /**
412
+ * Express runs this callback even when the bind failed (an unresolvable
413
+ * host leaves `address()` null), so confirm before claiming success —
414
+ * the error handler below reports the failure instead.
415
+ */
416
+ if (!isBound(bound.server))
417
+ return;
418
+ console.error(`Stream server listening on ${host}:${port}`);
419
+ });
420
+ /**
421
+ * A bind failure — port already taken, address not available, a host that
422
+ * does not resolve — arrives as an 'error' event. Left unhandled it kills
423
+ * the process with no explanation of what went wrong.
424
+ */
425
+ onServerError(bound.server, (error) => {
426
+ handleError(new Error(`Unable to bind ${host}:${port}${error.code ? ` (${error.code})` : ''}. ${error.message}`));
427
+ process.exitCode = 1;
312
428
  });
313
429
  }
314
430
  else {
315
431
  const server = await getServer();
316
- const transport = new stdio_js_1.StdioServerTransport();
432
+ const transport = new StdioServerTransport();
317
433
  await server.connect(transport);
318
434
  console.error('MCP server is running...');
319
435
  }
320
436
  }
321
- if (require.main === module) {
322
- main().catch((error) => {
323
- handleError(error);
324
- });
325
- }
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "@commercetools/commerce-mcp",
3
- "version": "4.0.0",
3
+ "version": "4.2.0",
4
+ "type": "module",
4
5
  "mcpName": "io.github.commercetools/commerce-mcp",
5
6
  "homepage": "https://github.com/commercetools/commerce-mcp",
6
7
  "description": "A command line tool for setting up commercetools MCP server",
7
- "bin": "dist/index.js",
8
+ "bin": "dist/cli.js",
8
9
  "files": [
10
+ "dist/cli.js",
9
11
  "dist/index.js",
10
12
  "LICENSE",
11
13
  "README.md",
@@ -13,18 +15,18 @@
13
15
  "package.json"
14
16
  ],
15
17
  "engines": {
16
- "node": ">=18"
18
+ "node": ">=20"
17
19
  },
18
20
  "publishConfig": {
19
21
  "access": "public"
20
22
  },
21
23
  "dependencies": {
22
- "@modelcontextprotocol/sdk": "^1.26.0",
24
+ "@modelcontextprotocol/server": "^2",
23
25
  "colors": "^1.4.0",
24
26
  "dotenv": "^17.2.1",
25
27
  "mcp-remote": "^0.1.18",
26
- "@commercetools/commerce-agent": "4.0.0",
27
- "@commercetools/processors": "0.0.0"
28
+ "@commercetools/commerce-agent": "4.2.0",
29
+ "@commercetools/processors": "0.1.0"
28
30
  },
29
31
  "keywords": [
30
32
  "mcp",
@@ -59,8 +61,8 @@
59
61
  "prebuild": "pnpm --filter @commercetools/processors run build && pnpm --filter @commercetools/commerce-agent run build",
60
62
  "dev": "concurrently -n BUILD,SERVER -c magenta,cyan \"pnpm dev:build\" \"pnpm dev:run\"",
61
63
  "dev:build": "tsup --env.NODE_ENV dev",
62
- "dev:run": "nodemon dist/index.js --watch",
63
- "build": "tsc && node -e \"require('fs').chmodSync('dist/index.js', '755')\"",
64
+ "dev:run": "nodemon --watch dist dist/cli.js",
65
+ "build": "tsc && node --input-type=module -e \"import {chmodSync} from 'node:fs'; chmodSync('dist/cli.js', '755')\"",
64
66
  "clean": "rm -rf dist",
65
67
  "lint": "eslint \"./**/*.ts*\"",
66
68
  "prettier": "prettier './**/*.{js,ts,md,html,css}' --write",