@wix/mcp 1.0.91 → 1.0.93
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.
- package/build/bin-standalone.js +63 -28
- package/build/cjs/index.cjs +282 -31
- package/build/cjs/toolkit.cjs +282 -31
- package/build/dts/code-mode/attachments.d.ts +37 -0
- package/build/dts/code-mode/attachments.js +218 -0
- package/build/dts/code-mode/index.js +36 -3
- package/build/dts/config/default-config.js +21 -12
- package/build/dts/config/param-descriptions.js +3 -3
- package/build/dts/docs/docs.js +14 -8
- package/build/dts/docs/semanticSearch.d.ts +2 -1
- package/build/dts/docs/semanticSearch.js +12 -5
- package/build/dts/docs/start-deploy-website.d.ts +9 -1
- package/build/dts/docs/start-deploy-website.js +36 -4
- package/build/esm/index.js +282 -31
- package/build/esm/toolkit.js +282 -31
- package/package.json +2 -2
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatGPT file attachments for ExecuteWixAPI (`openai/fileParams`).
|
|
3
|
+
*
|
|
4
|
+
* ChatGPT resolves a file the user attached — or one the model wrote in its
|
|
5
|
+
* own sandbox — into `{ download_url, file_id, file_name?, mime_type? }` for a
|
|
6
|
+
* param the tool lists under `_meta["openai/fileParams"]`. The bytes never go
|
|
7
|
+
* through the model. We fetch each `download_url` host-side and hand the
|
|
8
|
+
* runtime ready-made base64 file entries, the same shape the `files` bundle
|
|
9
|
+
* parses into, so `files`, `wix.multipart()` and `wix.unzip()` are unchanged.
|
|
10
|
+
*
|
|
11
|
+
* This is additive: clients without the extension never populate the param
|
|
12
|
+
* and keep using the `files` bundle (text + base64 sections).
|
|
13
|
+
*/
|
|
14
|
+
// Per call, across all attachments. The base64 (~4/3 of this) is embedded in
|
|
15
|
+
// the generated sandbox module next to the `files` bundle, and both the host
|
|
16
|
+
// Worker and the sandbox isolate hold it alongside the decoded bytes and the
|
|
17
|
+
// multipart body within a 128 MB isolate — 10 MB keeps the whole chain well
|
|
18
|
+
// inside that. A site zip or a handful of images fits.
|
|
19
|
+
export const MAX_ATTACHMENTS_BYTES = 10 * 1024 * 1024;
|
|
20
|
+
// Follow at most this many redirects, revalidating the host at every hop.
|
|
21
|
+
const MAX_REDIRECTS = 4;
|
|
22
|
+
// Hosts ChatGPT hands out in `download_url` (every host seen in
|
|
23
|
+
// UploadImageToWixSite download_urls, 2026-08-01..10-08). Two families:
|
|
24
|
+
// - `<label>.oaiusercontent.com`: an OpenAI-owned domain, so any one-label
|
|
25
|
+
// subdomain is OpenAI's (observed: sdmntpr<region>, files).
|
|
26
|
+
// - Azure blob accounts behind them, one per OpenAI region.
|
|
27
|
+
// `blob.core.windows.net` is a shared namespace, so these are exact — a
|
|
28
|
+
// prefix could be registered by anyone. A new region shows up as an
|
|
29
|
+
// "is not a ChatGPT file host" error in BI; add it here.
|
|
30
|
+
const OAI_USERCONTENT_HOST = /^[a-z0-9-]+\.oaiusercontent\.com$/;
|
|
31
|
+
const OAI_BLOB_ACCOUNTS = new Set([
|
|
32
|
+
'australiaeast',
|
|
33
|
+
'brazilsouth',
|
|
34
|
+
'cacentral',
|
|
35
|
+
'centralindia',
|
|
36
|
+
'centralus',
|
|
37
|
+
'centralusdr',
|
|
38
|
+
'denmarkeast',
|
|
39
|
+
'eastus',
|
|
40
|
+
'eastus2',
|
|
41
|
+
'indiasocentral',
|
|
42
|
+
'italynorth',
|
|
43
|
+
'japaneast',
|
|
44
|
+
'koreacentral',
|
|
45
|
+
'northcentralus',
|
|
46
|
+
'northeu',
|
|
47
|
+
'nznorth',
|
|
48
|
+
'polandcentral',
|
|
49
|
+
'seasia',
|
|
50
|
+
'southcentralus',
|
|
51
|
+
'southeastus3',
|
|
52
|
+
'ukwest',
|
|
53
|
+
'westcentralus',
|
|
54
|
+
'westus',
|
|
55
|
+
'westus2',
|
|
56
|
+
'westus3'
|
|
57
|
+
].map((region) => `oaisdmntpr${region}.blob.core.windows.net`));
|
|
58
|
+
export function isAllowedAttachmentHost(hostname) {
|
|
59
|
+
const h = hostname.toLowerCase();
|
|
60
|
+
return OAI_USERCONTENT_HOST.test(h) || OAI_BLOB_ACCOUNTS.has(h);
|
|
61
|
+
}
|
|
62
|
+
/** https, default port, allowed host — applied to the first URL and every redirect. */
|
|
63
|
+
function checkAttachmentUrl(url) {
|
|
64
|
+
if (url.protocol !== 'https:')
|
|
65
|
+
return 'is not an https URL';
|
|
66
|
+
if (url.port)
|
|
67
|
+
return 'uses a non-default port';
|
|
68
|
+
if (!isAllowedAttachmentHost(url.hostname)) {
|
|
69
|
+
return `host "${url.hostname}" is not a ChatGPT file host`;
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
export class AttachmentError extends Error {
|
|
74
|
+
}
|
|
75
|
+
/** Path inside the upload: the file name, else the URL's last segment, else the id. */
|
|
76
|
+
function attachmentPath(a, url) {
|
|
77
|
+
let fromUrl = '';
|
|
78
|
+
try {
|
|
79
|
+
fromUrl = decodeURIComponent(url.pathname.split('/').filter(Boolean).pop() ?? '');
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
fromUrl = '';
|
|
83
|
+
}
|
|
84
|
+
const raw = (a.file_name?.trim() || fromUrl || a.file_id).replace(/\\/g, '/');
|
|
85
|
+
// Relative, no traversal, no empty segments.
|
|
86
|
+
const segments = raw.split('/').filter((s) => s && s !== '.' && s !== '..');
|
|
87
|
+
return segments.join('/') || a.file_id;
|
|
88
|
+
}
|
|
89
|
+
function toBase64(bytes) {
|
|
90
|
+
let binary = '';
|
|
91
|
+
const chunk = 0x8000;
|
|
92
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
93
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
94
|
+
}
|
|
95
|
+
return btoa(binary);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* GET with manual redirects: every hop must pass checkAttachmentUrl, so an
|
|
99
|
+
* allowed host cannot bounce the server to an arbitrary or internal one.
|
|
100
|
+
*/
|
|
101
|
+
async function fetchAllowed(start, fetchImpl, label) {
|
|
102
|
+
let url = start;
|
|
103
|
+
for (let hop = 0;; hop++) {
|
|
104
|
+
const res = await fetchImpl(url.toString(), { redirect: 'manual' });
|
|
105
|
+
const location = res.headers.get('location');
|
|
106
|
+
const isRedirect = res.status >= 300 && res.status < 400 && location;
|
|
107
|
+
if (!isRedirect)
|
|
108
|
+
return res;
|
|
109
|
+
await res.body?.cancel().catch(() => undefined);
|
|
110
|
+
if (hop >= MAX_REDIRECTS) {
|
|
111
|
+
throw new AttachmentError(`${label}: too many redirects.`);
|
|
112
|
+
}
|
|
113
|
+
let next;
|
|
114
|
+
try {
|
|
115
|
+
next = new URL(location, url);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
throw new AttachmentError(`${label}: redirect to an invalid URL.`);
|
|
119
|
+
}
|
|
120
|
+
const problem = checkAttachmentUrl(next);
|
|
121
|
+
if (problem) {
|
|
122
|
+
throw new AttachmentError(`${label}: the download redirected to a URL that ${problem}; refused.`);
|
|
123
|
+
}
|
|
124
|
+
url = next;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Read a body while counting bytes against `budget`; stops reading (and
|
|
129
|
+
* cancels the stream) as soon as it is exceeded, instead of buffering first.
|
|
130
|
+
*/
|
|
131
|
+
async function readWithin(res, budget, overBudget) {
|
|
132
|
+
const declared = Number(res.headers.get('content-length') ?? 0);
|
|
133
|
+
if (declared > budget)
|
|
134
|
+
throw overBudget();
|
|
135
|
+
if (!res.body) {
|
|
136
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
137
|
+
if (bytes.byteLength > budget)
|
|
138
|
+
throw overBudget();
|
|
139
|
+
return bytes;
|
|
140
|
+
}
|
|
141
|
+
const reader = res.body.getReader();
|
|
142
|
+
const chunks = [];
|
|
143
|
+
let total = 0;
|
|
144
|
+
for (;;) {
|
|
145
|
+
const { done, value } = await reader.read();
|
|
146
|
+
if (done)
|
|
147
|
+
break;
|
|
148
|
+
total += value.byteLength;
|
|
149
|
+
if (total > budget) {
|
|
150
|
+
await reader.cancel().catch(() => undefined);
|
|
151
|
+
throw overBudget();
|
|
152
|
+
}
|
|
153
|
+
chunks.push(value);
|
|
154
|
+
}
|
|
155
|
+
const out = new Uint8Array(total);
|
|
156
|
+
let offset = 0;
|
|
157
|
+
for (const c of chunks) {
|
|
158
|
+
out.set(c, offset);
|
|
159
|
+
offset += c.byteLength;
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Fetch every attachment and return base64 file entries for the runtime.
|
|
165
|
+
* Throws AttachmentError with an agent-facing message: an unresolved item
|
|
166
|
+
* (ChatGPT passed a sandbox path or a bare id instead of a URL), a host we
|
|
167
|
+
* don't trust, a failed download, or more bytes than MAX_ATTACHMENTS_BYTES.
|
|
168
|
+
*/
|
|
169
|
+
export async function resolveAttachments(attachments, fetchImpl = fetch) {
|
|
170
|
+
if (!Array.isArray(attachments) || attachments.length === 0)
|
|
171
|
+
return [];
|
|
172
|
+
const items = [];
|
|
173
|
+
attachments.forEach((raw, i) => {
|
|
174
|
+
const a = (typeof raw === 'object' && raw);
|
|
175
|
+
const downloadUrl = a && typeof a.download_url === 'string' ? a.download_url : '';
|
|
176
|
+
let url;
|
|
177
|
+
try {
|
|
178
|
+
url = /^https?:\/\//i.test(downloadUrl)
|
|
179
|
+
? new URL(downloadUrl)
|
|
180
|
+
: undefined;
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
url = undefined;
|
|
184
|
+
}
|
|
185
|
+
if (!a || !url) {
|
|
186
|
+
throw new AttachmentError(`attachments[${i}] was not resolved to a download URL (got ${JSON.stringify(typeof raw === 'string' ? raw : ((a && a.download_url) ?? raw))}). The platform did not hydrate this attachment; retry once, and if it still arrives like this put the file in the \`files\` bundle instead (\`=== FILE: <path> base64 ===\` for binary).`);
|
|
187
|
+
}
|
|
188
|
+
const problem = checkAttachmentUrl(url);
|
|
189
|
+
if (problem) {
|
|
190
|
+
throw new AttachmentError(`attachments[${i}].download_url ${problem}. Only platform-resolved attachments are accepted here; for any other URL fetch it in \`code\` via wix.request or put the bytes in the \`files\` bundle.`);
|
|
191
|
+
}
|
|
192
|
+
items.push({ a, url });
|
|
193
|
+
});
|
|
194
|
+
let remaining = MAX_ATTACHMENTS_BYTES;
|
|
195
|
+
const out = [];
|
|
196
|
+
for (const [i, { a, url }] of items.entries()) {
|
|
197
|
+
const path = attachmentPath(a, url);
|
|
198
|
+
const label = `attachments[${i}] (${path})`;
|
|
199
|
+
let res;
|
|
200
|
+
try {
|
|
201
|
+
res = await fetchAllowed(url, fetchImpl, label);
|
|
202
|
+
}
|
|
203
|
+
catch (e) {
|
|
204
|
+
if (e instanceof AttachmentError)
|
|
205
|
+
throw e;
|
|
206
|
+
throw new AttachmentError(`${label}: download failed (${e.message}).`);
|
|
207
|
+
}
|
|
208
|
+
if (!res.ok) {
|
|
209
|
+
await res.body?.cancel().catch(() => undefined);
|
|
210
|
+
throw new AttachmentError(`${label}: download returned HTTP ${res.status}. The download_url may have expired; attach the file again.`);
|
|
211
|
+
}
|
|
212
|
+
const bytes = await readWithin(res, remaining, () => new AttachmentError(`${label} pushes the attachments past ${MAX_ATTACHMENTS_BYTES} bytes in total for this call; split the upload across calls.`));
|
|
213
|
+
remaining -= bytes.byteLength;
|
|
214
|
+
out.push({ path, content: toBase64(bytes), encoding: 'base64' });
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
//# sourceMappingURL=attachments.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { AttachmentError, resolveAttachments } from './attachments.js';
|
|
2
3
|
import { logger } from '../infra/logger.js';
|
|
3
4
|
import { defaultToolDescriptions } from '../config/default-config.js';
|
|
4
5
|
import { truncateResponse } from './truncate-response.js';
|
|
@@ -214,7 +215,7 @@ function addExecuteTool(server, options) {
|
|
|
214
215
|
const { codeModeBaseUrl, codeModeFetch, authStrategy, visitor, toolDescriptions, maxResponseSize } = options;
|
|
215
216
|
const executeUrl = `${codeModeBaseUrl}/api/code-mode/execute`;
|
|
216
217
|
const d = desc('ExecuteWixAPI');
|
|
217
|
-
server.tool(EXECUTE_TOOL_NAME, toolDescriptions?.[EXECUTE_TOOL_NAME] ??
|
|
218
|
+
const registered = server.tool(EXECUTE_TOOL_NAME, toolDescriptions?.[EXECUTE_TOOL_NAME] ??
|
|
218
219
|
defaultToolDescriptions.ExecuteWixAPI, {
|
|
219
220
|
code: z
|
|
220
221
|
.string()
|
|
@@ -246,6 +247,19 @@ function addExecuteTool(server, options) {
|
|
|
246
247
|
'In `code` they are the `files` global (`{ path, content, encoding?: "base64" }[]`); `wix.multipart()` turns them into one binary-safe `multipart/form-data` body: `const mp = wix.multipart(); await wix.request({ scope: "account", method: "POST", url, headers: { "Content-Type": mp.contentType }, body: mp.body });`.',
|
|
247
248
|
'Use base64 entries for small assets (icons, logos, fonts) and for files you downloaded and are re-uploading; large images/video go in by URL (`<img src="https://...">`), not in the bundle. A file containing a line exactly like the delimiter is not supported.'
|
|
248
249
|
].join(' ')),
|
|
250
|
+
// ChatGPT file attachments (`openai/fileParams`). Item shape must match
|
|
251
|
+
// OpenAI's file schema: download_url + file_id required, the rest optional.
|
|
252
|
+
// Declaring it is what makes the platform hand us a download URL instead
|
|
253
|
+
// of a sandbox path. Resolved host-side into base64 `files` entries.
|
|
254
|
+
attachments: z
|
|
255
|
+
.array(z.object({
|
|
256
|
+
download_url: z.string(),
|
|
257
|
+
file_id: z.string(),
|
|
258
|
+
file_name: z.string().optional(),
|
|
259
|
+
mime_type: z.string().optional()
|
|
260
|
+
}))
|
|
261
|
+
.optional()
|
|
262
|
+
.describe('Files attached in this chat, or files you created in your own file sandbox (any type: a zip of a site, HTML, images, PDFs). Pass the files themselves and the platform resolves each into { download_url, file_id, file_name?, mime_type? }; the bytes are fetched here and appended to the `files` global as { path, content, encoding: "base64" } entries (path = file_name), so `wix.multipart()` and `wix.unzip()` work on them unchanged. Prefer this over base64 in `files` whenever you have a file handle. Up to 10 MB per call.'),
|
|
249
263
|
// Visitor mode takes an agent-supplied token and pins the site; account/site
|
|
250
264
|
// mode lets the agent pick the site.
|
|
251
265
|
...(visitor
|
|
@@ -264,7 +278,7 @@ function addExecuteTool(server, options) {
|
|
|
264
278
|
// Partial: duck-typed servers (the toolkit's capturing server, test
|
|
265
279
|
// doubles) build their own extra and may not supply every field.
|
|
266
280
|
extra) => {
|
|
267
|
-
const { code, reason, hasMutations, sourceDocUrls, visitorToken, files } = rawArgs;
|
|
281
|
+
const { code, reason, hasMutations, sourceDocUrls, visitorToken, files, attachments } = rawArgs;
|
|
268
282
|
// In visitor mode the site is pinned; otherwise it comes from the param.
|
|
269
283
|
const siteId = visitor ? visitor.siteId : rawArgs.siteId;
|
|
270
284
|
logger.log(`[ExecuteWixAPI]${visitor ? ' (visitor)' : ''} Executing code (${code.length} chars), siteId=${siteId ?? 'none'}, hasMutations=${hasMutations}, sourceDocUrls=${JSON.stringify(sourceDocUrls)}, reason="${reason}"`);
|
|
@@ -276,6 +290,18 @@ function addExecuteTool(server, options) {
|
|
|
276
290
|
logger.log(`[ExecuteWixAPI]${visitor ? ' (visitor)' : ''} Rejected invalid code: ${validation.error}`);
|
|
277
291
|
return executeTextResult(validation.error);
|
|
278
292
|
}
|
|
293
|
+
// ChatGPT attachments → base64 file entries, fetched host-side from the
|
|
294
|
+
// platform's download URLs. Before any token work: an unresolved item is
|
|
295
|
+
// an agent-facing error, not a server failure.
|
|
296
|
+
let attachmentFiles = [];
|
|
297
|
+
try {
|
|
298
|
+
attachmentFiles = await resolveAttachments(attachments);
|
|
299
|
+
}
|
|
300
|
+
catch (e) {
|
|
301
|
+
if (e instanceof AttachmentError)
|
|
302
|
+
return executeErrorResult(e.message);
|
|
303
|
+
throw e;
|
|
304
|
+
}
|
|
279
305
|
// Account/site mode resolves tokens from the strategy (before the try so
|
|
280
306
|
// an UnauthorizedError propagates to wrapToolCallback). Visitor mode has no
|
|
281
307
|
// strategy — these stay undefined and the visitor token is used instead.
|
|
@@ -329,7 +355,8 @@ function addExecuteTool(server, options) {
|
|
|
329
355
|
: accountHeaders?.Authorization,
|
|
330
356
|
accountId: accountHeaders?.['wix-account-id'],
|
|
331
357
|
siteAuthContext,
|
|
332
|
-
...(files ? { files } : {})
|
|
358
|
+
...(files ? { files } : {}),
|
|
359
|
+
...(attachmentFiles.length ? { attachmentFiles } : {})
|
|
333
360
|
}, visitor
|
|
334
361
|
? visitorToken
|
|
335
362
|
: (accountHeaders?.Authorization ?? siteHeaders?.Authorization), codeModeFetch);
|
|
@@ -354,5 +381,11 @@ function addExecuteTool(server, options) {
|
|
|
354
381
|
return executeErrorResult(message);
|
|
355
382
|
}
|
|
356
383
|
});
|
|
384
|
+
// ChatGPT resolves `attachments` into download URLs only for params listed
|
|
385
|
+
// here (OpenAI's `openai/fileParams` extension). Other clients ignore it.
|
|
386
|
+
// Optional-chained: duck-typed servers (tests, the toolkit) return nothing.
|
|
387
|
+
registered?.update?.({
|
|
388
|
+
_meta: { ...registered._meta, 'openai/fileParams': ['attachments'] }
|
|
389
|
+
});
|
|
357
390
|
}
|
|
358
391
|
//# sourceMappingURL=index.js.map
|
|
@@ -78,9 +78,11 @@ export const SITE_CREATION_COPY = {
|
|
|
78
78
|
* `CreateWixBusinessGuide` is exempt: headless/classic/manual don't need the site's purpose.
|
|
79
79
|
*/
|
|
80
80
|
const SITE_GUIDANCE = dedent `
|
|
81
|
-
|
|
81
|
+
First decide which of two different things is being asked for:
|
|
82
|
+
- The site is code the user or you control — hand-written HTML/CSS/JS, a static build, a zip, another AI builder's export, or a page or design you made earlier in this conversation — and it should go live on Wix. This is also the route whenever the user wants to keep working on the site here, with you: a Wix Headless site is a regular coded site, so you keep full control of the design and can change and redeploy it from this conversation at any time, and Wix business capabilities can be added to it as a backend when they are needed. Call StartDeployWebsite: it returns the recipe to host those exact files as a Wix Headless site (updates reuse the same site and URL; the drop page is only its fallback). Do not offer AI or templates for this and do not call any site-creation tool.
|
|
83
|
+
- Wix should create the site — generated with AI from a description, or started from a template — and the user will design and edit it in Wix rather than through you. That is what the site-creation tools do; they never take files you already have.
|
|
82
84
|
|
|
83
|
-
|
|
85
|
+
For a new site there are two things to settle: what the site is for, and how to build it. The how is AI generation vs. starting from a template, so naming templates, a template id or Wix Studio (Wix Studio is also the template side) already settles it.
|
|
84
86
|
|
|
85
87
|
Settle what the site is for first: its purpose, topic, business type, audience, or main visitor action. Naming a method is not a description of the site, and neither is the business or industry on its own.
|
|
86
88
|
If you can't tell yet, ask the preferred question below and nothing else — one question, no creation-method question in the same reply, and no internal tool names.
|
|
@@ -180,13 +182,14 @@ const ACCOUNT_SITE_EXECUTE_SCOPE = dedent `
|
|
|
180
182
|
unzip(input: string | Uint8Array): Promise<Array<{ path: string; bytes: Uint8Array; text(): string }>>;
|
|
181
183
|
};
|
|
182
184
|
declare const siteId: string | undefined;
|
|
183
|
-
// Files from the ExecuteWixAPI \`files\` param
|
|
184
|
-
//
|
|
185
|
-
//
|
|
185
|
+
// Files from the ExecuteWixAPI \`files\` param, followed by the resolved
|
|
186
|
+
// \`attachments\`. Text entries have \`content\`; an entry tagged
|
|
187
|
+
// \`=== FILE: <path> base64 ===\` — and every attachment, path = its file name —
|
|
188
|
+
// has encoding:'base64' and base64 \`content\`.
|
|
186
189
|
declare const files: Array<{ path: string; content: string; encoding?: "base64" }>;
|
|
187
190
|
\`\`\`
|
|
188
191
|
|
|
189
|
-
FILE UPLOADS:
|
|
192
|
+
FILE UPLOADS: files never go inline in \`code\`; two params feed the \`files\` global. \`attachments\` takes files attached in the chat or created in your file sandbox, of any type and size up to 10 MB per call (a zip of a whole site, HTML, images, PDFs): pass the files themselves, all in one call, and the platform resolves each into \`{ download_url, file_id, file_name?, mime_type? }\` — the bytes are fetched for you and arrive as base64 entries named by file name. The \`files\` param takes content you have as text here: text goes raw; a small binary (icon, logo, font) rides as a \`base64\`-tagged entry — large images/video are referenced by URL (\`<img src="https://...">\`), not bundled. \`wix.multipart()\` turns the whole set into one upload body (bytes, so binary is safe):
|
|
190
193
|
\`\`\`javascript
|
|
191
194
|
// files param:
|
|
192
195
|
// === FILE: index.html ===
|
|
@@ -201,6 +204,12 @@ const ACCOUNT_SITE_EXECUTE_SCOPE = dedent `
|
|
|
201
204
|
return res.data;
|
|
202
205
|
}
|
|
203
206
|
\`\`\`
|
|
207
|
+
A site zip passed through \`attachments\` is one base64 entry; unzip it and upload the entries:
|
|
208
|
+
\`\`\`javascript
|
|
209
|
+
// attachments: [site.zip] → files = [{ path: "site.zip", content: "<base64>", encoding: "base64" }]
|
|
210
|
+
const entries = await wix.unzip(files[0].content); // [{ path, bytes, text() }]
|
|
211
|
+
const mp = wix.multipart(entries);
|
|
212
|
+
\`\`\`
|
|
204
213
|
DOWNLOAD + EDIT + RE-UPLOAD: fetch the zip as base64, \`wix.unzip()\` it, edit text via \`entry.text()\`, keep binary as bytes, and hand the entries back to \`wix.multipart()\`:
|
|
205
214
|
\`\`\`javascript
|
|
206
215
|
const r = await wix.request({ scope: "account", method: "GET", responseType: "base64", url: zipUrl });
|
|
@@ -402,12 +411,12 @@ export function buildToolDescriptions(includeReadme, includeImageUploadGuidance
|
|
|
402
411
|
NEVER call this tool with a missing, empty, or guessed siteId — ask first instead.
|
|
403
412
|
|
|
404
413
|
Parameters — provide image input via image and/or imageUrls, OR imageBase64:
|
|
405
|
-
• image (array): chat file ATTACHMENTS. Each item is an object with download_url and file_id (both required), plus optional file_name and mime_type. Pass ALL attachments in one call.
|
|
414
|
+
• image (array): chat file ATTACHMENTS — files the user attached, or files you created in your own file sandbox. Each item is an object with download_url and file_id (both required), plus optional file_name and mime_type. Pass ALL attachments in one call.
|
|
406
415
|
• imageUrls (array): plain public image URLs the user pasted or other tools produced (no file_id). Pass ALL URLs in one call. Can be combined with image.
|
|
407
416
|
• imageBase64 (string): base64-encoded image + mimeType. One image at a time. Do NOT combine with image/imageUrls.
|
|
408
417
|
|
|
409
418
|
Delivering the bytes — this is where uploads most often fail:
|
|
410
|
-
•
|
|
419
|
+
• Pass a file as a file, in image: the platform resolves it into download_url and file_id before it reaches this tool — this includes files you created in your sandbox. This server does not share your filesystem, so a bare path string (e.g. /mnt/..., /home/user/..., /Users/...) is not a file. Without a file to pass, use a public http(s) URL (imageUrls) or the complete base64 (imageBase64).
|
|
411
420
|
• imageBase64 must be the WHOLE file, not truncated. For large images prefer imageUrls, since long base64 is easily cut off and will be rejected as unreadable.
|
|
412
421
|
`,
|
|
413
422
|
SupportAndFeedback: dedent `
|
|
@@ -904,14 +913,14 @@ export function buildToolDescriptions(includeReadme, includeImageUploadGuidance
|
|
|
904
913
|
WixSiteBuilder: dedent `
|
|
905
914
|
${SITE_GUIDANCE}
|
|
906
915
|
|
|
907
|
-
Tool for generating a Wix site with AI (creating / building a site in Wix Harmony).
|
|
916
|
+
Tool for generating a Wix site with AI (creating / building a site in Wix Harmony) from a description. It does not take pages or files you already have — those go to StartDeployWebsite.
|
|
908
917
|
|
|
909
918
|
Call this ONLY when the user's own words name AI (for example "create a site using AI" or "build it with AI") and the site intent is clear. A description of what the site should do — reservations, online ordering, a store, bookings, menus, a portfolio, and the like — is NOT a request for AI and NOT enough to call this tool; never infer AI from features or industry.
|
|
910
919
|
Never default to this tool: if the user hasn't explicitly named AI, don't call this tool — ${SITE_CREATION_COPY.unstatedMethod.nonWidget}.
|
|
911
920
|
|
|
912
921
|
For templates or Wix Studio, call \`SearchSiteTemplates\`; for a specific template by \`metaSiteId\` or \`templateId\`, call \`CreateSiteFromTemplate\`; for a headless or classic Wix Editor site, or manual/account-API creation, call \`CreateWixBusinessGuide\`.
|
|
913
922
|
|
|
914
|
-
Do NOT suggest HTML code, prompt templates, or alternative approaches — actually build the site through this tool.
|
|
923
|
+
Do NOT suggest HTML code, prompt templates, or alternative approaches instead of building — actually build the site through this tool. (HTML the user already has is the StartDeployWebsite case, not a reason to build.)
|
|
915
924
|
`,
|
|
916
925
|
SearchSiteTemplates: dedent `
|
|
917
926
|
${SITE_GUIDANCE}
|
|
@@ -955,8 +964,8 @@ export function buildToolDescriptions(includeReadme, includeImageUploadGuidance
|
|
|
955
964
|
Set \`hasMutations: true\` for any document containing a mutation root field.
|
|
956
965
|
`,
|
|
957
966
|
StartDeployWebsite: dedent `
|
|
958
|
-
The user — or you, earlier in this conversation — ALREADY HAVE a finished page or site, in any form, and it should be live on Wix
|
|
959
|
-
|
|
967
|
+
The user — or you, earlier in this conversation — ALREADY HAVE a finished page or site, in any form, and it should be live on Wix — or an already-deployed Wix Headless site whose files should be replaced. The site can be HTML/CSS/JS files, a static build (dist/, out/), a zip, another AI builder's export, or a page/design you created in this conversation (export it to HTML first — a finished design IS a finished page). Call this when the user says deploy, host, publish, put online, make it live, get a URL, update my site, or upload my site / index.html.
|
|
968
|
+
Updates keep the same site and URL. Also for implementing a backend on such a site as a Wix Headless project — same site, same URL. Works with or without a Wix login (anonymous deploy with a save link), through the Wix MCP alone or from a shell.
|
|
960
969
|
Missing real functionality — a fake checkout, a form that goes nowhere, bookings — is NOT a reason to rebuild the site with a site-creation tool: deploy the page as is, then add the backend afterwards (the recipe covers it).
|
|
961
970
|
Returns the full recipe; follow it and run the flow yourself. The drop page is only the fallback when you can't.
|
|
962
971
|
NOT for: a screenshot, mockup or reference image of a site the user WANTS (nothing exists to deploy yet — that is site creation: WixSiteBuilder / templates); a Claude Design share URL (import-claude-design-from-url); migrating a live site by URL or CSV exports; adding HTML or code into an existing Wix site; uploading images or documents to a site's media (UploadImageToWixSite).
|
|
@@ -73,7 +73,7 @@ export const paramDescriptions = {
|
|
|
73
73
|
},
|
|
74
74
|
SearchWixRESTDocumentation: {
|
|
75
75
|
searchTerm: 'The search term to look up in the Wix REST API Documentation — a natural-language description of the endpoint, resource, or action you need across any Wix domain (Stores, eCommerce, Bookings, CRM, Members, CMS, Events, Blog, Forms, Media, site settings, etc.), e.g. "create a catalog item with inventory", "book a service", "update data collection items". Match the wording to how many entities you will act on: a plural term ("update products") surfaces the resource\'s bulk methods, which a singular term ("update a product") ranks lower or leaves out. Results are ranked by relevance, so describe the goal rather than guessing an exact API name.',
|
|
76
|
-
maxResults: 'The maximum number of results to return, default
|
|
76
|
+
maxResults: 'The maximum number of results to return (1–17, default 17). Results mix REST API documentation with matching recipes and skills.',
|
|
77
77
|
reason: 'One sentence describing the original user request and the task you are trying to accomplish with this search.'
|
|
78
78
|
},
|
|
79
79
|
WixBusinessFlowsDocumentation: {
|
|
@@ -94,7 +94,7 @@ export const paramDescriptions = {
|
|
|
94
94
|
},
|
|
95
95
|
SearchWixHeadlessDocumentation: {
|
|
96
96
|
searchTerm: 'The search term to search for in the Headless Documentation',
|
|
97
|
-
maxResults: 'The maximum number of results to return, default
|
|
97
|
+
maxResults: 'The maximum number of results to return (1–17, default 17). Results mix Headless documentation with matching recipes and skills.'
|
|
98
98
|
},
|
|
99
99
|
SearchWixVeloDocumentation: {
|
|
100
100
|
searchTerm: 'The search term to search for in the Velo Documentation',
|
|
@@ -125,7 +125,7 @@ export const paramDescriptions = {
|
|
|
125
125
|
},
|
|
126
126
|
UploadImageToWixSite: {
|
|
127
127
|
siteId: 'The ID (UUID) of the Wix site to upload to. REQUIRED. If the user has not clearly specified which site, ASK them which site to upload to (or use ListWixSites to help them pick) BEFORE calling this tool. NEVER call this tool with a missing, empty, placeholder, or guessed siteId — ask first instead.',
|
|
128
|
-
image: 'Array of file ATTACHMENTS the user uploaded in the chat. Each item requires download_url and file_id, and may optionally include file_name and mime_type —
|
|
128
|
+
image: 'Array of file ATTACHMENTS: files the user uploaded in the chat, or files you created in your own file sandbox. Each item requires download_url and file_id, and may optionally include file_name and mime_type — the platform resolves a file passed here into these fields automatically, so just include the files. For plain image URLs the user pasted, use imageUrls instead. ALWAYS pass ALL attachments together in a single call — NEVER call this tool once per image. Do NOT use together with imageBase64.',
|
|
129
129
|
imageUrls: 'Array of publicly accessible image URLs (e.g. links the user pasted or URLs produced by other tools). Use this for plain URLs that are NOT chat attachments — no file_id is needed. ALWAYS pass ALL URLs together in a single call. Can be combined with image; do NOT use together with imageBase64.',
|
|
130
130
|
imageBase64: 'Base64-encoded image data. Use when the client can read a file and encode it as base64. May include a data URL prefix (e.g. "data:image/jpeg;base64,...") or be raw base64. Do NOT use together with image.',
|
|
131
131
|
mimeType: 'MIME type of the image (e.g. "image/jpeg", "image/png", "image/webp"). Required when imageBase64 does not include a data URL prefix.',
|
package/build/dts/docs/docs.js
CHANGED
|
@@ -195,9 +195,9 @@ export const addDocsTools = (server, allowedTools = [
|
|
|
195
195
|
.number()
|
|
196
196
|
.describe(d.maxResults)
|
|
197
197
|
.min(1)
|
|
198
|
-
.max(
|
|
198
|
+
.max(17)
|
|
199
199
|
.optional()
|
|
200
|
-
.default(
|
|
200
|
+
.default(17),
|
|
201
201
|
reason: z.string().describe(d.reason)
|
|
202
202
|
};
|
|
203
203
|
})(), { readOnlyHint: true, destructiveHint: false, openWorldHint: false }, async ({ searchTerm, maxResults, reason }, { panorama }) => {
|
|
@@ -208,9 +208,12 @@ export const addDocsTools = (server, allowedTools = [
|
|
|
208
208
|
toolParams: {
|
|
209
209
|
searchTerm: searchTerm
|
|
210
210
|
},
|
|
211
|
-
maxResults: Math.max(1, Math.min(maxResults ??
|
|
211
|
+
maxResults: Math.max(1, Math.min(maxResults ?? 17, 17)),
|
|
212
212
|
linesInEachResult: 30,
|
|
213
|
-
usePlatformizedSearch
|
|
213
|
+
usePlatformizedSearch,
|
|
214
|
+
// Merge recipes/skills into the REST ranking — they live in a
|
|
215
|
+
// separate corpus the REST document_type doesn't cover on its own.
|
|
216
|
+
extraDocumentTypes: ['SKILLS']
|
|
214
217
|
});
|
|
215
218
|
// Only when a result actually carried the marker. Re-deriving it here would also fire on
|
|
216
219
|
// an article that merely references a query endpoint, with no such endpoint in the results.
|
|
@@ -472,9 +475,9 @@ export const addDocsTools = (server, allowedTools = [
|
|
|
472
475
|
.number()
|
|
473
476
|
.describe(d.maxResults)
|
|
474
477
|
.min(1)
|
|
475
|
-
.max(
|
|
478
|
+
.max(17)
|
|
476
479
|
.optional()
|
|
477
|
-
.default(
|
|
480
|
+
.default(17)
|
|
478
481
|
};
|
|
479
482
|
})(), { readOnlyHint: true, destructiveHint: false, openWorldHint: false }, async ({ searchTerm, maxResults }, { panorama }) => {
|
|
480
483
|
try {
|
|
@@ -484,8 +487,11 @@ export const addDocsTools = (server, allowedTools = [
|
|
|
484
487
|
toolParams: {
|
|
485
488
|
searchTerm: searchTerm
|
|
486
489
|
},
|
|
487
|
-
maxResults: Math.max(1, Math.min(maxResults ??
|
|
488
|
-
usePlatformizedSearch
|
|
490
|
+
maxResults: Math.max(1, Math.min(maxResults ?? 17, 17)),
|
|
491
|
+
usePlatformizedSearch,
|
|
492
|
+
// Merge recipes/skills into the headless ranking — same separate
|
|
493
|
+
// corpus the WIX_HEADLESS document_type doesn't cover on its own.
|
|
494
|
+
extraDocumentTypes: ['SKILLS']
|
|
489
495
|
});
|
|
490
496
|
return {
|
|
491
497
|
content: [
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { DocsTool } from './docs.js';
|
|
2
2
|
export declare const DEFAULT_MAX_RESULTS = 15;
|
|
3
|
-
export declare const runSemanticSearchAndFormat: ({ toolName, toolParams, maxResults, rerank, linesInEachResult, usePlatformizedSearch }: {
|
|
3
|
+
export declare const runSemanticSearchAndFormat: ({ toolName, toolParams, maxResults, rerank, linesInEachResult, usePlatformizedSearch, extraDocumentTypes }: {
|
|
4
4
|
toolName: DocsTool;
|
|
5
5
|
toolParams: Record<string, string>;
|
|
6
6
|
maxResults?: number;
|
|
7
7
|
rerank?: boolean;
|
|
8
8
|
linesInEachResult?: number | undefined;
|
|
9
9
|
usePlatformizedSearch?: boolean;
|
|
10
|
+
extraDocumentTypes?: string[];
|
|
10
11
|
}) => Promise<string>;
|
|
11
12
|
//# sourceMappingURL=semanticSearch.d.ts.map
|
|
@@ -70,9 +70,14 @@ const runSemanticSearch = async (toolName, toolParams, maxResults = 20, rerank =
|
|
|
70
70
|
logger.log('Ran RAG search successfully with params', JSON.stringify(toolParams), timeTaskFormatted, typeof data);
|
|
71
71
|
return data?.results;
|
|
72
72
|
};
|
|
73
|
-
const runPlatformizedSemanticSearch = async (toolName, searchTerm, maxResults = 20, linesInEachResult) => {
|
|
73
|
+
const runPlatformizedSemanticSearch = async (toolName, searchTerm, maxResults = 20, linesInEachResult, extraDocumentTypes) => {
|
|
74
74
|
const startTime = Date.now();
|
|
75
|
-
|
|
75
|
+
// When extra types are requested, search them in one combined ranking via the
|
|
76
|
+
// plural `document_types` field (mutually exclusive with `document_type`).
|
|
77
|
+
const documentTypes = extraDocumentTypes?.length
|
|
78
|
+
? [toolName, ...extraDocumentTypes]
|
|
79
|
+
: undefined;
|
|
80
|
+
logger.log(`[SemanticSearch/v2] Tool: ${documentTypes ? documentTypes.join('+') : toolName}, Query: "${searchTerm}"`);
|
|
76
81
|
const response = await fetch('https://www.wixapis.com/mcp-docs-search/v1/docs/search/markdown', {
|
|
77
82
|
method: 'POST',
|
|
78
83
|
headers: {
|
|
@@ -81,7 +86,9 @@ const runPlatformizedSemanticSearch = async (toolName, searchTerm, maxResults =
|
|
|
81
86
|
},
|
|
82
87
|
body: JSON.stringify({
|
|
83
88
|
search_term: searchTerm,
|
|
84
|
-
|
|
89
|
+
...(documentTypes
|
|
90
|
+
? { document_types: documentTypes }
|
|
91
|
+
: { document_type: toolName }),
|
|
85
92
|
maximum_results: maxResults,
|
|
86
93
|
lines_in_each_result: linesInEachResult
|
|
87
94
|
})
|
|
@@ -98,9 +105,9 @@ const runPlatformizedSemanticSearch = async (toolName, searchTerm, maxResults =
|
|
|
98
105
|
}
|
|
99
106
|
return content;
|
|
100
107
|
};
|
|
101
|
-
export const runSemanticSearchAndFormat = async ({ toolName, toolParams, maxResults = DEFAULT_MAX_RESULTS, rerank, linesInEachResult, usePlatformizedSearch = true }) => {
|
|
108
|
+
export const runSemanticSearchAndFormat = async ({ toolName, toolParams, maxResults = DEFAULT_MAX_RESULTS, rerank, linesInEachResult, usePlatformizedSearch = true, extraDocumentTypes }) => {
|
|
102
109
|
if (usePlatformizedSearch) {
|
|
103
|
-
return runPlatformizedSemanticSearch(toolName, toolParams.searchTerm, maxResults, linesInEachResult);
|
|
110
|
+
return runPlatformizedSemanticSearch(toolName, toolParams.searchTerm, maxResults, linesInEachResult, extraDocumentTypes);
|
|
104
111
|
}
|
|
105
112
|
else {
|
|
106
113
|
const results = await runSemanticSearch(toolName, toolParams, maxResults, rerank);
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { WixMcpServer } from '../wix-mcp-server.js';
|
|
2
|
-
export declare const
|
|
2
|
+
export declare const START_DEPLOY_WEBSITE_PARAM_DESCRIPTIONS: {
|
|
3
|
+
readonly userRequest: "The user's request that led here, quoted if short, otherwise summarized in a sentence.";
|
|
4
|
+
readonly siteDescription: "What the site is: its purpose and who it is for. One or two sentences.";
|
|
5
|
+
readonly siteForm: "The form the site exists in right now (HTML/CSS/JS files, a static build, a zip, another builder's export, a design created in this conversation, or something else), roughly how many files, and whether it includes images.";
|
|
6
|
+
readonly siteFunctionality: "What the site must be able to do for its visitors beyond showing pages, if anything (for example selling, bookings or reservations, tickets, menus and orders, rentals, forms, memberships or paid plans, donations, a blog, or anything else the user expects to work), in your own words. If it is a plain static site with no such needs, say so.";
|
|
7
|
+
readonly agentIdentity: "Which agent or product you are and the harness you run in.";
|
|
8
|
+
readonly wixAccess: "How you can reach Wix from where you run: through this MCP only, or also from a shell whose network can reach wixapis.com; whether you can run the Wix CLI (npx @wix/cli) there and whether it is logged in; and which Wix identity you act with — the user's login through this MCP, a CLI login, or none.";
|
|
9
|
+
};
|
|
10
|
+
export declare const START_DEPLOY_WEBSITE_PARAMS: Array<keyof typeof START_DEPLOY_WEBSITE_PARAM_DESCRIPTIONS>;
|
|
3
11
|
export declare const START_DEPLOY_WEBSITE_TOOL_NAME = "StartDeployWebsite";
|
|
4
12
|
export declare const START_DEPLOY_WEBSITE_RECIPE_URL = "https://dev.wix.com/docs/api-reference/account-level/sites/skills/upload-a-website-or-html-files.md";
|
|
5
13
|
export declare const DROP_PAGE_URL = "https://www.wix.com/headless/drop?utm_campaign=mcp";
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { defaultToolDescriptions } from '../config/default-config.js';
|
|
3
3
|
import { fetchArticleContent } from './fetch-article.js';
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
4
|
+
// These arguments are the only record of who deploys what; they are read by
|
|
5
|
+
// people via BI and do not change the response. One field per question so each
|
|
6
|
+
// can be analyzed on its own.
|
|
7
|
+
export const START_DEPLOY_WEBSITE_PARAM_DESCRIPTIONS = {
|
|
8
|
+
userRequest: "The user's request that led here, quoted if short, otherwise summarized in a sentence.",
|
|
9
|
+
siteDescription: 'What the site is: its purpose and who it is for. One or two sentences.',
|
|
10
|
+
siteForm: "The form the site exists in right now (HTML/CSS/JS files, a static build, a zip, another builder's export, a design created in this conversation, or something else), roughly how many files, and whether it includes images.",
|
|
11
|
+
siteFunctionality: 'What the site must be able to do for its visitors beyond showing pages, if anything (for example selling, bookings or reservations, tickets, menus and orders, rentals, forms, memberships or paid plans, donations, a blog, or anything else the user expects to work), in your own words. If it is a plain static site with no such needs, say so.',
|
|
12
|
+
agentIdentity: 'Which agent or product you are and the harness you run in.',
|
|
13
|
+
wixAccess: "How you can reach Wix from where you run: through this MCP only, or also from a shell whose network can reach wixapis.com; whether you can run the Wix CLI (npx @wix/cli) there and whether it is logged in; and which Wix identity you act with — the user's login through this MCP, a CLI login, or none."
|
|
14
|
+
};
|
|
15
|
+
export const START_DEPLOY_WEBSITE_PARAMS = Object.keys(START_DEPLOY_WEBSITE_PARAM_DESCRIPTIONS);
|
|
7
16
|
export const START_DEPLOY_WEBSITE_TOOL_NAME = 'StartDeployWebsite';
|
|
8
17
|
export const START_DEPLOY_WEBSITE_RECIPE_URL = 'https://dev.wix.com/docs/api-reference/account-level/sites/skills/upload-a-website-or-html-files.md';
|
|
9
18
|
export const DROP_PAGE_URL = 'https://www.wix.com/headless/drop?utm_campaign=mcp';
|
|
@@ -31,7 +40,30 @@ export function addStartDeployWebsiteTool(server, disableTools = [], toolDescrip
|
|
|
31
40
|
}
|
|
32
41
|
server.tool(START_DEPLOY_WEBSITE_TOOL_NAME, toolDescriptions?.[START_DEPLOY_WEBSITE_TOOL_NAME] ??
|
|
33
42
|
defaultToolDescriptions[START_DEPLOY_WEBSITE_TOOL_NAME], {
|
|
34
|
-
|
|
43
|
+
userRequest: z
|
|
44
|
+
.string()
|
|
45
|
+
.min(1)
|
|
46
|
+
.describe(START_DEPLOY_WEBSITE_PARAM_DESCRIPTIONS.userRequest),
|
|
47
|
+
siteDescription: z
|
|
48
|
+
.string()
|
|
49
|
+
.min(1)
|
|
50
|
+
.describe(START_DEPLOY_WEBSITE_PARAM_DESCRIPTIONS.siteDescription),
|
|
51
|
+
siteForm: z
|
|
52
|
+
.string()
|
|
53
|
+
.min(1)
|
|
54
|
+
.describe(START_DEPLOY_WEBSITE_PARAM_DESCRIPTIONS.siteForm),
|
|
55
|
+
siteFunctionality: z
|
|
56
|
+
.string()
|
|
57
|
+
.min(1)
|
|
58
|
+
.describe(START_DEPLOY_WEBSITE_PARAM_DESCRIPTIONS.siteFunctionality),
|
|
59
|
+
agentIdentity: z
|
|
60
|
+
.string()
|
|
61
|
+
.min(1)
|
|
62
|
+
.describe(START_DEPLOY_WEBSITE_PARAM_DESCRIPTIONS.agentIdentity),
|
|
63
|
+
wixAccess: z
|
|
64
|
+
.string()
|
|
65
|
+
.min(1)
|
|
66
|
+
.describe(START_DEPLOY_WEBSITE_PARAM_DESCRIPTIONS.wixAccess)
|
|
35
67
|
}, { readOnlyHint: true, destructiveHint: false, openWorldHint: false }, async (_args, { httpClient }) => {
|
|
36
68
|
let text = START_DEPLOY_WEBSITE_POINTER_RESPONSE;
|
|
37
69
|
try {
|