@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.
Files changed (117) hide show
  1. package/README.md +6 -4
  2. package/lib/adminUiPlugins.d.ts +73 -0
  3. package/lib/adminUiPlugins.js +164 -0
  4. package/lib/adminUiTheme.d.ts +25 -0
  5. package/lib/adminUiTheme.js +34 -0
  6. package/lib/chat/generateImageHandler.d.ts +273 -7
  7. package/lib/chat/utils.d.ts +8 -0
  8. package/lib/chat/utils.js +54 -2
  9. package/lib/createGraphQLServer.js +6 -0
  10. package/lib/express/chatHandler.js +52 -61
  11. package/lib/express/createMCPMiddleware.js +19 -78
  12. package/lib/express/index.d.ts +8 -2
  13. package/lib/express/index.js +55 -7
  14. package/lib/fastify/chatHandler.js +52 -61
  15. package/lib/fastify/index.d.ts +7 -1
  16. package/lib/fastify/index.js +58 -11
  17. package/lib/fastify/mcpHandler.js +24 -75
  18. package/lib/loaders/cartLoader.d.ts +13 -0
  19. package/lib/loaders/cartLoader.js +8 -0
  20. package/lib/loaders/enrollmentByOrderLoader.d.ts +9 -0
  21. package/lib/loaders/enrollmentByOrderLoader.js +17 -0
  22. package/lib/loaders/index.d.ts +14 -0
  23. package/lib/loaders/index.js +4 -0
  24. package/lib/loaders/orderDeliveryLoader.d.ts +9 -0
  25. package/lib/loaders/orderDeliveryLoader.js +12 -0
  26. package/lib/loaders/orderDiscountsLoader.d.ts +9 -0
  27. package/lib/loaders/orderDiscountsLoader.js +17 -0
  28. package/lib/loaders/orderPaymentLoader.d.ts +9 -0
  29. package/lib/loaders/orderPaymentLoader.js +12 -0
  30. package/lib/loaders/orderPositionsLoader.d.ts +9 -0
  31. package/lib/loaders/orderPositionsLoader.js +17 -0
  32. package/lib/loaders/tokenExportStatusLoader.d.ts +9 -0
  33. package/lib/loaders/tokenExportStatusLoader.js +31 -0
  34. package/lib/locale-context.d.ts +2 -4
  35. package/lib/locale-context.js +2 -1
  36. package/lib/mcp/handleMcpHttpRequest.d.ts +2 -0
  37. package/lib/mcp/handleMcpHttpRequest.js +83 -0
  38. package/lib/mcp/index.d.ts +1 -1
  39. package/lib/mcp/nodeHttpBridge.d.ts +3 -0
  40. package/lib/mcp/nodeHttpBridge.js +64 -0
  41. package/lib/mcp/resources/localization.d.ts +5 -1
  42. package/lib/mcp/resources/localization.js +93 -69
  43. package/lib/mcp/tools/assortment/handlers/getAssortment.js +1 -1
  44. package/lib/mcp/tools/assortment/index.d.ts +1 -1
  45. package/lib/mcp/tools/assortment/index.js +4 -1
  46. package/lib/mcp/tools/assortment/schemas.d.ts +2 -2
  47. package/lib/mcp/tools/filter/handlers/createFilter.js +2 -4
  48. package/lib/mcp/tools/filter/handlers/createFilterOption.js +2 -4
  49. package/lib/mcp/tools/filter/handlers/removeFilterOption.js +2 -4
  50. package/lib/mcp/tools/filter/handlers/updateFilter.js +2 -2
  51. package/lib/mcp/tools/filter/index.d.ts +1 -1
  52. package/lib/mcp/tools/filter/index.js +4 -1
  53. package/lib/mcp/tools/filter/schemas.d.ts +2 -2
  54. package/lib/mcp/tools/localization/index.d.ts +1 -1
  55. package/lib/mcp/tools/localization/index.js +4 -1
  56. package/lib/mcp/tools/localization/schemas.d.ts +2 -2
  57. package/lib/mcp/tools/order/index.d.ts +1 -1
  58. package/lib/mcp/tools/order/index.js +4 -1
  59. package/lib/mcp/tools/order/schemas.d.ts +2 -2
  60. package/lib/mcp/tools/product/handlers/getProduct.js +1 -1
  61. package/lib/mcp/tools/product/index.d.ts +1 -1
  62. package/lib/mcp/tools/product/index.js +4 -1
  63. package/lib/mcp/tools/product/schemas.d.ts +7 -7
  64. package/lib/mcp/tools/product/schemas.js +3 -2
  65. package/lib/mcp/tools/provider/index.d.ts +1 -1
  66. package/lib/mcp/tools/provider/index.js +4 -1
  67. package/lib/mcp/tools/provider/schemas.d.ts +2 -2
  68. package/lib/mcp/tools/quotation/index.d.ts +1 -1
  69. package/lib/mcp/tools/quotation/index.js +4 -1
  70. package/lib/mcp/tools/quotation/schemas.d.ts +2 -2
  71. package/lib/mcp/tools/system/index.d.ts +1 -1
  72. package/lib/mcp/tools/system/index.js +4 -1
  73. package/lib/mcp/tools/users/index.d.ts +1 -1
  74. package/lib/mcp/tools/users/index.js +4 -1
  75. package/lib/mcp/tools/users/schemas.d.ts +2 -2
  76. package/lib/mcp/utils/sharedSchemas.d.ts +1 -0
  77. package/lib/mcp/utils/sharedSchemas.js +1 -0
  78. package/lib/middleware/createAuthMiddleware.js +1 -3
  79. package/lib/resolvers/index.js +2 -1
  80. package/lib/resolvers/mutations/bookmarks/createBookmark.d.ts +1 -1
  81. package/lib/resolvers/mutations/bookmarks/createBookmark.js +4 -3
  82. package/lib/resolvers/mutations/filters/createFilter.js +3 -4
  83. package/lib/resolvers/mutations/filters/createFilterOption.d.ts +1 -1
  84. package/lib/resolvers/mutations/filters/createFilterOption.js +3 -4
  85. package/lib/resolvers/mutations/filters/removeFilterOption.d.ts +1 -1
  86. package/lib/resolvers/mutations/filters/removeFilterOption.js +2 -8
  87. package/lib/resolvers/mutations/filters/updateFilter.d.ts +1 -1
  88. package/lib/resolvers/mutations/filters/updateFilter.js +2 -5
  89. package/lib/resolvers/mutations/index.js +3 -3
  90. package/lib/resolvers/mutations/products/updateProductCommerce.js +10 -1
  91. package/lib/resolvers/scalars/LowerCaseString.d.ts +1 -1
  92. package/lib/resolvers/scalars/index.d.ts +1 -1
  93. package/lib/resolvers/type/filter/loaded-filter-types.d.ts +1 -1
  94. package/lib/resolvers/type/filter/loaded-filter-types.js +1 -1
  95. package/lib/resolvers/type/index.d.ts +12 -8
  96. package/lib/resolvers/type/product/product-bundle-types.d.ts +1 -1
  97. package/lib/resolvers/type/product/product-plan-types.d.ts +1 -1
  98. package/lib/resolvers/type/product/product-simple-types.d.ts +1 -1
  99. package/lib/resolvers/type/product/product-tokenized-types.d.ts +1 -1
  100. package/lib/resolvers/type/quotation-types.d.ts +4 -0
  101. package/lib/resolvers/type/quotation-types.js +1 -0
  102. package/lib/resolvers/type/token-types.d.ts +3 -3
  103. package/lib/resolvers/type/token-types.js +6 -5
  104. package/lib/resolvers/type/user-types.d.ts +1 -1
  105. package/lib/resolvers/type/user-types.js +2 -2
  106. package/lib/roles/all.js +1 -1
  107. package/lib/roles/index.js +2 -0
  108. package/lib/roles/loggedIn.js +11 -0
  109. package/lib/schema/inputTypes.js +3 -3
  110. package/lib/schema/scalars.js +1 -0
  111. package/lib/schema/types/filter.js +2 -2
  112. package/lib/schema/types/product/product.js +1 -1
  113. package/lib/schema/types/quotation.js +5 -0
  114. package/lib/schema/types/user.js +1 -1
  115. package/lib/utils/optionalPeerError.d.ts +1 -0
  116. package/lib/utils/optionalPeerError.js +6 -0
  117. 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
+ });
@@ -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: (({ acceptLang, acceptCountry }: {
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) => Promise<UnchainedLocaleContext>) & {
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>;
@@ -16,7 +16,8 @@ const uncachedResolveDefaultContext = async ({ acceptLang, acceptCountry }, unch
16
16
  };
17
17
  return newContext;
18
18
  };
19
- export const resolveDefaultContext = memoizeWithTTL(uncachedResolveDefaultContext, process.env.NODE_ENV === 'production' ? 1000 * 60 : 1, {
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,2 @@
1
+ import type { Context } from '../context.ts';
2
+ export default function handleMcpHttpRequest(context: Context | undefined, request: Request, parsedBody?: unknown): Promise<Response>;
@@ -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
+ }
@@ -1,3 +1,3 @@
1
- import type { McpServer as McpServerType } from '@modelcontextprotocol/sdk/server/mcp.js';
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,3 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ export declare function toWebRequest(req: IncomingMessage, res: ServerResponse, bodyText?: string): Request;
3
+ export declare function sendWebResponse(res: ServerResponse, response: Response): Promise<void>;
@@ -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/sdk/server/mcp.js';
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.resource('shop-languages', 'unchained://shop/languages', {
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
- const [languages, countries] = await Promise.all([
7
- context.modules.languages.findLanguages({ includeInactive: false }),
8
- context.modules.countries.findCountries({ includeInactive: false }),
9
- ]);
10
- const baseLanguageCodes = languages.map((l) => l.isoCode.toLowerCase());
11
- const availableCountryCodes = countries.map((c) => c.isoCode.toUpperCase());
12
- return {
13
- contents: [
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
- const currencies = await context.modules.currencies.findCurrencies({ includeInactive: false });
37
- return {
38
- contents: [
39
- {
40
- uri: 'unchained://shop/currencies',
41
- mimeType: 'application/json',
42
- text: JSON.stringify({
43
- currencies: currencies.map((c) => ({
44
- isoCode: c.isoCode,
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
- const countries = await context.modules.countries.findCountries({ includeInactive: false });
60
- return {
61
- contents: [
62
- {
63
- uri: 'unchained://shop/countries',
64
- mimeType: 'application/json',
65
- text: JSON.stringify({
66
- countries: countries.map((c) => ({
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/sdk/server/mcp.js';
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.tool('assortment_management', '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.', AssortmentManagementSchema, async (params) => assortmentManagement(context, params));
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 modules.filters.create({
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 modules.filters.createFilterOption(filterId, { value: option });
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 modules.filters.removeFilterOption({
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 modules.filters.update(filterId, updateData);
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/sdk/server/mcp.js';
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.tool('filter_management', '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.', FilterManagementSchema, async (params) => filterManagement(context, params));
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/sdk/server/mcp.js';
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.tool('localization_management', '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).', LocalizationManagementSchema, async (params) => localizationManagement(context, params));
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/sdk/server/mcp.js';
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.tool('order_management', '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.', OrderManagementSchema, async (params) => orderManagement(context, params));
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
  };