@commercetools/commerce-mcp 4.0.0 → 4.1.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 (2) hide show
  1. package/dist/index.js +117 -9
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,5 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
3
6
  Object.defineProperty(exports, "__esModule", { value: true });
4
7
  exports.resolveToolsForConfiguration = exports.applyResolvedToolsToConfiguration = exports.ACCEPTED_TOOLS = void 0;
5
8
  exports.parseArgs = parseArgs;
@@ -7,7 +10,9 @@ exports.main = main;
7
10
  const modelcontextprotocol_1 = require("@commercetools/commerce-agent/modelcontextprotocol");
8
11
  const processors_1 = require("@commercetools/processors");
9
12
  const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
10
- const colors_1 = require("colors");
13
+ const colors_1 = __importDefault(require("colors"));
14
+ const red = colors_1.default.red;
15
+ const yellow = colors_1.default.yellow;
11
16
  const HIDDEN_ARGS = ['customerId', 'isAdmin', 'storeKey', 'businessUnitKey'];
12
17
  const PUBLIC_ARGS = [
13
18
  'tools',
@@ -21,6 +26,10 @@ const PUBLIC_ARGS = [
21
26
  'dynamicToolLoadingThreshold',
22
27
  'toolOutputFormat',
23
28
  'logging',
29
+ 'host',
30
+ 'port',
31
+ 'allowedHosts',
32
+ 'allowedOrigins',
24
33
  ];
25
34
  const ACCEPTED_ARGS = [...PUBLIC_ARGS, ...HIDDEN_ARGS];
26
35
  var modelcontextprotocol_2 = require("@commercetools/commerce-agent/modelcontextprotocol");
@@ -63,6 +72,15 @@ function parseArgs(args) {
63
72
  else if (key == 'stateless') {
64
73
  env.stateless = value == 'true';
65
74
  }
75
+ else if (key == 'host') {
76
+ env.host = value;
77
+ }
78
+ else if (key == 'allowedHosts') {
79
+ env.allowedHosts = splitList(value);
80
+ }
81
+ else if (key == 'allowedOrigins') {
82
+ env.allowedOrigins = splitList(value);
83
+ }
66
84
  else if (key == 'port') {
67
85
  env.port = Number(value);
68
86
  }
@@ -130,6 +148,11 @@ function parseArgs(args) {
130
148
  env.remote = env.remote || process.env.REMOTE == 'true';
131
149
  env.logging = env.logging || process.env.LOGGING == 'true';
132
150
  env.stateless = env.stateless || process.env.STATELESS == 'true';
151
+ env.host = env.host || process.env.HOST || modelcontextprotocol_1.DEFAULT_HOST;
152
+ env.allowedHosts =
153
+ env.allowedHosts || splitList(process.env.ALLOWED_HOSTS) || undefined;
154
+ env.allowedOrigins =
155
+ env.allowedOrigins || splitList(process.env.ALLOWED_ORIGINS) || undefined;
133
156
  env.port = env.port || Number(process.env.PORT);
134
157
  options.businessUnitKey =
135
158
  options.businessUnitKey || process.env.BUSINESS_UNIT_KEY;
@@ -160,7 +183,7 @@ function parseArgs(args) {
160
183
  }
161
184
  break;
162
185
  case 'auth_token':
163
- if (!env.accessToken) {
186
+ if (!env.remote && !env.accessToken) {
164
187
  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
188
  }
166
189
  break;
@@ -260,14 +283,72 @@ function createAuthConfig(env) {
260
283
  throw new Error(`Unsupported auth type: ${env.authType}`);
261
284
  }
262
285
  }
286
+ function splitList(value) {
287
+ if (!value)
288
+ return undefined;
289
+ const entries = value
290
+ .split(',')
291
+ .map((entry) => entry.trim())
292
+ .filter(Boolean);
293
+ return entries.length > 0 ? entries : undefined;
294
+ }
295
+ /**
296
+ * Hostnames the server will answer for. Loopback always works; the interface
297
+ * it was told to bind is added so addressing the server by that address works
298
+ * without extra configuration, and `--allowedHosts` covers the rest (a domain
299
+ * in front of a proxy, a container hostname, ...).
300
+ */
301
+ function resolveAllowedHosts(env) {
302
+ const hosts = new Set([...modelcontextprotocol_1.LOOPBACK_HOSTNAMES, ...(env.allowedHosts ?? [])]);
303
+ const host = env.host;
304
+ if (host && !isWildcardHost(host)) {
305
+ hosts.add(host);
306
+ }
307
+ return [...hosts];
308
+ }
309
+ const LOOPBACK_HOSTS = ['127.0.0.1', 'localhost', '::1', '::ffff:127.0.0.1'];
310
+ /** Hosts that mean "listen on every interface" rather than a single address. */
311
+ const WILDCARD_HOSTS = ['0.0.0.0', '::', '::0', '*', ''];
312
+ function isLoopbackHost(host) {
313
+ return LOOPBACK_HOSTS.includes(host) || host.startsWith('127.');
314
+ }
315
+ function isWildcardHost(host) {
316
+ return WILDCARD_HOSTS.includes(host.trim());
317
+ }
318
+ /**
319
+ * Binding beyond loopback puts the server on the network, where anything that
320
+ * can reach the port can talk to it. That is a legitimate choice behind a
321
+ * gateway, but it should never happen without the operator noticing — and a
322
+ * wildcard bind deserves a louder warning than a single chosen interface,
323
+ * since it also covers interfaces the operator may not have had in mind.
324
+ */
325
+ function warnIfPubliclyBound(host) {
326
+ if (isLoopbackHost(host))
327
+ return;
328
+ const exposure = isWildcardHost(host)
329
+ ? `bound to ${host || '0.0.0.0'} — every network interface on this machine, including\n` +
330
+ ` any that is publicly routable`
331
+ : `bound to ${host}, so it is reachable from outside this machine`;
332
+ console.error(yellow(`\n\u26a0\ufe0f The MCP server is ${exposure}.\n` +
333
+ ` Callers still need a valid "Authorization: Bearer <token>" header, but make sure the port\n` +
334
+ ` is only exposed to networks you trust. Use --host=127.0.0.1 to keep it local.\n`));
335
+ }
336
+ /** True once the server actually holds an address. */
337
+ function isBound(server) {
338
+ const address = server?.address;
339
+ return typeof address === 'function' ? address.call(server) !== null : true;
340
+ }
341
+ function onServerError(server, listener) {
342
+ const on = server?.on;
343
+ if (typeof on === 'function')
344
+ on.call(server, 'error', listener);
345
+ }
263
346
  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`));
347
+ console.error(red('\n🚨 Error initializing commercetools MCP server:\n'));
348
+ console.error(yellow(` ${error.message}\n`));
266
349
  }
267
350
  async function main() {
268
- require('dotenv').config({
269
- quiet: true,
270
- });
351
+ require('dotenv').config({ quiet: true });
271
352
  const { options, env } = parseArgs(process.argv.slice(2));
272
353
  // Create the CommercetoolsCommerceAgent instance
273
354
  const selectedTools = options.tools;
@@ -302,13 +383,40 @@ async function main() {
302
383
  authConfig,
303
384
  configuration,
304
385
  stateless: env.stateless,
386
+ allowedHosts: resolveAllowedHosts(env),
387
+ allowedOrigins: env.allowedOrigins,
305
388
  streamableHttpOptions: {
306
389
  sessionIdGenerator: undefined,
307
390
  },
308
391
  });
309
392
  const port = env.port || 8080;
310
- streamServer.listen(port, function () {
311
- console.error(`Stream server listening on`, port);
393
+ // `*` and an empty value mean "every interface"; the OS needs 0.0.0.0.
394
+ const host = (0, modelcontextprotocol_1.normalizeBindHost)(env.host);
395
+ warnIfPubliclyBound(host);
396
+ /**
397
+ * Kept in a holder so the listen callback can read the server without a
398
+ * binding that is still in its temporal dead zone should an app
399
+ * implementation invoke the callback synchronously.
400
+ */
401
+ const bound = {};
402
+ bound.server = streamServer.listen(port, host, function () {
403
+ /**
404
+ * Express runs this callback even when the bind failed (an unresolvable
405
+ * host leaves `address()` null), so confirm before claiming success —
406
+ * the error handler below reports the failure instead.
407
+ */
408
+ if (!isBound(bound.server))
409
+ return;
410
+ console.error(`Stream server listening on ${host}:${port}`);
411
+ });
412
+ /**
413
+ * A bind failure — port already taken, address not available, a host that
414
+ * does not resolve — arrives as an 'error' event. Left unhandled it kills
415
+ * the process with no explanation of what went wrong.
416
+ */
417
+ onServerError(bound.server, (error) => {
418
+ handleError(new Error(`Unable to bind ${host}:${port}${error.code ? ` (${error.code})` : ''}. ${error.message}`));
419
+ process.exitCode = 1;
312
420
  });
313
421
  }
314
422
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commercetools/commerce-mcp",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "mcpName": "io.github.commercetools/commerce-mcp",
5
5
  "homepage": "https://github.com/commercetools/commerce-mcp",
6
6
  "description": "A command line tool for setting up commercetools MCP server",
@@ -23,7 +23,7 @@
23
23
  "colors": "^1.4.0",
24
24
  "dotenv": "^17.2.1",
25
25
  "mcp-remote": "^0.1.18",
26
- "@commercetools/commerce-agent": "4.0.0",
26
+ "@commercetools/commerce-agent": "4.1.0",
27
27
  "@commercetools/processors": "0.0.0"
28
28
  },
29
29
  "keywords": [