@wix/mcp 1.0.92 → 1.0.94

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 (38) hide show
  1. package/build/bin-standalone.js +180 -212
  2. package/build/cjs/index.cjs +757 -216
  3. package/build/cjs/toolkit.cjs +725 -209
  4. package/build/dts/api-call/index.js +6 -3
  5. package/build/dts/api-call/site-files.d.ts +28 -0
  6. package/build/dts/api-call/site-files.js +194 -0
  7. package/build/dts/api-call/upload-headless-website-files.d.ts +35 -0
  8. package/build/dts/api-call/upload-headless-website-files.js +171 -0
  9. package/build/dts/bin.js +3 -6
  10. package/build/dts/code-mode/attachments.d.ts +37 -0
  11. package/build/dts/code-mode/attachments.js +218 -0
  12. package/build/dts/code-mode/index.js +53 -6
  13. package/build/dts/config/default-config.d.ts +0 -4
  14. package/build/dts/config/default-config.js +48 -56
  15. package/build/dts/config/param-descriptions.js +5 -5
  16. package/build/dts/docs/docs.d.ts +4 -2
  17. package/build/dts/docs/docs.js +25 -15
  18. package/build/dts/docs/fetch-article.js +4 -1
  19. package/build/dts/docs/get-to-know.js +1 -1
  20. package/build/dts/docs/semanticSearch.d.ts +2 -1
  21. package/build/dts/docs/semanticSearch.js +12 -5
  22. package/build/dts/docs/start-headless-build.d.ts +19 -0
  23. package/build/dts/docs/start-headless-build.js +74 -0
  24. package/build/dts/graphql/index.js +3 -1
  25. package/build/dts/import-claude-design-tool/index.js +2 -2
  26. package/build/dts/index.d.ts +1 -0
  27. package/build/dts/index.js +1 -0
  28. package/build/dts/site-widget-tools/search-site-templates-tool/index.js +17 -7
  29. package/build/dts/toolkit-entry.d.cts +4 -2
  30. package/build/dts/toolkit-entry.d.ts +4 -2
  31. package/build/dts/toolkit.js +9 -0
  32. package/build/dts/wix-mcp-server.d.ts +2 -0
  33. package/build/dts/wix-mcp-server.js +4 -0
  34. package/build/esm/index.js +755 -216
  35. package/build/esm/toolkit.js +725 -209
  36. package/package.json +2 -2
  37. package/build/dts/docs/create-site-guide.d.ts +0 -3
  38. package/build/dts/docs/create-site-guide.js +0 -109
@@ -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))}). \`attachments\` works only in ChatGPT, which swaps each file for a download URL before the call reaches Wix. Here it arrived as written, and Wix cannot read a path on your machine, so retrying the same value fails the same way. Upload another way. With a shell that reaches the internet, send the file from there (for a site, the StartDeployWebsite recipe's curl drop). Otherwise leave \`attachments\` out and put the files in \`files\`: text as is, and a binary as a \`=== FILE: <path> base64 ===\` entry only when it is a few KB (an icon, a logo), since a larger base64 payload does not survive your own output. A larger image goes in by public URL or through the user (the Media Manager).`);
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';
@@ -7,6 +8,7 @@ import { validateExecuteCode } from './validation.js';
7
8
  import { desc } from '../config/param-descriptions.js';
8
9
  import { rethrowIfUnauthorized } from '../auth/index.js';
9
10
  import { WixFetchError } from '../wix-fetch-error.js';
11
+ import { addUploadHeadlessWebsiteFilesTool, UPLOAD_HEADLESS_WEBSITE_FILES_TOOL_NAME } from '../api-call/upload-headless-website-files.js';
10
12
  // Looks `fetch` up per call so a host or test that swaps the global is honored.
11
13
  const defaultCodeModeFetch = (url, init) => fetch(url, init);
12
14
  const SEARCH_TOOL_NAME = 'SearchWixAPISpec';
@@ -24,6 +26,15 @@ export async function addCodeModeTools(server, options) {
24
26
  maxResponseSize
25
27
  });
26
28
  }
29
+ // Account-owner flow, registered beside the code-mode tools (same file
30
+ // inputs, no runtime dependency); never in visitor mode.
31
+ if (!disableTools.includes(UPLOAD_HEADLESS_WEBSITE_FILES_TOOL_NAME) &&
32
+ authStrategy) {
33
+ addUploadHeadlessWebsiteFilesTool(server, {
34
+ authStrategy,
35
+ toolDescriptions
36
+ });
37
+ }
27
38
  }
28
39
  /**
29
40
  * Register SearchWixAPISpec. The description must stay a byte-stable
@@ -214,7 +225,7 @@ function addExecuteTool(server, options) {
214
225
  const { codeModeBaseUrl, codeModeFetch, authStrategy, visitor, toolDescriptions, maxResponseSize } = options;
215
226
  const executeUrl = `${codeModeBaseUrl}/api/code-mode/execute`;
216
227
  const d = desc('ExecuteWixAPI');
217
- server.tool(EXECUTE_TOOL_NAME, toolDescriptions?.[EXECUTE_TOOL_NAME] ??
228
+ const registered = server.tool(EXECUTE_TOOL_NAME, toolDescriptions?.[EXECUTE_TOOL_NAME] ??
218
229
  defaultToolDescriptions.ExecuteWixAPI, {
219
230
  code: z
220
231
  .string()
@@ -233,8 +244,8 @@ function addExecuteTool(server, options) {
233
244
  hasMutations: z
234
245
  .boolean()
235
246
  .describe(visitor
236
- ? 'Whether this code creates, updates, deletes, publishes, imports, uploads, or otherwise mutates site data on the visitor’s behalf. Set this to true for create/update/delete/bulk create/import/upload calls even if the reason is inspection, verification, or response-shape discovery. Read-only GET/query/list/search calls can use false.'
237
- : 'Whether this code creates, updates, deletes, publishes, installs, imports, uploads, or otherwise mutates Wix data or site/account state. Set this to true for create/update/delete/bulk create/import/upload calls even if the reason is inspection, verification, or response-shape discovery. Read-only GET/query/list/search calls can use false.'),
247
+ ? 'True if the code changes site data on the visitor’s behalf (create, update, delete, import, upload); false if it only reads (get, query, list, search).'
248
+ : 'True if the code changes anything in the Wix account or a site (create, update, delete, publish, install, import, upload); false if it only reads (get, query, list, search).'),
238
249
  sourceDocUrls: z.array(z.string()).describe(d.sourceDocUrls),
239
250
  files: z
240
251
  .string()
@@ -246,6 +257,19 @@ function addExecuteTool(server, options) {
246
257
  '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
258
  '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
259
  ].join(' ')),
260
+ // ChatGPT file attachments (`openai/fileParams`). Item shape must match
261
+ // OpenAI's file schema: download_url + file_id required, the rest optional.
262
+ // Declaring it is what makes the platform hand us a download URL instead
263
+ // of a sandbox path. Resolved host-side into base64 `files` entries.
264
+ attachments: z
265
+ .array(z.object({
266
+ download_url: z.string(),
267
+ file_id: z.string(),
268
+ file_name: z.string().optional(),
269
+ mime_type: z.string().optional()
270
+ }))
271
+ .optional()
272
+ .describe('ChatGPT only. Files attached in the chat or created in your sandbox (any type: a zip of a site, HTML, images, PDFs): pass each file as you have it, its file id or its /mnt/data path, and ChatGPT swaps it for { download_url, file_id, file_name?, mime_type? } before the call reaches Wix. No other client does this swap, so outside ChatGPT a path or file:// URL here fails at once; leave this out and use `files`, or upload from your shell if it reaches the internet. 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
273
  // Visitor mode takes an agent-supplied token and pins the site; account/site
250
274
  // mode lets the agent pick the site.
251
275
  ...(visitor
@@ -260,11 +284,15 @@ function addExecuteTool(server, options) {
260
284
  .optional()
261
285
  .describe('Default Wix site ID for the `wix.request()` calls in this execution. Optional in the schema, but required for site-level work: supply a `siteId` either here, as the default for every request, or on an individual request. Without one, a request runs at account scope, where site-level endpoints fail. Setting `scope: "account"` on a request runs it at account scope and ignores any `siteId`. Determine the target site with GetSiteContext or ListWixSites before calling; do not pass a guessed or empty value.')
262
286
  })
263
- }, { readOnlyHint: false, destructiveHint: true, openWorldHint: false }, async (rawArgs,
287
+ },
288
+ // openWorldHint true: agent code picks the Wix endpoints at run time (and may
289
+ // receive platform-resolved attachment URLs); the sandbox's host allowlist
290
+ // bounds it, but the operation set is open-ended, which is what the hint means.
291
+ { readOnlyHint: false, destructiveHint: true, openWorldHint: true }, async (rawArgs,
264
292
  // Partial: duck-typed servers (the toolkit's capturing server, test
265
293
  // doubles) build their own extra and may not supply every field.
266
294
  extra) => {
267
- const { code, reason, hasMutations, sourceDocUrls, visitorToken, files } = rawArgs;
295
+ const { code, reason, hasMutations, sourceDocUrls, visitorToken, files, attachments } = rawArgs;
268
296
  // In visitor mode the site is pinned; otherwise it comes from the param.
269
297
  const siteId = visitor ? visitor.siteId : rawArgs.siteId;
270
298
  logger.log(`[ExecuteWixAPI]${visitor ? ' (visitor)' : ''} Executing code (${code.length} chars), siteId=${siteId ?? 'none'}, hasMutations=${hasMutations}, sourceDocUrls=${JSON.stringify(sourceDocUrls)}, reason="${reason}"`);
@@ -276,6 +304,18 @@ function addExecuteTool(server, options) {
276
304
  logger.log(`[ExecuteWixAPI]${visitor ? ' (visitor)' : ''} Rejected invalid code: ${validation.error}`);
277
305
  return executeTextResult(validation.error);
278
306
  }
307
+ // ChatGPT attachments → base64 file entries, fetched host-side from the
308
+ // platform's download URLs. Before any token work: an unresolved item is
309
+ // an agent-facing error, not a server failure.
310
+ let attachmentFiles = [];
311
+ try {
312
+ attachmentFiles = await resolveAttachments(attachments);
313
+ }
314
+ catch (e) {
315
+ if (e instanceof AttachmentError)
316
+ return executeErrorResult(e.message);
317
+ throw e;
318
+ }
279
319
  // Account/site mode resolves tokens from the strategy (before the try so
280
320
  // an UnauthorizedError propagates to wrapToolCallback). Visitor mode has no
281
321
  // strategy — these stay undefined and the visitor token is used instead.
@@ -329,7 +369,8 @@ function addExecuteTool(server, options) {
329
369
  : accountHeaders?.Authorization,
330
370
  accountId: accountHeaders?.['wix-account-id'],
331
371
  siteAuthContext,
332
- ...(files ? { files } : {})
372
+ ...(files ? { files } : {}),
373
+ ...(attachmentFiles.length ? { attachmentFiles } : {})
333
374
  }, visitor
334
375
  ? visitorToken
335
376
  : (accountHeaders?.Authorization ?? siteHeaders?.Authorization), codeModeFetch);
@@ -354,5 +395,11 @@ function addExecuteTool(server, options) {
354
395
  return executeErrorResult(message);
355
396
  }
356
397
  });
398
+ // ChatGPT resolves `attachments` into download URLs only for params listed
399
+ // here (OpenAI's `openai/fileParams` extension). Other clients ignore it.
400
+ // Optional-chained: duck-typed servers (tests, the toolkit) return nothing.
401
+ registered?.update?.({
402
+ _meta: { ...registered._meta, 'openai/fileParams': ['attachments'] }
403
+ });
357
404
  }
358
405
  //# sourceMappingURL=index.js.map
@@ -33,10 +33,6 @@ export declare const SITE_CREATION_COPY: {
33
33
  readonly nonWidget: "you MUST ask whether they want to build with AI or start from a template, and create nothing and show nothing until they answer";
34
34
  readonly widget: "you MUST call `WixSiteCreation` and let them choose";
35
35
  };
36
- readonly normalCreationRoute: {
37
- readonly nonWidget: "route those through `WixSiteBuilder` or `SearchSiteTemplates`";
38
- readonly widget: "route those through `WixSiteCreation`";
39
- };
40
36
  };
41
37
  /** Rewrites non-widget copy into its widget form; a no-op on text that carries no divergence. */
42
38
  export declare const toWidget: (text: string) => string;