@unchainedshop/api 5.0.0-alpha.1 → 5.0.0-alpha.3
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/adminUiPlugins.d.ts +73 -0
- package/lib/adminUiPlugins.js +164 -0
- package/lib/adminUiTheme.d.ts +25 -0
- package/lib/adminUiTheme.js +34 -0
- package/lib/chat/generateImageHandler.d.ts +273 -7
- package/lib/chat/utils.d.ts +8 -0
- package/lib/chat/utils.js +54 -2
- package/lib/createGraphQLServer.js +6 -0
- package/lib/express/chatHandler.js +52 -61
- package/lib/express/createMCPMiddleware.js +19 -78
- package/lib/express/index.d.ts +8 -2
- package/lib/express/index.js +55 -7
- package/lib/fastify/chatHandler.js +52 -61
- package/lib/fastify/index.d.ts +7 -1
- package/lib/fastify/index.js +58 -11
- package/lib/fastify/mcpHandler.js +24 -75
- package/lib/loaders/cartLoader.d.ts +13 -0
- package/lib/loaders/cartLoader.js +8 -0
- package/lib/loaders/enrollmentByOrderLoader.d.ts +9 -0
- package/lib/loaders/enrollmentByOrderLoader.js +17 -0
- package/lib/loaders/index.d.ts +14 -0
- package/lib/loaders/index.js +4 -0
- package/lib/loaders/orderDeliveryLoader.d.ts +9 -0
- package/lib/loaders/orderDeliveryLoader.js +12 -0
- package/lib/loaders/orderDiscountsLoader.d.ts +9 -0
- package/lib/loaders/orderDiscountsLoader.js +17 -0
- package/lib/loaders/orderPaymentLoader.d.ts +9 -0
- package/lib/loaders/orderPaymentLoader.js +12 -0
- package/lib/loaders/orderPositionsLoader.d.ts +9 -0
- package/lib/loaders/orderPositionsLoader.js +17 -0
- package/lib/loaders/tokenExportStatusLoader.d.ts +9 -0
- package/lib/loaders/tokenExportStatusLoader.js +31 -0
- package/lib/locale-context.d.ts +2 -4
- package/lib/locale-context.js +2 -1
- 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/handlers/getAssortment.js +1 -1
- 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 +2 -2
- package/lib/mcp/tools/filter/handlers/createFilter.js +2 -4
- package/lib/mcp/tools/filter/handlers/createFilterOption.js +2 -4
- package/lib/mcp/tools/filter/handlers/removeFilterOption.js +2 -4
- package/lib/mcp/tools/filter/handlers/updateFilter.js +2 -2
- 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 +2 -2
- 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 +2 -2
- 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 +2 -2
- package/lib/mcp/tools/product/handlers/getProduct.js +1 -1
- 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 +7 -7
- package/lib/mcp/tools/product/schemas.js +3 -2
- 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 +2 -2
- 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 +2 -2
- package/lib/mcp/tools/system/index.d.ts +1 -1
- package/lib/mcp/tools/system/index.js +4 -1
- 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 +2 -2
- package/lib/mcp/utils/sharedSchemas.d.ts +1 -0
- package/lib/mcp/utils/sharedSchemas.js +1 -0
- package/lib/middleware/createAuthMiddleware.js +1 -3
- package/lib/resolvers/index.js +2 -1
- package/lib/resolvers/mutations/bookmarks/createBookmark.d.ts +1 -1
- package/lib/resolvers/mutations/bookmarks/createBookmark.js +4 -3
- package/lib/resolvers/mutations/filters/createFilter.js +3 -4
- package/lib/resolvers/mutations/filters/createFilterOption.d.ts +1 -1
- package/lib/resolvers/mutations/filters/createFilterOption.js +3 -4
- package/lib/resolvers/mutations/filters/removeFilterOption.d.ts +1 -1
- package/lib/resolvers/mutations/filters/removeFilterOption.js +2 -8
- package/lib/resolvers/mutations/filters/updateFilter.d.ts +1 -1
- package/lib/resolvers/mutations/filters/updateFilter.js +2 -5
- package/lib/resolvers/mutations/index.js +3 -3
- package/lib/resolvers/mutations/products/updateProductCommerce.js +10 -1
- package/lib/resolvers/scalars/LowerCaseString.d.ts +1 -1
- package/lib/resolvers/scalars/index.d.ts +1 -1
- 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 +12 -8
- package/lib/resolvers/type/product/product-bundle-types.d.ts +1 -1
- package/lib/resolvers/type/product/product-plan-types.d.ts +1 -1
- package/lib/resolvers/type/product/product-simple-types.d.ts +1 -1
- package/lib/resolvers/type/product/product-tokenized-types.d.ts +1 -1
- package/lib/resolvers/type/quotation-types.d.ts +4 -0
- package/lib/resolvers/type/quotation-types.js +1 -0
- package/lib/resolvers/type/token-types.d.ts +3 -3
- package/lib/resolvers/type/token-types.js +6 -5
- package/lib/resolvers/type/user-types.d.ts +1 -1
- package/lib/resolvers/type/user-types.js +2 -2
- package/lib/roles/all.js +1 -1
- package/lib/roles/index.js +2 -0
- package/lib/roles/loggedIn.js +11 -0
- package/lib/schema/inputTypes.js +3 -3
- package/lib/schema/scalars.js +1 -0
- package/lib/schema/types/filter.js +2 -2
- package/lib/schema/types/product/product.js +1 -1
- package/lib/schema/types/quotation.js +5 -0
- package/lib/schema/types/user.js +1 -1
- package/lib/utils/optionalPeerError.d.ts +1 -0
- package/lib/utils/optionalPeerError.js +6 -0
- package/package.json +39 -15
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { TokenStatus } from '@unchainedshop/core-warehousing';
|
|
2
|
+
import { WorkStatus } from '@unchainedshop/core-worker';
|
|
3
|
+
import DataLoader from 'dataloader';
|
|
4
|
+
export default (unchainedAPI) => new DataLoader(async (queries) => {
|
|
5
|
+
const tokensNeedingWorkCheck = [];
|
|
6
|
+
for (const { token } of queries) {
|
|
7
|
+
if (!(token.walletAddress && !token.userId)) {
|
|
8
|
+
tokensNeedingWorkCheck.push(token);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
let exportingTokenIds;
|
|
12
|
+
if (tokensNeedingWorkCheck.length > 0) {
|
|
13
|
+
const workItems = await unchainedAPI.modules.worker.findWorkQueue({
|
|
14
|
+
types: ['EXPORT_TOKEN'],
|
|
15
|
+
status: [WorkStatus.NEW, WorkStatus.ALLOCATED],
|
|
16
|
+
});
|
|
17
|
+
exportingTokenIds = new Set(workItems.map((item) => item.input?.token?._id).filter(Boolean));
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
exportingTokenIds = new Set();
|
|
21
|
+
}
|
|
22
|
+
return queries.map(({ token }) => {
|
|
23
|
+
if (token.walletAddress && !token.userId) {
|
|
24
|
+
return TokenStatus.DECENTRALIZED;
|
|
25
|
+
}
|
|
26
|
+
if (exportingTokenIds.has(token._id)) {
|
|
27
|
+
return TokenStatus.EXPORTING;
|
|
28
|
+
}
|
|
29
|
+
return TokenStatus.CENTRALIZED;
|
|
30
|
+
});
|
|
31
|
+
});
|
package/lib/locale-context.d.ts
CHANGED
|
@@ -6,12 +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
|
|
13
|
-
clear: () => void;
|
|
14
|
-
};
|
|
12
|
+
}, unchainedAPI: UnchainedCore], UnchainedLocaleContext>;
|
|
15
13
|
export declare const getLocaleContext: ({ getHeader, }: {
|
|
16
14
|
getHeader: UnchainedHTTPServerContext["getHeader"];
|
|
17
15
|
}, unchainedAPI: UnchainedCore) => Promise<UnchainedLocaleContext>;
|
package/lib/locale-context.js
CHANGED
|
@@ -16,7 +16,8 @@ const uncachedResolveDefaultContext = async ({ acceptLang, acceptCountry }, unch
|
|
|
16
16
|
};
|
|
17
17
|
return newContext;
|
|
18
18
|
};
|
|
19
|
-
export const resolveDefaultContext = memoizeWithTTL(uncachedResolveDefaultContext,
|
|
19
|
+
export const resolveDefaultContext = memoizeWithTTL(uncachedResolveDefaultContext, {
|
|
20
|
+
ttl: process.env.NODE_ENV === 'production' ? 1000 * 60 : 1,
|
|
20
21
|
cacheKey: (args) => {
|
|
21
22
|
const [{ acceptLang, acceptCountry }] = args;
|
|
22
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
|
};
|
|
@@ -3,7 +3,7 @@ import { getNormalizedAssortmentDetails } from "../../../utils/getNormalizedAsso
|
|
|
3
3
|
export default async function getAssortment(context, params) {
|
|
4
4
|
const { modules } = context;
|
|
5
5
|
const { assortmentId, slug } = params;
|
|
6
|
-
let query
|
|
6
|
+
let query;
|
|
7
7
|
if (assortmentId)
|
|
8
8
|
query = { assortmentId };
|
|
9
9
|
else if (slug)
|
|
@@ -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
|
};
|
|
@@ -193,11 +193,11 @@ export declare const AssortmentManagementSchema: {
|
|
|
193
193
|
limit: z.ZodOptional<z.ZodNumber>;
|
|
194
194
|
offset: z.ZodOptional<z.ZodNumber>;
|
|
195
195
|
action: z.ZodEnum<{
|
|
196
|
+
LIST: "LIST";
|
|
196
197
|
GET: "GET";
|
|
197
198
|
CREATE: "CREATE";
|
|
198
199
|
UPDATE: "UPDATE";
|
|
199
200
|
REMOVE: "REMOVE";
|
|
200
|
-
LIST: "LIST";
|
|
201
201
|
COUNT: "COUNT";
|
|
202
202
|
GET_TEXTS: "GET_TEXTS";
|
|
203
203
|
UPDATE_STATUS: "UPDATE_STATUS";
|
|
@@ -285,11 +285,11 @@ export declare const AssortmentManagementZodSchema: z.ZodObject<{
|
|
|
285
285
|
limit: z.ZodOptional<z.ZodNumber>;
|
|
286
286
|
offset: z.ZodOptional<z.ZodNumber>;
|
|
287
287
|
action: z.ZodEnum<{
|
|
288
|
+
LIST: "LIST";
|
|
288
289
|
GET: "GET";
|
|
289
290
|
CREATE: "CREATE";
|
|
290
291
|
UPDATE: "UPDATE";
|
|
291
292
|
REMOVE: "REMOVE";
|
|
292
|
-
LIST: "LIST";
|
|
293
293
|
COUNT: "COUNT";
|
|
294
294
|
GET_TEXTS: "GET_TEXTS";
|
|
295
295
|
UPDATE_STATUS: "UPDATE_STATUS";
|
|
@@ -1,16 +1,14 @@
|
|
|
1
|
-
import { FilterDirector } from '@unchainedshop/core';
|
|
2
1
|
import { getNormalizedFilterDetails } from "../../../utils/getNormalizedFilterDetails.js";
|
|
3
2
|
export default async function createFilter(context, params) {
|
|
4
|
-
const { modules } = context;
|
|
3
|
+
const { modules, services } = context;
|
|
5
4
|
const { filter, texts } = params;
|
|
6
5
|
const { key, type, options = [] } = filter;
|
|
7
|
-
const newFilter = await
|
|
6
|
+
const newFilter = await services.filters.createFilter({
|
|
8
7
|
key,
|
|
9
8
|
type,
|
|
10
9
|
options,
|
|
11
10
|
isActive: true,
|
|
12
11
|
});
|
|
13
|
-
await FilterDirector.invalidateProductIdCache(newFilter, context);
|
|
14
12
|
if (texts && texts.length > 0) {
|
|
15
13
|
await modules.filters.texts.updateTexts({ filterId: newFilter._id }, texts);
|
|
16
14
|
}
|
|
@@ -1,16 +1,14 @@
|
|
|
1
|
-
import { FilterDirector } from '@unchainedshop/core';
|
|
2
1
|
import { FilterNotFoundError } from "../../../../errors.js";
|
|
3
2
|
import { getNormalizedFilterDetails } from "../../../utils/getNormalizedFilterDetails.js";
|
|
4
3
|
export default async function createFilterOption(context, params) {
|
|
5
|
-
const { modules } = context;
|
|
4
|
+
const { modules, services } = context;
|
|
6
5
|
const { filterId, option, optionTexts } = params;
|
|
7
6
|
if (!(await modules.filters.filterExists({ filterId }))) {
|
|
8
7
|
throw new FilterNotFoundError({ filterId });
|
|
9
8
|
}
|
|
10
|
-
const newOption = await
|
|
9
|
+
const newOption = await services.filters.createFilterOption(filterId, { value: option });
|
|
11
10
|
if (!newOption)
|
|
12
11
|
return { filter: null };
|
|
13
|
-
await FilterDirector.invalidateProductIdCache(newOption, context);
|
|
14
12
|
if (optionTexts && optionTexts.length > 0) {
|
|
15
13
|
await modules.filters.texts.updateTexts({ filterId, filterOptionValue: option }, optionTexts);
|
|
16
14
|
}
|
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
import { FilterDirector } from '@unchainedshop/core';
|
|
2
1
|
import { FilterNotFoundError } from "../../../../errors.js";
|
|
3
2
|
import { getNormalizedFilterDetails } from "../../../utils/getNormalizedFilterDetails.js";
|
|
4
3
|
export default async function removeFilterOption(context, params) {
|
|
5
|
-
const { modules } = context;
|
|
4
|
+
const { modules, services } = context;
|
|
6
5
|
const { filterId, option } = params;
|
|
7
6
|
if (!(await modules.filters.filterExists({ filterId }))) {
|
|
8
7
|
throw new FilterNotFoundError({ filterId });
|
|
9
8
|
}
|
|
10
|
-
const removedFilterOption = await
|
|
9
|
+
const removedFilterOption = await services.filters.removeFilterOption({
|
|
11
10
|
filterId,
|
|
12
11
|
filterOptionValue: option,
|
|
13
12
|
});
|
|
14
13
|
if (!removedFilterOption)
|
|
15
14
|
return { filter: null };
|
|
16
|
-
await FilterDirector.invalidateProductIdCache(removedFilterOption, context);
|
|
17
15
|
const normalizedFilter = await getNormalizedFilterDetails(filterId, context);
|
|
18
16
|
return { filter: normalizedFilter };
|
|
19
17
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { FilterNotFoundError } from "../../../../errors.js";
|
|
2
2
|
import { getNormalizedFilterDetails } from "../../../utils/getNormalizedFilterDetails.js";
|
|
3
3
|
export default async function updateFilter(context, params) {
|
|
4
|
-
const { modules } = context;
|
|
4
|
+
const { modules, services } = context;
|
|
5
5
|
const { filterId, updateData } = params;
|
|
6
6
|
if (!(await modules.filters.filterExists({ filterId }))) {
|
|
7
7
|
throw new FilterNotFoundError({ filterId });
|
|
8
8
|
}
|
|
9
|
-
await
|
|
9
|
+
await services.filters.updateFilter(filterId, updateData);
|
|
10
10
|
const normalizedFilter = await getNormalizedFilterDetails(filterId, context);
|
|
11
11
|
return { filter: normalizedFilter };
|
|
12
12
|
}
|
|
@@ -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
|
};
|
|
@@ -133,11 +133,11 @@ export declare const FilterManagementSchema: {
|
|
|
133
133
|
limit: z.ZodOptional<z.ZodNumber>;
|
|
134
134
|
offset: z.ZodOptional<z.ZodNumber>;
|
|
135
135
|
action: z.ZodEnum<{
|
|
136
|
+
LIST: "LIST";
|
|
136
137
|
GET: "GET";
|
|
137
138
|
CREATE: "CREATE";
|
|
138
139
|
UPDATE: "UPDATE";
|
|
139
140
|
REMOVE: "REMOVE";
|
|
140
|
-
LIST: "LIST";
|
|
141
141
|
COUNT: "COUNT";
|
|
142
142
|
CREATE_OPTION: "CREATE_OPTION";
|
|
143
143
|
REMOVE_OPTION: "REMOVE_OPTION";
|
|
@@ -189,11 +189,11 @@ export declare const FilterManagementZodSchema: z.ZodObject<{
|
|
|
189
189
|
limit: z.ZodOptional<z.ZodNumber>;
|
|
190
190
|
offset: z.ZodOptional<z.ZodNumber>;
|
|
191
191
|
action: z.ZodEnum<{
|
|
192
|
+
LIST: "LIST";
|
|
192
193
|
GET: "GET";
|
|
193
194
|
CREATE: "CREATE";
|
|
194
195
|
UPDATE: "UPDATE";
|
|
195
196
|
REMOVE: "REMOVE";
|
|
196
|
-
LIST: "LIST";
|
|
197
197
|
COUNT: "COUNT";
|
|
198
198
|
CREATE_OPTION: "CREATE_OPTION";
|
|
199
199
|
REMOVE_OPTION: "REMOVE_OPTION";
|
|
@@ -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
|
};
|
|
@@ -132,11 +132,11 @@ export declare const LocalizationManagementSchema: {
|
|
|
132
132
|
limit: z.ZodOptional<z.ZodNumber>;
|
|
133
133
|
offset: z.ZodOptional<z.ZodNumber>;
|
|
134
134
|
action: z.ZodEnum<{
|
|
135
|
+
LIST: "LIST";
|
|
135
136
|
GET: "GET";
|
|
136
137
|
CREATE: "CREATE";
|
|
137
138
|
UPDATE: "UPDATE";
|
|
138
139
|
REMOVE: "REMOVE";
|
|
139
|
-
LIST: "LIST";
|
|
140
140
|
COUNT: "COUNT";
|
|
141
141
|
}>;
|
|
142
142
|
localizationType: z.ZodEnum<{
|
|
@@ -165,11 +165,11 @@ export declare const LocalizationManagementZodSchema: z.ZodObject<{
|
|
|
165
165
|
limit: z.ZodOptional<z.ZodNumber>;
|
|
166
166
|
offset: z.ZodOptional<z.ZodNumber>;
|
|
167
167
|
action: z.ZodEnum<{
|
|
168
|
+
LIST: "LIST";
|
|
168
169
|
GET: "GET";
|
|
169
170
|
CREATE: "CREATE";
|
|
170
171
|
UPDATE: "UPDATE";
|
|
171
172
|
REMOVE: "REMOVE";
|
|
172
|
-
LIST: "LIST";
|
|
173
173
|
COUNT: "COUNT";
|
|
174
174
|
}>;
|
|
175
175
|
localizationType: z.ZodEnum<{
|
|
@@ -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
|
};
|