@unchainedshop/api 4.8.21 → 4.8.23
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 +6 -4
- package/lib/chat/utils.d.ts +8 -0
- package/lib/chat/utils.js +54 -2
- package/lib/express/chatHandler.js +52 -61
- package/lib/express/createMCPMiddleware.js +18 -99
- package/lib/fastify/chatHandler.js +48 -55
- package/lib/fastify/mcpHandler.js +24 -94
- package/lib/locale-context.d.ts +2 -2
- package/lib/locale-context.js +3 -6
- package/lib/mcp/handleMcpHttpRequest.d.ts +2 -0
- package/lib/mcp/handleMcpHttpRequest.js +83 -0
- package/lib/mcp/index.d.ts +1 -1
- package/lib/mcp/nodeHttpBridge.d.ts +3 -0
- package/lib/mcp/nodeHttpBridge.js +64 -0
- package/lib/mcp/resources/localization.d.ts +5 -1
- package/lib/mcp/resources/localization.js +93 -69
- package/lib/mcp/tools/assortment/index.d.ts +1 -1
- package/lib/mcp/tools/assortment/index.js +4 -1
- package/lib/mcp/tools/assortment/schemas.d.ts +1 -3
- package/lib/mcp/tools/filter/index.d.ts +1 -1
- package/lib/mcp/tools/filter/index.js +4 -1
- package/lib/mcp/tools/filter/schemas.d.ts +1 -3
- package/lib/mcp/tools/localization/index.d.ts +1 -1
- package/lib/mcp/tools/localization/index.js +4 -1
- package/lib/mcp/tools/localization/schemas.d.ts +1 -3
- package/lib/mcp/tools/order/index.d.ts +1 -1
- package/lib/mcp/tools/order/index.js +4 -1
- package/lib/mcp/tools/order/schemas.d.ts +1 -3
- package/lib/mcp/tools/product/index.d.ts +1 -1
- package/lib/mcp/tools/product/index.js +4 -1
- package/lib/mcp/tools/product/schemas.d.ts +1 -3
- package/lib/mcp/tools/provider/index.d.ts +1 -1
- package/lib/mcp/tools/provider/index.js +4 -1
- package/lib/mcp/tools/provider/schemas.d.ts +1 -3
- package/lib/mcp/tools/quotation/index.d.ts +1 -1
- package/lib/mcp/tools/quotation/index.js +4 -1
- package/lib/mcp/tools/quotation/schemas.d.ts +1 -3
- package/lib/mcp/tools/system/index.d.ts +1 -1
- package/lib/mcp/tools/system/index.js +4 -1
- package/lib/mcp/tools/system/schemas.d.ts +1 -3
- package/lib/mcp/tools/users/index.d.ts +1 -1
- package/lib/mcp/tools/users/index.js +4 -1
- package/lib/mcp/tools/users/schemas.d.ts +1 -3
- package/lib/mcp/utils/sharedSchemas.d.ts +3 -5
- package/lib/mcp/utils/sharedSchemas.js +18 -2
- package/lib/resolvers/type/filter/loaded-filter-types.d.ts +1 -1
- package/lib/resolvers/type/filter/loaded-filter-types.js +1 -1
- package/lib/resolvers/type/index.d.ts +1 -1
- package/lib/schema/types/filter.js +2 -2
- package/lib/utils/optionalPeerError.d.ts +1 -0
- package/lib/utils/optionalPeerError.js +6 -0
- package/package.json +32 -8
|
@@ -1,102 +1,32 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
1
2
|
import { createLogger } from '@unchainedshop/logger';
|
|
3
|
+
import handleMcpHttpRequest from "../mcp/handleMcpHttpRequest.js";
|
|
4
|
+
import { toWebRequest } from "../mcp/nodeHttpBridge.js";
|
|
2
5
|
const logger = createLogger('unchained:api:mcp');
|
|
3
|
-
let StreamableHTTPServerTransport;
|
|
4
|
-
let isInitializeRequest;
|
|
5
|
-
let McpServer;
|
|
6
|
-
try {
|
|
7
|
-
const mcpSDKServerLibrary = await import('@modelcontextprotocol/sdk/server/streamableHttp.js');
|
|
8
|
-
const mcpSDKClient = await import('@modelcontextprotocol/sdk/types.js');
|
|
9
|
-
const mcpSDKServer = await import('@modelcontextprotocol/sdk/server/mcp.js');
|
|
10
|
-
StreamableHTTPServerTransport = mcpSDKServerLibrary.StreamableHTTPServerTransport;
|
|
11
|
-
isInitializeRequest = mcpSDKClient.isInitializeRequest;
|
|
12
|
-
McpServer = mcpSDKServer.McpServer;
|
|
13
|
-
}
|
|
14
|
-
catch {
|
|
15
|
-
logger.warn(`optional peer npm package '@modelcontextprotocol/sdk' not installed, mcp will not work`);
|
|
16
|
-
}
|
|
17
|
-
const transports = {};
|
|
18
6
|
const mcpHandler = async (req, res) => {
|
|
19
|
-
const user = req.unchainedContext.user;
|
|
20
|
-
if (!user) {
|
|
21
|
-
res.status(401);
|
|
22
|
-
res.header('WWW-Authenticate', `Bearer realm="Unchained MCP", error="invalid_token", resource="${process.env.ROOT_URL || 'http://localhost:4010'}",`);
|
|
23
|
-
return res.send(JSON.stringify({
|
|
24
|
-
error: 'invalid_token',
|
|
25
|
-
resource_metadata: `${process.env.ROOT_URL || 'http://localhost:4010'}/.well-known/oauth-protected-resource`,
|
|
26
|
-
}));
|
|
27
|
-
}
|
|
28
|
-
if (!(user.roles || []).includes('admin')) {
|
|
29
|
-
res.status(403);
|
|
30
|
-
return res.send(JSON.stringify({ error: 'forbidden', message: 'MCP requires admin privileges' }));
|
|
31
|
-
}
|
|
32
|
-
const currentUserId = user._id;
|
|
33
7
|
try {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
Object.assign(transports[sessionId].context, req.unchainedContext);
|
|
49
|
-
transport = transports[sessionId].transport;
|
|
50
|
-
}
|
|
51
|
-
else if (!sessionId && isInitializeRequest(req.body)) {
|
|
52
|
-
const contextHolder = req.unchainedContext;
|
|
53
|
-
transport = new StreamableHTTPServerTransport({
|
|
54
|
-
sessionIdGenerator: () => crypto.randomUUID(),
|
|
55
|
-
onsessioninitialized: (sessionId) => {
|
|
56
|
-
transports[sessionId] = { transport, userId: currentUserId, context: contextHolder };
|
|
57
|
-
},
|
|
58
|
-
});
|
|
59
|
-
transport.onclose = () => {
|
|
60
|
-
if (transport.sessionId) {
|
|
61
|
-
delete transports[transport.sessionId];
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
const roles = user.roles || [];
|
|
65
|
-
const { default: initMCPServer } = await import("../mcp/index.js");
|
|
66
|
-
const server = initMCPServer(new McpServer({
|
|
67
|
-
name: 'Unchained MCP Server',
|
|
68
|
-
version: '1.0.0',
|
|
69
|
-
}), contextHolder, roles);
|
|
70
|
-
await server.connect(transport);
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
return res.status(400).send(JSON.stringify({
|
|
74
|
-
jsonrpc: '2.0',
|
|
75
|
-
error: {
|
|
76
|
-
code: -32000,
|
|
77
|
-
message: 'Bad Request: No valid session ID provided',
|
|
78
|
-
},
|
|
79
|
-
id: null,
|
|
80
|
-
}));
|
|
81
|
-
}
|
|
82
|
-
await transport.handleRequest(req.raw, res.raw, req.body);
|
|
83
|
-
return res;
|
|
84
|
-
}
|
|
85
|
-
else if (req.method === 'GET' || req.method === 'DELETE') {
|
|
86
|
-
const sessionId = req.headers['mcp-session-id'];
|
|
87
|
-
if (!sessionId || !transports[sessionId] || transports[sessionId].userId !== currentUserId) {
|
|
88
|
-
return res.status(400).send('Invalid or missing session ID');
|
|
89
|
-
}
|
|
90
|
-
Object.assign(transports[sessionId].context, req.unchainedContext);
|
|
91
|
-
const transport = transports[sessionId].transport;
|
|
92
|
-
await transport.handleRequest(req.raw, res.raw);
|
|
93
|
-
return res;
|
|
94
|
-
}
|
|
95
|
-
return res.status(405).send('Method Not Allowed');
|
|
8
|
+
const bodyText = req.method === 'POST' && req.body !== undefined ? JSON.stringify(req.body) : undefined;
|
|
9
|
+
const response = await handleMcpHttpRequest(req.unchainedContext, toWebRequest(req.raw, res.raw, bodyText), req.method === 'POST' ? req.body : undefined);
|
|
10
|
+
res.status(response.status);
|
|
11
|
+
response.headers.forEach((value, key) => {
|
|
12
|
+
if (key === 'set-cookie')
|
|
13
|
+
return;
|
|
14
|
+
res.header(key, value);
|
|
15
|
+
});
|
|
16
|
+
const setCookie = response.headers.getSetCookie();
|
|
17
|
+
if (setCookie.length)
|
|
18
|
+
res.header('set-cookie', setCookie);
|
|
19
|
+
if (!response.body)
|
|
20
|
+
return res.send();
|
|
21
|
+
return res.send(Readable.fromWeb(response.body));
|
|
96
22
|
}
|
|
97
|
-
catch (
|
|
98
|
-
logger.error(
|
|
99
|
-
|
|
23
|
+
catch (error) {
|
|
24
|
+
logger.error(error);
|
|
25
|
+
if (!res.sent) {
|
|
26
|
+
return res.status(500).send({ error: 'Internal Server Error' });
|
|
27
|
+
}
|
|
28
|
+
res.raw.destroy();
|
|
29
|
+
return res;
|
|
100
30
|
}
|
|
101
31
|
};
|
|
102
32
|
export default mcpHandler;
|
package/lib/locale-context.d.ts
CHANGED
|
@@ -6,10 +6,10 @@ export interface UnchainedLocaleContext {
|
|
|
6
6
|
currencyCode: string;
|
|
7
7
|
}
|
|
8
8
|
export type GetHeaderFn = (key: string) => string | string[];
|
|
9
|
-
export declare const resolveDefaultContext: ({
|
|
9
|
+
export declare const resolveDefaultContext: import("@unchainedshop/utils/lib/memoize-with-ttl.js").MemoizedWithTTL<[{
|
|
10
10
|
acceptLang: any;
|
|
11
11
|
acceptCountry: any;
|
|
12
|
-
}, unchainedAPI: UnchainedCore
|
|
12
|
+
}, unchainedAPI: UnchainedCore], UnchainedLocaleContext>;
|
|
13
13
|
export declare const getLocaleContext: ({ getHeader, }: {
|
|
14
14
|
getHeader: UnchainedHTTPServerContext["getHeader"];
|
|
15
15
|
}, unchainedAPI: UnchainedCore) => Promise<UnchainedLocaleContext>;
|
package/lib/locale-context.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import { resolveBestSupported, resolveBestCurrency } from '@unchainedshop/utils';
|
|
2
|
-
import pMemoize from 'p-memoize';
|
|
3
|
-
import ExpiryMap from 'expiry-map';
|
|
1
|
+
import { resolveBestSupported, resolveBestCurrency, memoizeWithTTL } from '@unchainedshop/utils';
|
|
4
2
|
import { createLogger } from '@unchainedshop/logger';
|
|
5
3
|
const logger = createLogger('unchained:api');
|
|
6
|
-
const memoizeCache = new ExpiryMap(process.env.NODE_ENV === 'production' ? 1000 * 60 : 1);
|
|
7
4
|
const uncachedResolveDefaultContext = async ({ acceptLang, acceptCountry }, unchainedAPI) => {
|
|
8
5
|
const languages = await unchainedAPI.modules.languages.findLanguages({ includeInactive: false }, { projection: { isoCode: 1, isActive: 1 } });
|
|
9
6
|
const countries = await unchainedAPI.modules.countries.findCountries({ includeInactive: false }, { projection: { isoCode: 1, isActive: 1 } });
|
|
@@ -19,8 +16,8 @@ const uncachedResolveDefaultContext = async ({ acceptLang, acceptCountry }, unch
|
|
|
19
16
|
};
|
|
20
17
|
return newContext;
|
|
21
18
|
};
|
|
22
|
-
export const resolveDefaultContext =
|
|
23
|
-
|
|
19
|
+
export const resolveDefaultContext = memoizeWithTTL(uncachedResolveDefaultContext, {
|
|
20
|
+
ttl: process.env.NODE_ENV === 'production' ? 1000 * 60 : 1,
|
|
24
21
|
cacheKey: (args) => {
|
|
25
22
|
const [{ acceptLang, acceptCountry }] = args;
|
|
26
23
|
return `${acceptLang}-${acceptCountry}`;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { createLogger } from '@unchainedshop/logger';
|
|
2
|
+
import createMcpServer from "./index.js";
|
|
3
|
+
import { isPeerNotInstalledError } from "../utils/optionalPeerError.js";
|
|
4
|
+
const logger = createLogger('unchained:api:mcp');
|
|
5
|
+
let createMcpHandler;
|
|
6
|
+
let McpServer;
|
|
7
|
+
let hostHeaderValidationResponse;
|
|
8
|
+
let originValidationResponse;
|
|
9
|
+
let localhostAllowedHostnames;
|
|
10
|
+
let localhostAllowedOrigins;
|
|
11
|
+
try {
|
|
12
|
+
const mcpSDKServer = await import('@modelcontextprotocol/server');
|
|
13
|
+
createMcpHandler = mcpSDKServer.createMcpHandler;
|
|
14
|
+
McpServer = mcpSDKServer.McpServer;
|
|
15
|
+
hostHeaderValidationResponse = mcpSDKServer.hostHeaderValidationResponse;
|
|
16
|
+
originValidationResponse = mcpSDKServer.originValidationResponse;
|
|
17
|
+
localhostAllowedHostnames = mcpSDKServer.localhostAllowedHostnames;
|
|
18
|
+
localhostAllowedOrigins = mcpSDKServer.localhostAllowedOrigins;
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (isPeerNotInstalledError('@modelcontextprotocol/server', error)) {
|
|
22
|
+
logger.warn(`optional peer npm package '@modelcontextprotocol/server' not installed, mcp will not work`);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
logger.error(`failed to load '@modelcontextprotocol/server'`, error);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
let handler;
|
|
29
|
+
const getHandler = () => {
|
|
30
|
+
handler ??= createMcpHandler(async ({ authInfo }) => {
|
|
31
|
+
const { context, roles } = authInfo.extra;
|
|
32
|
+
return createMcpServer(new McpServer({ name: 'Unchained MCP Server', version: '1.0.0' }), context, roles);
|
|
33
|
+
});
|
|
34
|
+
return handler;
|
|
35
|
+
};
|
|
36
|
+
const resourceUrl = () => process.env.ROOT_URL || 'http://localhost:4010';
|
|
37
|
+
const configuredHostname = () => {
|
|
38
|
+
try {
|
|
39
|
+
return new URL(resourceUrl()).hostname;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return 'localhost';
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const allowedHostnames = () => [...new Set([...localhostAllowedHostnames(), configuredHostname()])];
|
|
46
|
+
const allowedOrigins = () => [...new Set([...localhostAllowedOrigins(), configuredHostname()])];
|
|
47
|
+
export default async function handleMcpHttpRequest(context, request, parsedBody) {
|
|
48
|
+
const user = context?.user;
|
|
49
|
+
if (!user) {
|
|
50
|
+
return Response.json({
|
|
51
|
+
error: 'invalid_token',
|
|
52
|
+
resource_metadata: `${resourceUrl()}/.well-known/oauth-protected-resource`,
|
|
53
|
+
}, {
|
|
54
|
+
status: 401,
|
|
55
|
+
headers: {
|
|
56
|
+
'WWW-Authenticate': `Bearer realm="Unchained MCP", error="invalid_token", resource="${resourceUrl()}",`,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
const roles = user.roles || [];
|
|
61
|
+
if (!roles.includes('admin')) {
|
|
62
|
+
return Response.json({ error: 'forbidden', message: 'MCP requires admin privileges' }, { status: 403 });
|
|
63
|
+
}
|
|
64
|
+
if (!createMcpHandler) {
|
|
65
|
+
return Response.json({
|
|
66
|
+
error: 'unavailable',
|
|
67
|
+
message: `MCP is enabled but the optional peer npm package '@modelcontextprotocol/server' is not installed`,
|
|
68
|
+
}, { status: 503 });
|
|
69
|
+
}
|
|
70
|
+
const validationFailure = hostHeaderValidationResponse(request, allowedHostnames()) ??
|
|
71
|
+
originValidationResponse(request, allowedOrigins());
|
|
72
|
+
if (validationFailure)
|
|
73
|
+
return validationFailure;
|
|
74
|
+
return getHandler().fetch(request, {
|
|
75
|
+
authInfo: {
|
|
76
|
+
token: 'unchained-context',
|
|
77
|
+
clientId: user._id,
|
|
78
|
+
scopes: roles,
|
|
79
|
+
extra: { context, roles },
|
|
80
|
+
},
|
|
81
|
+
parsedBody,
|
|
82
|
+
});
|
|
83
|
+
}
|
package/lib/mcp/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type { McpServer as McpServerType } from '@modelcontextprotocol/
|
|
1
|
+
import type { McpServer as McpServerType } from '@modelcontextprotocol/server';
|
|
2
2
|
import type { Context } from '../context.ts';
|
|
3
3
|
export default function createMcpServer(server: McpServerType, context: Context, roles: any): McpServerType;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Readable, pipeline } from 'node:stream';
|
|
2
|
+
export function toWebRequest(req, res, bodyText) {
|
|
3
|
+
const controller = new AbortController();
|
|
4
|
+
res.once('close', () => {
|
|
5
|
+
if (!res.writableFinished)
|
|
6
|
+
controller.abort();
|
|
7
|
+
});
|
|
8
|
+
const headers = new Headers();
|
|
9
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
10
|
+
if (Array.isArray(value)) {
|
|
11
|
+
for (const entry of value)
|
|
12
|
+
headers.append(key, entry);
|
|
13
|
+
}
|
|
14
|
+
else if (value != null) {
|
|
15
|
+
headers.set(key, value);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (bodyText !== undefined)
|
|
19
|
+
headers.delete('content-length');
|
|
20
|
+
let url;
|
|
21
|
+
try {
|
|
22
|
+
url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
try {
|
|
26
|
+
url = new URL(req.url || '/', 'http://localhost');
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
url = new URL('http://localhost/');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return new Request(url, {
|
|
33
|
+
method: req.method,
|
|
34
|
+
headers,
|
|
35
|
+
body: bodyText,
|
|
36
|
+
signal: controller.signal,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
export async function sendWebResponse(res, response) {
|
|
40
|
+
if (res.destroyed || res.writableEnded) {
|
|
41
|
+
await response.body?.cancel().catch(() => undefined);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const headers = {};
|
|
45
|
+
response.headers.forEach((value, key) => {
|
|
46
|
+
headers[key] = key === 'set-cookie' ? response.headers.getSetCookie() : value;
|
|
47
|
+
});
|
|
48
|
+
res.writeHead(response.status, headers);
|
|
49
|
+
if (!response.body) {
|
|
50
|
+
res.end();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const nodeStream = Readable.fromWeb(response.body);
|
|
54
|
+
await new Promise((resolve, reject) => {
|
|
55
|
+
pipeline(nodeStream, res, (error) => {
|
|
56
|
+
if (!error || error.code === 'ERR_STREAM_PREMATURE_CLOSE') {
|
|
57
|
+
resolve();
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
reject(error);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
-
import type { McpServer } from '@modelcontextprotocol/
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/server';
|
|
2
2
|
import type { Context } from '../../context.ts';
|
|
3
|
+
export declare function getShopLanguagesText(context: Context): Promise<string>;
|
|
4
|
+
export declare function getShopCurrenciesText(context: Context): Promise<string>;
|
|
5
|
+
export declare function getShopCountriesText(context: Context): Promise<string>;
|
|
6
|
+
export declare function buildChatResourceContext(context: Context | undefined): Promise<string>;
|
|
3
7
|
export declare const registerLocalizationResources: (server: McpServer, context: Context) => void;
|
|
@@ -1,77 +1,101 @@
|
|
|
1
|
+
import { createLogger } from '@unchainedshop/logger';
|
|
2
|
+
const logger = createLogger('unchained:api:mcp');
|
|
3
|
+
export async function getShopLanguagesText(context) {
|
|
4
|
+
const [languages, countries] = await Promise.all([
|
|
5
|
+
context.modules.languages.findLanguages({ includeInactive: false }),
|
|
6
|
+
context.modules.countries.findCountries({ includeInactive: false }),
|
|
7
|
+
]);
|
|
8
|
+
const baseLanguageCodes = languages.map((l) => l.isoCode.toLowerCase());
|
|
9
|
+
const availableCountryCodes = countries.map((c) => c.isoCode.toUpperCase());
|
|
10
|
+
return JSON.stringify({
|
|
11
|
+
baseLanguages: languages.map((l) => ({
|
|
12
|
+
isoCode: l.isoCode,
|
|
13
|
+
name: l.isoCode,
|
|
14
|
+
isActive: l.isActive,
|
|
15
|
+
})),
|
|
16
|
+
availableCountries: availableCountryCodes,
|
|
17
|
+
localeFormat: 'Use base language codes (e.g., "en", "fr", "de") or language-country combinations (e.g., "en-US", "fr-CH") following BCP 47 format',
|
|
18
|
+
validationRule: `Any combination of base languages [${baseLanguageCodes.join(', ')}] with available countries [${availableCountryCodes.join(', ')}] is acceptable for locale codes, as long as it makes contextual sense (e.g., "en-US" , "de-CH" , but "ja-DE" would be unusual)`,
|
|
19
|
+
note: 'If a required base language is missing, ask the user if they want to add it using localization_management tool with action: CREATE',
|
|
20
|
+
}, null, 2);
|
|
21
|
+
}
|
|
22
|
+
export async function getShopCurrenciesText(context) {
|
|
23
|
+
const currencies = await context.modules.currencies.findCurrencies({ includeInactive: false });
|
|
24
|
+
return JSON.stringify({
|
|
25
|
+
currencies: currencies.map((c) => ({
|
|
26
|
+
isoCode: c.isoCode,
|
|
27
|
+
name: c.isoCode,
|
|
28
|
+
isActive: c.isActive,
|
|
29
|
+
decimals: c.decimals,
|
|
30
|
+
})),
|
|
31
|
+
note: 'If a required currency is missing, ask the user if they want to add it using localization_management tool with action: CREATE',
|
|
32
|
+
}, null, 2);
|
|
33
|
+
}
|
|
34
|
+
export async function getShopCountriesText(context) {
|
|
35
|
+
const countries = await context.modules.countries.findCountries({ includeInactive: false });
|
|
36
|
+
return JSON.stringify({
|
|
37
|
+
countries: countries.map((c) => ({
|
|
38
|
+
isoCode: c.isoCode,
|
|
39
|
+
name: c.isoCode,
|
|
40
|
+
isActive: c.isActive,
|
|
41
|
+
})),
|
|
42
|
+
note: 'If a required country is missing, ask the user if they want to add it using localization_management tool with action: CREATE',
|
|
43
|
+
}, null, 2);
|
|
44
|
+
}
|
|
45
|
+
export async function buildChatResourceContext(context) {
|
|
46
|
+
if (!context?.user?.roles?.includes('admin'))
|
|
47
|
+
return '';
|
|
48
|
+
const sections = await Promise.all([
|
|
49
|
+
['shop-languages', getShopLanguagesText],
|
|
50
|
+
['shop-currencies', getShopCurrenciesText],
|
|
51
|
+
['shop-countries', getShopCountriesText],
|
|
52
|
+
].map(async ([name, getText]) => {
|
|
53
|
+
try {
|
|
54
|
+
return `${name}:\n${await getText(context)}`;
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
logger.error(`Failed to read resource ${name}: ${error.message}`);
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}));
|
|
61
|
+
const body = sections.filter(Boolean).join('\n\n');
|
|
62
|
+
return body ? `\n\nAVAILABLE SHOP CONFIGURATION:\n${body}` : '';
|
|
63
|
+
}
|
|
1
64
|
export const registerLocalizationResources = (server, context) => {
|
|
2
|
-
server.
|
|
65
|
+
server.registerResource('shop-languages', 'unchained://shop/languages', {
|
|
3
66
|
description: 'Available languages configured in the shop. Use these ISO codes when creating or updating products, filters, and assortments. Includes valid language-country dialect combinations.',
|
|
4
67
|
mimeType: 'application/json',
|
|
5
|
-
}, async () => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
uri: 'unchained://shop/languages',
|
|
16
|
-
mimeType: 'application/json',
|
|
17
|
-
text: JSON.stringify({
|
|
18
|
-
baseLanguages: languages.map((l) => ({
|
|
19
|
-
isoCode: l.isoCode,
|
|
20
|
-
name: l.isoCode,
|
|
21
|
-
isActive: l.isActive,
|
|
22
|
-
})),
|
|
23
|
-
availableCountries: availableCountryCodes,
|
|
24
|
-
localeFormat: 'Use base language codes (e.g., "en", "fr", "de") or language-country combinations (e.g., "en-US", "fr-CH") following BCP 47 format',
|
|
25
|
-
validationRule: `Any combination of base languages [${baseLanguageCodes.join(', ')}] with available countries [${availableCountryCodes.join(', ')}] is acceptable for locale codes, as long as it makes contextual sense (e.g., "en-US" , "de-CH" , but "ja-DE" would be unusual)`,
|
|
26
|
-
note: 'If a required base language is missing, ask the user if they want to add it using localization_management tool with action: CREATE',
|
|
27
|
-
}, null, 2),
|
|
28
|
-
},
|
|
29
|
-
],
|
|
30
|
-
};
|
|
31
|
-
});
|
|
32
|
-
server.resource('shop-currencies', 'unchained://shop/currencies', {
|
|
68
|
+
}, async () => ({
|
|
69
|
+
contents: [
|
|
70
|
+
{
|
|
71
|
+
uri: 'unchained://shop/languages',
|
|
72
|
+
mimeType: 'application/json',
|
|
73
|
+
text: await getShopLanguagesText(context),
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
}));
|
|
77
|
+
server.registerResource('shop-currencies', 'unchained://shop/currencies', {
|
|
33
78
|
description: 'Available currencies configured in the shop. Check decimal points for price conversions. All prices are stored as integers.',
|
|
34
79
|
mimeType: 'application/json',
|
|
35
|
-
}, async () => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
name: c.isoCode,
|
|
46
|
-
isActive: c.isActive,
|
|
47
|
-
decimals: c.decimals,
|
|
48
|
-
})),
|
|
49
|
-
note: 'If a required currency is missing, ask the user if they want to add it using localization_management tool with action: CREATE',
|
|
50
|
-
}, null, 2),
|
|
51
|
-
},
|
|
52
|
-
],
|
|
53
|
-
};
|
|
54
|
-
});
|
|
55
|
-
server.resource('shop-countries', 'unchained://shop/countries', {
|
|
80
|
+
}, async () => ({
|
|
81
|
+
contents: [
|
|
82
|
+
{
|
|
83
|
+
uri: 'unchained://shop/currencies',
|
|
84
|
+
mimeType: 'application/json',
|
|
85
|
+
text: await getShopCurrenciesText(context),
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
}));
|
|
89
|
+
server.registerResource('shop-countries', 'unchained://shop/countries', {
|
|
56
90
|
description: 'Available countries configured in the shop. Use these ISO codes for geographic operations.',
|
|
57
91
|
mimeType: 'application/json',
|
|
58
|
-
}, async () => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
isoCode: c.isoCode,
|
|
68
|
-
name: c.isoCode,
|
|
69
|
-
isActive: c.isActive,
|
|
70
|
-
})),
|
|
71
|
-
note: 'If a required country is missing, ask the user if they want to add it using localization_management tool with action: CREATE',
|
|
72
|
-
}, null, 2),
|
|
73
|
-
},
|
|
74
|
-
],
|
|
75
|
-
};
|
|
76
|
-
});
|
|
92
|
+
}, async () => ({
|
|
93
|
+
contents: [
|
|
94
|
+
{
|
|
95
|
+
uri: 'unchained://shop/countries',
|
|
96
|
+
mimeType: 'application/json',
|
|
97
|
+
text: await getShopCountriesText(context),
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
}));
|
|
77
101
|
};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type { McpServer } from '@modelcontextprotocol/
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/server';
|
|
2
2
|
import type { Context } from '../../../context.ts';
|
|
3
3
|
export declare const registerAssortmentTools: (server: McpServer, context: Context) => void;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { assortmentManagement, AssortmentManagementSchema } from "./assortmentManagement.js";
|
|
2
2
|
export const registerAssortmentTools = (server, context) => {
|
|
3
|
-
server.
|
|
3
|
+
server.registerTool('assortment_management', {
|
|
4
|
+
description: 'Unified assortment management system with comprehensive action-based operations: CREATE/UPDATE/REMOVE/GET/LIST/COUNT assortments, UPDATE_STATUS (activate/deactivate), ADD_MEDIA/REMOVE_MEDIA/REORDER_MEDIA/GET_MEDIA/UPDATE_MEDIA_TEXTS, ADD_PRODUCT/REMOVE_PRODUCT/GET_PRODUCTS/REORDER_PRODUCTS, ADD_FILTER/REMOVE_FILTER/GET_FILTERS/REORDER_FILTERS, ADD_LINK/REMOVE_LINK/GET_LINKS/REORDER_LINKS, GET_CHILDREN/SET_BASE, SEARCH_PRODUCTS, GET_TEXTS/GET_MEDIA_TEXTS. Supports comprehensive assortment management with type-specific validations and error handling.',
|
|
5
|
+
inputSchema: AssortmentManagementSchema,
|
|
6
|
+
}, async (params) => assortmentManagement(context, params));
|
|
4
7
|
};
|
|
@@ -179,9 +179,7 @@ export declare const actionValidators: {
|
|
|
179
179
|
assortmentMediaId: z.ZodMiniString<string>;
|
|
180
180
|
}, z.core.$strip>;
|
|
181
181
|
};
|
|
182
|
-
export declare const AssortmentManagementSchema:
|
|
183
|
-
action: z.core.$ZodType<string>;
|
|
184
|
-
} & Record<string, z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>;
|
|
182
|
+
export declare const AssortmentManagementSchema: import("@modelcontextprotocol/server").StandardSchemaWithJSON<import("../../utils/sharedSchemas.ts").ManagementParams, import("../../utils/sharedSchemas.ts").ManagementParams>;
|
|
185
183
|
export type { ManagementParams as AssortmentManagementParams } from '../../utils/sharedSchemas.ts';
|
|
186
184
|
export type ActionName = keyof typeof actionValidators;
|
|
187
185
|
export type Params<T extends ActionName> = z.infer<(typeof actionValidators)[T]>;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type { McpServer } from '@modelcontextprotocol/
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/server';
|
|
2
2
|
import type { Context } from '../../../context.ts';
|
|
3
3
|
export declare const registerFilterTools: (server: McpServer, context: Context) => void;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { filterManagement, FilterManagementSchema } from "./filterManagement.js";
|
|
2
2
|
export const registerFilterTools = (server, context) => {
|
|
3
|
-
server.
|
|
3
|
+
server.registerTool('filter_management', {
|
|
4
|
+
description: 'Comprehensive filter management system with unified CRUD operations. Supports: CREATE (new filters with localized texts), UPDATE (modify filter properties), REMOVE (delete with assortment cleanup), GET (retrieve single filter), LIST (paginated search with sorting), COUNT (total counts), CREATE_OPTION/REMOVE_OPTION (manage filter options), UPDATE_TEXTS/GET_TEXTS (localization management). Action-based routing with proper validation and error handling.',
|
|
5
|
+
inputSchema: FilterManagementSchema,
|
|
6
|
+
}, async (params) => filterManagement(context, params));
|
|
4
7
|
};
|
|
@@ -109,9 +109,7 @@ export declare const actionValidators: {
|
|
|
109
109
|
filterOptionValue: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
110
110
|
}, z.core.$strip>;
|
|
111
111
|
};
|
|
112
|
-
export declare const FilterManagementSchema:
|
|
113
|
-
action: z.core.$ZodType<string>;
|
|
114
|
-
} & Record<string, z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>;
|
|
112
|
+
export declare const FilterManagementSchema: import("@modelcontextprotocol/server").StandardSchemaWithJSON<import("../../utils/sharedSchemas.ts").ManagementParams, import("../../utils/sharedSchemas.ts").ManagementParams>;
|
|
115
113
|
export type { ManagementParams as FilterManagementParams } from '../../utils/sharedSchemas.ts';
|
|
116
114
|
export type ActionName = keyof typeof actionValidators;
|
|
117
115
|
export type Params<T extends ActionName> = z.infer<(typeof actionValidators)[T]>;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type { McpServer } from '@modelcontextprotocol/
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/server';
|
|
2
2
|
import type { Context } from '../../../context.ts';
|
|
3
3
|
export declare const registerLocalizationTools: (server: McpServer, context: Context) => void;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { localizationManagement, LocalizationManagementSchema } from "./localizationManagement.js";
|
|
2
2
|
export const registerLocalizationTools = (server, context) => {
|
|
3
|
-
server.
|
|
3
|
+
server.registerTool('localization_management', {
|
|
4
|
+
description: 'Unified localization management tool for localization operations across geographic, monetary, and language entities. Countries use 2-letter codes (US, DE, FR), currencies use 3-letter codes (USD, EUR, CHF) with optional blockchain support, languages use locale codes (en, de-CH).. Actions: CREATE (add new), UPDATE (modify existing), REMOVE (delete).',
|
|
5
|
+
inputSchema: LocalizationManagementSchema,
|
|
6
|
+
}, async (params) => localizationManagement(context, params));
|
|
4
7
|
};
|
|
@@ -86,9 +86,7 @@ export declare const actionValidators: {
|
|
|
86
86
|
}>;
|
|
87
87
|
}, z.core.$strip>;
|
|
88
88
|
};
|
|
89
|
-
export declare const LocalizationManagementSchema:
|
|
90
|
-
action: z.core.$ZodType<string>;
|
|
91
|
-
} & Record<string, z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>;
|
|
89
|
+
export declare const LocalizationManagementSchema: import("@modelcontextprotocol/server").StandardSchemaWithJSON<import("../../utils/sharedSchemas.ts").ManagementParams, import("../../utils/sharedSchemas.ts").ManagementParams>;
|
|
92
90
|
export type { ManagementParams as LocalizationManagementParams } from '../../utils/sharedSchemas.ts';
|
|
93
91
|
export type ActionName = keyof typeof actionValidators;
|
|
94
92
|
export type Params<T extends ActionName> = z.infer<(typeof actionValidators)[T]>;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type { McpServer } from '@modelcontextprotocol/
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/server';
|
|
2
2
|
import type { Context } from '../../../context.ts';
|
|
3
3
|
export declare const registerOrderTools: (server: McpServer, context: Context) => void;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { orderManagement, OrderManagementSchema } from "./orderManagement.js";
|
|
2
2
|
export const registerOrderTools = (server, context) => {
|
|
3
|
-
server.
|
|
3
|
+
server.registerTool('order_management', {
|
|
4
|
+
description: 'Unified order management and analytics system. Supports: LIST (get orders with filters and pagination), SALES_SUMMARY (daily sales analytics), MONTHLY_BREAKDOWN (12-month sales analysis), TOP_CUSTOMERS (highest spending customers), TOP_PRODUCTS (best-selling products). All actions support date filtering and provider-based segmentation with proper aggregation and normalization.',
|
|
5
|
+
inputSchema: OrderManagementSchema,
|
|
6
|
+
}, async (params) => orderManagement(context, params));
|
|
4
7
|
};
|
|
@@ -135,9 +135,7 @@ export declare const actionValidators: {
|
|
|
135
135
|
comment: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
136
136
|
}, z.core.$strip>;
|
|
137
137
|
};
|
|
138
|
-
export declare const OrderManagementSchema:
|
|
139
|
-
action: z.core.$ZodType<string>;
|
|
140
|
-
} & Record<string, z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>;
|
|
138
|
+
export declare const OrderManagementSchema: import("@modelcontextprotocol/server").StandardSchemaWithJSON<import("../../utils/sharedSchemas.ts").ManagementParams, import("../../utils/sharedSchemas.ts").ManagementParams>;
|
|
141
139
|
export type { ManagementParams as OrderManagementParams } from '../../utils/sharedSchemas.ts';
|
|
142
140
|
export type ActionName = keyof typeof actionValidators;
|
|
143
141
|
export type Params<T extends ActionName> = z.infer<(typeof actionValidators)[T]>;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type { McpServer } from '@modelcontextprotocol/
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/server';
|
|
2
2
|
import type { Context } from '../../../context.ts';
|
|
3
3
|
export declare const registerProductTools: (server: McpServer, context: Context) => void;
|