@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 +15 -7
- package/dist/cli.js +11 -0
- package/dist/index.js +151 -40
- package/package.json +10 -8
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
|
-
|
|
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}'
|
|
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
|
|
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": "
|
|
58
|
+
"command": "node",
|
|
50
59
|
"args": [
|
|
51
|
-
"
|
|
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/
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
const
|
|
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
|
-
|
|
27
|
-
|
|
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 (!
|
|
116
|
-
throw new Error(`Invalid tool: ${tool}. Accepted tools are: ${
|
|
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
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
|
377
|
+
? new FieldFilteringHandler(options.fieldFiltering)
|
|
287
378
|
: undefined,
|
|
288
379
|
},
|
|
289
380
|
};
|
|
290
|
-
const resolution =
|
|
291
|
-
|
|
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
|
|
386
|
+
return CommercetoolsCommerceAgent.create({
|
|
296
387
|
authConfig,
|
|
297
388
|
configuration,
|
|
298
389
|
});
|
|
299
390
|
}
|
|
300
391
|
if (env.remote) {
|
|
301
|
-
|
|
392
|
+
warnIfStateful(Boolean(env.stateless));
|
|
393
|
+
const streamServer = new CommercetoolsCommerceAgentStreamable({
|
|
302
394
|
authConfig,
|
|
303
395
|
configuration,
|
|
304
396
|
stateless: env.stateless,
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
},
|
|
397
|
+
allowedHosts: resolveAllowedHosts(env),
|
|
398
|
+
allowedOrigins: env.allowedOrigins,
|
|
308
399
|
});
|
|
309
400
|
const port = env.port || 8080;
|
|
310
|
-
|
|
311
|
-
|
|
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
|
|
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.
|
|
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/
|
|
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
|
+
"node": ">=20"
|
|
17
19
|
},
|
|
18
20
|
"publishConfig": {
|
|
19
21
|
"access": "public"
|
|
20
22
|
},
|
|
21
23
|
"dependencies": {
|
|
22
|
-
"@modelcontextprotocol/
|
|
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.
|
|
27
|
-
"@commercetools/processors": "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
|
|
63
|
-
"build": "tsc && node -e \"
|
|
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",
|