@wix/mcp 1.0.0

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.
@@ -0,0 +1,268 @@
1
+ import { z } from 'zod';
2
+ import { runSemanticSearchAndFormat } from './semanticSearch.js';
3
+ import { logger } from '../logger.js';
4
+ import { captureException } from '@sentry/node';
5
+ export const VALID_DOCS_TOOLS = [
6
+ 'WDS',
7
+ 'REST',
8
+ 'SDK',
9
+ 'BUILD_APPS',
10
+ 'WIX_HEADLESS'
11
+ ];
12
+ export const addDocsTools = (server, allowedTools = [
13
+ 'WDS',
14
+ 'REST',
15
+ 'SDK',
16
+ 'BUILD_APPS',
17
+ 'WIX_HEADLESS'
18
+ ]) => {
19
+ // WDS Documentation
20
+ if (allowedTools.includes('WDS')) {
21
+ server.tool('SearchWixWDSDocumentation', [
22
+ 'Searches the Wix Design System Documentation for components and patterns.',
23
+ 'Use this tool when you need to understand or implement UI components and design patterns in a Wix project.',
24
+ 'Search for specific component names, patterns, or UI requirements.',
25
+ "If you can't find what you need, try to rephrase your search term or use bigger maxResults value."
26
+ ].join('\n'), {
27
+ searchTerm: z
28
+ .string()
29
+ .describe('The search term to search for in the Wix Design System Documentation'),
30
+ maxResults: z
31
+ .number()
32
+ .describe('The maximum number of results to return, default is 5, max is 15')
33
+ .min(1)
34
+ .max(15)
35
+ .optional()
36
+ .default(5)
37
+ }, async ({ searchTerm, maxResults }, { panorama }) => {
38
+ try {
39
+ logger.log(`Searching for ${searchTerm} in Wix WDS`);
40
+ const result = await runSemanticSearchAndFormat('WDS', {
41
+ searchTerm: searchTerm
42
+ }, Math.max(1, Math.min(maxResults ?? 5, 15)));
43
+ return {
44
+ content: [
45
+ {
46
+ type: 'text',
47
+ text: result
48
+ }
49
+ ]
50
+ };
51
+ }
52
+ catch (error) {
53
+ panorama.errorMonitor().reportError(error);
54
+ captureException(error, {
55
+ tags: {
56
+ componentId: 'SearchWixWDSDocumentation',
57
+ toolName: 'SearchWixWDSDocumentation'
58
+ }
59
+ });
60
+ logger.error(`Error searching for ${searchTerm} in Wix WDS: ${error}`);
61
+ return {
62
+ isError: true,
63
+ content: [{ type: 'text', text: 'Error: ' + error.message }]
64
+ };
65
+ }
66
+ });
67
+ }
68
+ // REST Documentation
69
+ if (allowedTools.includes('REST')) {
70
+ server.tool('SearchWixRESTDocumentation', [
71
+ 'Searches the official Wix REST API documentation.',
72
+ 'Use this tool whenever you need to to interact with the Wix platform via HTTP requests.',
73
+ "Specify the API endpoint, resource, or action you need information about (e.g., 'get site details endpoint', 'create data collection', 'update product API', 'REST authentication').",
74
+ "If you can't find what you need, try to rephrase your search term or use bigger maxResults value."
75
+ ].join('\n'), {
76
+ searchTerm: z
77
+ .string()
78
+ .describe('The search term to search for in the Wix REST API Documentation'),
79
+ maxResults: z
80
+ .number()
81
+ .describe('The maximum number of results to return, default is 5, max is 15')
82
+ .min(1)
83
+ .max(15)
84
+ .optional()
85
+ .default(5)
86
+ }, async ({ searchTerm, maxResults }, { panorama }) => {
87
+ try {
88
+ logger.log(`Searching for ${searchTerm} in Wix REST API`);
89
+ const result = await runSemanticSearchAndFormat('REST', {
90
+ searchTerm: searchTerm
91
+ }, Math.max(1, Math.min(maxResults ?? 5, 15)));
92
+ return {
93
+ content: [
94
+ {
95
+ type: 'text',
96
+ text: result
97
+ }
98
+ ]
99
+ };
100
+ }
101
+ catch (error) {
102
+ panorama.errorMonitor().reportError(error);
103
+ captureException(error, {
104
+ tags: {
105
+ componentId: 'SearchWixRESTDocumentation',
106
+ toolName: 'SearchWixRESTDocumentation'
107
+ }
108
+ });
109
+ logger.error(`Error searching for ${searchTerm} in Wix REST API: ${error}`);
110
+ return {
111
+ isError: true,
112
+ content: [{ type: 'text', text: 'Error: ' + error.message }]
113
+ };
114
+ }
115
+ });
116
+ }
117
+ // SDK Documentation
118
+ if (allowedTools.includes('SDK')) {
119
+ server.tool('SearchWixSDKDocumentation', [
120
+ 'Searches the official Wix javascript SDK documentation.',
121
+ 'Use this tool whenever you need to write or modify Wix related SDK code.',
122
+ "Specify the SDK module, function, or feature you need information about (e.g., 'how to query all items from a data collection?', 'how to use wix-stores-backend', 'authentication methods in the SDK').",
123
+ "If you can't find what you need, try to rephrase your search term or use bigger maxResults value."
124
+ ].join('\n'), {
125
+ searchTerm: z
126
+ .string()
127
+ .describe('The search term to search for in the Wix SDK Documentation'),
128
+ maxResults: z
129
+ .number()
130
+ .describe('The maximum number of results to return, default is 5, max is 15')
131
+ .min(1)
132
+ .max(15)
133
+ .optional()
134
+ .default(5)
135
+ }, async ({ searchTerm, maxResults }, { panorama }) => {
136
+ try {
137
+ logger.log(`Searching for ${searchTerm} in Wix SDK`);
138
+ panorama
139
+ .transaction('SearchWixSDKDocumentation')
140
+ .start({ maxResults });
141
+ const result = await runSemanticSearchAndFormat('SDK', {
142
+ searchTerm: searchTerm
143
+ }, Math.max(1, Math.min(maxResults ?? 5, 15)));
144
+ panorama
145
+ .transaction('SearchWixSDKDocumentation')
146
+ .finish({ maxResults });
147
+ return {
148
+ content: [
149
+ {
150
+ type: 'text',
151
+ text: result
152
+ }
153
+ ]
154
+ };
155
+ }
156
+ catch (error) {
157
+ panorama.errorMonitor().reportError(error);
158
+ captureException(error, {
159
+ tags: {
160
+ componentId: 'SearchWixSDKDocumentation',
161
+ toolName: 'SearchWixSDKDocumentation'
162
+ }
163
+ });
164
+ logger.error(`Error searching for ${searchTerm} in Wix SDK: ${error}`);
165
+ return {
166
+ isError: true,
167
+ content: [{ type: 'text', text: 'Error: ' + error.message }]
168
+ };
169
+ }
170
+ });
171
+ }
172
+ // Build Apps Documentation
173
+ if (allowedTools.includes('BUILD_APPS')) {
174
+ server.tool('SearchBuildAppsDocumentation', [
175
+ 'Searches the official Build Apps documentation.',
176
+ 'Use this tool when you need to understand or implement Wix CLI applications related code.',
177
+ 'The search term should be a specific Wix CLI command or specific topic related to Wix CLI applications or its ecosystem (e.g. deployment, creating new extensions etc).',
178
+ "If you can't find what you need, try to rephrase your search term or use bigger maxResults value."
179
+ ].join('\n'), {
180
+ searchTerm: z
181
+ .string()
182
+ .describe('The search term to search for in the Build Apps Documentation'),
183
+ maxResults: z
184
+ .number()
185
+ .describe('The maximum number of results to return, default is 5, max is 15')
186
+ .min(1)
187
+ .max(15)
188
+ .optional()
189
+ .default(5)
190
+ }, async ({ searchTerm, maxResults }, { panorama }) => {
191
+ try {
192
+ logger.log(`Searching for ${searchTerm} in Build Apps`);
193
+ const result = await runSemanticSearchAndFormat('BUILD_APPS', {
194
+ searchTerm: `wix cli docs for ${searchTerm}`
195
+ }, Math.max(1, Math.min(maxResults ?? 5, 15)));
196
+ return {
197
+ content: [
198
+ {
199
+ type: 'text',
200
+ text: result
201
+ }
202
+ ]
203
+ };
204
+ }
205
+ catch (error) {
206
+ panorama.errorMonitor().reportError(error);
207
+ captureException(error, {
208
+ tags: {
209
+ componentId: 'SearchBuildAppsDocumentation',
210
+ toolName: 'SearchBuildAppsDocumentation'
211
+ }
212
+ });
213
+ logger.error(`Error searching for ${searchTerm} in Build Apps: ${error}`);
214
+ return {
215
+ content: [{ type: 'text', text: 'Error: ' + error.message }]
216
+ };
217
+ }
218
+ });
219
+ }
220
+ // Wix Headless Documentation
221
+ if (allowedTools.includes('WIX_HEADLESS')) {
222
+ server.tool('SearchWixHeadlessDocumentation', [
223
+ 'Searches the official Wix Headless Documentation.',
224
+ 'Use this tool when you need to understand or implement Headless related code.',
225
+ 'The search term should be a specific Wix Headless topic or feature you need information about.',
226
+ "If you can't find what you need, try to rephrase your search term or use bigger maxResults value."
227
+ ].join('\n'), {
228
+ searchTerm: z
229
+ .string()
230
+ .describe('The search term to search for in the Headless Documentation'),
231
+ maxResults: z
232
+ .number()
233
+ .describe('The maximum number of results to return, default is 5, max is 15')
234
+ .min(1)
235
+ .max(15)
236
+ .optional()
237
+ .default(5)
238
+ }, async ({ searchTerm, maxResults }, { panorama }) => {
239
+ try {
240
+ logger.log(`Searching for ${searchTerm} in Headless`);
241
+ const result = await runSemanticSearchAndFormat('WIX_HEADLESS', {
242
+ searchTerm: searchTerm
243
+ }, Math.max(1, Math.min(maxResults ?? 5, 15)));
244
+ return {
245
+ content: [
246
+ {
247
+ type: 'text',
248
+ text: result
249
+ }
250
+ ]
251
+ };
252
+ }
253
+ catch (error) {
254
+ panorama.errorMonitor().reportError(error);
255
+ captureException(error, {
256
+ tags: {
257
+ componentId: 'SearchWixHeadlessDocumentation',
258
+ toolName: 'SearchWixHeadlessDocumentation'
259
+ }
260
+ });
261
+ logger.error(`Error searching for ${searchTerm} in Wix Headless: ${error}`);
262
+ return {
263
+ content: [{ type: 'text', text: 'Error: ' + error.message }]
264
+ };
265
+ }
266
+ });
267
+ }
268
+ };
@@ -0,0 +1,86 @@
1
+ import { logger } from '../logger.js';
2
+ const MAX_CONTENT_LENGTH = 10000;
3
+ const runSemanticSearch = async (toolName, toolParams, maxResults = 20, rerank = false) => {
4
+ const startTime = new Date().getTime();
5
+ const url = new URL(`https://www.wixapis.com/mcp-docs-search/v1/search?maxResults=${maxResults}&rerank=${rerank}`);
6
+ if (toolName === 'SDK') {
7
+ url.searchParams.append('kbName', 'SDK_SCHEMAS_KB_ID');
8
+ url.searchParams.append('kbName', 'SDK_DOCS_KB_ID');
9
+ }
10
+ else if (toolName === 'REST') {
11
+ url.searchParams.append('kbName', 'REST_METHODS_KB_ID');
12
+ url.searchParams.append('kbName', 'REST_DOCS_KB_ID');
13
+ }
14
+ else if (toolName === 'WDS') {
15
+ url.searchParams.append('kbName', 'WDS_DOCS_KB_ID');
16
+ }
17
+ else if (toolName === 'BUILD_APPS') {
18
+ url.searchParams.append('kbName', 'BUILD_APPS_KB_ID');
19
+ }
20
+ else if (toolName === 'WIX_HEADLESS') {
21
+ url.searchParams.append('kbName', 'HEADLESS_KB_ID');
22
+ }
23
+ for (const [key, value] of Object.entries(toolParams)) {
24
+ url.searchParams.append(key, value);
25
+ }
26
+ logger.log('Running RAG search', url.toString());
27
+ const response = await fetch(url.toString(), {
28
+ headers: {
29
+ 'Content-Type': 'application/json',
30
+ 'x-time-budget': '180000',
31
+ 'x-wix-time-budget': '180000',
32
+ Cookie: process.env.WIX_COOKIE || ''
33
+ }
34
+ });
35
+ if (!response.ok) {
36
+ logger.error(`Failed to run rag search: ${response.statusText}`);
37
+ throw new Error(`Failed to run rag search: ${response.statusText}`);
38
+ }
39
+ const data = await response.json();
40
+ const endTime = new Date().getTime();
41
+ const timeTaskFormatted = (endTime - startTime) / 1000;
42
+ logger.log('Ran RAG search successfully with params', JSON.stringify(toolParams), timeTaskFormatted, typeof data);
43
+ return data?.results;
44
+ };
45
+ const getContentFromChunkedContent = (chunkedContent) => {
46
+ let content = '';
47
+ for (const chunk of chunkedContent) {
48
+ if (content.length + chunk.length > MAX_CONTENT_LENGTH) {
49
+ break;
50
+ }
51
+ content += '\n\n---\n\n' + chunk;
52
+ }
53
+ return content;
54
+ };
55
+ const formatResultsContent = (resultsFromKbs, maxResults) => {
56
+ // First, take the results from all KBs and sort them by overallMatchScore
57
+ const allResults = Object.entries(resultsFromKbs)
58
+ .map(([, { results }]) => results)
59
+ .flat();
60
+ if (allResults.length === 0) {
61
+ throw new Error('No result from search');
62
+ }
63
+ const sortedResults = allResults.sort((a, b) => b.scoresInfo.overallMatchScore - a.scoresInfo.overallMatchScore);
64
+ // Take the top maxResults results
65
+ const topResults = sortedResults.slice(0, maxResults);
66
+ // Concatenate the content of the top results
67
+ const concatenatedContent = topResults
68
+ .map((result) => {
69
+ const content = result.entry.content;
70
+ const chunkedContent = result.entry.chunkedContent;
71
+ if (content.length > MAX_CONTENT_LENGTH) {
72
+ logger.log(`Content is too long, using first chunk`, {
73
+ contentLength: content.length,
74
+ chunkedContentLength: chunkedContent.length
75
+ });
76
+ return getContentFromChunkedContent(chunkedContent);
77
+ }
78
+ return content;
79
+ })
80
+ .join('\n\n---\n\n');
81
+ return concatenatedContent;
82
+ };
83
+ export const runSemanticSearchAndFormat = async (toolName, toolParams, maxResults = 20, rerank = false) => {
84
+ const results = await runSemanticSearch(toolName, toolParams, maxResults, rerank);
85
+ return formatResultsContent(results, Math.min(maxResults, 10));
86
+ };
@@ -0,0 +1,289 @@
1
+ import { describe, test, expect, beforeEach, vi, afterAll } from 'vitest';
2
+ import { runSemanticSearchAndFormat } from './semanticSearch.js';
3
+ const sampleSearchResponse = {
4
+ results: {
5
+ SDK_DOCS_KB_ID: {
6
+ results: [
7
+ {
8
+ entry: {
9
+ content: 'SDK Documentation content 1'
10
+ },
11
+ scoresInfo: {
12
+ overallMatchScore: 0.95
13
+ }
14
+ },
15
+ {
16
+ entry: {
17
+ content: 'SDK Documentation content 2'
18
+ },
19
+ scoresInfo: {
20
+ overallMatchScore: 0.85
21
+ }
22
+ }
23
+ ]
24
+ },
25
+ SDK_SCHEMAS_KB_ID: {
26
+ results: [
27
+ {
28
+ entry: {
29
+ content: 'SDK Schema content 1'
30
+ },
31
+ scoresInfo: {
32
+ overallMatchScore: 0.9
33
+ }
34
+ }
35
+ ]
36
+ },
37
+ BUILD_APPS_KB_ID: {
38
+ results: [
39
+ {
40
+ entry: {
41
+ content: 'Build Apps content 1'
42
+ },
43
+ scoresInfo: {
44
+ overallMatchScore: 0.88
45
+ }
46
+ }
47
+ ]
48
+ }
49
+ }
50
+ };
51
+ const originalProcessEnv = process.env;
52
+ describe('Semantic Search', () => {
53
+ let fetchMock;
54
+ global.fetch = vi.fn();
55
+ beforeEach(() => {
56
+ vi.clearAllMocks();
57
+ process.env = { ...originalProcessEnv };
58
+ fetchMock = global.fetch;
59
+ fetchMock.mockReset();
60
+ fetchMock.mockResolvedValue({
61
+ ok: true,
62
+ status: 200,
63
+ statusText: 'OK',
64
+ json: async () => sampleSearchResponse
65
+ });
66
+ });
67
+ afterAll(() => {
68
+ process.env = originalProcessEnv;
69
+ });
70
+ test('performs SDK semantic search successfully', async () => {
71
+ const sdkResponse = {
72
+ results: {
73
+ SDK_DOCS_KB_ID: {
74
+ results: [
75
+ {
76
+ entry: {
77
+ content: 'SDK Documentation content 1'
78
+ },
79
+ scoresInfo: {
80
+ overallMatchScore: 0.95
81
+ }
82
+ },
83
+ {
84
+ entry: {
85
+ content: 'SDK Documentation content 2'
86
+ },
87
+ scoresInfo: {
88
+ overallMatchScore: 0.85
89
+ }
90
+ }
91
+ ]
92
+ },
93
+ SDK_SCHEMAS_KB_ID: {
94
+ results: [
95
+ {
96
+ entry: {
97
+ content: 'SDK Schema content 1'
98
+ },
99
+ scoresInfo: {
100
+ overallMatchScore: 0.9
101
+ }
102
+ }
103
+ ]
104
+ }
105
+ }
106
+ };
107
+ fetchMock.mockResolvedValueOnce({
108
+ ok: true,
109
+ status: 200,
110
+ json: async () => sdkResponse
111
+ });
112
+ const result = await runSemanticSearchAndFormat('SDK', {
113
+ searchTerm: 'wix-data query'
114
+ });
115
+ expect(fetchMock).toHaveBeenCalledTimes(1);
116
+ const [url] = fetchMock.mock.calls[0];
117
+ expect(url).toContain('mcp-docs-search/v1/search');
118
+ expect(url).toContain('maxResults=20');
119
+ expect(url).toContain('rerank=false');
120
+ expect(url).toContain('kbName=SDK_SCHEMAS_KB_ID');
121
+ expect(url).toContain('kbName=SDK_DOCS_KB_ID');
122
+ expect(url).toContain('searchTerm=wix-data+query');
123
+ // Results should be sorted by score and limited to 10
124
+ expect(result).toBe('SDK Documentation content 1\n\n---\n\nSDK Schema content 1\n\n---\n\nSDK Documentation content 2');
125
+ });
126
+ test('performs REST semantic search successfully', async () => {
127
+ const restResponse = {
128
+ results: {
129
+ REST_METHODS_KB_ID: {
130
+ results: [
131
+ {
132
+ entry: {
133
+ content: 'REST Methods content'
134
+ },
135
+ scoresInfo: {
136
+ overallMatchScore: 0.98
137
+ }
138
+ }
139
+ ]
140
+ },
141
+ REST_DOCS_KB_ID: {
142
+ results: [
143
+ {
144
+ entry: {
145
+ content: 'REST Documentation content'
146
+ },
147
+ scoresInfo: {
148
+ overallMatchScore: 0.95
149
+ }
150
+ }
151
+ ]
152
+ }
153
+ }
154
+ };
155
+ fetchMock.mockResolvedValueOnce({
156
+ ok: true,
157
+ status: 200,
158
+ json: async () => restResponse
159
+ });
160
+ const result = await runSemanticSearchAndFormat('REST', {
161
+ searchTerm: 'create collection'
162
+ });
163
+ expect(fetchMock).toHaveBeenCalledTimes(1);
164
+ const [url] = fetchMock.mock.calls[0];
165
+ expect(url).toContain('kbName=REST_METHODS_KB_ID');
166
+ expect(url).toContain('kbName=REST_DOCS_KB_ID');
167
+ expect(result).toBe('REST Methods content\n\n---\n\nREST Documentation content');
168
+ });
169
+ test('performs WDS semantic search successfully', async () => {
170
+ const wdsResponse = {
171
+ results: {
172
+ WDS_DOCS_KB_ID: {
173
+ results: [
174
+ {
175
+ entry: {
176
+ content: 'WDS Documentation content'
177
+ },
178
+ scoresInfo: {
179
+ overallMatchScore: 0.95
180
+ }
181
+ }
182
+ ]
183
+ }
184
+ }
185
+ };
186
+ fetchMock.mockResolvedValueOnce({
187
+ ok: true,
188
+ status: 200,
189
+ json: async () => wdsResponse
190
+ });
191
+ const result = await runSemanticSearchAndFormat('WDS', { searchTerm: 'Button component' }, 1);
192
+ expect(fetchMock).toHaveBeenCalledTimes(1);
193
+ const [url] = fetchMock.mock.calls[0];
194
+ expect(url).toContain('kbName=WDS_DOCS_KB_ID');
195
+ expect(result).toBe('WDS Documentation content');
196
+ });
197
+ test('performs BUILD_APPS semantic search successfully', async () => {
198
+ const buildAppsResponse = {
199
+ results: {
200
+ BUILD_APPS_KB_ID: {
201
+ results: [
202
+ {
203
+ entry: {
204
+ content: 'Build Apps content 1'
205
+ },
206
+ scoresInfo: {
207
+ overallMatchScore: 0.88
208
+ }
209
+ }
210
+ ]
211
+ }
212
+ }
213
+ };
214
+ fetchMock.mockResolvedValueOnce({
215
+ ok: true,
216
+ status: 200,
217
+ json: async () => buildAppsResponse
218
+ });
219
+ const result = await runSemanticSearchAndFormat('BUILD_APPS', { searchTerm: 'deploy app' }, 5);
220
+ expect(fetchMock).toHaveBeenCalledTimes(1);
221
+ const [url] = fetchMock.mock.calls[0];
222
+ expect(url).toContain('kbName=BUILD_APPS_KB_ID');
223
+ expect(result).toBe('Build Apps content 1');
224
+ });
225
+ test('performs WIX_HEADLESS semantic search successfully', async () => {
226
+ const wixHeadlessResponse = {
227
+ results: {
228
+ WIX_HEADLESS_KB_ID: {
229
+ results: [
230
+ {
231
+ entry: {
232
+ content: 'Wix Headless content 1'
233
+ },
234
+ scoresInfo: {
235
+ overallMatchScore: 0.92
236
+ }
237
+ }
238
+ ]
239
+ }
240
+ }
241
+ };
242
+ fetchMock.mockResolvedValueOnce({
243
+ ok: true,
244
+ status: 200,
245
+ json: async () => wixHeadlessResponse
246
+ });
247
+ const result = await runSemanticSearchAndFormat('WIX_HEADLESS', {
248
+ searchTerm: 'query'
249
+ });
250
+ expect(fetchMock).toHaveBeenCalledTimes(1);
251
+ const [url] = fetchMock.mock.calls[0];
252
+ expect(url).toContain('kbName=HEADLESS_KB_ID');
253
+ expect(result).toBe('Wix Headless content 1');
254
+ });
255
+ test('handles API error response', async () => {
256
+ fetchMock.mockResolvedValueOnce({
257
+ ok: false,
258
+ status: 500,
259
+ statusText: 'Internal Server Error'
260
+ });
261
+ await expect(runSemanticSearchAndFormat('SDK', { searchTerm: 'query' })).rejects.toThrow('Failed to run rag search: Internal Server Error');
262
+ });
263
+ test('handles empty results', async () => {
264
+ fetchMock.mockResolvedValueOnce({
265
+ ok: true,
266
+ status: 200,
267
+ json: async () => ({ results: {} })
268
+ });
269
+ await expect(runSemanticSearchAndFormat('SDK', { searchTerm: 'query' })).rejects.toThrow('No result from search');
270
+ });
271
+ test('respects maxResults parameter', async () => {
272
+ const maxResults = 5;
273
+ await runSemanticSearchAndFormat('SDK', { searchTerm: 'query' }, maxResults);
274
+ const [url] = fetchMock.mock.calls[0];
275
+ expect(url).toContain('maxResults=5');
276
+ });
277
+ test('respects rerank parameter', async () => {
278
+ await runSemanticSearchAndFormat('SDK', { searchTerm: 'query' }, 5, true);
279
+ const [url] = fetchMock.mock.calls[0];
280
+ expect(url).toContain('rerank=true');
281
+ });
282
+ test('uses WIX_COOKIE when available', async () => {
283
+ process.env.WIX_COOKIE = 'test-cookie';
284
+ await runSemanticSearchAndFormat('SDK', { searchTerm: 'query' });
285
+ expect(fetchMock).toHaveBeenCalledTimes(1);
286
+ const [, options] = fetchMock.mock.calls[0];
287
+ expect(options.headers.Cookie).toBe('test-cookie');
288
+ });
289
+ });