@wix/mcp 1.0.94 → 1.0.95

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 (79) hide show
  1. package/README.md +2 -2
  2. package/build/bin-standalone.js +1116 -321
  3. package/build/cjs/index.cjs +2813 -1147
  4. package/build/cjs/toolkit.cjs +1788 -362
  5. package/build/dts/api-call/headless-release.d.ts +56 -0
  6. package/build/dts/api-call/headless-release.js +135 -0
  7. package/build/dts/api-call/index.d.ts +9 -1
  8. package/build/dts/api-call/index.js +13 -7
  9. package/build/dts/api-call/site-files.d.ts +14 -0
  10. package/build/dts/api-call/site-files.js +106 -10
  11. package/build/dts/api-call/upload-headless-website-files.d.ts +88 -5
  12. package/build/dts/api-call/upload-headless-website-files.js +473 -20
  13. package/build/dts/api-call/upload-image-tool.d.ts +2 -0
  14. package/build/dts/api-call/upload-image-tool.js +8 -4
  15. package/build/dts/bin.d.ts +1 -1
  16. package/build/dts/bin.js +3 -1
  17. package/build/dts/code-mode/index.d.ts +6 -1
  18. package/build/dts/code-mode/index.js +30 -10
  19. package/build/dts/code-mode/truncate-response.js +5 -1
  20. package/build/dts/config/default-config.d.ts +35 -5
  21. package/build/dts/config/default-config.js +334 -178
  22. package/build/dts/config/file-params.d.ts +22 -0
  23. package/build/dts/config/file-params.js +24 -0
  24. package/build/dts/config/param-descriptions.d.ts +14 -2
  25. package/build/dts/config/param-descriptions.js +33 -14
  26. package/build/dts/constants.d.ts +1 -0
  27. package/build/dts/constants.js +1 -0
  28. package/build/dts/docs/docs.d.ts +12 -1
  29. package/build/dts/docs/docs.js +210 -42
  30. package/build/dts/docs/fetch-article.d.ts +21 -0
  31. package/build/dts/docs/fetch-article.js +66 -4
  32. package/build/dts/docs/get-to-know.d.ts +7 -2
  33. package/build/dts/docs/get-to-know.js +36 -14
  34. package/build/dts/docs/mintlify-docs.d.ts +121 -0
  35. package/build/dts/docs/mintlify-docs.js +391 -0
  36. package/build/dts/docs/semanticSearch.d.ts +2 -1
  37. package/build/dts/docs/semanticSearch.js +14 -7
  38. package/build/dts/docs/start-deploy-website.d.ts +7 -6
  39. package/build/dts/docs/start-deploy-website.js +13 -8
  40. package/build/dts/docs/start-headless-build.d.ts +8 -10
  41. package/build/dts/docs/start-headless-build.js +14 -28
  42. package/build/dts/index.d.ts +2 -2
  43. package/build/dts/index.js +2 -2
  44. package/build/dts/infra/redact-personal-data.d.ts +4 -0
  45. package/build/dts/infra/redact-personal-data.js +79 -0
  46. package/build/dts/site-widget-tools/constants.d.ts +49 -1
  47. package/build/dts/site-widget-tools/constants.js +64 -7
  48. package/build/dts/site-widget-tools/index.d.ts +1 -0
  49. package/build/dts/site-widget-tools/index.js +1 -0
  50. package/build/dts/site-widget-tools/pick-images-tool/index.d.ts +14 -0
  51. package/build/dts/site-widget-tools/pick-images-tool/index.js +138 -0
  52. package/build/dts/site-widget-tools/search-site-templates-tool/index.js +16 -2
  53. package/build/dts/site-widget-tools/site-builder-tool/index.js +22 -2
  54. package/build/dts/site-widget-tools/site-creation-tool/index.d.ts +7 -1
  55. package/build/dts/site-widget-tools/site-creation-tool/index.js +22 -51
  56. package/build/dts/site-widget-tools/suggested-domain-tool/index.js +6 -3
  57. package/build/dts/support/index.d.ts +4 -0
  58. package/build/dts/support/index.js +13 -7
  59. package/build/dts/toolkit-entry.d.cts +15 -10
  60. package/build/dts/toolkit-entry.d.ts +15 -10
  61. package/build/dts/toolkit.d.ts +2 -2
  62. package/build/dts/toolkit.js +14 -7
  63. package/build/dts/wix-mcp-server.d.ts +10 -1
  64. package/build/dts/wix-mcp-server.js +25 -7
  65. package/build/dts/wixel/constants.d.ts +8 -1
  66. package/build/dts/wixel/constants.js +8 -1
  67. package/build/dts/wixel/index.d.ts +1 -1
  68. package/build/dts/wixel/index.js +2 -2
  69. package/build/dts/wixel/wixel-a2a-client.d.ts +83 -0
  70. package/build/dts/wixel/wixel-a2a-client.js +166 -0
  71. package/build/dts/wixel/wixel-bi.d.ts +2 -1
  72. package/build/dts/wixel/wixel-bi.js +2 -1
  73. package/build/dts/wixel/{external-agent-api.d.ts → wixel-session-entries.d.ts} +11 -33
  74. package/build/dts/wixel/{external-agent-api.js → wixel-session-entries.js} +33 -54
  75. package/build/dts/wixel/wixel-session-tools.d.ts +21 -26
  76. package/build/dts/wixel/wixel-session-tools.js +494 -599
  77. package/build/esm/index.js +2824 -1170
  78. package/build/esm/toolkit.js +1786 -360
  79. package/package.json +4 -4
@@ -5,22 +5,44 @@
5
5
  * `/headless-business/{msid}/drop`. Provisioning the site is NOT done here —
6
6
  * the recipe's step 1 (StartDeployWebsite → provision) stays with the agent.
7
7
  *
8
+ * A Wix Astro build among the files (`.wix/build-metadata.json` beside `dist/`,
9
+ * zipped from the project root) takes headless-business-setup's bundle routes
10
+ * instead, server-side in this handler: `bundle-upload-url` with the zip's size, a
11
+ * PUT of the zip, a PUT of the project's source zip when the upload holds the
12
+ * source and the site's repository takes it, then `release-bundle`. The service
13
+ * provisions the site's managed project and deploys. Same inputs, same siteId.
14
+ *
8
15
  * Takes the same file inputs as ExecuteWixAPI — the `files` text bundle and
9
16
  * ChatGPT `attachments` (openai/fileParams) — and does the work directly: parse,
10
- * unzip, check the site shape, POST, return the live URL. No sandbox, no
17
+ * unzip, check the site shape, upload, return the live URL. No sandbox, no
11
18
  * agent-written code, no runtime dependency.
19
+ *
20
+ * A Wix Astro build from a client that cannot hand a file to the tool (no attachment
21
+ * swap, bytes only as text) has a third form, in two calls over the same routes:
22
+ * `prepare: true` with the build zip's `size` returns presigned PUT URLs for the
23
+ * build zip and the project's source zip (the agent uploads them with curl from its
24
+ * sandbox), and a second call with the `uploadKey` releases the uploaded build
25
+ * server-side. A static site keeps the drop: `files`, `attachments`, or the
26
+ * StartDeployWebsite recipe's own drop. Same schema for every client; the
27
+ * description speaks in capabilities, not names.
12
28
  */
13
29
  import { z } from 'zod';
14
30
  import { AttachmentError, resolveAttachments } from '../code-mode/attachments.js';
15
- import { base64ToBytes, buildFormData, normalizeSiteFiles, parseFileBundle } from './site-files.js';
16
- import { HEADLESS_GUIDE_URL } from '../docs/start-headless-build.js';
31
+ import { base64ToBytes, buildFormData, expandSiteFiles, isZip, staticSiteFiles, parseFileBundle, zipStored } from './site-files.js';
32
+ import { buildSourceFiles, findHeadlessBuild, withoutEmptyStatics } from './headless-release.js';
33
+ import { fileParamNote } from '../config/file-params.js';
34
+ import { WIX_BUSINESS_FEATURES } from '../site-widget-tools/constants.js';
35
+ import { NO_PERSONAL_DATA } from '../docs/start-deploy-website.js';
17
36
  import { logger } from '../infra/logger.js';
37
+ import { redactPersonalData } from '../infra/redact-personal-data.js';
18
38
  import { defaultToolDescriptions } from '../config/default-config.js';
19
39
  import { rethrowIfUnauthorized } from '../auth/index.js';
20
40
  import { WixFetchError } from '../wix-fetch-error.js';
21
- import { START_DEPLOY_WEBSITE_PARAM_DESCRIPTIONS, START_DEPLOY_WEBSITE_RECIPE_URL as RECIPE_URL } from '../docs/start-deploy-website.js';
41
+ import { START_DEPLOY_WEBSITE_PARAM_DESCRIPTIONS } from '../docs/start-deploy-website.js';
22
42
  export const UPLOAD_HEADLESS_WEBSITE_FILES_TOOL_NAME = 'UploadHeadlessWebsiteFiles';
23
43
  const HEADLESS_BUSINESS = 'https://www.wixapis.com/headless-business-setup/v1/headless-business';
44
+ /** The largest site zip `bundle-upload-url` presigns (headless-business-setup's MAX_BUNDLE_BYTES). */
45
+ export const MAX_BUNDLE_BYTES = 100 * 1024 * 1024;
24
46
  /**
25
47
  * Attribution the drop endpoint stamps on its upload BI event (`source` from
26
48
  * `campaign`, `agent` from `agent`, both ≤ 64 chars) — the same parser as the
@@ -36,13 +58,23 @@ export function dropAttribution(clientName) {
36
58
  return `?campaign=mcp&agent=${encodeURIComponent(slug || 'wix-mcp')}`;
37
59
  }
38
60
  export const UPLOAD_HEADLESS_WEBSITE_FILES_PARAM_DESCRIPTIONS = {
39
- files: 'The site files as text, so file bytes never pass through a code string. Sections delimited by a line `=== FILE: <path> ===` (text, raw) or `=== FILE: <path> base64 ===` (binary, base64); `<path>` is relative to the site root and must include `index.html`. Use base64 for small binaries (icons, logos, fonts); large images/video belong behind a URL in the HTML, not in the bundle. Optional when `attachments` carries the files.',
40
- attachments: 'ChatGPT only. Files attached in the chat or created in your sandbox — any type: a zip of the whole site, HTML, CSS, JS, images. 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, with a shell that reaches the internet, drop the files with curl as the StartDeployWebsite recipe shows. A zip is unpacked; a top-level folder wrapping the site is stripped. Prefer this over base64 in `files` whenever you have a file handle. Up to 10 MB per call.',
41
- siteId: 'The metaSiteId of the Wix Headless site to upload into: the one returned by the provision call of the StartDeployWebsite recipe for a new site, or of a site deployed earlier to replace its files. Every upload replaces the whole file set, so send all files; the URL stays the same.',
61
+ files: "The site files as text, so file bytes never pass through a code string. Sections delimited by a line `=== FILE: <path> ===` (text, raw) or `=== FILE: <path> base64 ===` (binary, base64); `<path>` is relative to the site root; a static site must include `index.html`. Use base64 for small binaries (icons, logos, fonts); large images/video belong behind a URL in the HTML, not in the bundle. Optional when `attachments` carries the files. A Wix Astro build (`.wix/build-metadata.json` and `dist/` as `astro build` with the Wix hosting adapter wrote them, zipped from the project root) needs no `index.html`: it is released to the site's Wix hosting instead of dropped, and the project's other files in the upload are stored as its source when the site's repository accepts it.",
62
+ attachments: fileParamNote('for a static site use `files`, or drop the files as the StartDeployWebsite recipe shows; for a Wix Astro build from a shell that reaches the internet, zip it, call with `prepare: true` and its `size` and PUT the zips with curl') +
63
+ " Any type: a zip of the whole site, HTML, CSS, JS, images. A zip is unpacked; a top-level folder wrapping the site is stripped. A Wix Astro build zipped with the whole project (minus `node_modules`) is released with its source stored when the site's repository accepts it. Prefer this over base64 in `files` whenever you have a file handle. Up to 10 MB per call.",
64
+ prepare: "Phase one of the two-call upload of a Wix Astro build, for a shell that reaches the internet and no file handle (a sandbox whose network is limited to a few hosts cannot upload to the URLs): zip the build first, then `true` with its `size` returns presigned upload URLs for the build zip and, when the site's repository takes it, the project source zip, as `curl` commands to run verbatim, with the exact second call. The two calls: {siteId, prepare: true, size}, then {siteId, uploadKey, sourceUploadId?, expectedHeadRevisionId?, releaseNotes?}. No `files`, `attachments`, `uploadKey` or `sourceUploadId` on the first; `expectedHeadRevisionId` and `releaseNotes` given on the first are echoed into the second. Not for a static site: send its files in `files` or `attachments`, or drop them as the StartDeployWebsite recipe shows.",
65
+ size: `Phase one, with \`prepare: true\` only, and required there: the build zip's length in bytes (\`wc -c < build.zip\`), 1 to ${MAX_BUNDLE_BYTES} (100 MB). The upload URL takes a body of exactly this length; a zip changed after the prepare needs a new prepare.`,
66
+ uploadKey: 'Phase two of the two-call upload: the `uploadKey` the `prepare` call returned, after the build zip was PUT to its URL. Releases that build to the site: {siteId, uploadKey, sourceUploadId?, expectedHeadRevisionId?, releaseNotes?}. Excludes `files` and `attachments`.',
67
+ sourceUploadId: 'Phase two: the `sourceUploadId` the `prepare` call returned, after the source zip was PUT to its URL. Stores the project source with the release. Needs `uploadKey`; leave it out when the prepare returned no source upload.',
68
+ expectedHeadRevisionId: 'The repository revision your project started from, so the source replaces only that and a commit made since (another chat, a Dev Machine) is never dropped unseen: the revision you downloaded the project at; for a project started here, the `headRevisionId` the `prepare` call returned. Leave it out only for a repository with nothing in it yet. Goes with `uploadKey`, or with `files`/`attachments` holding a build.',
69
+ releaseNotes: 'Optional: one line on what this release changes, at most 200 characters, kept with the deployment of a Wix Astro build, whether it comes in `files`/`attachments` or as the second call with `uploadKey` (given on the `prepare` call it is echoed into the second). A static drop keeps no notes.',
70
+ siteId: 'The metaSiteId of the Wix Headless site to upload into: the one returned by the provision call of the StartDeployWebsite recipe for a new site, or of a site deployed earlier to replace its files. Every upload replaces the whole file set, so send all files; the URL stays the same. A build released here replaces static pages dropped earlier, at the same URL.',
42
71
  calledStartDeployWebsite: 'Whether you called StartDeployWebsite earlier in this conversation and are following the recipe it returned. Answer truthfully; it is recorded, not checked.',
43
72
  agentIdentity: START_DEPLOY_WEBSITE_PARAM_DESCRIPTIONS.agentIdentity,
44
- reason: 'One sentence: the user request that led here and what is being published.'
73
+ reason: `One sentence: the user request that led here and what is being published. ${NO_PERSONAL_DATA}`
45
74
  };
75
+ // The wix-manage skill's recipes, which the success reply points to for what comes next.
76
+ const UPLOAD_RECIPE_SKILL_URL = 'https://www.wix.com/skills/wix-manage/references/sites/upload-static-site.md';
77
+ const DOMAIN_RECIPE_SKILL_URL = 'https://www.wix.com/skills/wix-manage/references/domains/domain-search-purchase-and-connect.md';
46
78
  export function formatDropResult(r) {
47
79
  const shown = r.files.slice(0, 60);
48
80
  return [
@@ -52,10 +84,195 @@ export function formatDropResult(r) {
52
84
  `Dashboard: https://manage.wix.com/dashboard/${r.metaSiteId}`,
53
85
  `${r.files.length} file(s): ${shown.join(', ')}${r.files.length > shown.length ? ', …' : ''}`,
54
86
  '',
55
- 'Give the user the live URL and the dashboard link.',
56
- `The upload step is done. For what comes next — changing the site later, connecting a domain, or adding a Wix backend (forms, bookings, a store) to this same site — continue with the guide you are following, from where it left off: the StartDeployWebsite recipe (${RECIPE_URL}) or, for a site built through StartHeadlessBuild, the headless guide (${HEADLESS_GUIDE_URL}).`
87
+ `Give the user both links, each on its own line and named for what it is: the live site, and the site's dashboard. Then offer a custom domain, bought through Wix or one they already own (${DOMAIN_RECIPE_SKILL_URL}).`,
88
+ '',
89
+ `- To change this site later: "Change it later" in ${UPLOAD_RECIPE_SKILL_URL}, with this metaSiteId (${r.metaSiteId}). The same site every time, never a new one.`,
90
+ `- Wix business features (${WIX_BUSINESS_FEATURES}): call StartHeadlessBuild with startingPoint "an existing Wix Headless site"; this site is one.`
91
+ ].join('\n');
92
+ }
93
+ /** The curl that PUTs a zip to its presigned URL, with the headers the route returned. */
94
+ function curlPut(zipName, put, fallback) {
95
+ const headers = Object.entries(put.headers && Object.keys(put.headers).length ? put.headers : fallback);
96
+ return [
97
+ 'curl -X PUT',
98
+ ...headers.map(([k, v]) => `-H "${k}: ${v}"`),
99
+ `--data-binary @${zipName} "${put.putUrl}"`
100
+ ].join(' ');
101
+ }
102
+ const BUILD_ZIP_CONTENTS = 'Build zip, from the project root after `astro build` with the Wix hosting adapter: `dist/` (with `dist/_wix/app-manifest.json`) and `.wix/build-metadata.json`.';
103
+ const SOURCE_ZIP_CONTENTS = 'Source zip, from the project root: the project minus `node_modules`, `dist`, `.wix`, `.astro`, `.env*` and `.agents`.';
104
+ /** The prepare reply: what the agent runs, verbatim, and the exact second call. */
105
+ export function formatPrepareResult(siteId, urls, size, given = {}) {
106
+ const expiry = urls.expiresAt ? ` They expire at ${urls.expiresAt}.` : '';
107
+ const secondCall = {
108
+ siteId,
109
+ uploadKey: urls.uploadKey
110
+ };
111
+ const zipHeaders = {
112
+ 'content-type': 'application/zip',
113
+ ...(size ? { 'content-length': String(size) } : {})
114
+ };
115
+ const lines = [
116
+ `Upload URLs ready for site ${siteId}.${expiry} Two steps, then call this tool again.`,
117
+ '',
118
+ `1. ${BUILD_ZIP_CONTENTS} Upload the zip you measured${size ? ` (${size} bytes)` : ''}; the URL takes a body of exactly that length, so a zip changed since needs a new prepare with its size:`,
119
+ ` ${curlPut('build.zip', urls, zipHeaders)}`
120
+ ];
121
+ // The agent's own revision wins: it is the one its project started from. Given, it is echoed
122
+ // whether or not a source half came back.
123
+ if (given.expectedHeadRevisionId)
124
+ secondCall.expectedHeadRevisionId = given.expectedHeadRevisionId;
125
+ if (urls.source) {
126
+ const s = urls.source;
127
+ secondCall.sourceUploadId = s.uploadId;
128
+ if (!secondCall.expectedHeadRevisionId && s.headRevisionId)
129
+ secondCall.expectedHeadRevisionId = s.headRevisionId;
130
+ lines.push('', `2. ${SOURCE_ZIP_CONTENTS} Upload it:`, ` ${curlPut('source.zip', s, { 'content-type': 'application/zip' })}`, ...(s.expiresAt && s.expiresAt !== urls.expiresAt
131
+ ? [` This URL expires at ${s.expiresAt}.`]
132
+ : []), ...(s.headRevisionId
133
+ ? [
134
+ ` The repository's head revision is ${s.headRevisionId}. The second call passes \`expectedHeadRevisionId\`: this head for a project started here; if you downloaded the project from the repository, the revision you downloaded it at instead, so a commit made since is never dropped.`
135
+ ]
136
+ : []));
137
+ }
138
+ else {
139
+ lines.push('', "2. The site's repository takes no source this time: the project's source is not uploaded, and the second call carries no `sourceUploadId`.");
140
+ }
141
+ if (given.releaseNotes)
142
+ secondCall.releaseNotes = releaseNotesForService(given.releaseNotes);
143
+ lines.push('', `3. Call UploadHeadlessWebsiteFiles again with ${JSON.stringify(secondCall)} (plus \`calledStartDeployWebsite\`, \`agentIdentity\`, \`reason\`). No \`files\` or \`attachments\` on that call.`);
144
+ return lines.join('\n');
145
+ }
146
+ /** The release-bundle reply, for both the two-call form and a build uploaded as files. */
147
+ export function formatBundleReleaseResult(siteId, r, notes = []) {
148
+ return [
149
+ 'Released; the site is live.',
150
+ `Live URL: ${r.siteUrl ?? '(not returned)'}`,
151
+ `metaSiteId: ${siteId} — pass it as \`siteId\` to release this site again, the same way. The URL stays.`,
152
+ `Dashboard: https://manage.wix.com/dashboard/${siteId}`,
153
+ `Deployment ${r.deploymentId ?? '(id not returned)'}.${r.revision?.id ? ` Source stored as revision ${r.revision.id}.` : ''}`,
154
+ ...notes,
155
+ '',
156
+ 'This release replaced whatever the site served before, at the same URL. A change to the site is a change to the project: edit, build if it is an Astro project, upload again.',
157
+ "Give the user both links, each on its own line and named for what it is: the live site, and the site's dashboard."
57
158
  ].join('\n');
58
159
  }
160
+ const startOver = (retry) => retry === 'prepare'
161
+ ? "call with `prepare: true` and the zip's `size` again, upload the zips to the new URLs, and release with the new `uploadKey`"
162
+ : 'call this tool again with the same files';
163
+ const apiBody = (err) => err.response.data ? ` (${err.response.data.slice(0, 300)})` : '';
164
+ /** The bundle-upload-url route's errors, as the agent should act on them. */
165
+ export function describeBundleUploadUrlError(err) {
166
+ if (!(err instanceof WixFetchError)) {
167
+ return err instanceof Error ? err.message : String(err);
168
+ }
169
+ const body = apiBody(err);
170
+ switch (err.status) {
171
+ case 400:
172
+ return `The upload URL was refused: \`size\` is the build zip's length in bytes, an integer from 1 to ${MAX_BUNDLE_BYTES} (100 MB). A larger build: leave out what the site does not serve (source maps, unused assets).${body}`;
173
+ case 429:
174
+ return `Too many unreleased uploads are waiting for this site: release one with its \`uploadKey\`, or wait for a running release to finish, then ask again.${body}`;
175
+ default:
176
+ return err.message;
177
+ }
178
+ }
179
+ /**
180
+ * The site token, repeated in the release body: a build whose server code is over the
181
+ * service's gRPC limit is deployed over REST as the caller, the way the CLI deploys, and
182
+ * the gateway does not hand the Authorization header on to the service.
183
+ */
184
+ function deployAuthorization(siteHeaders) {
185
+ const authorization = siteHeaders.Authorization ?? siteHeaders.authorization;
186
+ return authorization ? { deployAuthorization: authorization } : {};
187
+ }
188
+ /** The deployment label the service keeps is 200 characters; longer notes are cut, not refused. */
189
+ export const RELEASE_NOTES_MAX_CHARS = 200;
190
+ export function releaseNotesForService(notes) {
191
+ // Code points, not UTF-16 units, so an emoji at the boundary stays whole.
192
+ const chars = [...notes.replace(/\s+/g, ' ').trim()];
193
+ return chars.length <= RELEASE_NOTES_MAX_CHARS
194
+ ? chars.join('')
195
+ : `${chars
196
+ .slice(0, RELEASE_NOTES_MAX_CHARS - 1)
197
+ .join('')
198
+ .trimEnd()}…`;
199
+ }
200
+ /** The release-bundle route's errors, as the agent should act on them. */
201
+ export function describeBundleReleaseError(err, uploadKey, retry = 'prepare') {
202
+ if (!(err instanceof WixFetchError)) {
203
+ return err instanceof Error ? err.message : String(err);
204
+ }
205
+ const body = apiBody(err);
206
+ const code = err.response.data ?? '';
207
+ switch (err.status) {
208
+ case 404:
209
+ return `Nothing uploaded for uploadKey ${uploadKey}: the build zip was not PUT to its URL, or the URL expired. To try again, ${startOver(retry)}.${body}`;
210
+ case 409:
211
+ return `The project's source repository moved since the upload URLs were made; nothing was released. ${retry === 'prepare' ? "Call with `prepare: true` and the zip's `size` again, re-upload the source zip to the new URL, and release with the new `uploadKey`, `sourceUploadId` and `expectedHeadRevisionId`." : 'Download the project again from the repository, carry your change over, and upload with `expectedHeadRevisionId` set to the revision you downloaded.'}${body}`;
212
+ case 403:
213
+ return `Wix refused to deploy this build onto the site's project; nothing was released, and the same call will be refused again. Do not call \`prepare\` again. Tell the user the release is blocked on Wix's side and give them the request id from the error.${body}`;
214
+ case 413:
215
+ case 428:
216
+ return `The build's server code is larger than the release can carry from here; nothing was released. Do not call \`prepare\` again. Tell the user the release is blocked on Wix's side and give them the request id from the error.${body}`;
217
+ case 400:
218
+ if (code.includes('EXPECTED_HEAD_REQUIRED')) {
219
+ return `The site's repository already holds work, and the source replaces only what you started from; nothing was released. ${retry === 'prepare' ? "Call with `prepare: true` and the zip's `size` again and pass the `headRevisionId` it returns as `expectedHeadRevisionId` (with its new `uploadKey` and `sourceUploadId`)." : 'Call again with `expectedHeadRevisionId`: the revision you downloaded the project at, so the source replaces only that. To replace the repository with this upload on purpose, pass its current head.'}${body}`;
220
+ }
221
+ if (code.includes('BUNDLE_TOO_LARGE')) {
222
+ return `The build zip is over ${MAX_BUNDLE_BYTES} bytes (100 MB): leave out what the site does not serve (source maps, unused assets), then ${startOver(retry)}.${body}`;
223
+ }
224
+ if (code.includes('INVALID_RELEASE')) {
225
+ return `The release was refused by Wix's deployment service; nothing was released. The staged upload is kept: call again with the same \`uploadKey\` and the field the message names fixed (release notes are cut to ${RELEASE_NOTES_MAX_CHARS} characters here).${body}`;
226
+ }
227
+ if (code.includes('INVALID_UPLOAD_KEY')) {
228
+ return `\`uploadKey\` is the lowercase UUID the \`prepare\` call returned; pass it as it came.${body}`;
229
+ }
230
+ return `The uploaded zip was not accepted. Zip \`dist/\` and \`.wix/build-metadata.json\` from the project root after \`astro build\` with the Wix hosting adapter, then ${startOver(retry)}. A static site goes in \`files\` or \`attachments\` instead, or is dropped as the StartDeployWebsite recipe shows.${body}`;
231
+ default:
232
+ if (err.status >= 500) {
233
+ const what = err.status === 503
234
+ ? 'The release could not hold its upload'
235
+ : `Wix's release step failed on its side (HTTP ${err.status})`;
236
+ // Both forms retry the staged upload itself: the two-call form with its uploadKey, the
237
+ // one-call form by switching to that key instead of sending the files again, which would
238
+ // stage another upload. One retry, then the user hears the request id.
239
+ return `${what}; nothing was released. The staged upload is kept under uploadKey ${uploadKey}, so do not call \`prepare\` again and do not send the files again: every upload staged and not released counts against the site, which refuses new ones after a few. Retry once, after a minute, by calling with \`uploadKey: "${uploadKey}"\`${retry === 'resend' ? ' (plus the `expectedHeadRevisionId` and `releaseNotes` you passed, if any; the source is stored with the next release)' : ' and the same ids and `releaseNotes`'}. If that fails too, stop and tell the user the release is blocked on Wix's side, with the request id from the error.${body}`;
240
+ }
241
+ return err.message;
242
+ }
243
+ }
244
+ /** The inputs of the two-call form against each other and against the one-call form. */
245
+ function validateBundleArgs(a) {
246
+ const hasContent = !!a.files?.trim() || !!(a.attachments && a.attachments.length);
247
+ if (a.prepare) {
248
+ if (hasContent || a.uploadKey || a.sourceUploadId) {
249
+ return '`prepare: true` goes with `siteId` and `size`: no `files`, `attachments`, `uploadKey` or `sourceUploadId` on that call. Example: {siteId, prepare: true, size}, then {siteId, uploadKey, sourceUploadId?, expectedHeadRevisionId?, releaseNotes?}.';
250
+ }
251
+ if (a.size === undefined) {
252
+ return "`prepare: true` needs `size`: zip the build first and pass the zip's length in bytes (`wc -c < build.zip`).";
253
+ }
254
+ if (!Number.isInteger(a.size) || a.size < 1 || a.size > MAX_BUNDLE_BYTES) {
255
+ return `\`size\` is the build zip's length in bytes, an integer from 1 to ${MAX_BUNDLE_BYTES} (100 MB); got ${a.size}.`;
256
+ }
257
+ return null;
258
+ }
259
+ if (a.size !== undefined) {
260
+ return '`size` goes only with `prepare: true`: it is the length of the build zip that call makes an upload URL for.';
261
+ }
262
+ if (a.uploadKey) {
263
+ if (hasContent) {
264
+ return '`uploadKey` releases a build zip already uploaded to its URL: pass no `files` or `attachments` with it.';
265
+ }
266
+ return null;
267
+ }
268
+ if (a.sourceUploadId) {
269
+ return '`sourceUploadId` goes with the `uploadKey` the `prepare` call returned.';
270
+ }
271
+ if (a.expectedHeadRevisionId && !hasContent) {
272
+ return '`expectedHeadRevisionId` goes with `uploadKey`, or with `files`/`attachments` holding a build.';
273
+ }
274
+ return null;
275
+ }
59
276
  async function readJson(res, what) {
60
277
  const text = await res.text();
61
278
  if (!res.ok)
@@ -67,6 +284,63 @@ async function readJson(res, what) {
67
284
  throw new Error(`${what} returned a non-JSON body: ${text.slice(0, 200)}`);
68
285
  }
69
286
  }
287
+ /** A PUT to a presigned URL; content-length is left to fetch, which sends the body's own length. */
288
+ async function putZip(fetchImpl, put, bytes, what) {
289
+ const headers = Object.fromEntries(Object.entries(put.headers ?? { 'content-type': 'application/zip' }).filter(([k]) => k.toLowerCase() !== 'content-length'));
290
+ const res = await fetchImpl(put.putUrl, {
291
+ method: 'PUT',
292
+ headers,
293
+ body: bytes
294
+ });
295
+ if (!res.ok) {
296
+ const text = await res.text().catch(() => '');
297
+ throw new Error(`The ${what} upload was refused (${res.status}${text ? `: ${text.slice(0, 200)}` : ''}); call this tool again with the same files.`);
298
+ }
299
+ }
300
+ /**
301
+ * A build that came as files, released over the bundle routes: an upload URL for the
302
+ * site zip's size, the zip PUT there, the source zip PUT to the repository's URL when
303
+ * the service offered one and the upload holds source files, then the release with
304
+ * the ids. `onStaged` hears the uploadKey once the site zip is staged.
305
+ */
306
+ export async function releaseBuildByUrl(o) {
307
+ const log = o.log ?? (() => { });
308
+ const headers = { ...o.siteHeaders, 'Content-Type': 'application/json' };
309
+ const urls = (await readJson(await o.fetchImpl(`${HEADLESS_BUSINESS}/${o.siteId}/bundle-upload-url`, {
310
+ method: 'POST',
311
+ headers,
312
+ body: JSON.stringify({ size: o.siteZip.byteLength })
313
+ }), 'bundle-upload-url'));
314
+ if (!urls.uploadKey || !urls.putUrl) {
315
+ throw new Error('bundle-upload-url returned no uploadKey or putUrl.');
316
+ }
317
+ await putZip(o.fetchImpl, urls, o.siteZip, 'site zip');
318
+ o.onStaged?.(urls.uploadKey);
319
+ log(`staged ${urls.uploadKey} (${o.siteZip.byteLength} bytes), source ${urls.source ? `offered, ${o.sourceFiles.length} file(s)` : 'not offered'}`);
320
+ const source = urls.source && o.sourceFiles.length > 0 ? urls.source : undefined;
321
+ if (source) {
322
+ await putZip(o.fetchImpl, source, zipStored(o.sourceFiles), 'source zip');
323
+ }
324
+ return (await readJson(await o.fetchImpl(`${HEADLESS_BUSINESS}/${o.siteId}/release-bundle`, {
325
+ method: 'POST',
326
+ headers,
327
+ body: JSON.stringify({
328
+ uploadKey: urls.uploadKey,
329
+ ...deployAuthorization(o.siteHeaders),
330
+ ...(o.releaseNotes
331
+ ? { releaseNotes: releaseNotesForService(o.releaseNotes) }
332
+ : {}),
333
+ ...(source
334
+ ? {
335
+ sourceUploadId: source.uploadId,
336
+ ...(o.expectedHeadRevisionId
337
+ ? { expectedHeadRevisionId: o.expectedHeadRevisionId }
338
+ : {})
339
+ }
340
+ : {})
341
+ })
342
+ }), 'release-bundle'));
343
+ }
70
344
  function errorResult(message) {
71
345
  return {
72
346
  isError: true,
@@ -97,6 +371,16 @@ export function addUploadHeadlessWebsiteFilesTool(server, options) {
97
371
  }))
98
372
  .optional()
99
373
  .describe(d.attachments),
374
+ prepare: z.boolean().optional().describe(d.prepare),
375
+ size: z.number().int().optional().describe(d.size),
376
+ uploadKey: z.string().min(1).optional().describe(d.uploadKey),
377
+ sourceUploadId: z.string().min(1).optional().describe(d.sourceUploadId),
378
+ expectedHeadRevisionId: z
379
+ .string()
380
+ .min(1)
381
+ .optional()
382
+ .describe(d.expectedHeadRevisionId),
383
+ releaseNotes: z.string().optional().describe(d.releaseNotes),
100
384
  siteId: z
101
385
  .string()
102
386
  .uuid('siteId must be a Wix metaSiteId (UUID)')
@@ -110,25 +394,187 @@ export function addUploadHeadlessWebsiteFilesTool(server, options) {
110
394
  // openWorldHint true: it fetches agent-supplied attachment URLs and publishes a
111
395
  // public website.
112
396
  { readOnlyHint: false, destructiveHint: true, openWorldHint: true }, async (rawArgs, extra) => {
113
- const { files, attachments, siteId, calledStartDeployWebsite, agentIdentity, reason } = rawArgs;
114
- logger.log(`[UploadHeadlessWebsiteFiles] siteId=${siteId} calledStartDeployWebsite=${calledStartDeployWebsite} agent="${agentIdentity}" files=${files?.length ?? 0} chars attachments=${attachments?.length ?? 0} reason="${reason}"`);
397
+ const { files, attachments, prepare, size, uploadKey, sourceUploadId, expectedHeadRevisionId, releaseNotes, siteId, calledStartDeployWebsite, agentIdentity, reason } = rawArgs;
398
+ const phase = prepare
399
+ ? 'prepare'
400
+ : uploadKey
401
+ ? 'release-bundle'
402
+ : 'files';
403
+ logger.log(`[UploadHeadlessWebsiteFiles] siteId=${siteId} phase=${phase} calledStartDeployWebsite=${calledStartDeployWebsite} agent="${redactPersonalData(agentIdentity)}" files=${files?.length ?? 0} chars attachments=${attachments?.length ?? 0} reason="${redactPersonalData(reason)}"`);
404
+ const invalid = validateBundleArgs(rawArgs);
405
+ if (invalid)
406
+ return errorResult(invalid);
407
+ if (prepare || uploadKey) {
408
+ // The two-call form: both routes take the site token (outside the try so
409
+ // UnauthorizedError reaches the wrapper).
410
+ const siteHeaders = await authStrategy.getSiteAuthHeaders(siteId);
411
+ const headers = {
412
+ ...siteHeaders,
413
+ 'Content-Type': 'application/json'
414
+ };
415
+ try {
416
+ extra?.setToolBiParams?.({ siteId });
417
+ if (prepare) {
418
+ const urls = (await readJson(await fetchImpl(`${HEADLESS_BUSINESS}/${siteId}/bundle-upload-url`, {
419
+ method: 'POST',
420
+ headers,
421
+ body: JSON.stringify({ size })
422
+ }), 'bundle-upload-url'));
423
+ if (!urls.uploadKey || !urls.putUrl) {
424
+ throw new Error('bundle-upload-url returned no uploadKey or putUrl.');
425
+ }
426
+ extra?.setToolBiParams?.({
427
+ siteId,
428
+ upload: { path: 'prepare', sourceOffered: Boolean(urls.source) }
429
+ });
430
+ return {
431
+ content: [
432
+ {
433
+ type: 'text',
434
+ text: formatPrepareResult(siteId, urls, size, {
435
+ expectedHeadRevisionId,
436
+ releaseNotes
437
+ })
438
+ }
439
+ ]
440
+ };
441
+ }
442
+ const released = (await readJson(await fetchImpl(`${HEADLESS_BUSINESS}/${siteId}/release-bundle`, {
443
+ method: 'POST',
444
+ headers,
445
+ body: JSON.stringify({
446
+ uploadKey,
447
+ ...deployAuthorization(siteHeaders),
448
+ ...(sourceUploadId ? { sourceUploadId } : {}),
449
+ ...(expectedHeadRevisionId ? { expectedHeadRevisionId } : {}),
450
+ ...(releaseNotes
451
+ ? { releaseNotes: releaseNotesForService(releaseNotes) }
452
+ : {})
453
+ })
454
+ }), 'release-bundle'));
455
+ extra?.setToolBiParams?.({
456
+ siteId,
457
+ upload: {
458
+ path: 'release-bundle',
459
+ sourceStored: Boolean(released.revision?.id)
460
+ }
461
+ });
462
+ return {
463
+ content: [
464
+ {
465
+ type: 'text',
466
+ text: formatBundleReleaseResult(siteId, released)
467
+ }
468
+ ]
469
+ };
470
+ }
471
+ catch (err) {
472
+ const message = err instanceof Error ? err.message : String(err);
473
+ logger.log(`[UploadHeadlessWebsiteFiles] ${phase} error: ${message}`);
474
+ rethrowIfUnauthorized(err);
475
+ await authStrategy.enforceValidWixToken?.(err);
476
+ return errorResult(uploadKey
477
+ ? describeBundleReleaseError(err, uploadKey)
478
+ : describeBundleUploadUrlError(err));
479
+ }
480
+ }
115
481
  if (!files?.trim() && !(attachments && attachments.length)) {
116
- return errorResult('Nothing to publish: pass the site in `files` (text bundle) or `attachments` (files from this chat or your sandbox).');
482
+ return errorResult('Nothing to publish: pass the site in `files` (text bundle) or `attachments` (files from this chat or your sandbox), or, for a Wix Astro build from a shell that reaches the internet, zip it and call with `prepare: true` and its `size`.');
117
483
  }
118
- // 1. Gather bytes: bundle + resolved attachments. All agent-facing errors.
119
- let siteFiles;
484
+ // 1. Gather bytes: bundle + resolved attachments, zips unpacked. A Wix Astro build
485
+ // among them is released (below); anything else is a static site and is dropped.
486
+ // All agent-facing errors.
487
+ let siteFiles = [];
488
+ let uploaded = [];
489
+ let given = [];
490
+ let build = null;
120
491
  try {
121
492
  const fromAttachments = (await resolveAttachments(attachments, fetchImpl)).map((a) => ({ path: a.path, bytes: base64ToBytes(a.content) }));
122
- siteFiles = await normalizeSiteFiles([
123
- ...parseFileBundle(files),
124
- ...fromAttachments
125
- ]);
493
+ given = [...parseFileBundle(files), ...fromAttachments];
494
+ uploaded = await expandSiteFiles(given);
495
+ build = findHeadlessBuild(uploaded);
496
+ if (!build)
497
+ siteFiles = staticSiteFiles(uploaded);
126
498
  }
127
499
  catch (e) {
128
500
  if (e instanceof AttachmentError || e instanceof Error)
129
501
  return errorResult(e.message);
130
502
  throw e;
131
503
  }
504
+ if (!build && expectedHeadRevisionId) {
505
+ return errorResult("`expectedHeadRevisionId` applies to a Wix Astro build, whose source is stored in the site's repository; these files are a static site, which has none. Call again without it.");
506
+ }
507
+ if (build) {
508
+ // 2. Site auth (outside the try so UnauthorizedError reaches the wrapper).
509
+ const siteHeaders = await authStrategy.getSiteAuthHeaders(siteId);
510
+ let stagedKey;
511
+ try {
512
+ extra?.setToolBiParams?.({ siteId });
513
+ // 3. The bundle routes, as the two-call form runs them, from here. The hosting
514
+ // refuses an empty static file, so a build with one is zipped again without it.
515
+ const sourceFiles = buildSourceFiles(uploaded, build);
516
+ const statics = withoutEmptyStatics(uploaded, build);
517
+ const released = await releaseBuildByUrl({
518
+ siteId,
519
+ // One zip as given goes as is; anything else is zipped from the expanded files.
520
+ siteZip: given.length === 1 &&
521
+ isZip(given[0]) &&
522
+ statics.dropped.length === 0
523
+ ? given[0].bytes
524
+ : zipStored(statics.files),
525
+ sourceFiles,
526
+ expectedHeadRevisionId,
527
+ releaseNotes,
528
+ siteHeaders,
529
+ fetchImpl,
530
+ onStaged: (key) => {
531
+ stagedKey = key;
532
+ },
533
+ log: (line) => logger.log(`[UploadHeadlessWebsiteFiles] release ${line}`)
534
+ });
535
+ const sourceStored = Boolean(released.revision?.id);
536
+ extra?.setToolBiParams?.({
537
+ siteId,
538
+ upload: {
539
+ path: 'build',
540
+ sourceStored,
541
+ sourceFiles: sourceFiles.length,
542
+ emptyStaticsDropped: statics.dropped.length
543
+ }
544
+ });
545
+ const notes = [
546
+ ...(statics.dropped.length
547
+ ? [
548
+ `Left out, empty: ${statics.dropped.slice(0, 20).join(', ')} (the hosting serves no file of size 0).`
549
+ ]
550
+ : []),
551
+ ...(sourceStored
552
+ ? []
553
+ : [
554
+ sourceFiles.length
555
+ ? "The project's source was not stored: the site's repository took none this time."
556
+ : "The project's source was not stored: the upload held only the build. A later chat can continue this site only from its source; next time zip the whole project from its root, leaving out node_modules, .env files and the caches."
557
+ ])
558
+ ];
559
+ return {
560
+ content: [
561
+ {
562
+ type: 'text',
563
+ text: formatBundleReleaseResult(siteId, released, notes)
564
+ }
565
+ ]
566
+ };
567
+ }
568
+ catch (err) {
569
+ const message = err instanceof Error ? err.message : String(err);
570
+ logger.log(`[UploadHeadlessWebsiteFiles] release error: ${message}`);
571
+ rethrowIfUnauthorized(err);
572
+ await authStrategy.enforceValidWixToken?.(err);
573
+ return errorResult(stagedKey
574
+ ? describeBundleReleaseError(err, stagedKey, 'resend')
575
+ : describeBundleUploadUrlError(err));
576
+ }
577
+ }
132
578
  // 2. Account auth (outside the try so UnauthorizedError reaches the wrapper).
133
579
  const authHeaders = await authStrategy.getAccountAuthHeaders();
134
580
  try {
@@ -140,6 +586,10 @@ export function addUploadHeadlessWebsiteFilesTool(server, options) {
140
586
  headers: { ...authHeaders }, // fetch sets the multipart boundary itself
141
587
  body: buildFormData(siteFiles)
142
588
  }), 'drop');
589
+ extra?.setToolBiParams?.({
590
+ siteId: metaSiteId,
591
+ upload: { path: 'drop', files: siteFiles.length }
592
+ });
143
593
  return {
144
594
  content: [
145
595
  {
@@ -149,7 +599,10 @@ export function addUploadHeadlessWebsiteFilesTool(server, options) {
149
599
  siteUrl: dropped.siteUrl,
150
600
  uploadId: dropped.uploadId,
151
601
  files: siteFiles.map((f) => f.path)
152
- })
602
+ }) +
603
+ (releaseNotes
604
+ ? '\n\nRelease notes are kept for a Wix Astro build only; a static drop has none, so yours were not stored.'
605
+ : '')
153
606
  }
154
607
  ]
155
608
  };
@@ -5,5 +5,7 @@ export declare const UPLOAD_IMAGE_TOOL_NAME = "UploadImageToWixSite";
5
5
  export declare function addUploadImageTool(server: WixMcpServer, strategy: McpAuthenticationStrategy, options?: {
6
6
  description?: string;
7
7
  hints?: ToolHintsConfig;
8
+ /** Set when the session also has the image picker (Claude clients). */
9
+ imagePickerToolName?: string;
8
10
  }): void;
9
11
  //# sourceMappingURL=upload-image-tool.d.ts.map
@@ -51,7 +51,7 @@ function errorResponse(text) {
51
51
  // ---------------------------------------------------------------------------
52
52
  // Upload flows
53
53
  // ---------------------------------------------------------------------------
54
- async function uploadFromUrl(item, authHeaders, displayName) {
54
+ async function uploadFromUrl(item, authHeaders, displayName, imagePickerToolName) {
55
55
  if (!item.download_url) {
56
56
  return {
57
57
  success: false,
@@ -61,7 +61,7 @@ async function uploadFromUrl(item, authHeaders, displayName) {
61
61
  if (!/^https?:\/\//i.test(item.download_url)) {
62
62
  return {
63
63
  success: false,
64
- error: `download_url must be a publicly accessible http(s) URL, not a local or sandbox file path (got: "${item.download_url}"). For a user-attached file, pass the file so the platform resolves it to a real download_url; if you only have the raw bytes, use imageBase64 instead.`
64
+ error: `This server can't read files on your machine or sandbox (got: "${item.download_url}"). Send this image as imageBase64 (the whole file, one per call) or as a public URL in imageUrls${imagePickerToolName ? `, or call ${imagePickerToolName} so the user picks it` : ''}.`
65
65
  };
66
66
  }
67
67
  try {
@@ -165,7 +165,11 @@ function formatBatchResults(results) {
165
165
  // Tool registration
166
166
  // ---------------------------------------------------------------------------
167
167
  export function addUploadImageTool(server, strategy, options) {
168
- const description = options?.description ?? defaultToolDescriptions.UploadImageToWixSite;
168
+ const imagePickerToolName = options?.imagePickerToolName;
169
+ const baseDescription = options?.description || defaultToolDescriptions.UploadImageToWixSite;
170
+ const description = imagePickerToolName
171
+ ? `${baseDescription}\nIf you can't deliver the image any of these ways, call ${imagePickerToolName} so the user picks it.`
172
+ : baseDescription;
169
173
  const hints = {
170
174
  readOnlyHint: options?.hints?.readOnlyHint ?? false,
171
175
  destructiveHint: options?.hints?.destructiveHint ?? false,
@@ -226,7 +230,7 @@ export function addUploadImageTool(server, strategy, options) {
226
230
  logger.log(`[WixMCP] Uploading ${total} image(s) to Wix site: ${siteId}`);
227
231
  try {
228
232
  const authHeaders = await strategy.getSiteAuthHeaders(siteId);
229
- const urlResults = await Promise.all(urlItems.map((item) => uploadFromUrl(item, authHeaders, displayName)));
233
+ const urlResults = await Promise.all(urlItems.map((item) => uploadFromUrl(item, authHeaders, displayName, imagePickerToolName)));
230
234
  let base64Result = null;
231
235
  if (imageBase64) {
232
236
  const result = await uploadFromBase64(imageBase64, mimeType, authHeaders, displayName);
@@ -1,5 +1,5 @@
1
1
  import './infra/sentry.js';
2
- declare const PUBLIC_TOOLS: readonly ["WDS", "REST", "SDK", "BUILD_APPS", "WIX_HEADLESS", "VELO", "BUSINESS_SOLUTIONS", "CLI", "OVERVIEW"];
2
+ declare const PUBLIC_TOOLS: readonly ["WDS", "REST", "SDK", "BUILD_APPS", "WIX_HEADLESS", "VELO", "BUSINESS_SOLUTIONS", "CLI", "OVERVIEW", "HELP_CENTER"];
3
3
  declare const EXPERIMENTAL_TOOLS: readonly ["WIX_API", "CLI_COMMAND", "GET_TO_KNOW_WIX", "CREATE_WIX_BUSINESS_GUIDE", "VELO_README", "WIX_API_THROUGH_FS", "GRAPHQL"];
4
4
  type Tool = (typeof PUBLIC_TOOLS)[number] | (typeof EXPERIMENTAL_TOOLS)[number];
5
5
  export declare const DEFAULT_TOOLS: Tool[];