@wix/mcp 1.0.93 → 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.
- package/build/bin-standalone.js +143 -193
- package/build/cjs/index.cjs +503 -196
- package/build/cjs/toolkit.cjs +471 -189
- package/build/dts/api-call/index.js +6 -3
- package/build/dts/api-call/site-files.d.ts +28 -0
- package/build/dts/api-call/site-files.js +194 -0
- package/build/dts/api-call/upload-headless-website-files.d.ts +35 -0
- package/build/dts/api-call/upload-headless-website-files.js +171 -0
- package/build/dts/bin.js +3 -6
- package/build/dts/code-mode/attachments.js +1 -1
- package/build/dts/code-mode/index.js +18 -4
- package/build/dts/config/default-config.d.ts +0 -4
- package/build/dts/config/default-config.js +36 -51
- package/build/dts/config/param-descriptions.js +2 -2
- package/build/dts/docs/docs.d.ts +4 -2
- package/build/dts/docs/docs.js +11 -7
- package/build/dts/docs/fetch-article.js +4 -1
- package/build/dts/docs/get-to-know.js +1 -1
- package/build/dts/docs/start-headless-build.d.ts +19 -0
- package/build/dts/docs/start-headless-build.js +74 -0
- package/build/dts/graphql/index.js +3 -1
- package/build/dts/import-claude-design-tool/index.js +2 -2
- package/build/dts/index.d.ts +1 -0
- package/build/dts/index.js +1 -0
- package/build/dts/site-widget-tools/search-site-templates-tool/index.js +17 -7
- package/build/dts/toolkit-entry.d.cts +4 -2
- package/build/dts/toolkit-entry.d.ts +4 -2
- package/build/dts/toolkit.js +9 -0
- package/build/dts/wix-mcp-server.d.ts +2 -0
- package/build/dts/wix-mcp-server.js +4 -0
- package/build/esm/index.js +501 -196
- package/build/esm/toolkit.js +471 -189
- package/package.json +2 -2
- package/build/dts/docs/create-site-guide.d.ts +0 -3
- package/build/dts/docs/create-site-guide.js +0 -109
|
@@ -84,10 +84,13 @@ export function addApiCallTool(server, strategy, allowedTools = ['CallWixSiteAPI
|
|
|
84
84
|
reason: z.string().describe(d.reason),
|
|
85
85
|
sourceDocUrl: z.string().describe(d.sourceDocUrl)
|
|
86
86
|
};
|
|
87
|
-
})(),
|
|
87
|
+
})(),
|
|
88
|
+
// openWorldHint true: a free-form URL into the whole Wix REST surface is an
|
|
89
|
+
// open-ended target, even though auth is bounded to the user's account.
|
|
90
|
+
getHints('CallWixSiteAPI', {
|
|
88
91
|
readOnlyHint: false,
|
|
89
92
|
destructiveHint: true,
|
|
90
|
-
openWorldHint:
|
|
93
|
+
openWorldHint: true
|
|
91
94
|
}), async ({ url, body, method, siteId, reason, sourceDocUrl }, { httpClient, setToolBiParams }) => {
|
|
92
95
|
logger.log(`[CallWixSiteAPI] siteId=${siteId}, method=${method}, url=${url}, sourceDocUrl=${sourceDocUrl}, reason="${reason}", body=${JSON.stringify(body)}`);
|
|
93
96
|
const requestHeaders = {
|
|
@@ -255,7 +258,7 @@ export function addApiCallTool(server, strategy, allowedTools = ['CallWixSiteAPI
|
|
|
255
258
|
})(), getHints('ManageWixSite', {
|
|
256
259
|
readOnlyHint: false,
|
|
257
260
|
destructiveHint: true,
|
|
258
|
-
openWorldHint:
|
|
261
|
+
openWorldHint: true
|
|
259
262
|
}), async ({ url, body, method, reason }, { httpClient, setToolBiParams }) => {
|
|
260
263
|
logger.log(`Calling Wix Account level API: ${url}, reason="${reason}", body: ${JSON.stringify(body)}`);
|
|
261
264
|
const { isAllowed, error: urlError } = isApiUrlAllowed(url);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site files for UploadHeadlessWebsiteFiles, handled directly in the tool (no sandbox):
|
|
3
|
+
* the `=== FILE: … ===` text bundle, zip extraction, site-shape checks and the
|
|
4
|
+
* multipart body for the drop endpoint. Same formats the code-mode runtime
|
|
5
|
+
* accepts, so an agent can switch between ExecuteWixAPI and this tool freely.
|
|
6
|
+
*/
|
|
7
|
+
export interface SiteFile {
|
|
8
|
+
path: string;
|
|
9
|
+
bytes: Uint8Array;
|
|
10
|
+
}
|
|
11
|
+
export declare const MAX_FILES_BUNDLE_CHARS = 4000000;
|
|
12
|
+
export declare const MAX_UNZIP_BYTES: number;
|
|
13
|
+
export declare const MAX_SITE_FILES = 2000;
|
|
14
|
+
export declare function base64ToBytes(b64: string): Uint8Array;
|
|
15
|
+
/** Parse the `=== FILE: <path> ===` / `=== FILE: <path> base64 ===` bundle into bytes. */
|
|
16
|
+
export declare function parseFileBundle(bundle: string | undefined): SiteFile[];
|
|
17
|
+
/** Unzip (stored + deflate entries) into raw bytes. Validates the EOCD and the declared sizes. */
|
|
18
|
+
export declare function unzip(z: Uint8Array): Promise<SiteFile[]>;
|
|
19
|
+
export declare const isZip: (f: SiteFile) => boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Turn the raw inputs into the file set to drop: unpack zips, drop junk, strip a
|
|
22
|
+
* single wrapping folder, require a root index.html. Throws an agent-facing error.
|
|
23
|
+
*/
|
|
24
|
+
export declare function normalizeSiteFiles(input: SiteFile[]): Promise<SiteFile[]>;
|
|
25
|
+
export declare function guessContentType(path: string): string;
|
|
26
|
+
/** The drop endpoint's body: one `files` part per file, the path as the filename. */
|
|
27
|
+
export declare function buildFormData(files: SiteFile[]): FormData;
|
|
28
|
+
//# sourceMappingURL=site-files.d.ts.map
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site files for UploadHeadlessWebsiteFiles, handled directly in the tool (no sandbox):
|
|
3
|
+
* the `=== FILE: … ===` text bundle, zip extraction, site-shape checks and the
|
|
4
|
+
* multipart body for the drop endpoint. Same formats the code-mode runtime
|
|
5
|
+
* accepts, so an agent can switch between ExecuteWixAPI and this tool freely.
|
|
6
|
+
*/
|
|
7
|
+
// Keep the bundle to text + small base64 assets; large media belongs behind a URL.
|
|
8
|
+
export const MAX_FILES_BUNDLE_CHARS = 4_000_000;
|
|
9
|
+
// A dropped site is capped at 20 MB; anything expanding near this is a bomb.
|
|
10
|
+
export const MAX_UNZIP_BYTES = 64 * 1024 * 1024;
|
|
11
|
+
export const MAX_SITE_FILES = 2_000;
|
|
12
|
+
const textEncoder = new TextEncoder();
|
|
13
|
+
export function base64ToBytes(b64) {
|
|
14
|
+
const clean = b64.replace(/^data:[^,]*,/, '').replace(/\s+/g, '');
|
|
15
|
+
return Uint8Array.from(atob(clean), (c) => c.charCodeAt(0));
|
|
16
|
+
}
|
|
17
|
+
/** Parse the `=== FILE: <path> ===` / `=== FILE: <path> base64 ===` bundle into bytes. */
|
|
18
|
+
export function parseFileBundle(bundle) {
|
|
19
|
+
if (!bundle || !bundle.trim())
|
|
20
|
+
return [];
|
|
21
|
+
if (bundle.length > MAX_FILES_BUNDLE_CHARS) {
|
|
22
|
+
throw new Error(`files bundle is too large (${bundle.length} chars; max ${MAX_FILES_BUNDLE_CHARS}). Keep it to text and small base64 assets; pass larger files through attachments or reference media by URL.`);
|
|
23
|
+
}
|
|
24
|
+
const delimiter = /^=== FILE: (.+?)(?: (base64))? ===[ \t]*$/gm;
|
|
25
|
+
const matches = [...bundle.matchAll(delimiter)];
|
|
26
|
+
if (matches.length === 0) {
|
|
27
|
+
throw new Error('files bundle has no `=== FILE: <path> ===` sections. Start each file with that line (add ` base64` before the closing === for binary content).');
|
|
28
|
+
}
|
|
29
|
+
return matches.map((match, i) => {
|
|
30
|
+
const path = match[1].trim();
|
|
31
|
+
const start = (match.index ?? 0) + match[0].length;
|
|
32
|
+
const end = i + 1 < matches.length
|
|
33
|
+
? (matches[i + 1].index ?? bundle.length)
|
|
34
|
+
: bundle.length;
|
|
35
|
+
const content = bundle
|
|
36
|
+
.slice(start, end)
|
|
37
|
+
.replace(/^\r?\n/, '')
|
|
38
|
+
.replace(/\r?\n$/, '');
|
|
39
|
+
return {
|
|
40
|
+
path,
|
|
41
|
+
bytes: match[2] === 'base64'
|
|
42
|
+
? base64ToBytes(content)
|
|
43
|
+
: textEncoder.encode(content)
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/** Unzip (stored + deflate entries) into raw bytes. Validates the EOCD and the declared sizes. */
|
|
48
|
+
export async function unzip(z) {
|
|
49
|
+
const u16 = (o) => z[o] | (z[o + 1] << 8);
|
|
50
|
+
const u32 = (o) => (u16(o) | (u16(o + 2) << 16)) >>> 0;
|
|
51
|
+
let end = z.length - 22;
|
|
52
|
+
while (end >= 0 &&
|
|
53
|
+
!(u32(end) === 0x06054b50 && end + 22 + u16(end + 20) === z.length))
|
|
54
|
+
end--;
|
|
55
|
+
if (end < 0)
|
|
56
|
+
throw new Error('not a zip file (no end-of-central-directory)');
|
|
57
|
+
const out = [];
|
|
58
|
+
let p = u32(end + 16);
|
|
59
|
+
let total = 0;
|
|
60
|
+
const decoder = new TextDecoder();
|
|
61
|
+
for (let i = 0, n = u16(end + 10); i < n; i++) {
|
|
62
|
+
const path = decoder.decode(z.slice(p + 46, p + 46 + u16(p + 28)));
|
|
63
|
+
const method = u16(p + 10);
|
|
64
|
+
const size = u32(p + 24);
|
|
65
|
+
total += size;
|
|
66
|
+
if (total > MAX_UNZIP_BYTES) {
|
|
67
|
+
throw new Error(`zip expands past ${MAX_UNZIP_BYTES} bytes; a static site is at most 20 MB`);
|
|
68
|
+
}
|
|
69
|
+
const at = u32(p + 42);
|
|
70
|
+
const start = at + 30 + u16(at + 26) + u16(at + 28);
|
|
71
|
+
const comp = z.slice(start, start + u32(p + 20));
|
|
72
|
+
p += 46 + u16(p + 28) + u16(p + 30) + u16(p + 32);
|
|
73
|
+
if (path.endsWith('/'))
|
|
74
|
+
continue; // directory entry
|
|
75
|
+
let bytes;
|
|
76
|
+
if (method === 0)
|
|
77
|
+
bytes = comp;
|
|
78
|
+
else if (method === 8) {
|
|
79
|
+
bytes = new Uint8Array(await new Response(new Blob([comp])
|
|
80
|
+
.stream()
|
|
81
|
+
.pipeThrough(new DecompressionStream('deflate-raw'))).arrayBuffer());
|
|
82
|
+
}
|
|
83
|
+
else
|
|
84
|
+
throw new Error(`zip entry '${path}' uses unsupported compression method ${method}`);
|
|
85
|
+
if (bytes.length !== size) {
|
|
86
|
+
throw new Error(`zip entry '${path}' declares ${size} bytes but holds ${bytes.length}`);
|
|
87
|
+
}
|
|
88
|
+
out.push({ path, bytes });
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
// A zip is recognised by its bytes (local-file-header or empty-archive signature), not its name:
|
|
93
|
+
// an attachment can arrive without a file name, and a zip's path then carries no extension.
|
|
94
|
+
export const isZip = (f) => (f.bytes.length >= 4 &&
|
|
95
|
+
f.bytes[0] === 0x50 &&
|
|
96
|
+
f.bytes[1] === 0x4b &&
|
|
97
|
+
((f.bytes[2] === 0x03 && f.bytes[3] === 0x04) ||
|
|
98
|
+
(f.bytes[2] === 0x05 && f.bytes[3] === 0x06))) ||
|
|
99
|
+
/\.zip$/i.test(f.path);
|
|
100
|
+
const isRootIndex = (p) => /^index\.html?$/i.test(p);
|
|
101
|
+
/**
|
|
102
|
+
* Turn the raw inputs into the file set to drop: unpack zips, drop junk, strip a
|
|
103
|
+
* single wrapping folder, require a root index.html. Throws an agent-facing error.
|
|
104
|
+
*/
|
|
105
|
+
export async function normalizeSiteFiles(input) {
|
|
106
|
+
let list = [];
|
|
107
|
+
for (const f of input) {
|
|
108
|
+
if (isZip(f))
|
|
109
|
+
list.push(...(await unzip(f.bytes)));
|
|
110
|
+
else
|
|
111
|
+
list.push(f);
|
|
112
|
+
}
|
|
113
|
+
list = list
|
|
114
|
+
.map((f) => ({
|
|
115
|
+
...f,
|
|
116
|
+
path: f.path.replace(/\\/g, '/').replace(/^\.?\//, '')
|
|
117
|
+
}))
|
|
118
|
+
.filter((f) => f.path &&
|
|
119
|
+
!f.path.startsWith('__MACOSX/') &&
|
|
120
|
+
!/(^|\/)\.DS_Store$/.test(f.path) &&
|
|
121
|
+
!f.path.split('/').includes('..'));
|
|
122
|
+
if (list.length === 0) {
|
|
123
|
+
throw new Error('No files to upload: pass the site in `files` or `attachments`.');
|
|
124
|
+
}
|
|
125
|
+
if (list.length > MAX_SITE_FILES) {
|
|
126
|
+
throw new Error(`Too many files (${list.length}); a site upload is at most ${MAX_SITE_FILES}.`);
|
|
127
|
+
}
|
|
128
|
+
// A zip of a folder: strip the single top-level directory so index.html sits at the root.
|
|
129
|
+
if (!list.some((f) => isRootIndex(f.path))) {
|
|
130
|
+
const first = list[0].path.split('/')[0];
|
|
131
|
+
if (list.every((f) => f.path.includes('/') && f.path.split('/')[0] === first)) {
|
|
132
|
+
list = list.map((f) => ({
|
|
133
|
+
...f,
|
|
134
|
+
path: f.path.split('/').slice(1).join('/')
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// A single root HTML file is the homepage whatever its name — the drop endpoint's
|
|
139
|
+
// own rule (headless-business-setup renameToIndexHtml), applied here so the
|
|
140
|
+
// shape check below doesn't reject what the endpoint would accept.
|
|
141
|
+
const rootHtml = list.filter((f) => !f.path.includes('/') && /\.html?$/i.test(f.path));
|
|
142
|
+
if (!list.some((f) => isRootIndex(f.path)) && rootHtml.length === 1) {
|
|
143
|
+
list = list.map((f) => f === rootHtml[0] ? { ...f, path: 'index.html' } : f);
|
|
144
|
+
}
|
|
145
|
+
if (!list.some((f) => isRootIndex(f.path))) {
|
|
146
|
+
const shown = list
|
|
147
|
+
.slice(0, 8)
|
|
148
|
+
.map((f) => f.path)
|
|
149
|
+
.join(', ');
|
|
150
|
+
throw new Error(`The site needs an index.html at its root. Files received: ${shown}${list.length > 8 ? ', …' : ''}`);
|
|
151
|
+
}
|
|
152
|
+
return list;
|
|
153
|
+
}
|
|
154
|
+
export function guessContentType(path) {
|
|
155
|
+
const ext = (/\.([a-zA-Z0-9]+)$/.exec(path)?.[1] ?? '').toLowerCase();
|
|
156
|
+
const map = {
|
|
157
|
+
html: 'text/html',
|
|
158
|
+
htm: 'text/html',
|
|
159
|
+
css: 'text/css',
|
|
160
|
+
js: 'text/javascript',
|
|
161
|
+
mjs: 'text/javascript',
|
|
162
|
+
json: 'application/json',
|
|
163
|
+
svg: 'image/svg+xml',
|
|
164
|
+
txt: 'text/plain',
|
|
165
|
+
md: 'text/markdown',
|
|
166
|
+
xml: 'application/xml',
|
|
167
|
+
webmanifest: 'application/manifest+json',
|
|
168
|
+
ico: 'image/x-icon',
|
|
169
|
+
png: 'image/png',
|
|
170
|
+
jpg: 'image/jpeg',
|
|
171
|
+
jpeg: 'image/jpeg',
|
|
172
|
+
gif: 'image/gif',
|
|
173
|
+
webp: 'image/webp',
|
|
174
|
+
avif: 'image/avif',
|
|
175
|
+
woff: 'font/woff',
|
|
176
|
+
woff2: 'font/woff2',
|
|
177
|
+
ttf: 'font/ttf',
|
|
178
|
+
otf: 'font/otf',
|
|
179
|
+
pdf: 'application/pdf',
|
|
180
|
+
mp4: 'video/mp4',
|
|
181
|
+
webm: 'video/webm',
|
|
182
|
+
mp3: 'audio/mpeg'
|
|
183
|
+
};
|
|
184
|
+
return map[ext] ?? 'application/octet-stream';
|
|
185
|
+
}
|
|
186
|
+
/** The drop endpoint's body: one `files` part per file, the path as the filename. */
|
|
187
|
+
export function buildFormData(files) {
|
|
188
|
+
const fd = new FormData();
|
|
189
|
+
for (const f of files) {
|
|
190
|
+
fd.append('files', new Blob([f.bytes], { type: guessContentType(f.path) }), f.path);
|
|
191
|
+
}
|
|
192
|
+
return fd;
|
|
193
|
+
}
|
|
194
|
+
//# sourceMappingURL=site-files.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { WixMcpServer } from '../wix-mcp-server.js';
|
|
2
|
+
import { type McpAuthenticationStrategy } from '../auth/index.js';
|
|
3
|
+
export declare const UPLOAD_HEADLESS_WEBSITE_FILES_TOOL_NAME = "UploadHeadlessWebsiteFiles";
|
|
4
|
+
/**
|
|
5
|
+
* Attribution the drop endpoint stamps on its upload BI event (`source` from
|
|
6
|
+
* `campaign`, `agent` from `agent`, both ≤ 64 chars) — the same parser as the
|
|
7
|
+
* anonymous route, and what the headless Drop dashboard keys its "MCP API"
|
|
8
|
+
* entry point on. `agent` is the MCP client's name as a lowercase slug.
|
|
9
|
+
*/
|
|
10
|
+
export declare function dropAttribution(clientName?: string): string;
|
|
11
|
+
export interface UploadHeadlessWebsiteFilesOptions {
|
|
12
|
+
authStrategy: McpAuthenticationStrategy;
|
|
13
|
+
toolDescriptions?: Record<string, string>;
|
|
14
|
+
/** Test seam; defaults to the global fetch. */
|
|
15
|
+
fetchImpl?: typeof fetch;
|
|
16
|
+
/** Client identity for drop attribution when the server cannot tell (the toolkit's configured name). */
|
|
17
|
+
clientName?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare const UPLOAD_HEADLESS_WEBSITE_FILES_PARAM_DESCRIPTIONS: {
|
|
20
|
+
readonly 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.";
|
|
21
|
+
readonly 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.";
|
|
22
|
+
readonly 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.";
|
|
23
|
+
readonly calledStartDeployWebsite: "Whether you called StartDeployWebsite earlier in this conversation and are following the recipe it returned. Answer truthfully; it is recorded, not checked.";
|
|
24
|
+
readonly agentIdentity: "Which agent or product you are and the harness you run in.";
|
|
25
|
+
readonly reason: "One sentence: the user request that led here and what is being published.";
|
|
26
|
+
};
|
|
27
|
+
export interface DropResult {
|
|
28
|
+
metaSiteId: string;
|
|
29
|
+
siteUrl?: string;
|
|
30
|
+
uploadId?: string;
|
|
31
|
+
files: string[];
|
|
32
|
+
}
|
|
33
|
+
export declare function formatDropResult(r: DropResult): string;
|
|
34
|
+
export declare function addUploadHeadlessWebsiteFilesTool(server: WixMcpServer, options: UploadHeadlessWebsiteFilesOptions): void;
|
|
35
|
+
//# sourceMappingURL=upload-headless-website-files.d.ts.map
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UploadHeadlessWebsiteFiles — the upload step of the drop flow, as one
|
|
3
|
+
* individually exposed operation: the files of an existing drop site are
|
|
4
|
+
* replaced (or first filled) with one multipart POST to
|
|
5
|
+
* `/headless-business/{msid}/drop`. Provisioning the site is NOT done here —
|
|
6
|
+
* the recipe's step 1 (StartDeployWebsite → provision) stays with the agent.
|
|
7
|
+
*
|
|
8
|
+
* Takes the same file inputs as ExecuteWixAPI — the `files` text bundle and
|
|
9
|
+
* ChatGPT `attachments` (openai/fileParams) — and does the work directly: parse,
|
|
10
|
+
* unzip, check the site shape, POST, return the live URL. No sandbox, no
|
|
11
|
+
* agent-written code, no runtime dependency.
|
|
12
|
+
*/
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
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';
|
|
17
|
+
import { logger } from '../infra/logger.js';
|
|
18
|
+
import { defaultToolDescriptions } from '../config/default-config.js';
|
|
19
|
+
import { rethrowIfUnauthorized } from '../auth/index.js';
|
|
20
|
+
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';
|
|
22
|
+
export const UPLOAD_HEADLESS_WEBSITE_FILES_TOOL_NAME = 'UploadHeadlessWebsiteFiles';
|
|
23
|
+
const HEADLESS_BUSINESS = 'https://www.wixapis.com/headless-business-setup/v1/headless-business';
|
|
24
|
+
/**
|
|
25
|
+
* Attribution the drop endpoint stamps on its upload BI event (`source` from
|
|
26
|
+
* `campaign`, `agent` from `agent`, both ≤ 64 chars) — the same parser as the
|
|
27
|
+
* anonymous route, and what the headless Drop dashboard keys its "MCP API"
|
|
28
|
+
* entry point on. `agent` is the MCP client's name as a lowercase slug.
|
|
29
|
+
*/
|
|
30
|
+
export function dropAttribution(clientName) {
|
|
31
|
+
const slug = (clientName ?? '')
|
|
32
|
+
.toLowerCase()
|
|
33
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
34
|
+
.replace(/^-+|-+$/g, '')
|
|
35
|
+
.slice(0, 64);
|
|
36
|
+
return `?campaign=mcp&agent=${encodeURIComponent(slug || 'wix-mcp')}`;
|
|
37
|
+
}
|
|
38
|
+
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.',
|
|
42
|
+
calledStartDeployWebsite: 'Whether you called StartDeployWebsite earlier in this conversation and are following the recipe it returned. Answer truthfully; it is recorded, not checked.',
|
|
43
|
+
agentIdentity: START_DEPLOY_WEBSITE_PARAM_DESCRIPTIONS.agentIdentity,
|
|
44
|
+
reason: 'One sentence: the user request that led here and what is being published.'
|
|
45
|
+
};
|
|
46
|
+
export function formatDropResult(r) {
|
|
47
|
+
const shown = r.files.slice(0, 60);
|
|
48
|
+
return [
|
|
49
|
+
'Uploaded; the site is live with this file set.',
|
|
50
|
+
`Live URL: ${r.siteUrl ?? '(not returned)'}`,
|
|
51
|
+
`metaSiteId: ${r.metaSiteId} — pass it as \`siteId\` to update this site later; every upload replaces the whole file set.`,
|
|
52
|
+
`Dashboard: https://manage.wix.com/dashboard/${r.metaSiteId}`,
|
|
53
|
+
`${r.files.length} file(s): ${shown.join(', ')}${r.files.length > shown.length ? ', …' : ''}`,
|
|
54
|
+
'',
|
|
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}).`
|
|
57
|
+
].join('\n');
|
|
58
|
+
}
|
|
59
|
+
async function readJson(res, what) {
|
|
60
|
+
const text = await res.text();
|
|
61
|
+
if (!res.ok)
|
|
62
|
+
throw new WixFetchError(res.status, res.statusText, `${what}: ${text}`);
|
|
63
|
+
try {
|
|
64
|
+
return JSON.parse(text);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
throw new Error(`${what} returned a non-JSON body: ${text.slice(0, 200)}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function errorResult(message) {
|
|
71
|
+
return {
|
|
72
|
+
isError: true,
|
|
73
|
+
content: [
|
|
74
|
+
{
|
|
75
|
+
type: 'text',
|
|
76
|
+
text: `UploadHeadlessWebsiteFiles error: ${message}`
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
export function addUploadHeadlessWebsiteFilesTool(server, options) {
|
|
82
|
+
const { authStrategy, toolDescriptions } = options;
|
|
83
|
+
const clientName = () => options.clientName ??
|
|
84
|
+
server.clientName?.() ??
|
|
85
|
+
server.server?.getClientVersion?.()?.name;
|
|
86
|
+
const fetchImpl = options.fetchImpl ?? ((u, i) => fetch(u, i));
|
|
87
|
+
const d = UPLOAD_HEADLESS_WEBSITE_FILES_PARAM_DESCRIPTIONS;
|
|
88
|
+
const registered = server.tool(UPLOAD_HEADLESS_WEBSITE_FILES_TOOL_NAME, toolDescriptions?.[UPLOAD_HEADLESS_WEBSITE_FILES_TOOL_NAME] ??
|
|
89
|
+
defaultToolDescriptions.UploadHeadlessWebsiteFiles, {
|
|
90
|
+
files: z.string().optional().describe(d.files),
|
|
91
|
+
attachments: z
|
|
92
|
+
.array(z.object({
|
|
93
|
+
download_url: z.string(),
|
|
94
|
+
file_id: z.string(),
|
|
95
|
+
file_name: z.string().optional(),
|
|
96
|
+
mime_type: z.string().optional()
|
|
97
|
+
}))
|
|
98
|
+
.optional()
|
|
99
|
+
.describe(d.attachments),
|
|
100
|
+
siteId: z
|
|
101
|
+
.string()
|
|
102
|
+
.uuid('siteId must be a Wix metaSiteId (UUID)')
|
|
103
|
+
.describe(d.siteId),
|
|
104
|
+
calledStartDeployWebsite: z
|
|
105
|
+
.boolean()
|
|
106
|
+
.describe(d.calledStartDeployWebsite),
|
|
107
|
+
agentIdentity: z.string().min(1).describe(d.agentIdentity),
|
|
108
|
+
reason: z.string().describe(d.reason)
|
|
109
|
+
},
|
|
110
|
+
// openWorldHint true: it fetches agent-supplied attachment URLs and publishes a
|
|
111
|
+
// public website.
|
|
112
|
+
{ 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}"`);
|
|
115
|
+
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).');
|
|
117
|
+
}
|
|
118
|
+
// 1. Gather bytes: bundle + resolved attachments. All agent-facing errors.
|
|
119
|
+
let siteFiles;
|
|
120
|
+
try {
|
|
121
|
+
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
|
+
]);
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
if (e instanceof AttachmentError || e instanceof Error)
|
|
129
|
+
return errorResult(e.message);
|
|
130
|
+
throw e;
|
|
131
|
+
}
|
|
132
|
+
// 2. Account auth (outside the try so UnauthorizedError reaches the wrapper).
|
|
133
|
+
const authHeaders = await authStrategy.getAccountAuthHeaders();
|
|
134
|
+
try {
|
|
135
|
+
const metaSiteId = siteId;
|
|
136
|
+
extra?.setToolBiParams?.({ siteId: metaSiteId });
|
|
137
|
+
// 3. One multipart POST — the site goes live with this file set.
|
|
138
|
+
const dropped = await readJson(await fetchImpl(`${HEADLESS_BUSINESS}/${metaSiteId}/drop${dropAttribution(clientName())}`, {
|
|
139
|
+
method: 'POST',
|
|
140
|
+
headers: { ...authHeaders }, // fetch sets the multipart boundary itself
|
|
141
|
+
body: buildFormData(siteFiles)
|
|
142
|
+
}), 'drop');
|
|
143
|
+
return {
|
|
144
|
+
content: [
|
|
145
|
+
{
|
|
146
|
+
type: 'text',
|
|
147
|
+
text: formatDropResult({
|
|
148
|
+
metaSiteId,
|
|
149
|
+
siteUrl: dropped.siteUrl,
|
|
150
|
+
uploadId: dropped.uploadId,
|
|
151
|
+
files: siteFiles.map((f) => f.path)
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
]
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
159
|
+
logger.log(`[UploadHeadlessWebsiteFiles] Error: ${message}`);
|
|
160
|
+
rethrowIfUnauthorized(err);
|
|
161
|
+
await authStrategy.enforceValidWixToken?.(err);
|
|
162
|
+
return errorResult(message);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
// ChatGPT resolves `attachments` into download URLs only for params listed
|
|
166
|
+
// here (OpenAI's `openai/fileParams` extension). Other clients ignore it.
|
|
167
|
+
registered?.update?.({
|
|
168
|
+
_meta: { ...registered._meta, 'openai/fileParams': ['attachments'] }
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=upload-headless-website-files.js.map
|
package/build/dts/bin.js
CHANGED
|
@@ -21,7 +21,7 @@ const EXPERIMENTAL_TOOLS = [
|
|
|
21
21
|
'WIX_API',
|
|
22
22
|
'CLI_COMMAND',
|
|
23
23
|
'GET_TO_KNOW_WIX',
|
|
24
|
-
'CREATE_WIX_BUSINESS_GUIDE',
|
|
24
|
+
'CREATE_WIX_BUSINESS_GUIDE', // accepted so old configs still start; the tool was removed
|
|
25
25
|
'VELO_README',
|
|
26
26
|
'WIX_API_THROUGH_FS',
|
|
27
27
|
'GRAPHQL'
|
|
@@ -90,15 +90,12 @@ logger.log('Active tools:', activeTools);
|
|
|
90
90
|
const docsTools = activeTools.filter((tool) => VALID_DOCS_TOOLS.includes(tool));
|
|
91
91
|
const isWixCliAuth = Boolean(parsedArgs['wixCliAuth']);
|
|
92
92
|
const hasGetToKnowWix = activeTools.includes('GET_TO_KNOW_WIX') || isWixCliAuth;
|
|
93
|
-
const hasCreateWixBusinessGuide = activeTools.includes('CREATE_WIX_BUSINESS_GUIDE');
|
|
94
93
|
const hasDynamicContext = Boolean(parsedArgs['dynamicContext']) || isWixCliAuth;
|
|
95
|
-
if (docsTools.length > 0 || hasGetToKnowWix
|
|
94
|
+
if (docsTools.length > 0 || hasGetToKnowWix) {
|
|
96
95
|
logger.log('Adding docs tools:', docsTools);
|
|
97
96
|
addDocsTools(server, docsTools, {
|
|
98
97
|
getToKnowWixEnabled: hasGetToKnowWix,
|
|
99
|
-
|
|
100
|
-
dynamicContextEnabled: hasDynamicContext,
|
|
101
|
-
cliEnabled: true // bin.ts is always a CLI client (e.g. Claude Code)
|
|
98
|
+
dynamicContextEnabled: hasDynamicContext
|
|
102
99
|
});
|
|
103
100
|
}
|
|
104
101
|
if (activeTools.includes('VELO_README')) {
|
|
@@ -183,7 +183,7 @@ export async function resolveAttachments(attachments, fetchImpl = fetch) {
|
|
|
183
183
|
url = undefined;
|
|
184
184
|
}
|
|
185
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))}).
|
|
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
187
|
}
|
|
188
188
|
const problem = checkAttachmentUrl(url);
|
|
189
189
|
if (problem) {
|
|
@@ -8,6 +8,7 @@ import { validateExecuteCode } from './validation.js';
|
|
|
8
8
|
import { desc } from '../config/param-descriptions.js';
|
|
9
9
|
import { rethrowIfUnauthorized } from '../auth/index.js';
|
|
10
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';
|
|
11
12
|
// Looks `fetch` up per call so a host or test that swaps the global is honored.
|
|
12
13
|
const defaultCodeModeFetch = (url, init) => fetch(url, init);
|
|
13
14
|
const SEARCH_TOOL_NAME = 'SearchWixAPISpec';
|
|
@@ -25,6 +26,15 @@ export async function addCodeModeTools(server, options) {
|
|
|
25
26
|
maxResponseSize
|
|
26
27
|
});
|
|
27
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
|
+
}
|
|
28
38
|
}
|
|
29
39
|
/**
|
|
30
40
|
* Register SearchWixAPISpec. The description must stay a byte-stable
|
|
@@ -234,8 +244,8 @@ function addExecuteTool(server, options) {
|
|
|
234
244
|
hasMutations: z
|
|
235
245
|
.boolean()
|
|
236
246
|
.describe(visitor
|
|
237
|
-
? '
|
|
238
|
-
: '
|
|
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).'),
|
|
239
249
|
sourceDocUrls: z.array(z.string()).describe(d.sourceDocUrls),
|
|
240
250
|
files: z
|
|
241
251
|
.string()
|
|
@@ -259,7 +269,7 @@ function addExecuteTool(server, options) {
|
|
|
259
269
|
mime_type: z.string().optional()
|
|
260
270
|
}))
|
|
261
271
|
.optional()
|
|
262
|
-
.describe('Files attached in
|
|
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.'),
|
|
263
273
|
// Visitor mode takes an agent-supplied token and pins the site; account/site
|
|
264
274
|
// mode lets the agent pick the site.
|
|
265
275
|
...(visitor
|
|
@@ -274,7 +284,11 @@ function addExecuteTool(server, options) {
|
|
|
274
284
|
.optional()
|
|
275
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.')
|
|
276
286
|
})
|
|
277
|
-
},
|
|
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,
|
|
278
292
|
// Partial: duck-typed servers (the toolkit's capturing server, test
|
|
279
293
|
// doubles) build their own extra and may not supply every field.
|
|
280
294
|
extra) => {
|
|
@@ -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;
|