@wix/mcp 1.0.86 → 1.0.88

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.
@@ -17655,6 +17655,7 @@ function rethrowIfUnauthorized(error) {
17655
17655
 
17656
17656
  // src/config/api-request-rules.ts
17657
17657
  var ARRAY_QUERY_PARAMS_RULE = "Repeat array query params once per value: `?fieldsets=URL&fieldsets=RICH_CONTENT`. Do NOT comma-join values (`fieldsets=URL,RICH_CONTENT`), do NOT leave a trailing comma, and do NOT use a value the method schema does not list, even if a sibling method accepts it (draft posts have no `SEO` fieldset).";
17658
+ var WQL_FILTER_RULE = "Query and search filters are written in WQL: each field takes a single operator, so combine conditions with the logical operators \u2014 `$and` and `$or` take an array of expressions, `$not` takes one, and they can nest.";
17658
17659
 
17659
17660
  // src/config/default-config.ts
17660
17661
  var AGENT_GOAL = dedent_default`
@@ -17755,7 +17756,7 @@ var SITE_CREATION_COPY = {
17755
17756
  var SITE_GUIDANCE = dedent_default`
17756
17757
  A new-site request has 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.
17757
17758
 
17758
- One case skips all of this: the user ALREADY HAS the finished site as files — hand-written HTML/CSS/JS, a zip, or the export of an AI builder. There is nothing to build, so don't offer AI or templates and don't call any site-creation tool. Send them to https://www.wix.com/headless/drop?utm_campaign=mcp — they drag the files in and get a live Wix-hosted site. Give that URL exactly as written, keeping the utm_campaign=mcp parameter.
17759
+ One case skips all of this: the user ALREADY HAS the finished site as files — hand-written HTML/CSS/JS, a zip, or the export of an AI builder. There is nothing to build, so don't offer AI or templates and don't call any site-creation tool. For a full agentic flow, read the recipe first and publish the files yourself: https://dev.wix.com/docs/api-reference/account-level/sites/skills/upload-a-website-or-html-files.md — it walks you through creating the site and uploading the files end to end. If you can't follow it, or it doesn't apply (you can't reach the file bytes, or something fails), fall back to sending the user to https://www.wix.com/headless/drop?utm_campaign=mcp — they drag the files in and get a live Wix-hosted site. Give that URL exactly as written, keeping the utm_campaign=mcp parameter.
17759
17760
 
17760
17761
  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.
17761
17762
  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.
@@ -17814,18 +17815,56 @@ var ACCOUNT_SITE_EXECUTE_SCOPE = dedent_default`
17814
17815
  siteId?: string;
17815
17816
  method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
17816
17817
  url: string;
17818
+ // Plain object → JSON. A string is sent verbatim (set your own Content-Type,
17819
+ // e.g. multipart/form-data). A Uint8Array/ArrayBuffer is sent as raw bytes
17820
+ // for binary uploads (set your own Content-Type).
17817
17821
  body?: unknown;
17818
17822
  headers?: Record<string, string>;
17823
+ // "base64" returns the response body as a base64 string (data), with
17824
+ // encoding: "base64" on the result — use it for binary downloads (zip,
17825
+ // image, pdf); decoding those as text corrupts them. Binary content types
17826
+ // are returned as base64 automatically even without this. Decode, edit and
17827
+ // re-upload binary WITHIN this script and return only a small summary. If you
17828
+ // must return the bytes, return the wix.request response object itself (so
17829
+ // encoding: "base64" is preserved and it isn't truncated); a bare base64
17830
+ // string you return is treated as plain text and may be truncated.
17831
+ responseType?: "base64";
17819
17832
  }
17820
17833
  interface WixResponse<T = unknown> {
17821
17834
  status: number;
17822
17835
  data: T;
17836
+ encoding?: "base64";
17823
17837
  json(): Promise<T>;
17824
17838
  }
17825
17839
  declare const wix: {
17826
17840
  request<T = unknown>(options: WixRequestOptions): Promise<WixResponse<T>>;
17841
+ // Build a multipart/form-data body from files (defaults to the \`files\`
17842
+ // global). Each file becomes a part named fieldName (default "files") with
17843
+ // its path as the filename. Pass body + contentType to wix.request.
17844
+ multipart(
17845
+ files?: Array<{ path: string; content: string; contentType?: string }>,
17846
+ options?: { fieldName?: string }
17847
+ ): { body: string; contentType: string };
17827
17848
  };
17828
17849
  declare const siteId: string | undefined;
17850
+ // Files from the ExecuteWixAPI \`files\` param (raw content, no escaping).
17851
+ declare const files: Array<{ path: string; content: string }>;
17852
+ \`\`\`
17853
+
17854
+ FILE UPLOADS: pass file bodies via the \`files\` param (not inside \`code\` — no escaping), then build the multipart body with \`wix.multipart()\`:
17855
+ \`\`\`javascript
17856
+ // files param: "=== FILE: index.html ===\\n<!doctype html>...\\n=== FILE: app.js ===\\nconsole.log(1)"
17857
+ async () => {
17858
+ const mp = wix.multipart(); // parts named "files", filename = each path
17859
+ const res = await wix.request({
17860
+ scope: "account",
17861
+ method: "POST",
17862
+ url: "https://www.wixapis.com/.../upload",
17863
+ headers: { "Content-Type": mp.contentType },
17864
+ body: mp.body
17865
+ });
17866
+ return res.data;
17867
+ }
17829
17868
  \`\`\`
17830
17869
  `;
17831
17870
  var EXECUTE_WIX_API_WQL = dedent_default`
@@ -17958,6 +17997,7 @@ function buildToolDescriptions(includeReadme, includeImageUploadGuidance = true)
17958
17997
  ReadFullDocsArticle: dedent_default`
17959
17998
  Fetches the full Wix docs article or method article with code examples for using the method.
17960
17999
  Docs articles looks like this: https://dev.wix.com/docs/... and they can either be general docs articles or method articles.
18000
+ Take the URL from the user's message or an earlier tool result. To reach an article you have no URL for, search or browse the docs first.
17961
18001
  For REST docs, use the URL as-is. For SDK docs, the URL SHOULD include ?apiView=SDK.
17962
18002
  ${reminder}
17963
18003
  `,
@@ -18090,7 +18130,7 @@ function buildToolDescriptions(includeReadme, includeImageUploadGuidance = true)
18090
18130
  `,
18091
18131
  "import-claude-design-from-url": dedent_default`
18092
18132
  Import a design created in Claude's design tool into Wix. Takes the signed claudeusercontent.com URL Claude Design produces for its self-contained HTML bundle (all images, fonts, and styles inlined); any other host is rejected. Creates a live Wix-hosted site and returns its URL.
18093
- NOT for site files the user provides themselves (their own HTML/CSS/JS, a zip, another builder's export) — for those, don't call this tool: send the user to https://www.wix.com/headless/drop?utm_campaign=mcp, where they upload the files and get a live hosted site. Give that URL exactly as written, keeping the utm_campaign=mcp parameter.
18133
+ NOT for site files the user provides themselves (their own HTML/CSS/JS, a zip, another builder's export) — for those, don't call this tool. For a full agentic flow, read the recipe and publish the files yourself: https://dev.wix.com/docs/api-reference/account-level/sites/skills/upload-a-website-or-html-files.md — it covers creating the site and uploading the files end to end. If you can't follow it, or it doesn't apply, fall back to sending the user to https://www.wix.com/headless/drop?utm_campaign=mcp, where they upload the files and get a live hosted site. Give that URL exactly as written, keeping the utm_campaign=mcp parameter.
18094
18134
  `,
18095
18135
  RunWixCliCommand: dedent_default`
18096
18136
  Executes Wix CLI commands for Wix app development and management.
@@ -18517,7 +18557,7 @@ function buildToolDescriptions(includeReadme, includeImageUploadGuidance = true)
18517
18557
  CreateWixBusinessGuide: dedent_default`
18518
18558
  Provides comprehensive documentation for creating a new Wix business (site, app, etc.).
18519
18559
  Call this ONLY when the user explicitly asks for a headless site, a Wix Classic Editor site, or manual/advanced account/API-based site creation — these don't need the site's purpose first. For normal site-creation ${SITE_CREATION_COPY.normalCreationRoute.nonWidget}; for a specific template id, call \`CreateSiteFromTemplate\`.
18520
- Exception, even when the user says "headless": if they ALREADY HAVE the finished site as files (their own HTML/CSS/JS, a zip, an AI-builder export) and want those files hosted, don't call this tool — send them to https://www.wix.com/headless/drop?utm_campaign=mcp to upload the files and get a live hosted site (give that URL exactly, keeping the utm_campaign=mcp parameter). This guide's CLI flow is for BUILDING a headless frontend, not for hosting ready-made files.
18560
+ Exception, even when the user says "headless": if they ALREADY HAVE the finished site as files (their own HTML/CSS/JS, a zip, an AI-builder export) and want those files hosted, don't call this tool. For a full agentic flow, read the recipe and publish the files yourself: https://dev.wix.com/docs/api-reference/account-level/sites/skills/upload-a-website-or-html-files.md — it covers creating the site and uploading the files end to end. If you can't follow it, or it doesn't apply, fall back to sending the user to https://www.wix.com/headless/drop?utm_campaign=mcp to upload the files and get a live hosted site (give that URL exactly, keeping the utm_campaign=mcp parameter). This guide's CLI flow is for BUILDING a headless frontend, not for hosting ready-made files.
18521
18561
 
18522
18562
  ## What This Tool Contains
18523
18563
 
@@ -19154,7 +19194,10 @@ var paramDescriptions = {
19154
19194
  ARRAY_QUERY_PARAMS_RULE
19155
19195
  ].join("\n"),
19156
19196
  method: "The HTTP method to use for the API call (e.g. GET, POST, PUT, DELETE)",
19157
- body: 'The request body as a JSON object or array with all the required fields and values, including nested objects. Pass the actual value (object or array), NOT a JSON string. YOU MUST NEVER MAKE UP A BODY - the body should be based on the conversation context, i.e from the user prompt OR got into the conversation context by the "ReadFullDocsArticle" tool OR by the "SearchWixAPISpec" tool OR by the "ReadFullDocsMethodSchema" tool - i.e based on the API docs, a relevant recipe you read (preferably), a code example you found in the docs, a schema you read etc.. YOU MUST NEVER ASSUME YOU KNOW WHAT THE BODY SCHEMA IS WITHOUT CONCRETE EXAMPLES OR SCHEMA DEFINITIONS FROM THE CONVERSATION CONTEXT. Prefer reading relevant recipes if you have them in context for understand the body schema for API calls.',
19197
+ body: [
19198
+ 'The request body as a JSON object or array with all the required fields and values, including nested objects. Pass the actual value (object or array), NOT a JSON string. YOU MUST NEVER MAKE UP A BODY - the body should be based on the conversation context, i.e from the user prompt OR got into the conversation context by the "ReadFullDocsArticle" tool OR by the "SearchWixAPISpec" tool OR by the "ReadFullDocsMethodSchema" tool - i.e based on the API docs, a relevant recipe you read (preferably), a code example you found in the docs, a schema you read etc.. YOU MUST NEVER ASSUME YOU KNOW WHAT THE BODY SCHEMA IS WITHOUT CONCRETE EXAMPLES OR SCHEMA DEFINITIONS FROM THE CONVERSATION CONTEXT. Prefer reading relevant recipes if you have them in context for understand the body schema for API calls.',
19199
+ WQL_FILTER_RULE
19200
+ ].join("\n"),
19158
19201
  reason: "One sentence explaining the original user request and why you are calling this API to complete it.",
19159
19202
  sourceDocUrl: [
19160
19203
  "The URL of the documentation or recipe where you found this API endpoint.",
@@ -19478,15 +19521,17 @@ Failed to fetch content: ${errorMsg}`
19478
19521
  const baseContentsPromise = Promise.all(
19479
19522
  baseDocsWithoutRecipes.map(fetchDoc)
19480
19523
  );
19481
- const skillsContentPromise = getSkillsByPortal(httpClient, {
19524
+ const skillsContentPromise = skillsTags?.length ? getSkillsByPortal(httpClient, {
19482
19525
  portalId: REST_PORTAL_ID,
19483
19526
  ...repo && { repo },
19484
19527
  ...commitHash && { commitHash },
19485
- ...skillsTags && skillsTags.length > 0 && { tags: skillsTags }
19528
+ tags: skillsTags
19486
19529
  }).catch((err) => {
19487
19530
  console.error("[WixREADME] Failed to fetch skills by portal:", err);
19488
19531
  return null;
19489
- });
19532
+ }) : fetchDoc({ action: "include", url: RECIPES_INDEX_URL }).then(
19533
+ (doc) => doc?.text ?? null
19534
+ );
19490
19535
  const filter = {};
19491
19536
  if (siteId) filter.siteId = siteId;
19492
19537
  else if (siteName) filter.siteName = siteName;
@@ -20525,6 +20570,14 @@ var import_http_client2 = __toESM(require_index_node(), 1);
20525
20570
 
20526
20571
  // src/tool-utils.ts
20527
20572
  var REST_METHOD_CONTRACT_RETRY_HINT = "Before retrying, make sure you have everything needed for a correct call: the right endpoint URL, HTTP method, request body shape, and response shape. Use the SearchWixRESTDocumentation tool or BrowseWixRESTDocsMenu tool to find the correct method or relevant related articles, the ReadFullDocsArticle tool to read the method description and all code examples, and the SearchWixAPISpec tool for precise field names and schema details.";
20573
+ var SINGLE_OPERATOR_PER_FIELD_HINT = 'WQL takes one operator per field object. Split the conditions into separate expressions under the matching logical operator \u2014 `{"$and": [{"field": {"$gte": ...}}, {"field": {"$lte": ...}}]}` \u2014 and retry without re-searching the docs.';
20574
+ var FIELD_CAPABILITIES_HINT = "This endpoint declares its filterable and sortable fields with their operators. Read the map with SearchWixAPISpec (`queryMethodData.queryFieldsCapabilitiesMap`, or `searchMethodData.searchFieldsCapabilitiesMap` on search methods) and use only what it lists; otherwise fetch a bounded page and filter or sort the results yourself.";
20575
+ var getRetryHint = (errorDetails) => {
20576
+ if (!/Filter contains unsupported operator/i.test(errorDetails)) {
20577
+ return REST_METHOD_CONTRACT_RETRY_HINT;
20578
+ }
20579
+ return /"operator":"\{/.test(errorDetails) ? SINGLE_OPERATOR_PER_FIELD_HINT : FIELD_CAPABILITIES_HINT;
20580
+ };
20528
20581
  var safeParseJSON = (text) => {
20529
20582
  try {
20530
20583
  return JSON.parse(text);
@@ -20548,7 +20601,7 @@ var buildErrorMessage = ({
20548
20601
  ].filter((str) => !!str).join("\n");
20549
20602
  return `${baseMessage}
20550
20603
 
20551
- ${REST_METHOD_CONTRACT_RETRY_HINT}`;
20604
+ ${getRetryHint(String(errorDetails))}`;
20552
20605
  };
20553
20606
  var handleWixAPIResponse = async (response) => {
20554
20607
  const requestId = response.headers?.["x-wix-request-id"];
@@ -22215,12 +22268,31 @@ function formatExecuteResult(data, maxResponseSize, reportBi) {
22215
22268
  reportBi?.({
22216
22269
  originalLength: typeof data.result === "string" ? data.result.length : JSON.stringify(data.result)?.length
22217
22270
  });
22271
+ const base64Text = base64PayloadText(data.result);
22272
+ if (base64Text !== null) {
22273
+ if (base64Text.length <= MAX_ALLOWED_RESPONSE_CHARS) {
22274
+ return { content: [{ type: "text", text: base64Text }] };
22275
+ }
22276
+ const approxKb = Math.ceil(base64Text.length / 1024).toLocaleString();
22277
+ const limitKb = Math.floor(
22278
+ MAX_ALLOWED_RESPONSE_CHARS / 1024
22279
+ ).toLocaleString();
22280
+ return executeErrorResult(
22281
+ `Binary (base64) result is ~${approxKb} KB, too large to return intact (limit ~${limitKb} KB); truncating it would corrupt the base64. Process the bytes inside your ExecuteWixAPI script \u2014 decode, edit, and re-upload in the same call \u2014 and return only a small summary.`
22282
+ );
22283
+ }
22218
22284
  return {
22219
22285
  content: [
22220
22286
  { type: "text", text: truncateResponse(data.result, maxResponseSize) }
22221
22287
  ]
22222
22288
  };
22223
22289
  }
22290
+ function base64PayloadText(result) {
22291
+ if (result !== null && typeof result === "object" && result.encoding === "base64" && typeof result.data === "string") {
22292
+ return JSON.stringify(result);
22293
+ }
22294
+ return null;
22295
+ }
22224
22296
  function addExecuteTool(server, options) {
22225
22297
  const {
22226
22298
  codeModeBaseUrl,
@@ -22252,6 +22324,14 @@ function addExecuteTool(server, options) {
22252
22324
  visitor ? "Whether this code creates, updates, deletes, publishes, imports, uploads, or otherwise mutates site data on the visitor\u2019s 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." : "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."
22253
22325
  ),
22254
22326
  sourceDocUrls: external_exports.array(external_exports.string()).describe(d.sourceDocUrls),
22327
+ files: external_exports.string().optional().describe(
22328
+ [
22329
+ "Optional bundle of raw file contents for uploads, so file bytes never go through the `code` JavaScript (no escaping, no string-literal/`${}`/backtick pitfalls).",
22330
+ "Format: one or more sections, each a line `=== FILE: <path> ===` on its own, followed by that file's raw text content; `<path>` is the path relative to the site root (e.g. `index.html`, `assets/app.js`). Example: `=== FILE: index.html ===\\n<!doctype html>...\\n=== FILE: assets/app.js ===\\nconsole.log(1)`.",
22331
+ 'In `code`, the parsed files are the `files` global (array of `{ path, content }`), and `wix.multipart(files)` returns `{ body, contentType }` for a `multipart/form-data` upload \u2014 e.g. `const mp = wix.multipart(); await wix.request({ scope: "account", method: "POST", url, headers: { "Content-Type": mp.contentType }, body: mp.body });`.',
22332
+ "Text files only; for binary assets pass bytes as a base64 request body instead. A file whose content itself contains a line exactly like the delimiter is not supported."
22333
+ ].join(" ")
22334
+ ),
22255
22335
  // Visitor mode takes an agent-supplied token and pins the site; account/site
22256
22336
  // mode lets the agent pick the site.
22257
22337
  ...visitor ? {
@@ -22266,7 +22346,7 @@ function addExecuteTool(server, options) {
22266
22346
  },
22267
22347
  { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
22268
22348
  async (rawArgs, extra) => {
22269
- const { code, reason, hasMutations, sourceDocUrls, visitorToken } = rawArgs;
22349
+ const { code, reason, hasMutations, sourceDocUrls, visitorToken, files } = rawArgs;
22270
22350
  const siteId = visitor ? visitor.siteId : rawArgs.siteId;
22271
22351
  logger.log(
22272
22352
  `[ExecuteWixAPI]${visitor ? " (visitor)" : ""} Executing code (${code.length} chars), siteId=${siteId ?? "none"}, hasMutations=${hasMutations}, sourceDocUrls=${JSON.stringify(sourceDocUrls)}, reason="${reason}"`
@@ -22311,7 +22391,8 @@ function addExecuteTool(server, options) {
22311
22391
  siteToken: visitor ? visitorToken : siteHeaders?.Authorization,
22312
22392
  accountToken: visitor ? visitorToken : accountHeaders?.Authorization,
22313
22393
  accountId: accountHeaders?.["wix-account-id"],
22314
- siteAuthContext
22394
+ siteAuthContext,
22395
+ ...files ? { files } : {}
22315
22396
  },
22316
22397
  visitor ? visitorToken : accountHeaders?.Authorization ?? siteHeaders?.Authorization,
22317
22398
  codeModeFetch
@@ -2,6 +2,7 @@ import { z } from 'zod';
2
2
  import { logger } from '../infra/logger.js';
3
3
  import { defaultToolDescriptions } from '../config/default-config.js';
4
4
  import { truncateResponse } from './truncate-response.js';
5
+ import { MAX_ALLOWED_RESPONSE_CHARS } from '../infra/response-size-limits.js';
5
6
  import { validateExecuteCode } from './validation.js';
6
7
  import { desc } from '../config/param-descriptions.js';
7
8
  import { rethrowIfUnauthorized } from '../auth/index.js';
@@ -176,12 +177,39 @@ function formatExecuteResult(data, maxResponseSize, reportBi) {
176
177
  ? data.result.length
177
178
  : JSON.stringify(data.result)?.length
178
179
  });
180
+ // A base64/binary payload (a downloaded zip, image, …) must never be sliced:
181
+ // a cut base64 string decodes to garbage, so the usual char-limit truncation
182
+ // would silently corrupt it. Return it whole up to the hard ceiling; past
183
+ // that, fail cleanly rather than hand back a broken blob.
184
+ const base64Text = base64PayloadText(data.result);
185
+ if (base64Text !== null) {
186
+ if (base64Text.length <= MAX_ALLOWED_RESPONSE_CHARS) {
187
+ return { content: [{ type: 'text', text: base64Text }] };
188
+ }
189
+ const approxKb = Math.ceil(base64Text.length / 1024).toLocaleString();
190
+ const limitKb = Math.floor(MAX_ALLOWED_RESPONSE_CHARS / 1024).toLocaleString();
191
+ return executeErrorResult(`Binary (base64) result is ~${approxKb} KB, too large to return intact (limit ~${limitKb} KB); truncating it would corrupt the base64. Process the bytes inside your ExecuteWixAPI script — decode, edit, and re-upload in the same call — and return only a small summary.`);
192
+ }
179
193
  return {
180
194
  content: [
181
195
  { type: 'text', text: truncateResponse(data.result, maxResponseSize) }
182
196
  ]
183
197
  };
184
198
  }
199
+ // A base64 payload the caller returned as a WixResponse carrying
200
+ // `encoding: "base64"` — the shape `wix.request` produces for a binary body.
201
+ // We key off that explicit marker, never a content heuristic: a long string in
202
+ // the base64 alphabet can be ordinary text (e.g. "x".repeat(30000)) and must
203
+ // still truncate normally. Returns the text to keep intact, or null otherwise.
204
+ function base64PayloadText(result) {
205
+ if (result !== null &&
206
+ typeof result === 'object' &&
207
+ result.encoding === 'base64' &&
208
+ typeof result.data === 'string') {
209
+ return JSON.stringify(result);
210
+ }
211
+ return null;
212
+ }
185
213
  function addExecuteTool(server, options) {
186
214
  const { codeModeBaseUrl, codeModeFetch, authStrategy, visitor, toolDescriptions, maxResponseSize } = options;
187
215
  const executeUrl = `${codeModeBaseUrl}/api/code-mode/execute`;
@@ -208,6 +236,15 @@ function addExecuteTool(server, options) {
208
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.'
209
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.'),
210
238
  sourceDocUrls: z.array(z.string()).describe(d.sourceDocUrls),
239
+ files: z
240
+ .string()
241
+ .optional()
242
+ .describe([
243
+ 'Optional bundle of raw file contents for uploads, so file bytes never go through the `code` JavaScript (no escaping, no string-literal/`${}`/backtick pitfalls).',
244
+ "Format: one or more sections, each a line `=== FILE: <path> ===` on its own, followed by that file's raw text content; `<path>` is the path relative to the site root (e.g. `index.html`, `assets/app.js`). Example: `=== FILE: index.html ===\\n<!doctype html>...\\n=== FILE: assets/app.js ===\\nconsole.log(1)`.",
245
+ 'In `code`, the parsed files are the `files` global (array of `{ path, content }`), and `wix.multipart(files)` returns `{ body, contentType }` for a `multipart/form-data` upload — e.g. `const mp = wix.multipart(); await wix.request({ scope: "account", method: "POST", url, headers: { "Content-Type": mp.contentType }, body: mp.body });`.',
246
+ 'Text files only; for binary assets pass bytes as a base64 request body instead. A file whose content itself contains a line exactly like the delimiter is not supported.'
247
+ ].join(' ')),
211
248
  // Visitor mode takes an agent-supplied token and pins the site; account/site
212
249
  // mode lets the agent pick the site.
213
250
  ...(visitor
@@ -226,7 +263,7 @@ function addExecuteTool(server, options) {
226
263
  // Partial: duck-typed servers (the toolkit's capturing server, test
227
264
  // doubles) build their own extra and may not supply every field.
228
265
  extra) => {
229
- const { code, reason, hasMutations, sourceDocUrls, visitorToken } = rawArgs;
266
+ const { code, reason, hasMutations, sourceDocUrls, visitorToken, files } = rawArgs;
230
267
  // In visitor mode the site is pinned; otherwise it comes from the param.
231
268
  const siteId = visitor ? visitor.siteId : rawArgs.siteId;
232
269
  logger.log(`[ExecuteWixAPI]${visitor ? ' (visitor)' : ''} Executing code (${code.length} chars), siteId=${siteId ?? 'none'}, hasMutations=${hasMutations}, sourceDocUrls=${JSON.stringify(sourceDocUrls)}, reason="${reason}"`);
@@ -290,7 +327,8 @@ function addExecuteTool(server, options) {
290
327
  ? visitorToken
291
328
  : accountHeaders?.Authorization,
292
329
  accountId: accountHeaders?.['wix-account-id'],
293
- siteAuthContext
330
+ siteAuthContext,
331
+ ...(files ? { files } : {})
294
332
  }, visitor
295
333
  ? visitorToken
296
334
  : (accountHeaders?.Authorization ?? siteHeaders?.Authorization), codeModeFetch);
@@ -8,4 +8,5 @@
8
8
  * single unknown enum member and rejected with HTTP 400.
9
9
  */
10
10
  export declare const ARRAY_QUERY_PARAMS_RULE = "Repeat array query params once per value: `?fieldsets=URL&fieldsets=RICH_CONTENT`. Do NOT comma-join values (`fieldsets=URL,RICH_CONTENT`), do NOT leave a trailing comma, and do NOT use a value the method schema does not list, even if a sibling method accepts it (draft posts have no `SEO` fieldset).";
11
+ export declare const WQL_FILTER_RULE = "Query and search filters are written in WQL: each field takes a single operator, so combine conditions with the logical operators \u2014 `$and` and `$or` take an array of expressions, `$not` takes one, and they can nest.";
11
12
  //# sourceMappingURL=api-request-rules.d.ts.map
@@ -8,4 +8,5 @@
8
8
  * single unknown enum member and rejected with HTTP 400.
9
9
  */
10
10
  export const ARRAY_QUERY_PARAMS_RULE = 'Repeat array query params once per value: `?fieldsets=URL&fieldsets=RICH_CONTENT`. Do NOT comma-join values (`fieldsets=URL,RICH_CONTENT`), do NOT leave a trailing comma, and do NOT use a value the method schema does not list, even if a sibling method accepts it (draft posts have no `SEO` fieldset).';
11
+ export const WQL_FILTER_RULE = 'Query and search filters are written in WQL: each field takes a single operator, so combine conditions with the logical operators — `$and` and `$or` take an array of expressions, `$not` takes one, and they can nest.';
11
12
  //# sourceMappingURL=api-request-rules.js.map
@@ -80,7 +80,7 @@ export const SITE_CREATION_COPY = {
80
80
  const SITE_GUIDANCE = dedent `
81
81
  A new-site request has 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.
82
82
 
83
- One case skips all of this: the user ALREADY HAS the finished site as files — hand-written HTML/CSS/JS, a zip, or the export of an AI builder. There is nothing to build, so don't offer AI or templates and don't call any site-creation tool. Send them to https://www.wix.com/headless/drop?utm_campaign=mcp — they drag the files in and get a live Wix-hosted site. Give that URL exactly as written, keeping the utm_campaign=mcp parameter.
83
+ One case skips all of this: the user ALREADY HAS the finished site as files — hand-written HTML/CSS/JS, a zip, or the export of an AI builder. There is nothing to build, so don't offer AI or templates and don't call any site-creation tool. For a full agentic flow, read the recipe first and publish the files yourself: https://dev.wix.com/docs/api-reference/account-level/sites/skills/upload-a-website-or-html-files.md — it walks you through creating the site and uploading the files end to end. If you can't follow it, or it doesn't apply (you can't reach the file bytes, or something fails), fall back to sending the user to https://www.wix.com/headless/drop?utm_campaign=mcp — they drag the files in and get a live Wix-hosted site. Give that URL exactly as written, keeping the utm_campaign=mcp parameter.
84
84
 
85
85
  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
86
  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.
@@ -142,18 +142,56 @@ const ACCOUNT_SITE_EXECUTE_SCOPE = dedent `
142
142
  siteId?: string;
143
143
  method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
144
144
  url: string;
145
+ // Plain object → JSON. A string is sent verbatim (set your own Content-Type,
146
+ // e.g. multipart/form-data). A Uint8Array/ArrayBuffer is sent as raw bytes
147
+ // for binary uploads (set your own Content-Type).
145
148
  body?: unknown;
146
149
  headers?: Record<string, string>;
150
+ // "base64" returns the response body as a base64 string (data), with
151
+ // encoding: "base64" on the result — use it for binary downloads (zip,
152
+ // image, pdf); decoding those as text corrupts them. Binary content types
153
+ // are returned as base64 automatically even without this. Decode, edit and
154
+ // re-upload binary WITHIN this script and return only a small summary. If you
155
+ // must return the bytes, return the wix.request response object itself (so
156
+ // encoding: "base64" is preserved and it isn't truncated); a bare base64
157
+ // string you return is treated as plain text and may be truncated.
158
+ responseType?: "base64";
147
159
  }
148
160
  interface WixResponse<T = unknown> {
149
161
  status: number;
150
162
  data: T;
163
+ encoding?: "base64";
151
164
  json(): Promise<T>;
152
165
  }
153
166
  declare const wix: {
154
167
  request<T = unknown>(options: WixRequestOptions): Promise<WixResponse<T>>;
168
+ // Build a multipart/form-data body from files (defaults to the \`files\`
169
+ // global). Each file becomes a part named fieldName (default "files") with
170
+ // its path as the filename. Pass body + contentType to wix.request.
171
+ multipart(
172
+ files?: Array<{ path: string; content: string; contentType?: string }>,
173
+ options?: { fieldName?: string }
174
+ ): { body: string; contentType: string };
155
175
  };
156
176
  declare const siteId: string | undefined;
177
+ // Files from the ExecuteWixAPI \`files\` param (raw content, no escaping).
178
+ declare const files: Array<{ path: string; content: string }>;
179
+ \`\`\`
180
+
181
+ FILE UPLOADS: pass file bodies via the \`files\` param (not inside \`code\` — no escaping), then build the multipart body with \`wix.multipart()\`:
182
+ \`\`\`javascript
183
+ // files param: "=== FILE: index.html ===\\n<!doctype html>...\\n=== FILE: app.js ===\\nconsole.log(1)"
184
+ async () => {
185
+ const mp = wix.multipart(); // parts named "files", filename = each path
186
+ const res = await wix.request({
187
+ scope: "account",
188
+ method: "POST",
189
+ url: "https://www.wixapis.com/.../upload",
190
+ headers: { "Content-Type": mp.contentType },
191
+ body: mp.body
192
+ });
193
+ return res.data;
194
+ }
157
195
  \`\`\`
158
196
  `;
159
197
  const EXECUTE_WIX_API_WQL = dedent `
@@ -283,6 +321,7 @@ export function buildToolDescriptions(includeReadme, includeImageUploadGuidance
283
321
  ReadFullDocsArticle: dedent `
284
322
  Fetches the full Wix docs article or method article with code examples for using the method.
285
323
  Docs articles looks like this: https://dev.wix.com/docs/... and they can either be general docs articles or method articles.
324
+ Take the URL from the user's message or an earlier tool result. To reach an article you have no URL for, search or browse the docs first.
286
325
  For REST docs, use the URL as-is. For SDK docs, the URL SHOULD include ?apiView=SDK.
287
326
  ${reminder}
288
327
  `,
@@ -415,7 +454,7 @@ export function buildToolDescriptions(includeReadme, includeImageUploadGuidance
415
454
  `,
416
455
  'import-claude-design-from-url': dedent `
417
456
  Import a design created in Claude's design tool into Wix. Takes the signed claudeusercontent.com URL Claude Design produces for its self-contained HTML bundle (all images, fonts, and styles inlined); any other host is rejected. Creates a live Wix-hosted site and returns its URL.
418
- NOT for site files the user provides themselves (their own HTML/CSS/JS, a zip, another builder's export) — for those, don't call this tool: send the user to https://www.wix.com/headless/drop?utm_campaign=mcp, where they upload the files and get a live hosted site. Give that URL exactly as written, keeping the utm_campaign=mcp parameter.
457
+ NOT for site files the user provides themselves (their own HTML/CSS/JS, a zip, another builder's export) — for those, don't call this tool. For a full agentic flow, read the recipe and publish the files yourself: https://dev.wix.com/docs/api-reference/account-level/sites/skills/upload-a-website-or-html-files.md — it covers creating the site and uploading the files end to end. If you can't follow it, or it doesn't apply, fall back to sending the user to https://www.wix.com/headless/drop?utm_campaign=mcp, where they upload the files and get a live hosted site. Give that URL exactly as written, keeping the utm_campaign=mcp parameter.
419
458
  `,
420
459
  RunWixCliCommand: dedent `
421
460
  Executes Wix CLI commands for Wix app development and management.
@@ -842,7 +881,7 @@ export function buildToolDescriptions(includeReadme, includeImageUploadGuidance
842
881
  CreateWixBusinessGuide: dedent `
843
882
  Provides comprehensive documentation for creating a new Wix business (site, app, etc.).
844
883
  Call this ONLY when the user explicitly asks for a headless site, a Wix Classic Editor site, or manual/advanced account/API-based site creation — these don't need the site's purpose first. For normal site-creation ${SITE_CREATION_COPY.normalCreationRoute.nonWidget}; for a specific template id, call \`CreateSiteFromTemplate\`.
845
- Exception, even when the user says "headless": if they ALREADY HAVE the finished site as files (their own HTML/CSS/JS, a zip, an AI-builder export) and want those files hosted, don't call this tool — send them to https://www.wix.com/headless/drop?utm_campaign=mcp to upload the files and get a live hosted site (give that URL exactly, keeping the utm_campaign=mcp parameter). This guide's CLI flow is for BUILDING a headless frontend, not for hosting ready-made files.
884
+ Exception, even when the user says "headless": if they ALREADY HAVE the finished site as files (their own HTML/CSS/JS, a zip, an AI-builder export) and want those files hosted, don't call this tool. For a full agentic flow, read the recipe and publish the files yourself: https://dev.wix.com/docs/api-reference/account-level/sites/skills/upload-a-website-or-html-files.md — it covers creating the site and uploading the files end to end. If you can't follow it, or it doesn't apply, fall back to sending the user to https://www.wix.com/headless/drop?utm_campaign=mcp to upload the files and get a live hosted site (give that URL exactly, keeping the utm_campaign=mcp parameter). This guide's CLI flow is for BUILDING a headless frontend, not for hosting ready-made files.
846
885
 
847
886
  ## What This Tool Contains
848
887
 
@@ -1,5 +1,5 @@
1
1
  import { ALLOWED_HOSTNAMES } from '../api-call/validation.js';
2
- import { ARRAY_QUERY_PARAMS_RULE } from './api-request-rules.js';
2
+ import { ARRAY_QUERY_PARAMS_RULE, WQL_FILTER_RULE } from './api-request-rules.js';
3
3
  /**
4
4
  * Centralized parameter descriptions organized by tool.
5
5
  * Each tool has all its parameter descriptions in one place.
@@ -15,7 +15,10 @@ export const paramDescriptions = {
15
15
  ARRAY_QUERY_PARAMS_RULE
16
16
  ].join('\n'),
17
17
  method: 'The HTTP method to use for the API call (e.g. GET, POST, PUT, DELETE)',
18
- body: 'The request body as a JSON object or array with all the required fields and values, including nested objects. Pass the actual value (object or array), NOT a JSON string. YOU MUST NEVER MAKE UP A BODY - the body should be based on the conversation context, i.e from the user prompt OR got into the conversation context by the "ReadFullDocsArticle" tool OR by the "SearchWixAPISpec" tool OR by the "ReadFullDocsMethodSchema" tool - i.e based on the API docs, a relevant recipe you read (preferably), a code example you found in the docs, a schema you read etc.. YOU MUST NEVER ASSUME YOU KNOW WHAT THE BODY SCHEMA IS WITHOUT CONCRETE EXAMPLES OR SCHEMA DEFINITIONS FROM THE CONVERSATION CONTEXT. Prefer reading relevant recipes if you have them in context for understand the body schema for API calls.',
18
+ body: [
19
+ 'The request body as a JSON object or array with all the required fields and values, including nested objects. Pass the actual value (object or array), NOT a JSON string. YOU MUST NEVER MAKE UP A BODY - the body should be based on the conversation context, i.e from the user prompt OR got into the conversation context by the "ReadFullDocsArticle" tool OR by the "SearchWixAPISpec" tool OR by the "ReadFullDocsMethodSchema" tool - i.e based on the API docs, a relevant recipe you read (preferably), a code example you found in the docs, a schema you read etc.. YOU MUST NEVER ASSUME YOU KNOW WHAT THE BODY SCHEMA IS WITHOUT CONCRETE EXAMPLES OR SCHEMA DEFINITIONS FROM THE CONVERSATION CONTEXT. Prefer reading relevant recipes if you have them in context for understand the body schema for API calls.',
20
+ WQL_FILTER_RULE
21
+ ].join('\n'),
19
22
  reason: 'One sentence explaining the original user request and why you are calling this API to complete it.',
20
23
  sourceDocUrl: [
21
24
  'The URL of the documentation or recipe where you found this API endpoint.',
@@ -169,17 +169,20 @@ export async function addGetToKnowTools({ server, disableTools = [], toolDescrip
169
169
  // return '';
170
170
  // });
171
171
  const baseContentsPromise = Promise.all(baseDocsWithoutRecipes.map(fetchDoc));
172
- // Fetch the skills index from the portal. In PR mode, pass repo+commitHash
173
- // to get the PR-aware index (includes recipes added via YAML in the PR).
174
- const skillsContentPromise = getSkillsByPortal(httpClient, {
175
- portalId: REST_PORTAL_ID,
176
- ...(repo && { repo }),
177
- ...(commitHash && { commitHash }),
178
- ...(skillsTags && skillsTags.length > 0 && { tags: skillsTags })
179
- }).catch((err) => {
180
- console.error('[WixREADME] Failed to fetch skills by portal:', err);
181
- return null;
182
- });
172
+ // Filtered lists still come from the portal. The full management index is
173
+ // authored alongside the recipes and is fetched like any other article,
174
+ // using its commit version when a PR override is configured.
175
+ const skillsContentPromise = skillsTags?.length
176
+ ? getSkillsByPortal(httpClient, {
177
+ portalId: REST_PORTAL_ID,
178
+ ...(repo && { repo }),
179
+ ...(commitHash && { commitHash }),
180
+ tags: skillsTags
181
+ }).catch((err) => {
182
+ console.error('[WixREADME] Failed to fetch skills by portal:', err);
183
+ return null;
184
+ })
185
+ : fetchDoc({ action: 'include', url: RECIPES_INDEX_URL }).then((doc) => doc?.text ?? null);
183
186
  // Auto-inject site context. With no site targeted this is the account-wide
184
187
  // view: single-site accounts "just work", and accounts with more than the
185
188
  // cap show the first N sites plus a note to use GetSiteContext for the rest.
@@ -86,11 +86,11 @@ export function addImportClaudeDesignTool(server, authStrategy, options) {
86
86
  designUrl = new URL(args.url);
87
87
  }
88
88
  catch {
89
- return errorResult('Invalid design URL — this tool only accepts the claudeusercontent.com URL produced by Claude Design. For site files the user provided themselves, do not retry: send the user to https://www.wix.com/headless/drop?utm_campaign=mcp to upload them and get a live hosted site.');
89
+ return errorResult("Invalid design URL — this tool only accepts the claudeusercontent.com URL produced by Claude Design. For site files the user provided themselves, do not retry this tool. For a full agentic flow, read the recipe and publish the files yourself: https://dev.wix.com/docs/api-reference/account-level/sites/skills/upload-a-website-or-html-files.md — it covers creating the site and uploading the files end to end. If you can't follow it, or it doesn't apply, send the user to https://www.wix.com/headless/drop?utm_campaign=mcp to upload them and get a live hosted site.");
90
90
  }
91
91
  if (designUrl.protocol !== 'https:' ||
92
92
  !designUrl.hostname.endsWith(CLAUDE_USER_CONTENT_HOST_SUFFIX)) {
93
- return errorResult('Invalid design URL — this tool only accepts the claudeusercontent.com URL produced by Claude Design. For site files the user provided themselves, do not retry: send the user to https://www.wix.com/headless/drop?utm_campaign=mcp to upload them and get a live hosted site.');
93
+ return errorResult("Invalid design URL — this tool only accepts the claudeusercontent.com URL produced by Claude Design. For site files the user provided themselves, do not retry this tool. For a full agentic flow, read the recipe and publish the files yourself: https://dev.wix.com/docs/api-reference/account-level/sites/skills/upload-a-website-or-html-files.md — it covers creating the site and uploading the files end to end. If you can't follow it, or it doesn't apply, send the user to https://www.wix.com/headless/drop?utm_campaign=mcp to upload them and get a live hosted site.");
94
94
  }
95
95
  // Resolve the caller's account identity BEFORE the design fetch — the
96
96
  // design URL is single-use, so an unauthenticated caller must fail
@@ -1,5 +1,16 @@
1
1
  const REST_METHOD_CONTRACT_RETRY_HINT = 'Before retrying, make sure you have everything needed for a correct call: the right endpoint URL, HTTP method, request body shape, and response shape. ' +
2
2
  'Use the SearchWixRESTDocumentation tool or BrowseWixRESTDocsMenu tool to find the correct method or relevant related articles, the ReadFullDocsArticle tool to read the method description and all code examples, and the SearchWixAPISpec tool for precise field names and schema details.';
3
+ const SINGLE_OPERATOR_PER_FIELD_HINT = 'WQL takes one operator per field object. Split the conditions into separate expressions under the matching logical operator — `{"$and": [{"field": {"$gte": ...}}, {"field": {"$lte": ...}}]}` — and retry without re-searching the docs.';
4
+ const FIELD_CAPABILITIES_HINT = 'This endpoint declares its filterable and sortable fields with their operators. Read the map with SearchWixAPISpec (`queryMethodData.queryFieldsCapabilitiesMap`, or `searchMethodData.searchFieldsCapabilitiesMap` on search methods) and use only what it lists; otherwise fetch a bounded page and filter or sort the results yourself.';
5
+ const getRetryHint = (errorDetails) => {
6
+ if (!/Filter contains unsupported operator/i.test(errorDetails)) {
7
+ return REST_METHOD_CONTRACT_RETRY_HINT;
8
+ }
9
+ // The API reports a field object with several operators as one unknown operator whose name is that object.
10
+ return /"operator":"\{/.test(errorDetails)
11
+ ? SINGLE_OPERATOR_PER_FIELD_HINT
12
+ : FIELD_CAPABILITIES_HINT;
13
+ };
3
14
  export const DEFAULT_OPENAI_SECURITY_SCHEMES = [
4
15
  { type: 'oauth2', scopes: ['offline_access'] }
5
16
  ];
@@ -28,7 +39,7 @@ export const buildErrorMessage = ({ status, statusText, requestId, responseData
28
39
  ]
29
40
  .filter((str) => !!str)
30
41
  .join('\n');
31
- return `${baseMessage}\n\n${REST_METHOD_CONTRACT_RETRY_HINT}`;
42
+ return `${baseMessage}\n\n${getRetryHint(String(errorDetails))}`;
32
43
  };
33
44
  export const handleWixAPIResponse = async (response) => {
34
45
  const requestId = response.headers?.['x-wix-request-id'];