@unchainedshop/api 5.0.0-alpha.2 → 5.0.0-alpha.4

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 (113) hide show
  1. package/README.md +8 -6
  2. package/lib/adminUiPlugins.d.ts +97 -0
  3. package/lib/adminUiPlugins.js +164 -0
  4. package/lib/auth.d.ts +0 -8
  5. package/lib/auth.js +0 -20
  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 +63 -61
  11. package/lib/express/createMCPMiddleware.js +19 -78
  12. package/lib/express/index.d.ts +6 -2
  13. package/lib/express/index.js +67 -7
  14. package/lib/fastify/chatHandler.js +61 -61
  15. package/lib/fastify/index.d.ts +5 -1
  16. package/lib/fastify/index.js +70 -11
  17. package/lib/fastify/mcpHandler.js +24 -75
  18. package/lib/loaders/assortmentLinksLoader.js +24 -22
  19. package/lib/loaders/assortmentMediaTextLoader.js +1 -0
  20. package/lib/loaders/assortmentProductLoader.js +7 -4
  21. package/lib/loaders/assortmentTextLoader.js +1 -0
  22. package/lib/loaders/cartLoader.d.ts +13 -0
  23. package/lib/loaders/cartLoader.js +8 -0
  24. package/lib/loaders/filterTextLoader.js +1 -0
  25. package/lib/loaders/index.d.ts +9 -0
  26. package/lib/loaders/index.js +2 -0
  27. package/lib/loaders/productMediaTextLoader.js +1 -0
  28. package/lib/loaders/productTextLoader.js +1 -0
  29. package/lib/loaders/productVariationTextLoader.js +1 -0
  30. package/lib/locale-context.d.ts +2 -4
  31. package/lib/locale-context.js +2 -1
  32. package/lib/mcp/handleMcpHttpRequest.d.ts +2 -0
  33. package/lib/mcp/handleMcpHttpRequest.js +83 -0
  34. package/lib/mcp/index.d.ts +1 -1
  35. package/lib/mcp/nodeHttpBridge.d.ts +3 -0
  36. package/lib/mcp/nodeHttpBridge.js +64 -0
  37. package/lib/mcp/resources/localization.d.ts +5 -1
  38. package/lib/mcp/resources/localization.js +93 -69
  39. package/lib/mcp/tools/assortment/handlers/getAssortmentFilters.js +1 -1
  40. package/lib/mcp/tools/assortment/handlers/getAssortmentProducts.js +1 -1
  41. package/lib/mcp/tools/assortment/index.d.ts +1 -1
  42. package/lib/mcp/tools/assortment/index.js +4 -1
  43. package/lib/mcp/tools/assortment/schemas.d.ts +2 -2
  44. package/lib/mcp/tools/filter/handlers/createFilter.js +2 -4
  45. package/lib/mcp/tools/filter/handlers/createFilterOption.js +2 -4
  46. package/lib/mcp/tools/filter/handlers/removeFilterOption.js +2 -4
  47. package/lib/mcp/tools/filter/handlers/updateFilter.js +2 -2
  48. package/lib/mcp/tools/filter/index.d.ts +1 -1
  49. package/lib/mcp/tools/filter/index.js +4 -1
  50. package/lib/mcp/tools/filter/schemas.d.ts +2 -2
  51. package/lib/mcp/tools/localization/index.d.ts +1 -1
  52. package/lib/mcp/tools/localization/index.js +4 -1
  53. package/lib/mcp/tools/localization/schemas.d.ts +2 -2
  54. package/lib/mcp/tools/order/index.d.ts +1 -1
  55. package/lib/mcp/tools/order/index.js +4 -1
  56. package/lib/mcp/tools/order/schemas.d.ts +2 -2
  57. package/lib/mcp/tools/product/index.d.ts +1 -1
  58. package/lib/mcp/tools/product/index.js +4 -1
  59. package/lib/mcp/tools/product/schemas.d.ts +7 -7
  60. package/lib/mcp/tools/product/schemas.js +3 -2
  61. package/lib/mcp/tools/provider/index.d.ts +1 -1
  62. package/lib/mcp/tools/provider/index.js +4 -1
  63. package/lib/mcp/tools/provider/schemas.d.ts +2 -2
  64. package/lib/mcp/tools/quotation/index.d.ts +1 -1
  65. package/lib/mcp/tools/quotation/index.js +4 -1
  66. package/lib/mcp/tools/quotation/schemas.d.ts +2 -2
  67. package/lib/mcp/tools/system/index.d.ts +1 -1
  68. package/lib/mcp/tools/system/index.js +4 -1
  69. package/lib/mcp/tools/users/handlers/getUserPaymentCredentials.js +1 -0
  70. package/lib/mcp/tools/users/index.d.ts +1 -1
  71. package/lib/mcp/tools/users/index.js +4 -1
  72. package/lib/mcp/tools/users/schemas.d.ts +2 -2
  73. package/lib/mcp/utils/getNormalizedAssortmentDetails.js +2 -2
  74. package/lib/mcp/utils/getNormalizedUserDetails.js +1 -0
  75. package/lib/mcp/utils/sharedSchemas.d.ts +1 -0
  76. package/lib/mcp/utils/sharedSchemas.js +1 -0
  77. package/lib/middleware/createAuthMiddleware.js +2 -39
  78. package/lib/resolvers/index.js +2 -1
  79. package/lib/resolvers/mutations/bookmarks/createBookmark.d.ts +1 -1
  80. package/lib/resolvers/mutations/bookmarks/createBookmark.js +4 -3
  81. package/lib/resolvers/mutations/filters/createFilter.js +3 -4
  82. package/lib/resolvers/mutations/filters/createFilterOption.d.ts +1 -1
  83. package/lib/resolvers/mutations/filters/createFilterOption.js +3 -4
  84. package/lib/resolvers/mutations/filters/removeFilterOption.d.ts +1 -1
  85. package/lib/resolvers/mutations/filters/removeFilterOption.js +2 -8
  86. package/lib/resolvers/mutations/filters/updateFilter.d.ts +1 -1
  87. package/lib/resolvers/mutations/filters/updateFilter.js +2 -5
  88. package/lib/resolvers/mutations/index.js +3 -3
  89. package/lib/resolvers/mutations/orders/checkoutCart.js +1 -1
  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/assortment/assortment-types.js +2 -2
  94. package/lib/resolvers/type/filter/loaded-filter-types.d.ts +1 -1
  95. package/lib/resolvers/type/filter/loaded-filter-types.js +1 -1
  96. package/lib/resolvers/type/index.d.ts +9 -5
  97. package/lib/resolvers/type/product/product-bundle-types.d.ts +1 -1
  98. package/lib/resolvers/type/product/product-plan-types.d.ts +1 -1
  99. package/lib/resolvers/type/product/product-simple-types.d.ts +1 -1
  100. package/lib/resolvers/type/product/product-tokenized-types.d.ts +1 -1
  101. package/lib/resolvers/type/quotation-types.d.ts +4 -0
  102. package/lib/resolvers/type/quotation-types.js +1 -0
  103. package/lib/resolvers/type/user-types.js +3 -2
  104. package/lib/roles/index.js +2 -0
  105. package/lib/roles/loggedIn.js +11 -0
  106. package/lib/schema/inputTypes.js +3 -3
  107. package/lib/schema/scalars.js +1 -0
  108. package/lib/schema/types/filter.js +2 -2
  109. package/lib/schema/types/product/product.js +1 -1
  110. package/lib/schema/types/quotation.js +5 -0
  111. package/lib/utils/optionalPeerError.d.ts +1 -0
  112. package/lib/utils/optionalPeerError.js +6 -0
  113. package/package.json +44 -15
@@ -1,89 +1,69 @@
1
1
  import generateImageHandler from "../chat/generateImageHandler.js";
2
2
  import defaultSystemPrompt from "../chat/defaultSystemPrompt.js";
3
3
  import normalizeToolsIndex from "../chat/normalizeToolsIndex.js";
4
- import { errorHandler } from "../chat/utils.js";
4
+ import { chatErrorStatus, createChatRequestLifecycle, errorHandler, logOptionalPeerLoadError, } from "../chat/utils.js";
5
+ import { buildChatResourceContext } from "../mcp/resources/localization.js";
5
6
  import { createLogger } from '@unchainedshop/logger';
6
7
  const logger = createLogger('unchained:api:chat');
7
8
  let convertToModelMessages;
8
9
  let stepCountIs;
9
10
  let streamText;
10
11
  let createMCPClient;
11
- let StreamableHTTPClientTransport;
12
- let Client;
13
12
  try {
14
13
  const aiTools = await import('ai');
15
- const mcpTools = await import('@ai-sdk/mcp');
16
- const mcpSDKClientLibrary = await import('@modelcontextprotocol/sdk/client/streamableHttp.js');
17
- const mcpSDKClient = await import('@modelcontextprotocol/sdk/client/index.js');
18
- StreamableHTTPClientTransport = mcpSDKClientLibrary.StreamableHTTPClientTransport;
19
- Client = mcpSDKClient.Client;
20
14
  convertToModelMessages = aiTools.convertToModelMessages;
21
15
  stepCountIs = aiTools.stepCountIs;
22
16
  streamText = aiTools.streamText;
17
+ }
18
+ catch (error) {
19
+ logOptionalPeerLoadError('ai', error);
20
+ }
21
+ try {
22
+ const mcpTools = await import('@ai-sdk/mcp');
23
23
  createMCPClient = mcpTools.createMCPClient;
24
24
  }
25
- catch {
26
- logger.warn(`optional peer npm packages 'ai' and '@ai-sdk/mcp' not installed, chat will not work`);
25
+ catch (error) {
26
+ logOptionalPeerLoadError('@ai-sdk/mcp', error);
27
27
  }
28
28
  const setupMCPChatHandler = (chatConfiguration) => {
29
29
  if (!chatConfiguration?.model) {
30
30
  throw new Error('Model is required');
31
31
  }
32
- const { tools: additionalTools = {}, unchainedMCPUrl = `${process.env.ROOT_URL}/mcp`, imageGenerationTool, ...restChatConfig } = chatConfiguration;
32
+ const { tools: additionalTools = {}, unchainedMCPUrl = `${process.env.ROOT_URL}/mcp`, model, imageGenerationTool, abortSignal: configuredAbortSignal, onAbort: configuredOnAbort, onEnd: configuredOnEnd, onFinish: configuredOnFinish, ...restChatConfig } = chatConfiguration;
33
33
  const system = chatConfiguration.system ?? defaultSystemPrompt;
34
34
  const mcpChatHandler = async (req, res) => {
35
+ if (req.method === 'OPTIONS') {
36
+ res.headers({
37
+ 'access-control-allow-credentials': 'true',
38
+ 'access-control-allow-private-network': 'true',
39
+ });
40
+ return res.status(200).send();
41
+ }
42
+ const unchainedContext = req.unchainedContext;
43
+ const user = unchainedContext?.user;
44
+ if (!user) {
45
+ return res.status(401).send({ error: 'unauthorized' });
46
+ }
47
+ if (!(user.roles || []).includes('admin')) {
48
+ return res.status(403).send({ error: 'forbidden', message: 'Chat requires admin privileges' });
49
+ }
35
50
  let client;
51
+ const lifecycle = createChatRequestLifecycle(res.raw, configuredAbortSignal);
36
52
  try {
37
- if (req.method === 'OPTIONS') {
38
- res.headers({
39
- 'access-control-allow-credentials': 'true',
40
- 'access-control-allow-private-network': 'true',
41
- });
42
- return res.status(200).send();
43
- }
44
- const resourceTransport = new StreamableHTTPClientTransport(new URL(unchainedMCPUrl), {
45
- requestInit: {
46
- headers: {
47
- Cookie: req.headers.cookie || '',
48
- },
49
- },
50
- });
51
- const sdkClient = new Client({ name: 'unchained-chat-client', version: '1.0.0' });
52
- await sdkClient.connect(resourceTransport);
53
- const transport = new StreamableHTTPClientTransport(new URL(unchainedMCPUrl), {
54
- requestInit: {
53
+ client = await createMCPClient({
54
+ transport: {
55
+ type: 'http',
56
+ url: unchainedMCPUrl,
55
57
  headers: {
56
58
  Cookie: req.headers.cookie || '',
59
+ ...(req.headers.authorization ? { Authorization: req.headers.authorization } : {}),
57
60
  },
58
61
  },
62
+ initializationOptions: { signal: lifecycle.signal },
59
63
  });
60
- client = await createMCPClient({
61
- transport,
62
- });
64
+ lifecycle.setClientClose(() => client.close());
63
65
  const defaultUnchainedTools = await client.tools();
64
- let resourceContext = '';
65
- try {
66
- const resources = await sdkClient.listResources();
67
- if (resources?.resources) {
68
- const resourceTexts = await Promise.all(resources.resources.map(async (resource) => {
69
- try {
70
- const content = await sdkClient.readResource({ uri: resource.uri });
71
- if (content?.contents?.[0]?.text) {
72
- return `${resource.name}:\n${content.contents[0].text}`;
73
- }
74
- }
75
- catch (e) {
76
- logger.error(`Failed to read resource ${resource.uri}: ${e.message}`);
77
- }
78
- return null;
79
- }));
80
- resourceContext =
81
- '\n\nAVAILABLE SHOP CONFIGURATION:\n' + resourceTexts.filter(Boolean).join('\n\n');
82
- }
83
- }
84
- catch (e) {
85
- logger.error(`Failed to fetch MCP resources: ${e.message}`);
86
- }
66
+ const resourceContext = await buildChatResourceContext(unchainedContext);
87
67
  const tools = {
88
68
  ...defaultUnchainedTools,
89
69
  ...additionalTools,
@@ -92,6 +72,7 @@ const setupMCPChatHandler = (chatConfiguration) => {
92
72
  tools.generateImage = generateImageHandler(req)(imageGenerationTool);
93
73
  }
94
74
  if (req.method === 'GET') {
75
+ await lifecycle.close();
95
76
  return res.status(200).send({
96
77
  tools: normalizeToolsIndex(tools),
97
78
  cached: false,
@@ -127,14 +108,28 @@ const setupMCPChatHandler = (chatConfiguration) => {
127
108
  const messagesToInclude = normalizedMessages.slice(startIndex);
128
109
  const result = streamText({
129
110
  stopWhen: stepCountIs(500),
130
- temperature: 0.2,
131
111
  maxRetries: 3,
132
112
  ...restChatConfig,
113
+ abortSignal: lifecycle.signal,
133
114
  messages: messagesToInclude,
134
115
  system: system + resourceContext,
116
+ model,
135
117
  tools: cacheControlledTools,
136
- onFinish: async () => {
137
- await client?.close();
118
+ onEnd: async (event) => {
119
+ try {
120
+ await (configuredOnEnd ?? configuredOnFinish)?.(event);
121
+ }
122
+ finally {
123
+ await lifecycle.close();
124
+ }
125
+ },
126
+ onAbort: async (event) => {
127
+ try {
128
+ await configuredOnAbort?.(event);
129
+ }
130
+ finally {
131
+ await lifecycle.close();
132
+ }
138
133
  },
139
134
  providerOptions: {
140
135
  anthropic: {
@@ -144,21 +139,26 @@ const setupMCPChatHandler = (chatConfiguration) => {
144
139
  },
145
140
  },
146
141
  });
142
+ void result.finishReason.then(undefined, async () => {
143
+ await lifecycle.close();
144
+ });
147
145
  return res.send(result.toUIMessageStreamResponse({
148
146
  onError: errorHandler,
149
147
  }));
150
148
  }
151
149
  catch (err) {
152
150
  logger.error(err);
153
- await client?.close();
154
- res.status(500);
151
+ await lifecycle.close();
152
+ if (lifecycle.signal.aborted || res.raw.destroyed)
153
+ return res;
154
+ res.status(chatErrorStatus(err));
155
155
  return res.send({ error: errorHandler(err) });
156
156
  }
157
157
  };
158
158
  return mcpChatHandler;
159
159
  };
160
160
  export const connectChat = (app, chatConfiguration) => {
161
- if (!createMCPClient) {
161
+ if (!createMCPClient || !streamText) {
162
162
  logger.warn('Optional dependencies for AI SDK Chat Handler are not installed. Please install @ai-sdk/mcp and ai packages to use this feature.');
163
163
  return;
164
164
  }
@@ -4,9 +4,13 @@ import type { UnchainedCore } from '@unchainedshop/core';
4
4
  import type { FastifyBaseLogger, FastifyInstance, FastifyPluginAsync } from 'fastify';
5
5
  import { connectChat } from './chatHandler.ts';
6
6
  import type { ChatConfiguration } from '../chat/utils.ts';
7
+ import { type AdminUIPluginConfig, type AdminUIThemeConfig } from '../adminUiPlugins.ts';
8
+ export type { AdminUIPluginConfig, AdminUIPluginEntityConfig, AdminUIPluginPageConfig, AdminUIPluginTabConfig, AdminUIPluginWidgetConfig, AdminUIPluginSlotConfig, AdminUIThemeTokens, AdminUIThemeConfig, } from '../adminUiPlugins.ts';
7
9
  export interface AdminUIRouterOptions {
8
- prefix: string;
10
+ prefix?: string;
9
11
  enabled?: boolean;
12
+ theme?: AdminUIThemeConfig;
13
+ plugins?: AdminUIPluginConfig[];
10
14
  }
11
15
  export declare const unchainedLogger: (prefix: string) => FastifyBaseLogger;
12
16
  export declare const connect: (fastify: FastifyInstance, { graphqlHandler, unchainedAPI, }: {
@@ -6,8 +6,11 @@ import { createLogger } from '@unchainedshop/logger';
6
6
  import mcpHandler from "./mcpHandler.js";
7
7
  import { connectChat } from "./chatHandler.js";
8
8
  import { mountRoutes } from "./mountRoutes.js";
9
- import { readFileSync } from 'node:fs';
9
+ import { createHash } from 'node:crypto';
10
+ import { existsSync, readFileSync } from 'node:fs';
11
+ import { join } from 'node:path';
10
12
  import { createBackchannelLogoutRoute } from "../handlers/createBackchannelLogoutHandler.js";
13
+ import { preparePluginAssets, resolveAdminUIPath, } from "../adminUiPlugins.js";
11
14
  const resolveUserRemoteAddress = (req, trustProxy = false) => {
12
15
  let remoteAddress;
13
16
  if (trustProxy) {
@@ -120,9 +123,60 @@ export const connect = async (fastify, { graphqlHandler, unchainedAPI, }, { allo
120
123
  }
121
124
  mountRoutes(fastify, unchainedAPI, routes);
122
125
  if (adminUI) {
126
+ const adminUIOptions = typeof adminUI === 'object' ? adminUI : undefined;
127
+ const adminUIPlugins = adminUIOptions?.plugins || [];
128
+ let themePromise;
129
+ const resolveThemeCSS = async () => {
130
+ let css = '/* default theme */';
131
+ if (adminUIOptions?.theme) {
132
+ try {
133
+ const { generateThemeCSS } = await import('@unchainedshop/admin-ui/theme');
134
+ css = generateThemeCSS(adminUIOptions.theme);
135
+ }
136
+ catch {
137
+ fastify.log.warn("npm dependency @unchainedshop/admin-ui is not installed, can't apply admin-ui theme");
138
+ }
139
+ }
140
+ const etag = `"${createHash('sha256').update(css).digest('hex').slice(0, 8)}"`;
141
+ return { css, etag };
142
+ };
143
+ fastify.get('/admin-ui-theme.css', async (request, reply) => {
144
+ const { css, etag } = await (themePromise ||= resolveThemeCSS());
145
+ if (request.headers['if-none-match'] === etag) {
146
+ return reply.code(304).send();
147
+ }
148
+ return reply
149
+ .header('Cache-Control', 'public, max-age=0, must-revalidate')
150
+ .header('ETag', etag)
151
+ .type('text/css')
152
+ .send(css);
153
+ });
154
+ const devMode = process.env.NODE_ENV !== 'production';
155
+ const { routes: pluginRoutes } = preparePluginAssets(adminUIPlugins, fastify.log, {
156
+ devMode,
157
+ });
158
+ for (const [path, asset] of pluginRoutes) {
159
+ fastify.get(path, async (request, reply) => {
160
+ if (asset.etag) {
161
+ const ifNoneMatch = request.headers['if-none-match'];
162
+ if (ifNoneMatch === asset.etag) {
163
+ return reply.code(304).send();
164
+ }
165
+ reply.header('ETag', asset.etag);
166
+ }
167
+ const body = typeof asset.content === 'function' ? asset.content() : asset.content;
168
+ return reply
169
+ .header('Cache-Control', asset.cacheControl)
170
+ .header('X-Content-Type-Options', 'nosniff')
171
+ .header('Access-Control-Allow-Origin', '*')
172
+ .type(asset.contentType)
173
+ .send(body);
174
+ });
175
+ }
123
176
  fastify.register(adminUIRouter, {
124
177
  enabled: true,
125
- prefix: typeof adminUI === 'object' ? adminUI.prefix : '/',
178
+ prefix: adminUIOptions?.prefix || '/',
179
+ plugins: adminUIPlugins,
126
180
  });
127
181
  }
128
182
  };
@@ -142,15 +196,6 @@ const fallbackLandingPageHandler = (request, reply) => {
142
196
  return reply.status(404).send();
143
197
  }
144
198
  };
145
- const resolveAdminUIPath = () => {
146
- try {
147
- const staticURL = import.meta.resolve('@unchainedshop/admin-ui');
148
- return new URL(staticURL).pathname.split('/').slice(0, -1).join('/');
149
- }
150
- catch {
151
- return null;
152
- }
153
- };
154
199
  export const adminUIRouter = async (fastify, opts) => {
155
200
  try {
156
201
  let fastifyStatic;
@@ -164,9 +209,23 @@ export const adminUIRouter = async (fastify, opts) => {
164
209
  if (fastifyStatic) {
165
210
  const adminUIPath = resolveAdminUIPath();
166
211
  if (adminUIPath) {
212
+ const indexHtml = readFileSync(join(adminUIPath, 'index.html'), 'utf-8');
213
+ const extHtmlPath = join(adminUIPath, 'ext', '[[...slug]]', 'index.html');
214
+ const extHtml = existsSync(extHtmlPath) ? readFileSync(extHtmlPath, 'utf-8') : null;
167
215
  await fastify.register(fastifyStatic, {
168
216
  root: adminUIPath,
169
217
  prefix: opts.prefix || '/',
218
+ wildcard: false,
219
+ });
220
+ fastify.setNotFoundHandler(async (request, reply) => {
221
+ if (request.method === 'GET' && !request.url.includes('.')) {
222
+ const urlPath = request.url.split('?')[0].replace(/\/+$/, '');
223
+ if (extHtml && (urlPath === '/ext' || urlPath.startsWith('/ext/'))) {
224
+ return reply.type('text/html').send(extHtml);
225
+ }
226
+ return reply.type('text/html').send(indexHtml);
227
+ }
228
+ return reply.code(404).send({ error: 'Not Found' });
170
229
  });
171
230
  return;
172
231
  }
@@ -1,83 +1,32 @@
1
- import initMCPServer from "../mcp/index.js";
1
+ import { Readable } from 'node:stream';
2
2
  import { createLogger } from '@unchainedshop/logger';
3
+ import handleMcpHttpRequest from "../mcp/handleMcpHttpRequest.js";
4
+ import { toWebRequest } from "../mcp/nodeHttpBridge.js";
3
5
  const logger = createLogger('unchained:api:mcp');
4
- let StreamableHTTPServerTransport;
5
- let isInitializeRequest;
6
- let McpServer;
7
- try {
8
- const mcpSDKServerLibrary = await import('@modelcontextprotocol/sdk/server/streamableHttp.js');
9
- const mcpSDKClient = await import('@modelcontextprotocol/sdk/types.js');
10
- const mcpSDKServer = await import('@modelcontextprotocol/sdk/server/mcp.js');
11
- StreamableHTTPServerTransport = mcpSDKServerLibrary.StreamableHTTPServerTransport;
12
- isInitializeRequest = mcpSDKClient.isInitializeRequest;
13
- McpServer = mcpSDKServer.McpServer;
14
- }
15
- catch {
16
- logger.warn(`optional peer npm package '@modelcontextprotocol/sdk' not installed, mcp will not work`);
17
- }
18
- const transports = {};
19
6
  const mcpHandler = async (req, res) => {
20
- if (!req.unchainedContext.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
7
  try {
29
- if (req.method === 'POST') {
30
- const sessionId = req.headers['mcp-session-id'];
31
- let transport;
32
- if (sessionId && transports[sessionId]) {
33
- transport = transports[sessionId];
34
- }
35
- else if (!sessionId && isInitializeRequest(req.body)) {
36
- transport = new StreamableHTTPServerTransport({
37
- sessionIdGenerator: () => crypto.randomUUID(),
38
- onsessioninitialized: (sessionId) => {
39
- transports[sessionId] = transport;
40
- },
41
- });
42
- transport.onclose = () => {
43
- if (transport.sessionId) {
44
- delete transports[transport.sessionId];
45
- }
46
- };
47
- const roles = req.unchainedContext.user.roles || [];
48
- const server = initMCPServer(new McpServer({
49
- name: 'Unchained MCP Server',
50
- version: '1.0.0',
51
- }), req.unchainedContext, roles);
52
- await server.connect(transport);
53
- }
54
- else {
55
- return res.status(400).send(JSON.stringify({
56
- jsonrpc: '2.0',
57
- error: {
58
- code: -32000,
59
- message: 'Bad Request: No valid session ID provided',
60
- },
61
- id: null,
62
- }));
63
- }
64
- await transport.handleRequest(req.raw, res.raw, req.body);
65
- return res;
66
- }
67
- else if (req.method === 'GET' || req.method === 'DELETE') {
68
- const sessionId = req.headers['mcp-session-id'];
69
- if (!sessionId || !transports[sessionId]) {
70
- return res.status(400).send('Invalid or missing session ID');
71
- }
72
- const transport = transports[sessionId];
73
- await transport.handleRequest(req.raw, res.raw);
74
- return res;
75
- }
76
- 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));
77
22
  }
78
- catch (e) {
79
- logger.error(e);
80
- return res.status(503).send({ name: e.name, code: e.code, message: e.message });
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;
81
30
  }
82
31
  };
83
32
  export default mcpHandler;
@@ -1,38 +1,40 @@
1
1
  import DataLoader from 'dataloader';
2
2
  export default (unchainedAPI) => new DataLoader(async (queries) => {
3
- const parentAssortmentIds = queries.flatMap((q) => q.parentAssortmentId).filter(Boolean);
4
- const childAssortmentIds = queries.flatMap((q) => q.childAssortmentId).filter(Boolean);
5
- const assortmentIds = queries.flatMap((q) => q.assortmentId).filter(Boolean);
3
+ const parentAssortmentIds = [
4
+ ...new Set(queries.flatMap((q) => q.parentAssortmentId).filter(Boolean)),
5
+ ];
6
+ const childAssortmentIds = [
7
+ ...new Set(queries.flatMap((q) => q.childAssortmentId).filter(Boolean)),
8
+ ];
9
+ const assortmentIds = [
10
+ ...new Set(queries.flatMap((q) => q.assortmentId).filter(Boolean)),
11
+ ];
6
12
  const allLinks = await unchainedAPI.modules.assortments.links.findLinks({
7
- assortmentIds: [...new Set([...parentAssortmentIds, ...childAssortmentIds, ...assortmentIds])],
13
+ ...(parentAssortmentIds.length ? { parentAssortmentIds } : {}),
14
+ ...(childAssortmentIds.length ? { childAssortmentIds } : {}),
15
+ ...(assortmentIds.length ? { assortmentIds } : {}),
8
16
  });
9
- const parentAssortmentLinkMap = {};
10
- const childAssortmentLinkMap = {};
17
+ const parentAssortmentLinkMap = new Map();
18
+ const childAssortmentLinkMap = new Map();
11
19
  for (const link of allLinks) {
12
- if (!parentAssortmentLinkMap[link.parentAssortmentId]) {
13
- parentAssortmentLinkMap[link.parentAssortmentId] = [link];
14
- }
15
- else {
16
- parentAssortmentLinkMap[link.parentAssortmentId].push(link);
17
- }
18
- if (!childAssortmentLinkMap[link.childAssortmentId]) {
19
- childAssortmentLinkMap[link.childAssortmentId] = [link];
20
- }
21
- else {
22
- childAssortmentLinkMap[link.childAssortmentId].push(link);
23
- }
20
+ const parentLinks = parentAssortmentLinkMap.get(link.parentAssortmentId) || [];
21
+ parentLinks.push(link);
22
+ parentAssortmentLinkMap.set(link.parentAssortmentId, parentLinks);
23
+ const childLinks = childAssortmentLinkMap.get(link.childAssortmentId) || [];
24
+ childLinks.push(link);
25
+ childAssortmentLinkMap.set(link.childAssortmentId, childLinks);
24
26
  }
25
27
  return queries.map((q) => {
26
28
  if (q.parentAssortmentId) {
27
- return parentAssortmentLinkMap[q.parentAssortmentId] || [];
29
+ return parentAssortmentLinkMap.get(q.parentAssortmentId) || [];
28
30
  }
29
31
  else if (q.childAssortmentId) {
30
- return childAssortmentLinkMap[q.childAssortmentId] || [];
32
+ return childAssortmentLinkMap.get(q.childAssortmentId) || [];
31
33
  }
32
34
  if (q.assortmentId) {
33
35
  return [
34
- ...(parentAssortmentLinkMap[q.assortmentId] || []),
35
- ...(childAssortmentLinkMap[q.assortmentId] || []),
36
+ ...(parentAssortmentLinkMap.get(q.assortmentId) || []),
37
+ ...(childAssortmentLinkMap.get(q.assortmentId) || []),
36
38
  ];
37
39
  }
38
40
  return [];
@@ -6,6 +6,7 @@ export default (unchainedAPI) => new DataLoader(async (queries) => {
6
6
  const texts = await unchainedAPI.modules.assortments.media.texts.findMediaTexts({ assortmentMediaIds }, {
7
7
  sort: {
8
8
  assortmentMediaId: 1,
9
+ _id: 1,
9
10
  },
10
11
  });
11
12
  const localeMap = buildLocaleMap(queries, texts);
@@ -1,13 +1,16 @@
1
1
  import DataLoader from 'dataloader';
2
2
  export default (unchainedAPI) => new DataLoader(async (queries) => {
3
3
  const assortmentIds = [...new Set(queries.map((q) => q.assortmentId).filter(Boolean))];
4
+ const productIds = [...new Set(queries.map((q) => q.productId).filter(Boolean))];
4
5
  const assortmentProducts = await unchainedAPI.modules.assortments.products.findAssortmentProducts({
5
6
  assortmentIds,
7
+ productIds,
6
8
  });
7
- const assortmentProductMap = {};
9
+ const assortmentProductMap = new Map();
8
10
  for (const assortmentProduct of assortmentProducts) {
9
- assortmentProductMap[assortmentProduct.assortmentId + assortmentProduct.productId] =
10
- assortmentProduct;
11
+ const productsById = assortmentProductMap.get(assortmentProduct.assortmentId) || new Map();
12
+ productsById.set(assortmentProduct.productId, assortmentProduct);
13
+ assortmentProductMap.set(assortmentProduct.assortmentId, productsById);
11
14
  }
12
- return queries.map((q) => assortmentProductMap[q.assortmentId + q.productId]);
15
+ return queries.map((q) => assortmentProductMap.get(q.assortmentId)?.get(q.productId));
13
16
  });
@@ -6,6 +6,7 @@ export default (unchainedAPI) => new DataLoader(async (queries) => {
6
6
  const texts = await unchainedAPI.modules.assortments.texts.findTexts({ assortmentIds }, {
7
7
  sort: {
8
8
  assortmentId: 1,
9
+ _id: 1,
9
10
  },
10
11
  });
11
12
  const localeMap = buildLocaleMap(queries, texts);
@@ -0,0 +1,13 @@
1
+ import type { UnchainedCore } from '@unchainedshop/core';
2
+ import type { Order } from '@unchainedshop/core-orders';
3
+ import DataLoader from 'dataloader';
4
+ declare const _default: (unchainedAPI: UnchainedCore) => DataLoader<{
5
+ userId: string;
6
+ countryCode?: string;
7
+ orderNumber?: string;
8
+ }, Order | null, {
9
+ userId: string;
10
+ countryCode?: string;
11
+ orderNumber?: string;
12
+ }>;
13
+ export default _default;
@@ -0,0 +1,8 @@
1
+ import DataLoader from 'dataloader';
2
+ export default (unchainedAPI) => new DataLoader(async (queries) => {
3
+ const userIds = [...new Set(queries.map((q) => q.userId).filter(Boolean))];
4
+ const carts = await unchainedAPI.modules.orders.findCarts({ userIds });
5
+ return queries.map((q) => carts.find((cart) => cart.userId === q.userId &&
6
+ (!q.countryCode || cart.countryCode === q.countryCode) &&
7
+ (!q.orderNumber || cart.orderNumber === q.orderNumber)) || null);
8
+ });
@@ -6,6 +6,7 @@ export default (unchainedAPI) => new DataLoader(async (queries) => {
6
6
  const texts = await unchainedAPI.modules.filters.texts.findTexts({ filterIds }, {
7
7
  sort: {
8
8
  filterId: 1,
9
+ _id: 1,
9
10
  },
10
11
  });
11
12
  const localeMap = buildLocaleMap(queries, texts);
@@ -166,6 +166,15 @@ declare const loaders: (unchainedAPI: UnchainedCore) => {
166
166
  }, import("@unchainedshop/core-orders").Order, {
167
167
  orderId: string;
168
168
  }>;
169
+ cartLoader: import("dataloader")<{
170
+ userId: string;
171
+ countryCode?: string;
172
+ orderNumber?: string;
173
+ }, import("@unchainedshop/core-orders").Order | null, {
174
+ userId: string;
175
+ countryCode?: string;
176
+ orderNumber?: string;
177
+ }>;
169
178
  quotationLoader: import("dataloader")<{
170
179
  quotationId: string;
171
180
  }, import("@unchainedshop/core-quotations").Quotation | null, {
@@ -26,6 +26,7 @@ import deliveryProviderLoader from "./deliveryProviderLoader.js";
26
26
  import paymentProviderLoader from "./paymentProviderLoader.js";
27
27
  import warehousingProviderLoader from "./warehousingProviderLoader.js";
28
28
  import orderLoader from "./orderLoader.js";
29
+ import cartLoader from "./cartLoader.js";
29
30
  import quotationLoader from "./quotationLoader.js";
30
31
  import tokenExportStatusLoader from "./tokenExportStatusLoader.js";
31
32
  const loaders = (unchainedAPI) => {
@@ -58,6 +59,7 @@ const loaders = (unchainedAPI) => {
58
59
  paymentProviderLoader: paymentProviderLoader(unchainedAPI),
59
60
  warehousingProviderLoader: warehousingProviderLoader(unchainedAPI),
60
61
  orderLoader: orderLoader(unchainedAPI),
62
+ cartLoader: cartLoader(unchainedAPI),
61
63
  quotationLoader: quotationLoader(unchainedAPI),
62
64
  tokenExportStatusLoader: tokenExportStatusLoader(unchainedAPI),
63
65
  };
@@ -6,6 +6,7 @@ export default (unchainedAPI) => new DataLoader(async (queries) => {
6
6
  const texts = await unchainedAPI.modules.products.media.texts.findMediaTexts({ productMediaIds }, {
7
7
  sort: {
8
8
  productMediaId: 1,
9
+ _id: 1,
9
10
  },
10
11
  });
11
12
  const localeMap = buildLocaleMap(queries, texts);
@@ -6,6 +6,7 @@ export default (unchainedAPI) => new DataLoader(async (queries) => {
6
6
  const texts = await unchainedAPI.modules.products.texts.findTexts({ productIds }, {
7
7
  sort: {
8
8
  productId: 1,
9
+ _id: 1,
9
10
  },
10
11
  });
11
12
  const localeMap = buildLocaleMap(queries, texts);
@@ -8,6 +8,7 @@ export default (unchainedAPI) => new DataLoader(async (queries) => {
8
8
  }, {
9
9
  sort: {
10
10
  productVariationId: 1,
11
+ _id: 1,
11
12
  },
12
13
  });
13
14
  const localeMap = buildLocaleMap(queries, texts);
@@ -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>;