@unchainedshop/api 4.0.0-rc.15 → 4.0.0-rc.16

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/lib/chat/defaultSystemPrompt.d.ts +3 -0
  2. package/lib/chat/defaultSystemPrompt.d.ts.map +1 -0
  3. package/lib/chat/defaultSystemPrompt.js +7 -0
  4. package/lib/chat/defaultSystemPrompt.js.map +1 -0
  5. package/lib/chat/generateImageHandler.d.ts +7 -0
  6. package/lib/chat/generateImageHandler.d.ts.map +1 -0
  7. package/lib/chat/generateImageHandler.js +64 -0
  8. package/lib/chat/generateImageHandler.js.map +1 -0
  9. package/lib/chat/normalizeToolsIndex.d.ts +7 -0
  10. package/lib/chat/normalizeToolsIndex.d.ts.map +1 -0
  11. package/lib/chat/normalizeToolsIndex.js +11 -0
  12. package/lib/chat/normalizeToolsIndex.js.map +1 -0
  13. package/lib/chat/utils.d.ts +13 -0
  14. package/lib/chat/utils.d.ts.map +1 -0
  15. package/lib/chat/utils.js +32 -0
  16. package/lib/chat/utils.js.map +1 -0
  17. package/lib/context.d.ts +3 -0
  18. package/lib/context.d.ts.map +1 -1
  19. package/lib/context.js.map +1 -1
  20. package/lib/express/chatHandler.d.ts +5 -0
  21. package/lib/express/chatHandler.d.ts.map +1 -0
  22. package/lib/express/chatHandler.js +116 -0
  23. package/lib/express/chatHandler.js.map +1 -0
  24. package/lib/express/createBulkImportMiddleware.d.ts.map +1 -1
  25. package/lib/express/createBulkImportMiddleware.js +6 -7
  26. package/lib/express/createBulkImportMiddleware.js.map +1 -1
  27. package/lib/express/createERCMetadataMiddleware.d.ts.map +1 -1
  28. package/lib/express/createERCMetadataMiddleware.js +5 -9
  29. package/lib/express/createERCMetadataMiddleware.js.map +1 -1
  30. package/lib/express/createTempUploadMiddleware.d.ts.map +1 -1
  31. package/lib/express/createTempUploadMiddleware.js +4 -8
  32. package/lib/express/createTempUploadMiddleware.js.map +1 -1
  33. package/lib/express/index.d.ts +1 -0
  34. package/lib/express/index.d.ts.map +1 -1
  35. package/lib/express/index.js +1 -0
  36. package/lib/express/index.js.map +1 -1
  37. package/lib/fastify/bulkImportHandler.js +5 -6
  38. package/lib/fastify/bulkImportHandler.js.map +1 -1
  39. package/lib/fastify/chatHandler.d.ts +4 -0
  40. package/lib/fastify/chatHandler.d.ts.map +1 -0
  41. package/lib/fastify/chatHandler.js +110 -0
  42. package/lib/fastify/chatHandler.js.map +1 -0
  43. package/lib/fastify/ercMetadataHandler.d.ts.map +1 -1
  44. package/lib/fastify/ercMetadataHandler.js +3 -6
  45. package/lib/fastify/ercMetadataHandler.js.map +1 -1
  46. package/lib/fastify/index.d.ts +6 -1
  47. package/lib/fastify/index.d.ts.map +1 -1
  48. package/lib/fastify/index.js +28 -0
  49. package/lib/fastify/index.js.map +1 -1
  50. package/lib/fastify/mcpHandler.js +2 -2
  51. package/lib/fastify/mcpHandler.js.map +1 -1
  52. package/lib/fastify/tempUploadHandler.d.ts.map +1 -1
  53. package/lib/fastify/tempUploadHandler.js +3 -6
  54. package/lib/fastify/tempUploadHandler.js.map +1 -1
  55. package/lib/mcp/tools/product/handlers/addProductMedia.d.ts +26 -1
  56. package/lib/mcp/tools/product/handlers/addProductMedia.d.ts.map +1 -1
  57. package/lib/mcp/tools/product/handlers/addProductMedia.js +4 -4
  58. package/lib/mcp/tools/product/handlers/addProductMedia.js.map +1 -1
  59. package/lib/mcp/tools/product/handlers/updateProduct.d.ts.map +1 -1
  60. package/lib/mcp/tools/product/handlers/updateProduct.js +10 -2
  61. package/lib/mcp/tools/product/handlers/updateProduct.js.map +1 -1
  62. package/lib/mcp/tools/product/schemas.d.ts.map +1 -1
  63. package/lib/mcp/tools/product/schemas.js +3 -3
  64. package/lib/mcp/tools/product/schemas.js.map +1 -1
  65. package/lib/mcp/tools/users/schemas.d.ts +14 -14
  66. package/lib/mcp/utils/getNormalizedProductDetails.d.ts.map +1 -1
  67. package/lib/mcp/utils/getNormalizedProductDetails.js +3 -1
  68. package/lib/mcp/utils/getNormalizedProductDetails.js.map +1 -1
  69. package/lib/resolvers/mutations/orders/checkoutCart.js +1 -1
  70. package/lib/resolvers/mutations/orders/checkoutCart.js.map +1 -1
  71. package/lib/resolvers/queries/shopInfo.d.ts.map +1 -1
  72. package/lib/resolvers/queries/shopInfo.js +49 -3
  73. package/lib/resolvers/queries/shopInfo.js.map +1 -1
  74. package/lib/resolvers/type/index.d.ts +1 -1
  75. package/lib/resolvers/type/media-types.d.ts.map +1 -1
  76. package/lib/resolvers/type/media-types.js +3 -1
  77. package/lib/resolvers/type/media-types.js.map +1 -1
  78. package/lib/resolvers/type/price-types.d.ts +1 -1
  79. package/lib/resolvers/type/price-types.d.ts.map +1 -1
  80. package/lib/resolvers/type/price-types.js +5 -2
  81. package/lib/resolvers/type/price-types.js.map +1 -1
  82. package/lib/resolvers/type/shop-types.d.ts.map +1 -1
  83. package/lib/resolvers/type/shop-types.js.map +1 -1
  84. package/lib/schema/query.d.ts.map +1 -1
  85. package/lib/schema/query.js +2 -0
  86. package/lib/schema/query.js.map +1 -1
  87. package/lib/schema/types/shop.d.ts.map +1 -1
  88. package/lib/schema/types/shop.js +3 -0
  89. package/lib/schema/types/shop.js.map +1 -1
  90. package/package.json +13 -3
  91. package/src/chat/defaultSystemPrompt.ts +7 -0
  92. package/src/chat/generateImageHandler.ts +78 -0
  93. package/src/chat/normalizeToolsIndex.ts +11 -0
  94. package/src/chat/utils.ts +31 -0
  95. package/src/context.ts +3 -0
  96. package/src/express/chatHandler.ts +147 -0
  97. package/src/express/createBulkImportMiddleware.ts +7 -7
  98. package/src/express/createERCMetadataMiddleware.ts +5 -9
  99. package/src/express/createTempUploadMiddleware.ts +4 -9
  100. package/src/express/index.ts +2 -0
  101. package/src/fastify/bulkImportHandler.ts +6 -6
  102. package/src/fastify/chatHandler.ts +140 -0
  103. package/src/fastify/ercMetadataHandler.ts +3 -6
  104. package/src/fastify/index.ts +37 -1
  105. package/src/fastify/mcpHandler.ts +2 -2
  106. package/src/fastify/tempUploadHandler.ts +3 -6
  107. package/src/mcp/tools/product/handlers/addProductMedia.ts +4 -5
  108. package/src/mcp/tools/product/handlers/updateProduct.ts +12 -1
  109. package/src/mcp/tools/product/schemas.ts +7 -3
  110. package/src/mcp/utils/getNormalizedProductDetails.ts +4 -1
  111. package/src/resolvers/mutations/orders/checkoutCart.ts +1 -1
  112. package/src/resolvers/queries/shopInfo.ts +49 -3
  113. package/src/resolvers/type/media-types.ts +4 -1
  114. package/src/resolvers/type/price-types.ts +5 -3
  115. package/src/resolvers/type/shop-types.ts +0 -1
  116. package/src/schema/query.ts +2 -0
  117. package/src/schema/types/shop.ts +3 -0
@@ -0,0 +1,147 @@
1
+ import express from 'express';
2
+ import type { Express, Request, RequestHandler, Response } from 'express';
3
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
4
+ import {
5
+ convertToModelMessages,
6
+ experimental_createMCPClient as createMCPClient,
7
+ MCPTransport,
8
+ stepCountIs,
9
+ streamText,
10
+ ToolSet,
11
+ } from 'ai';
12
+ import { ChatConfiguration, errorHandler } from '../chat/utils.js';
13
+ import generateImageHandler from '../chat/generateImageHandler.js';
14
+ import defaultSystemPrompt from '../chat/defaultSystemPrompt.js';
15
+ import normalizeToolsIndex from '../chat/normalizeToolsIndex.js';
16
+
17
+ export const expressRouter = () => {
18
+ const router = express.Router();
19
+
20
+ const staticURL = import.meta.resolve('@unchainedshop/admin-ui');
21
+ const staticPath = new URL(staticURL).pathname.split('/').slice(0, -1).join('/');
22
+
23
+ router.use(express.static(staticPath));
24
+
25
+ router.get(/(.*)/, (_, res) => {
26
+ res.sendFile(`${staticPath}/index.html`);
27
+ });
28
+
29
+ return router;
30
+ };
31
+
32
+ const setupMCPChatHandler = (chatConfiguration: ChatConfiguration & any): RequestHandler => {
33
+ if (!chatConfiguration || !chatConfiguration.model) {
34
+ throw new Error('Model is required');
35
+ }
36
+
37
+ const {
38
+ tools: additionalTools = {},
39
+ unchainedMCPUrl = `${process.env.ROOT_URL}/mcp`,
40
+ model,
41
+ imageGenerationTool,
42
+ ...restChatConfig
43
+ }: any = chatConfiguration;
44
+
45
+ const system = chatConfiguration.system ?? defaultSystemPrompt;
46
+
47
+ const mcpChatHandler: RequestHandler = async (req: Request, res: Response): Promise<void> => {
48
+ if (req.method === 'OPTIONS') {
49
+ res.setHeader('access-control-allow-credentials', 'true');
50
+ res.setHeader('access-control-allow-private-network', 'true');
51
+ res.status(200).end();
52
+ return;
53
+ }
54
+ if (req.method !== 'POST' && req.method !== 'GET') {
55
+ res.status(405).json({ error: 'Method Not Allowed. Use POST.' });
56
+ return;
57
+ }
58
+
59
+ const client = await createMCPClient({
60
+ transport: new StreamableHTTPClientTransport(new URL(unchainedMCPUrl), {
61
+ requestInit: {
62
+ headers: {
63
+ Cookie: req.headers.cookie || '',
64
+ },
65
+ },
66
+ }) as MCPTransport,
67
+ });
68
+
69
+ try {
70
+ const defaultUnchainedTools = await client.tools();
71
+ const tools: ToolSet = {
72
+ ...defaultUnchainedTools,
73
+ ...additionalTools,
74
+ };
75
+ if (imageGenerationTool) {
76
+ tools.generateImage = generateImageHandler(req)(imageGenerationTool) as any;
77
+ }
78
+
79
+ if (req.method === 'GET') {
80
+ res.status(200).json({
81
+ tools: normalizeToolsIndex(tools),
82
+ cached: false,
83
+ });
84
+ return;
85
+ }
86
+ const { messages } = req.body;
87
+
88
+ const cacheControlledTools = { ...tools };
89
+ const keys = Object.keys(cacheControlledTools);
90
+ const lastKey = keys[keys.length - 1];
91
+
92
+ cacheControlledTools[lastKey] = {
93
+ ...cacheControlledTools[lastKey],
94
+ providerOptions: {
95
+ anthropic: { cacheControl: { type: 'ephemeral' } },
96
+ },
97
+ };
98
+
99
+ const normalizedMessages = convertToModelMessages(messages, { tools: cacheControlledTools });
100
+
101
+ if (normalizedMessages.length > 0) {
102
+ const lastIndex = normalizedMessages.length - 1;
103
+ normalizedMessages[lastIndex] = {
104
+ ...normalizedMessages[lastIndex],
105
+ providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' } } },
106
+ };
107
+ }
108
+ const result = streamText({
109
+ stopWhen: stepCountIs(10),
110
+ ...restChatConfig,
111
+ system,
112
+ model,
113
+ tools: cacheControlledTools,
114
+ onFinish: async () => {
115
+ await client?.close();
116
+ },
117
+ messages: normalizedMessages.slice(-20),
118
+ providerOptions: {
119
+ anthropic: {
120
+ cacheControl: {
121
+ type: 'ephemeral',
122
+ },
123
+ },
124
+ },
125
+ });
126
+
127
+ result.pipeUIMessageStreamToResponse(res, {
128
+ onError: errorHandler,
129
+ });
130
+ } catch (err) {
131
+ await client?.close();
132
+ res.status(500).json({ error: errorHandler(err) });
133
+ }
134
+ };
135
+
136
+ return mcpChatHandler;
137
+ };
138
+
139
+ export const connectChat = (app: Express, chatConfiguration: ChatConfiguration) => {
140
+ const handler = setupMCPChatHandler(chatConfiguration);
141
+ if (!handler) {
142
+ throw new Error('Invalid chat configuration: model is required.');
143
+ }
144
+ app.post('/chat', express.json({ limit: '10mb' }), handler);
145
+ app.options('/chat', handler);
146
+ app.get('/chat/tools', handler);
147
+ };
@@ -7,8 +7,7 @@ const logger = createLogger('unchained:bulk-import');
7
7
 
8
8
  const methodWrongHandler = (res) => {
9
9
  logger.error('Method not supported, return 404');
10
- res.writeHead(404);
11
- res.end();
10
+ res.status(404).end();
12
11
  return;
13
12
  };
14
13
 
@@ -41,6 +40,9 @@ export default async function bulkImportMiddleware(
41
40
  meta: {},
42
41
  });
43
42
 
43
+ const validationStream = await context.services.files.createDownloadStream({ fileId: file._id });
44
+ await context.bulkImporter.validateEventStream(validationStream);
45
+
44
46
  input.payloadId = file._id;
45
47
  input.payloadSize = file.size;
46
48
 
@@ -53,12 +55,10 @@ export default async function bulkImportMiddleware(
53
55
  priority: 10,
54
56
  });
55
57
 
56
- res.writeHead(200);
57
- res.end(JSON.stringify(work));
58
+ res.status(200).send(work);
58
59
  } catch (e) {
59
- logger.error(e.message);
60
- res.writeHead(503);
61
- res.end(JSON.stringify({ name: e.name, code: e.code, message: e.message }));
60
+ logger.error(e);
61
+ res.status(503).send({ name: e.name, code: e.code, message: e.message });
62
62
  return;
63
63
  }
64
64
  }
@@ -8,8 +8,7 @@ const { ROOT_URL = 'http://localhost:4010' } = process.env;
8
8
 
9
9
  const methodWrongHandler = (res) => {
10
10
  logger.error('Method not supported, return 404');
11
- res.writeHead(404);
12
- res.end();
11
+ res.status(404).end();
13
12
  return;
14
13
  };
15
14
 
@@ -51,15 +50,12 @@ const ercMetadataMiddleware: RequestHandler = async (
51
50
  return methodWrongHandler(res);
52
51
  }
53
52
 
54
- const body = JSON.stringify(ercMetadata);
55
53
  res.status(200);
56
- res.setHeader('Content-Type', 'application/json');
57
- res.setHeader('Content-Length', new TextEncoder().encode(body).length);
58
- return res.send(body);
54
+ return res.send(ercMetadata);
59
55
  } catch (e) {
60
- logger.error(e.message);
61
- res.writeHead(503);
62
- res.end(JSON.stringify({ name: e.name, code: e.code, message: e.message }));
56
+ logger.error(e);
57
+ res.status(503);
58
+ res.send({ name: e.name, code: e.code, message: e.message });
63
59
  return;
64
60
  }
65
61
  };
@@ -8,8 +8,7 @@ const logger = createLogger('unchained:temp-upload');
8
8
 
9
9
  const methodWrongHandler = (res) => {
10
10
  logger.error('Method not supported, return 404');
11
- res.writeHead(404);
12
- res.end();
11
+ res.status(404).end();
13
12
  return;
14
13
  };
15
14
 
@@ -47,14 +46,10 @@ export default async function tempUploadMiddleware(
47
46
  const fileUploadAdapter = getFileAdapter();
48
47
  const rawUrl = await fileUploadAdapter.createDownloadURL(file);
49
48
  const url = context.modules.files.normalizeUrl(rawUrl, {});
50
- const body = JSON.stringify({ fileId: file._id, url, expires: expires.toISOString() });
51
-
52
- res.writeHead(200);
53
- res.end(body);
49
+ res.status(200).send({ fileId: file._id, url, expires: expires.toISOString() });
54
50
  } catch (e) {
55
- logger.error(e.message);
56
- res.writeHead(503);
57
- res.end(JSON.stringify({ name: e.name, code: e.code, message: e.message }));
51
+ logger.error(e);
52
+ res.status(503).send({ name: e.name, code: e.code, message: e.message });
58
53
  return;
59
54
  }
60
55
  }
@@ -16,6 +16,8 @@ import createTempUploadMiddleware from './createTempUploadMiddleware.js';
16
16
  import createMCPMiddleware from './createMCPMiddleware.js';
17
17
  import { API_EVENTS } from '../events.js';
18
18
 
19
+ export * from './chatHandler.js';
20
+
19
21
  const resolveUserRemoteAddress = (req: e.Request) => {
20
22
  const remoteAddress =
21
23
  (req.headers['x-real-ip'] as string) ||
@@ -32,6 +32,9 @@ const bulkImportHandler: RouteHandlerMethod = async (
32
32
  chunkSizeBytes: 1024 * 1024 * 5, // 5MB chunks
33
33
  });
34
34
 
35
+ const validationStream = await context.services.files.createDownloadStream({ fileId: file._id });
36
+ await context.bulkImporter.validateEventStream(validationStream);
37
+
35
38
  input.payloadId = file._id;
36
39
  input.payloadSize = file.size;
37
40
 
@@ -44,15 +47,12 @@ const bulkImportHandler: RouteHandlerMethod = async (
44
47
  priority: 10,
45
48
  });
46
49
 
47
- const body = JSON.stringify(work);
48
50
  res.status(200);
49
- res.header('Content-Length', new TextEncoder().encode(body).length);
50
- res.header('Content-Type', 'application/json');
51
- return res.send(body);
51
+ return res.send(work);
52
52
  } catch (e) {
53
- logger.error(e.message);
53
+ logger.error(e);
54
54
  res.status(503);
55
- return res.send(JSON.stringify({ name: e.name, code: e.code, message: e.message }));
55
+ return res.send({ name: e.name, code: e.code, message: e.message });
56
56
  }
57
57
  };
58
58
 
@@ -0,0 +1,140 @@
1
+ import { FastifyInstance, RouteHandlerMethod, FastifyRequest } from 'fastify';
2
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
3
+ import {
4
+ convertToModelMessages,
5
+ experimental_createMCPClient as createMCPClient,
6
+ MCPTransport,
7
+ stepCountIs,
8
+ streamText,
9
+ ToolSet,
10
+ } from 'ai';
11
+ import generateImageHandler from '../chat/generateImageHandler.js';
12
+ import defaultSystemPrompt from '../chat/defaultSystemPrompt.js';
13
+ import normalizeToolsIndex from '../chat/normalizeToolsIndex.js';
14
+ import { ChatConfiguration, errorHandler } from '../chat/utils.js';
15
+ import { createLogger } from '@unchainedshop/logger';
16
+
17
+ const logger = createLogger('unchained:api:chat');
18
+
19
+ const setupMCPChatHandler = (chatConfiguration: ChatConfiguration & any) => {
20
+ if (!chatConfiguration?.model) {
21
+ throw new Error('Model is required');
22
+ }
23
+
24
+ const {
25
+ tools: additionalTools = {},
26
+ unchainedMCPUrl = `${process.env.ROOT_URL}/mcp`,
27
+ imageGenerationTool,
28
+ ...restChatConfig
29
+ } = chatConfiguration;
30
+
31
+ const system = chatConfiguration.system ?? defaultSystemPrompt;
32
+
33
+ const mcpChatHandler: RouteHandlerMethod = async (req: FastifyRequest, res) => {
34
+ let client;
35
+ try {
36
+ if (req.method === 'OPTIONS') {
37
+ res.headers({
38
+ 'access-control-allow-credentials': 'true',
39
+ 'access-control-allow-private-network': 'true',
40
+ });
41
+ return res.status(200).send();
42
+ }
43
+
44
+ client = await createMCPClient({
45
+ transport: new StreamableHTTPClientTransport(new URL(unchainedMCPUrl), {
46
+ authProvider: null,
47
+ requestInit: {
48
+ headers: {
49
+ Cookie: req.headers.cookie || '',
50
+ },
51
+ },
52
+ }) as MCPTransport,
53
+ });
54
+
55
+ const defaultUnchainedTools = await client.tools();
56
+ const tools: ToolSet = {
57
+ ...defaultUnchainedTools,
58
+ ...additionalTools,
59
+ };
60
+ if (imageGenerationTool) {
61
+ tools.generateImage = generateImageHandler(req)(imageGenerationTool) as any;
62
+ }
63
+
64
+ if (req.method === 'GET') {
65
+ return res.status(200).send({
66
+ tools: normalizeToolsIndex(tools),
67
+ cached: false,
68
+ });
69
+ }
70
+
71
+ const { messages } = req.body as any;
72
+
73
+ const cacheControlledTools = { ...tools };
74
+ const keys = Object.keys(cacheControlledTools);
75
+ const lastKey = keys[keys.length - 1];
76
+
77
+ cacheControlledTools[lastKey] = {
78
+ ...cacheControlledTools[lastKey],
79
+ providerOptions: {
80
+ anthropic: { cacheControl: { type: 'ephemeral' } },
81
+ },
82
+ };
83
+
84
+ const normalizedMessages = convertToModelMessages(messages, { tools: cacheControlledTools });
85
+
86
+ if (normalizedMessages.length > 0) {
87
+ const lastIndex = normalizedMessages.length - 1;
88
+ normalizedMessages[lastIndex] = {
89
+ ...normalizedMessages[lastIndex],
90
+ providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' } } },
91
+ };
92
+ }
93
+ const result = streamText({
94
+ stopWhen: stepCountIs(500),
95
+ ...restChatConfig,
96
+ messages: normalizedMessages.slice(-20),
97
+ system,
98
+ tools: cacheControlledTools,
99
+ onFinish: async () => {
100
+ await client?.close();
101
+ },
102
+ providerOptions: {
103
+ anthropic: {
104
+ cacheControl: {
105
+ type: 'ephemeral',
106
+ },
107
+ },
108
+ },
109
+ });
110
+
111
+ return res.send(
112
+ result.toUIMessageStreamResponse({
113
+ onError: errorHandler,
114
+ }),
115
+ );
116
+ } catch (err: any) {
117
+ logger.error(err);
118
+ await client?.close();
119
+ res.status(500);
120
+ return res.send({ error: errorHandler(err) });
121
+ }
122
+ };
123
+ return mcpChatHandler;
124
+ };
125
+
126
+ export const connectChat = (app: FastifyInstance, chatConfiguration: ChatConfiguration) => {
127
+ const handler = setupMCPChatHandler(chatConfiguration);
128
+
129
+ app.route({
130
+ url: '/chat',
131
+ method: ['POST', 'OPTIONS'],
132
+ handler,
133
+ });
134
+
135
+ app.route({
136
+ url: '/chat/tools',
137
+ method: ['GET', 'OPTIONS'],
138
+ handler,
139
+ });
140
+ };
@@ -39,15 +39,12 @@ const ercMetadataHandler: RouteHandlerMethod = async (
39
39
 
40
40
  if (!ercMetadata) return notFoundHandler(res);
41
41
 
42
- const body = JSON.stringify(ercMetadata);
43
42
  res.status(200);
44
- res.header('Content-Length', new TextEncoder().encode(body).length);
45
- res.header('Content-Type', 'application/json');
46
- return res.send(body);
43
+ return res.send(ercMetadata);
47
44
  } catch (e) {
48
- logger.error(e.message);
45
+ logger.error(e);
49
46
  res.status(503);
50
- return res.send(JSON.stringify({ name: e.name, code: e.code, message: e.message }));
47
+ return res.send({ name: e.name, code: e.code, message: e.message });
51
48
  }
52
49
  };
53
50
 
@@ -11,10 +11,12 @@ import { User } from '@unchainedshop/core-users';
11
11
  import fastifySession from '@fastify/session';
12
12
  import fastifyCookie from '@fastify/cookie';
13
13
  import fastifyMultipart from '@fastify/multipart';
14
- import { FastifyBaseLogger, FastifyInstance, FastifyRequest } from 'fastify';
14
+ import { FastifyBaseLogger, FastifyInstance, FastifyPluginAsync, FastifyRequest } from 'fastify';
15
15
  import { createLogger } from '@unchainedshop/logger';
16
16
  import mcpHandler from './mcpHandler.js';
17
17
  import tempUploadHandler from './tempUploadHandler.js';
18
+ import fastifyStatic from '@fastify/static';
19
+ export * from './chatHandler.js';
18
20
 
19
21
  const resolveUserRemoteAddress = (req: FastifyRequest) => {
20
22
  const remoteAddress =
@@ -234,3 +236,37 @@ export const connect = (
234
236
  registered();
235
237
  });
236
238
  };
239
+
240
+ interface FastifyRouterOptions {
241
+ prefix?: string;
242
+ }
243
+
244
+ export const fastifyRouter: FastifyPluginAsync<FastifyRouterOptions> = async (
245
+ fastify: FastifyInstance,
246
+ opts,
247
+ ) => {
248
+ try {
249
+ const staticURL = import.meta.resolve('@unchainedshop/admin-ui');
250
+ const staticPath = new URL(staticURL).pathname.split('/').slice(0, -1).join('/');
251
+
252
+ await fastify.register(fastifyStatic, {
253
+ root: staticPath,
254
+ prefix: opts.prefix || '/',
255
+ });
256
+
257
+ fastify.setNotFoundHandler((request, reply) => {
258
+ if (request.raw.method === 'GET') {
259
+ return reply.type('text/html').sendFile('index.html');
260
+ } else {
261
+ return reply.status(404).send();
262
+ }
263
+ });
264
+ } catch {
265
+ if (process.env.NODE_ENV !== 'production') {
266
+ // Trying the default admin ui dev port
267
+ fastify.get('/', async (request, reply) => {
268
+ return reply.redirect('http://localhost:3000');
269
+ });
270
+ }
271
+ }
272
+ };
@@ -85,8 +85,8 @@ const mcpHandler: RouteHandlerMethod = async (
85
85
 
86
86
  return res.status(405).send('Method Not Allowed');
87
87
  } catch (e) {
88
- logger.error(e.message);
89
- return res.status(503).send(JSON.stringify({ name: e.name, code: e.code, message: e.message }));
88
+ logger.error(e);
89
+ return res.status(503).send({ name: e.name, code: e.code, message: e.message });
90
90
  }
91
91
  };
92
92
 
@@ -37,15 +37,12 @@ const tempUploadHandler: RouteHandlerMethod = async (
37
37
  const fileUploadAdapter = getFileAdapter();
38
38
  const rawUrl = await fileUploadAdapter.createDownloadURL(file);
39
39
  const url = context.modules.files.normalizeUrl(rawUrl, {});
40
- const body = JSON.stringify({ fileId: file._id, url, expires: expires.toISOString() });
41
40
  res.status(200);
42
- res.header('Content-Length', new TextEncoder().encode(body).length);
43
- res.header('Content-Type', 'application/json');
44
- return res.send(body);
41
+ return res.send({ fileId: file._id, url, expires: expires.toISOString() });
45
42
  } catch (e) {
46
- logger.error(e.message);
43
+ logger.error(e);
47
44
  res.status(503);
48
- return res.send(JSON.stringify({ name: e.name, code: e.code, message: e.message }));
45
+ return res.send({ name: e.name, code: e.code, message: e.message });
49
46
  }
50
47
  };
51
48
 
@@ -1,6 +1,6 @@
1
1
  import { Context } from '../../../../context.js';
2
2
  import { ProductNotFoundError } from '../../../../errors.js';
3
- import normalizeMediaUrl from '../../../utils/normalizeMediaUrl.js';
3
+ import { getNormalizedProductDetails } from '../../../utils/getNormalizedProductDetails.js';
4
4
  import { Params } from '../schemas.js';
5
5
 
6
6
  export default async function addProductMedia(context: Context, params: Params<'ADD_MEDIA'>) {
@@ -39,8 +39,7 @@ export default async function addProductMedia(context: Context, params: Params<'
39
39
  if (file.expires && new Date(file.expires).getTime() < Date.now()) {
40
40
  throw new Error(`File upload expired: ${fileId}`);
41
41
  }
42
-
43
- const linked = await services.files.linkFile({ fileId, size, type });
44
- const media = await normalizeMediaUrl([{ ...linked, mediaId: linked._id }], context);
45
- return { media };
42
+ await services.files.linkFile({ fileId, size, type });
43
+ const updatedProduct = await getNormalizedProductDetails(productId, context);
44
+ return { product: updatedProduct };
46
45
  }
@@ -1,6 +1,8 @@
1
1
  import { Context } from '../../../../context.js';
2
2
  import { ProductTypes } from '@unchainedshop/core-products';
3
3
  import {
4
+ CountryNotFoundError,
5
+ CurrencyNotFoundError,
4
6
  ProductNotFoundError,
5
7
  ProductWrongStatusError,
6
8
  ProductWrongTypeError,
@@ -9,7 +11,7 @@ import { getNormalizedProductDetails } from '../../../utils/getNormalizedProduct
9
11
  import { Params } from '../schemas.js';
10
12
 
11
13
  export default async function updateProduct(context: Context, params: Params<'UPDATE'>) {
12
- const { modules } = context;
14
+ const { modules, loaders } = context;
13
15
  const { productId, product } = params;
14
16
 
15
17
  const existingProduct = await modules.products.findProduct({ productId });
@@ -64,6 +66,15 @@ export default async function updateProduct(context: Context, params: Params<'UP
64
66
  }
65
67
 
66
68
  if (product.commerce !== undefined) {
69
+ await Promise.all(
70
+ product.commerce.pricing.map(async ({ countryCode, currencyCode }) => {
71
+ const currency = await loaders.currencyLoader.load({ isoCode: currencyCode });
72
+ if (!currency) throw new CurrencyNotFoundError({ currencyCode });
73
+
74
+ const country = await loaders.countryLoader.load({ isoCode: countryCode });
75
+ if (!country) throw new CountryNotFoundError({ countryCode });
76
+ }),
77
+ );
67
78
  updateData.commerce = product.commerce;
68
79
  }
69
80
 
@@ -111,15 +111,19 @@ export const ProductSchema = z.object({
111
111
  .min(2)
112
112
  .max(2)
113
113
  .describe(
114
- 'ISO country code (e.g., US, DE) make sure the country exists in the system before using it. if it does not exists prompt the user to add the country. NEVER add it automatically',
114
+ 'ISO country code (e.g., US, DE) make sure the country/countryCode exists in the system before using it. if it does not exists prompt the user to add the country. NEVER add it automatically',
115
115
  ),
116
116
  }),
117
117
  )
118
118
  .nonempty()
119
- .describe('List of price configurations'),
119
+ .describe(
120
+ "List of price configurations always use countryCode and currencyCodes that are already registered in the system, don't use non existing iso codes.",
121
+ ),
120
122
  })
121
123
  .optional()
122
- .describe('Commerce info - Available for ALL except CONFIGURABLE_PRODUCT & BUNDLE_PRODUCT'),
124
+ .describe(
125
+ "Commerce info - Available for ALL except CONFIGURABLE_PRODUCT. always use countryCode and currencyCodes that are already registered in the system, don't use non existing iso codes.",
126
+ ),
123
127
  });
124
128
 
125
129
  export const actionValidators = {
@@ -1,6 +1,9 @@
1
1
  import { ProductTypes } from '@unchainedshop/core-products';
2
2
  import { Context } from '../../context.js';
3
3
  import normalizeMediaUrl from './normalizeMediaUrl.js';
4
+ import { createLogger } from '@unchainedshop/logger';
5
+
6
+ const logger = createLogger('unchained:api:mcp');
4
7
 
5
8
  export async function getNormalizedProductDetails(productId: string, context: Context) {
6
9
  const { modules, locale, loaders } = context;
@@ -84,7 +87,7 @@ export async function getNormalizedProductDetails(productId: string, context: Co
84
87
  });
85
88
  } catch {
86
89
  // Pricing might not be available for all products
87
- console.warn('Pricing not available for product:', product._id);
90
+ logger.warn('Pricing not available for product:', product._id);
88
91
  }
89
92
 
90
93
  const productMedias = await modules.products.media.findProductMedias({ productId });
@@ -30,7 +30,7 @@ export default async function checkoutCart(
30
30
  const checkedOutOrder = await services.orders.checkoutOrder(order._id, transactionContext);
31
31
  return checkedOutOrder;
32
32
  } catch (error) {
33
- logger.error(error.message, { userId, orderId: order._id });
33
+ logger.error(error, { userId, orderId: order._id });
34
34
  throw new OrderCheckoutError({
35
35
  userId,
36
36
  orderId: order._id,