@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
@@ -1,89 +1,60 @@
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
+ }
35
42
  let client;
43
+ const lifecycle = createChatRequestLifecycle(res.raw, configuredAbortSignal);
36
44
  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: {
45
+ client = await createMCPClient({
46
+ transport: {
47
+ type: 'http',
48
+ url: unchainedMCPUrl,
55
49
  headers: {
56
50
  Cookie: req.headers.cookie || '',
57
51
  },
58
52
  },
53
+ initializationOptions: { signal: lifecycle.signal },
59
54
  });
60
- client = await createMCPClient({
61
- transport,
62
- });
55
+ lifecycle.setClientClose(() => client.close());
63
56
  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
- }
57
+ const resourceContext = await buildChatResourceContext(req.unchainedContext);
87
58
  const tools = {
88
59
  ...defaultUnchainedTools,
89
60
  ...additionalTools,
@@ -92,6 +63,7 @@ const setupMCPChatHandler = (chatConfiguration) => {
92
63
  tools.generateImage = generateImageHandler(req)(imageGenerationTool);
93
64
  }
94
65
  if (req.method === 'GET') {
66
+ await lifecycle.close();
95
67
  return res.status(200).send({
96
68
  tools: normalizeToolsIndex(tools),
97
69
  cached: false,
@@ -127,14 +99,28 @@ const setupMCPChatHandler = (chatConfiguration) => {
127
99
  const messagesToInclude = normalizedMessages.slice(startIndex);
128
100
  const result = streamText({
129
101
  stopWhen: stepCountIs(500),
130
- temperature: 0.2,
131
102
  maxRetries: 3,
132
103
  ...restChatConfig,
104
+ abortSignal: lifecycle.signal,
133
105
  messages: messagesToInclude,
134
106
  system: system + resourceContext,
107
+ model,
135
108
  tools: cacheControlledTools,
136
- onFinish: async () => {
137
- await client?.close();
109
+ onEnd: async (event) => {
110
+ try {
111
+ await (configuredOnEnd ?? configuredOnFinish)?.(event);
112
+ }
113
+ finally {
114
+ await lifecycle.close();
115
+ }
116
+ },
117
+ onAbort: async (event) => {
118
+ try {
119
+ await configuredOnAbort?.(event);
120
+ }
121
+ finally {
122
+ await lifecycle.close();
123
+ }
138
124
  },
139
125
  providerOptions: {
140
126
  anthropic: {
@@ -144,21 +130,26 @@ const setupMCPChatHandler = (chatConfiguration) => {
144
130
  },
145
131
  },
146
132
  });
133
+ void result.finishReason.then(undefined, async () => {
134
+ await lifecycle.close();
135
+ });
147
136
  return res.send(result.toUIMessageStreamResponse({
148
137
  onError: errorHandler,
149
138
  }));
150
139
  }
151
140
  catch (err) {
152
141
  logger.error(err);
153
- await client?.close();
154
- res.status(500);
142
+ await lifecycle.close();
143
+ if (lifecycle.signal.aborted || res.raw.destroyed)
144
+ return res;
145
+ res.status(chatErrorStatus(err));
155
146
  return res.send({ error: errorHandler(err) });
156
147
  }
157
148
  };
158
149
  return mcpChatHandler;
159
150
  };
160
151
  export const connectChat = (app, chatConfiguration) => {
161
- if (!createMCPClient) {
152
+ if (!createMCPClient || !streamText) {
162
153
  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
154
  return;
164
155
  }
@@ -4,9 +4,15 @@ 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 AdminUIThemeConfig } from '@unchainedshop/admin-ui/theme';
8
+ import { type AdminUIPluginConfig } from '../adminUiPlugins.ts';
9
+ export type { AdminUIPluginConfig, AdminUIPluginEntityConfig, AdminUIPluginPageConfig, AdminUIPluginTabConfig, AdminUIPluginWidgetConfig, AdminUIPluginSlotConfig, } from '../adminUiPlugins.ts';
10
+ export type { AdminUIThemeTokens, AdminUIThemeConfig } from '@unchainedshop/admin-ui/theme';
7
11
  export interface AdminUIRouterOptions {
8
- prefix: string;
12
+ prefix?: string;
9
13
  enabled?: boolean;
14
+ theme?: AdminUIThemeConfig;
15
+ plugins?: AdminUIPluginConfig[];
10
16
  }
11
17
  export declare const unchainedLogger: (prefix: string) => FastifyBaseLogger;
12
18
  export declare const connect: (fastify: FastifyInstance, { graphqlHandler, unchainedAPI, }: {
@@ -6,8 +6,12 @@ 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 { generateThemeCSS } from '@unchainedshop/admin-ui/theme';
14
+ import { preparePluginAssets, resolveAdminUIPath } from "../adminUiPlugins.js";
11
15
  const resolveUserRemoteAddress = (req, trustProxy = false) => {
12
16
  let remoteAddress;
13
17
  if (trustProxy) {
@@ -120,9 +124,47 @@ export const connect = async (fastify, { graphqlHandler, unchainedAPI, }, { allo
120
124
  }
121
125
  mountRoutes(fastify, unchainedAPI, routes);
122
126
  if (adminUI) {
127
+ const adminUIOptions = typeof adminUI === 'object' ? adminUI : undefined;
128
+ const adminUIPlugins = adminUIOptions?.plugins || [];
129
+ const themeCSS = generateThemeCSS(adminUIOptions?.theme);
130
+ const themeHash = createHash('sha256').update(themeCSS).digest('hex').slice(0, 8);
131
+ const themeEtag = `"${themeHash}"`;
132
+ fastify.get('/admin-ui-theme.css', async (request, reply) => {
133
+ if (request.headers['if-none-match'] === themeEtag) {
134
+ return reply.code(304).send();
135
+ }
136
+ return reply
137
+ .header('Cache-Control', 'public, max-age=0, must-revalidate')
138
+ .header('ETag', themeEtag)
139
+ .type('text/css')
140
+ .send(themeCSS);
141
+ });
142
+ const devMode = process.env.NODE_ENV !== 'production';
143
+ const { routes: pluginRoutes } = preparePluginAssets(adminUIPlugins, fastify.log, {
144
+ devMode,
145
+ });
146
+ for (const [path, asset] of pluginRoutes) {
147
+ fastify.get(path, async (request, reply) => {
148
+ if (asset.etag) {
149
+ const ifNoneMatch = request.headers['if-none-match'];
150
+ if (ifNoneMatch === asset.etag) {
151
+ return reply.code(304).send();
152
+ }
153
+ reply.header('ETag', asset.etag);
154
+ }
155
+ const body = typeof asset.content === 'function' ? asset.content() : asset.content;
156
+ return reply
157
+ .header('Cache-Control', asset.cacheControl)
158
+ .header('X-Content-Type-Options', 'nosniff')
159
+ .header('Access-Control-Allow-Origin', '*')
160
+ .type(asset.contentType)
161
+ .send(body);
162
+ });
163
+ }
123
164
  fastify.register(adminUIRouter, {
124
165
  enabled: true,
125
- prefix: typeof adminUI === 'object' ? adminUI.prefix : '/',
166
+ prefix: adminUIOptions?.prefix || '/',
167
+ plugins: adminUIPlugins,
126
168
  });
127
169
  }
128
170
  };
@@ -142,15 +184,6 @@ const fallbackLandingPageHandler = (request, reply) => {
142
184
  return reply.status(404).send();
143
185
  }
144
186
  };
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
187
  export const adminUIRouter = async (fastify, opts) => {
155
188
  try {
156
189
  let fastifyStatic;
@@ -164,9 +197,23 @@ export const adminUIRouter = async (fastify, opts) => {
164
197
  if (fastifyStatic) {
165
198
  const adminUIPath = resolveAdminUIPath();
166
199
  if (adminUIPath) {
200
+ const indexHtml = readFileSync(join(adminUIPath, 'index.html'), 'utf-8');
201
+ const extHtmlPath = join(adminUIPath, 'ext', '[[...slug]]', 'index.html');
202
+ const extHtml = existsSync(extHtmlPath) ? readFileSync(extHtmlPath, 'utf-8') : null;
167
203
  await fastify.register(fastifyStatic, {
168
204
  root: adminUIPath,
169
205
  prefix: opts.prefix || '/',
206
+ wildcard: false,
207
+ });
208
+ fastify.setNotFoundHandler(async (request, reply) => {
209
+ if (request.method === 'GET' && !request.url.includes('.')) {
210
+ const urlPath = request.url.split('?')[0].replace(/\/+$/, '');
211
+ if (extHtml && (urlPath === '/ext' || urlPath.startsWith('/ext/'))) {
212
+ return reply.type('text/html').send(extHtml);
213
+ }
214
+ return reply.type('text/html').send(indexHtml);
215
+ }
216
+ return reply.code(404).send({ error: 'Not Found' });
170
217
  });
171
218
  return;
172
219
  }
@@ -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;
@@ -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
+ });
@@ -0,0 +1,9 @@
1
+ import type { UnchainedCore } from '@unchainedshop/core';
2
+ import type { Enrollment } from '@unchainedshop/core-enrollments';
3
+ import DataLoader from 'dataloader';
4
+ declare const _default: (unchainedAPI: UnchainedCore) => DataLoader<{
5
+ orderId: string;
6
+ }, Enrollment | null, {
7
+ orderId: string;
8
+ }>;
9
+ export default _default;
@@ -0,0 +1,17 @@
1
+ import DataLoader from 'dataloader';
2
+ export default (unchainedAPI) => new DataLoader(async (queries) => {
3
+ const orderIds = [...new Set(queries.map((q) => q.orderId).filter(Boolean))];
4
+ const requestedOrderIds = new Set(orderIds);
5
+ const enrollments = await unchainedAPI.modules.enrollments.findEnrollmentsByOrderIds({
6
+ orderIds,
7
+ });
8
+ const enrollmentMap = {};
9
+ for (const enrollment of enrollments) {
10
+ for (const period of enrollment.periods || []) {
11
+ if (period.orderId && requestedOrderIds.has(period.orderId) && !enrollmentMap[period.orderId]) {
12
+ enrollmentMap[period.orderId] = enrollment;
13
+ }
14
+ }
15
+ }
16
+ return queries.map((q) => enrollmentMap[q.orderId] ?? null);
17
+ });
@@ -166,11 +166,25 @@ 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, {
172
181
  quotationId: string;
173
182
  }>;
183
+ tokenExportStatusLoader: import("dataloader")<{
184
+ token: import("@unchainedshop/core-warehousing").TokenSurrogate;
185
+ }, import("@unchainedshop/core-warehousing").TokenStatus, {
186
+ token: import("@unchainedshop/core-warehousing").TokenSurrogate;
187
+ }>;
174
188
  };
175
189
  export type UnchainedLoaders = Awaited<ReturnType<typeof loaders>>;
176
190
  export default loaders;
@@ -26,7 +26,9 @@ 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";
31
+ import tokenExportStatusLoader from "./tokenExportStatusLoader.js";
30
32
  const loaders = (unchainedAPI) => {
31
33
  return {
32
34
  assortmentLoader: assortmentLoader(unchainedAPI),
@@ -57,7 +59,9 @@ const loaders = (unchainedAPI) => {
57
59
  paymentProviderLoader: paymentProviderLoader(unchainedAPI),
58
60
  warehousingProviderLoader: warehousingProviderLoader(unchainedAPI),
59
61
  orderLoader: orderLoader(unchainedAPI),
62
+ cartLoader: cartLoader(unchainedAPI),
60
63
  quotationLoader: quotationLoader(unchainedAPI),
64
+ tokenExportStatusLoader: tokenExportStatusLoader(unchainedAPI),
61
65
  };
62
66
  };
63
67
  export default loaders;
@@ -0,0 +1,9 @@
1
+ import type { UnchainedCore } from '@unchainedshop/core';
2
+ import type { OrderDelivery } from '@unchainedshop/core-orders';
3
+ import DataLoader from 'dataloader';
4
+ declare const _default: (unchainedAPI: UnchainedCore) => DataLoader<{
5
+ orderDeliveryId: string;
6
+ }, OrderDelivery | null, {
7
+ orderDeliveryId: string;
8
+ }>;
9
+ export default _default;
@@ -0,0 +1,12 @@
1
+ import DataLoader from 'dataloader';
2
+ export default (unchainedAPI) => new DataLoader(async (queries) => {
3
+ const orderDeliveryIds = [...new Set(queries.map((q) => q.orderDeliveryId).filter(Boolean))];
4
+ const deliveries = await unchainedAPI.modules.orders.deliveries.findDeliveries({
5
+ orderDeliveryIds,
6
+ });
7
+ const deliveryMap = {};
8
+ for (const delivery of deliveries) {
9
+ deliveryMap[delivery._id] = delivery;
10
+ }
11
+ return queries.map((q) => deliveryMap[q.orderDeliveryId] ?? null);
12
+ });
@@ -0,0 +1,9 @@
1
+ import type { UnchainedCore } from '@unchainedshop/core';
2
+ import type { OrderDiscount } from '@unchainedshop/core-orders';
3
+ import DataLoader from 'dataloader';
4
+ declare const _default: (unchainedAPI: UnchainedCore) => DataLoader<{
5
+ orderId: string;
6
+ }, OrderDiscount[], {
7
+ orderId: string;
8
+ }>;
9
+ export default _default;
@@ -0,0 +1,17 @@
1
+ import DataLoader from 'dataloader';
2
+ export default (unchainedAPI) => new DataLoader(async (queries) => {
3
+ const orderIds = [...new Set(queries.map((q) => q.orderId).filter(Boolean))];
4
+ const discounts = await unchainedAPI.modules.orders.discounts.findOrderDiscounts({
5
+ orderIds,
6
+ });
7
+ const discountsMap = {};
8
+ for (const discount of discounts) {
9
+ if (!discountsMap[discount.orderId]) {
10
+ discountsMap[discount.orderId] = [discount];
11
+ }
12
+ else {
13
+ discountsMap[discount.orderId].push(discount);
14
+ }
15
+ }
16
+ return queries.map((q) => discountsMap[q.orderId] || []);
17
+ });
@@ -0,0 +1,9 @@
1
+ import type { UnchainedCore } from '@unchainedshop/core';
2
+ import type { OrderPayment } from '@unchainedshop/core-orders';
3
+ import DataLoader from 'dataloader';
4
+ declare const _default: (unchainedAPI: UnchainedCore) => DataLoader<{
5
+ orderPaymentId: string;
6
+ }, OrderPayment | null, {
7
+ orderPaymentId: string;
8
+ }>;
9
+ export default _default;
@@ -0,0 +1,12 @@
1
+ import DataLoader from 'dataloader';
2
+ export default (unchainedAPI) => new DataLoader(async (queries) => {
3
+ const orderPaymentIds = [...new Set(queries.map((q) => q.orderPaymentId).filter(Boolean))];
4
+ const payments = await unchainedAPI.modules.orders.payments.findOrderPayments({
5
+ orderPaymentIds,
6
+ });
7
+ const paymentMap = {};
8
+ for (const payment of payments) {
9
+ paymentMap[payment._id] = payment;
10
+ }
11
+ return queries.map((q) => paymentMap[q.orderPaymentId] ?? null);
12
+ });
@@ -0,0 +1,9 @@
1
+ import type { UnchainedCore } from '@unchainedshop/core';
2
+ import type { OrderPosition } from '@unchainedshop/core-orders';
3
+ import DataLoader from 'dataloader';
4
+ declare const _default: (unchainedAPI: UnchainedCore) => DataLoader<{
5
+ orderId: string;
6
+ }, OrderPosition[], {
7
+ orderId: string;
8
+ }>;
9
+ export default _default;
@@ -0,0 +1,17 @@
1
+ import DataLoader from 'dataloader';
2
+ export default (unchainedAPI) => new DataLoader(async (queries) => {
3
+ const orderIds = [...new Set(queries.map((q) => q.orderId).filter(Boolean))];
4
+ const positions = await unchainedAPI.modules.orders.positions.findOrderPositions({
5
+ orderIds,
6
+ });
7
+ const positionsMap = {};
8
+ for (const position of positions) {
9
+ if (!positionsMap[position.orderId]) {
10
+ positionsMap[position.orderId] = [position];
11
+ }
12
+ else {
13
+ positionsMap[position.orderId].push(position);
14
+ }
15
+ }
16
+ return queries.map((q) => positionsMap[q.orderId] || []);
17
+ });
@@ -0,0 +1,9 @@
1
+ import type { UnchainedCore } from '@unchainedshop/core';
2
+ import type { TokenSurrogate, TokenStatus as TokenStatusType } from '@unchainedshop/core-warehousing';
3
+ import DataLoader from 'dataloader';
4
+ declare const _default: (unchainedAPI: UnchainedCore) => DataLoader<{
5
+ token: TokenSurrogate;
6
+ }, TokenStatusType, {
7
+ token: TokenSurrogate;
8
+ }>;
9
+ export default _default;