htmlhost-mcp 1.4.0 → 1.6.0
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/README.md +20 -2
- package/package.json +5 -2
- package/src/api.mjs +12 -11
- package/src/server.mjs +65 -14
package/README.md
CHANGED
|
@@ -4,6 +4,12 @@ An [MCP (Model Context Protocol)](https://modelcontextprotocol.io) server that l
|
|
|
4
4
|
|
|
5
5
|
Works with **Cursor**, **Claude Desktop**, **Windsurf**, **Cline**, **Zed**, and any MCP-compatible tool.
|
|
6
6
|
|
|
7
|
+
## Editing contract
|
|
8
|
+
|
|
9
|
+
The server sends clients instructions to follow **inspect → stage → validate → repair → deploy**, with matching guidance on its pull and deploy tools. Read the [agent guide](https://htmlhost.co/agents.md) before editing. Preserve separate project files and shared header/footer markers, inherit the existing design on new pages, and validate desktop/mobile rendering with the client's own tools. Report checks that could not run.
|
|
10
|
+
|
|
11
|
+
MCP tools manage hosting; they do not execute the browser editor's validation loop. Update an existing project by deploying its complete folder with its slug, rather than sending one generated HTML file.
|
|
12
|
+
|
|
7
13
|
## Setup
|
|
8
14
|
|
|
9
15
|
### 1. Install
|
|
@@ -88,7 +94,7 @@ htmlhost-mcp
|
|
|
88
94
|
|---|---|
|
|
89
95
|
| `deploy` | Deploy a local file or directory to htmlhost.co |
|
|
90
96
|
| `deploy_html` | Deploy raw HTML content directly (no local files needed) |
|
|
91
|
-
| `list_sites` | List
|
|
97
|
+
| `list_sites` | List your sites and the ones shared with you |
|
|
92
98
|
| `get_site` | Get details about a specific site (pages, assets, version) |
|
|
93
99
|
| `pull_site` | Download a site's files to a local directory |
|
|
94
100
|
| `delete_site` | Permanently delete a site |
|
|
@@ -126,7 +132,7 @@ ttl (optional) Time-to-live: 1d, 7d, 30d, or never
|
|
|
126
132
|
|
|
127
133
|
### `list_sites`
|
|
128
134
|
|
|
129
|
-
Returns all sites, their URLs, sizes, TTLs, and plan usage.
|
|
135
|
+
Returns all your sites, their URLs, sizes, TTLs, and plan usage, plus `shared`: the sites shared with you as a collaborator, with their owner. You can pull, deploy to and restore those with your own key; only the owner can delete them.
|
|
130
136
|
|
|
131
137
|
### `get_site`
|
|
132
138
|
|
|
@@ -206,3 +212,15 @@ echo '{"jsonrpc":"2.0","method":"initialize","params":{"capabilities":{}},"id":1
|
|
|
206
212
|
## License
|
|
207
213
|
|
|
208
214
|
MIT
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
## Unpublished sites and drafts
|
|
218
|
+
|
|
219
|
+
Unpublished websites remain private until publishing is explicitly confirmed. Collaborators can publish with confirmation; only owners can unpublish. The server also cancels deployments still running when someone unpublishes.
|
|
220
|
+
|
|
221
|
+
- `deploy` / `deploy_html`: use `isDraft: true` to save without changing publication. To publish an unpublished site, obtain the user's explicit agreement to make it public, then pass `confirmPublish: true`.
|
|
222
|
+
- `restore_version` also requires `confirmPublish: true` for an unpublished website; its general `confirm` argument is insufficient.
|
|
223
|
+
- Never automatically retry a publication refusal with confirmation enabled. Older clients are safely blocked by the server.
|
|
224
|
+
- `unpublish_site`: owner only; pass `confirm: true` after agreement to take the site offline.
|
|
225
|
+
- `list_sites` returns `isPublished` and `draftVersion` for owned and shared sites.
|
|
226
|
+
- `pull_site` defaults to `source: "working"` (including the saved draft). Use `source: "published"` only to deliberately retrieve last-deployed files, or `source: "draft"` to require a draft.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "htmlhost-mcp",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "MCP server for htmlhost.co
|
|
3
|
+
"version": "1.6.0",
|
|
4
|
+
"description": "MCP server for htmlhost.co \u2014 deploy, list, and manage sites from AI coding tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"htmlhost-mcp": "bin/htmlhost-mcp.mjs"
|
|
@@ -33,5 +33,8 @@
|
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@modelcontextprotocol/server": "^2.1.0",
|
|
35
35
|
"zod": "^4.6.5"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"test": "node --experimental-test-module-mocks --test test/"
|
|
36
39
|
}
|
|
37
40
|
}
|
package/src/api.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* so signing in once (the `login` tool or `htmlhost login`) covers both.
|
|
6
6
|
*/
|
|
7
7
|
import { readFileSync, writeFileSync, statSync, readdirSync, mkdirSync, existsSync } from "node:fs";
|
|
8
|
-
import { join, basename, dirname, resolve } from "node:path";
|
|
8
|
+
import { join, basename, dirname, resolve, sep } from "node:path";
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { createHash } from "node:crypto";
|
|
11
11
|
|
|
@@ -14,7 +14,7 @@ import { createHash } from "node:crypto";
|
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
const CONFIG_PATH = join(homedir(), ".htmlhostrc");
|
|
16
16
|
/** Sent with every request, so the API can tell which version is calling. Kept in step with server.mjs. */
|
|
17
|
-
export const CLIENT_VERSION = "1.
|
|
17
|
+
export const CLIENT_VERSION = "1.6.0";
|
|
18
18
|
const DEFAULT_API = "https://htmlhost.co";
|
|
19
19
|
|
|
20
20
|
export function readConfig() {
|
|
@@ -79,7 +79,7 @@ export async function apiGet(path) {
|
|
|
79
79
|
return withRetry(async () => {
|
|
80
80
|
const res = await fetch(`${getApi()}${path}`, { headers: headers() });
|
|
81
81
|
const data = await res.json();
|
|
82
|
-
if (!res.ok) throw new ApiError(data.error || `HTTP ${res.status}`, res.status);
|
|
82
|
+
if (!res.ok) throw new ApiError(data.code === "PUBLICATION_CONFIRMATION_REQUIRED" ? `${data.error} Ask the user for explicit permission before retrying with confirmPublish: true, or use isDraft: true.` : data.error || `HTTP ${res.status}`, res.status);
|
|
83
83
|
return data;
|
|
84
84
|
}, `GET ${path}`);
|
|
85
85
|
}
|
|
@@ -92,9 +92,9 @@ export async function apiPost(path, body) {
|
|
|
92
92
|
body: JSON.stringify(body),
|
|
93
93
|
});
|
|
94
94
|
const data = await res.json();
|
|
95
|
-
if (!res.ok) throw new ApiError(data.error || `HTTP ${res.status}`, res.status);
|
|
95
|
+
if (!res.ok) throw new ApiError(data.code === "PUBLICATION_CONFIRMATION_REQUIRED" ? `${data.error} Ask the user for explicit permission before retrying with confirmPublish: true, or use isDraft: true.` : data.error || `HTTP ${res.status}`, res.status);
|
|
96
96
|
return data;
|
|
97
|
-
}, `POST ${path}
|
|
97
|
+
}, `POST ${path}`, body?.confirmPublish === true ? 1 : 3);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
export async function apiDelete(path) {
|
|
@@ -104,7 +104,7 @@ export async function apiDelete(path) {
|
|
|
104
104
|
headers: headers(),
|
|
105
105
|
});
|
|
106
106
|
const data = await res.json();
|
|
107
|
-
if (!res.ok) throw new ApiError(data.error || `HTTP ${res.status}`, res.status);
|
|
107
|
+
if (!res.ok) throw new ApiError(data.code === "PUBLICATION_CONFIRMATION_REQUIRED" ? `${data.error} Ask the user for explicit permission before retrying with confirmPublish: true, or use isDraft: true.` : data.error || `HTTP ${res.status}`, res.status);
|
|
108
108
|
return data;
|
|
109
109
|
}, `DELETE ${path}`);
|
|
110
110
|
}
|
|
@@ -161,7 +161,8 @@ export function findAllFiles(baseDir, currentDir) {
|
|
|
161
161
|
} else if (entry.isFile()) {
|
|
162
162
|
let size;
|
|
163
163
|
try { size = statSync(fullPath).size; } catch { continue; }
|
|
164
|
-
|
|
164
|
+
// Always with "/" (Windows paths have "\", an address no browser can ask for).
|
|
165
|
+
results.push({ filePath: fullPath, relativePath: fullPath.slice(baseDir.length + 1).split(sep).join("/"), size });
|
|
165
166
|
}
|
|
166
167
|
}
|
|
167
168
|
return results;
|
|
@@ -181,7 +182,7 @@ export function pagePathForFile(relativePath) {
|
|
|
181
182
|
* Deploy a directory to htmlhost. Non-interactive version of CLI deploy.
|
|
182
183
|
* Returns { slug, url, version, pageCount }.
|
|
183
184
|
*/
|
|
184
|
-
export async function deployDirectory(dirPath, { slug = null, title = null, ttl = null } = {}) {
|
|
185
|
+
export async function deployDirectory(dirPath, { slug = null, title = null, ttl = null, confirmPublish = false, isDraft = false } = {}) {
|
|
185
186
|
const resolvedDir = resolve(dirPath);
|
|
186
187
|
const allFiles = findAllFiles(resolvedDir, resolvedDir);
|
|
187
188
|
|
|
@@ -247,7 +248,7 @@ export async function deployDirectory(dirPath, { slug = null, title = null, ttl
|
|
|
247
248
|
|
|
248
249
|
// Final deploy. A root `_redirects` file (Netlify style) replaces the site's
|
|
249
250
|
// redirects from this file; rules added in the dashboard stay. No file, no change.
|
|
250
|
-
const body = { pages: pagePayloads, assets: assetPayloads };
|
|
251
|
+
const body = { pages: pagePayloads, assets: assetPayloads, confirmPublish, isDraft };
|
|
251
252
|
const redirectsPath = join(resolvedDir, "_redirects");
|
|
252
253
|
if (existsSync(redirectsPath)) body.redirects = readFileSync(redirectsPath, "utf8");
|
|
253
254
|
if (ttl) body.ttl = ttl;
|
|
@@ -261,11 +262,11 @@ export async function deployDirectory(dirPath, { slug = null, title = null, ttl
|
|
|
261
262
|
/**
|
|
262
263
|
* Deploy a single HTML file. Non-interactive version of CLI deploy.
|
|
263
264
|
*/
|
|
264
|
-
export async function deploySingleFile(filePath, { slug = null, title = null, ttl = null } = {}) {
|
|
265
|
+
export async function deploySingleFile(filePath, { slug = null, title = null, ttl = null, confirmPublish = false, isDraft = false } = {}) {
|
|
265
266
|
const resolvedPath = resolve(filePath);
|
|
266
267
|
const html = readFileSync(resolvedPath, "utf8");
|
|
267
268
|
|
|
268
|
-
const body = { html };
|
|
269
|
+
const body = { html, confirmPublish, isDraft };
|
|
269
270
|
if (ttl) body.ttl = ttl;
|
|
270
271
|
if (slug) body.slug = slug;
|
|
271
272
|
if (title) body.title = title;
|
package/src/server.mjs
CHANGED
|
@@ -63,6 +63,14 @@ export function createServer() {
|
|
|
63
63
|
const server = new McpServer({
|
|
64
64
|
name: "htmlhost",
|
|
65
65
|
version: VERSION,
|
|
66
|
+
}, {
|
|
67
|
+
instructions: "Read https://htmlhost.co/agents.md before building or editing a site. " +
|
|
68
|
+
"For existing sites: pull current files into a safe working directory, inspect source and the existing design, stage precise edits, validate, repair failures, then deploy. " +
|
|
69
|
+
"For focused edits, inspect the element, parent layout and relevant styles first; expand investigation as needed without skipping validation. " +
|
|
70
|
+
"When resuming staged work, verify the original project is unchanged and retain validation failures; never treat a pause as successful completion. " +
|
|
71
|
+
"Keep HTML/CSS/JS as separate files. New pages inherit the site's design, header, footer and dependencies; preserve shared markers and synchronize shared copies. " +
|
|
72
|
+
"Check references, forms and desktop/mobile rendering with your own tools, and disclose unavailable checks. These hosting tools do not run the editor's validation loop. " +
|
|
73
|
+
"Deploy the complete project folder with the existing slug when updating; do not replace a multi-file site with deploy_html or a lone HTML file.",
|
|
66
74
|
});
|
|
67
75
|
|
|
68
76
|
// -------------------------------------------------------------------
|
|
@@ -75,6 +83,7 @@ export function createServer() {
|
|
|
75
83
|
description:
|
|
76
84
|
"Deploy a file or directory to htmlhost.co. " +
|
|
77
85
|
"Pass an absolute path to a directory (deploys all files) or a single HTML file. " +
|
|
86
|
+
"Inspect and validate changes before deploying. For an existing multi-file site, deploy its complete folder with slug to preserve its pages and assets. " +
|
|
78
87
|
"Optionally set slug (to update an existing site), title, and ttl. " +
|
|
79
88
|
"A `_redirects` file at the directory's root (one `/old /new [301|302]` per line) sets the site's redirects and isn't published.",
|
|
80
89
|
inputSchema: z.object({
|
|
@@ -82,16 +91,18 @@ export function createServer() {
|
|
|
82
91
|
slug: z.string().optional().describe("Existing site slug to update (omit to create a new site)"),
|
|
83
92
|
title: z.string().optional().describe("Title for the site"),
|
|
84
93
|
ttl: z.string().optional().describe("Time-to-live: 1d, 7d, 30d, or never"),
|
|
94
|
+
isDraft: z.boolean().optional().describe("Save files as a draft without publishing. Does not unpublish a live site."),
|
|
95
|
+
confirmPublish: z.boolean().optional().describe("Set true ONLY after the user explicitly agrees to make an unpublished website public. Ordinary editing or deploy instructions are not confirmation."),
|
|
85
96
|
}),
|
|
86
97
|
},
|
|
87
|
-
wrap(async ({ path, slug, title, ttl }) => {
|
|
98
|
+
wrap(async ({ path, slug, title, ttl, isDraft, confirmPublish }) => {
|
|
88
99
|
if (!isLoggedIn()) return NOT_SIGNED_IN;
|
|
89
100
|
|
|
90
101
|
const resolvedPath = resolve(path);
|
|
91
102
|
if (!existsSync(resolvedPath)) return `Path not found: ${path}`;
|
|
92
103
|
|
|
93
104
|
const stat = statSync(resolvedPath);
|
|
94
|
-
const opts = { slug, title, ttl };
|
|
105
|
+
const opts = { slug, title, ttl, isDraft, confirmPublish };
|
|
95
106
|
|
|
96
107
|
let data;
|
|
97
108
|
if (stat.isDirectory()) {
|
|
@@ -102,7 +113,9 @@ export function createServer() {
|
|
|
102
113
|
|
|
103
114
|
const pc = data.pageCount || 1;
|
|
104
115
|
return {
|
|
105
|
-
message: "Deployed successfully",
|
|
116
|
+
message: data.isPublished === false ? "Saved as an unpublished draft" : isDraft ? "Draft saved; live site unchanged" : "Deployed successfully",
|
|
117
|
+
isPublished: data.isPublished,
|
|
118
|
+
draftVersion: data.draftVersion,
|
|
106
119
|
url: `https://${data.url}`,
|
|
107
120
|
slug: data.slug,
|
|
108
121
|
version: data.version,
|
|
@@ -122,8 +135,9 @@ export function createServer() {
|
|
|
122
135
|
{
|
|
123
136
|
title: "List sites",
|
|
124
137
|
description:
|
|
125
|
-
"List
|
|
126
|
-
"Returns slug, URL, title, size, TTL, and expiration for each site."
|
|
138
|
+
"List the authenticated user's sites on htmlhost.co, and the sites shared with them as a collaborator (`shared`). " +
|
|
139
|
+
"Returns slug, URL, title, size, TTL, and expiration for each site. " +
|
|
140
|
+
"Shared sites can be pulled, deployed to and restored with this key like your own, but only their owner can delete them.",
|
|
127
141
|
inputSchema: z.object({}),
|
|
128
142
|
},
|
|
129
143
|
wrap(async () => {
|
|
@@ -137,6 +151,19 @@ export function createServer() {
|
|
|
137
151
|
size: s.size,
|
|
138
152
|
ttl: s.ttl,
|
|
139
153
|
isMultiPage: s.isMultiPage,
|
|
154
|
+
isPublished: s.isPublished,
|
|
155
|
+
draftVersion: s.draftVersion,
|
|
156
|
+
expiresAt: s.expiresAt || null,
|
|
157
|
+
}));
|
|
158
|
+
|
|
159
|
+
const shared = (data.shared || []).map((s) => ({
|
|
160
|
+
slug: s.slug,
|
|
161
|
+
url: `https://${s.slug}.htmlhost.co`,
|
|
162
|
+
title: s.title || null,
|
|
163
|
+
owner: s.owner || null,
|
|
164
|
+
isMultiPage: s.isMultiPage,
|
|
165
|
+
isPublished: s.isPublished,
|
|
166
|
+
draftVersion: s.draftVersion,
|
|
140
167
|
expiresAt: s.expiresAt || null,
|
|
141
168
|
}));
|
|
142
169
|
|
|
@@ -144,6 +171,7 @@ export function createServer() {
|
|
|
144
171
|
count: sites.length,
|
|
145
172
|
usage: data.usage,
|
|
146
173
|
sites,
|
|
174
|
+
...(shared.length && { shared }),
|
|
147
175
|
};
|
|
148
176
|
})
|
|
149
177
|
);
|
|
@@ -172,6 +200,9 @@ export function createServer() {
|
|
|
172
200
|
url: `https://${cleanSlug}.htmlhost.co`,
|
|
173
201
|
version: data.version,
|
|
174
202
|
updatedAt: data.updatedAt,
|
|
203
|
+
isPublished: data.isPublished,
|
|
204
|
+
draftVersion: data.draftVersion,
|
|
205
|
+
source: data.source,
|
|
175
206
|
pages: (data.pages || []).map((p) => ({
|
|
176
207
|
path: p.path,
|
|
177
208
|
sizeBytes: p.sizeBytes || (p.html ? Buffer.byteLength(p.html) : 0),
|
|
@@ -194,20 +225,22 @@ export function createServer() {
|
|
|
194
225
|
title: "Pull site to local directory",
|
|
195
226
|
description:
|
|
196
227
|
"Download all pages and assets from a deployed htmlhost site into a local directory. " +
|
|
197
|
-
"Creates the directory if it doesn't exist."
|
|
228
|
+
"Creates the directory if it doesn't exist; existing files may be overwritten, so use a fresh directory when local work must be preserved. " +
|
|
229
|
+
"Read https://htmlhost.co/agents.md, inspect the downloaded source and design, then validate staged edits before deploying the complete folder.",
|
|
198
230
|
inputSchema: z.object({
|
|
199
231
|
slug: z.string().describe("The site slug to pull"),
|
|
200
232
|
directory: z.string().describe("Absolute path to the local directory to write files into"),
|
|
233
|
+
source: z.enum(["working", "draft", "published"]).optional().describe("Default working includes the latest saved draft. published explicitly retrieves the last deployed files."),
|
|
201
234
|
}),
|
|
202
235
|
},
|
|
203
|
-
wrap(async ({ slug, directory }) => {
|
|
236
|
+
wrap(async ({ slug, directory, source = "working" }) => {
|
|
204
237
|
if (!isLoggedIn()) return NOT_SIGNED_IN;
|
|
205
238
|
|
|
206
239
|
const cleanSlug = slug.replace(/\.htmlhost\.co$/, "");
|
|
207
240
|
const destDir = resolve(directory);
|
|
208
241
|
mkdirSync(destDir, { recursive: true });
|
|
209
242
|
|
|
210
|
-
const data = await apiGet(`/api/sites/pull?slug=${encodeURIComponent(cleanSlug)}`);
|
|
243
|
+
const data = await apiGet(`/api/sites/pull?slug=${encodeURIComponent(cleanSlug)}&source=${source}`);
|
|
211
244
|
const written = [];
|
|
212
245
|
|
|
213
246
|
// Write pages
|
|
@@ -245,6 +278,9 @@ export function createServer() {
|
|
|
245
278
|
|
|
246
279
|
return {
|
|
247
280
|
message: `Pulled ${written.length} files from ${cleanSlug}.htmlhost.co`,
|
|
281
|
+
source: data.source,
|
|
282
|
+
isPublished: data.isPublished,
|
|
283
|
+
draftVersion: data.draftVersion,
|
|
248
284
|
directory: destDir,
|
|
249
285
|
version: data.version,
|
|
250
286
|
files: written,
|
|
@@ -322,18 +358,20 @@ export function createServer() {
|
|
|
322
358
|
title: "Deploy HTML content",
|
|
323
359
|
description:
|
|
324
360
|
"Deploy raw HTML content directly (no local files needed). " +
|
|
325
|
-
"
|
|
361
|
+
"For standalone single-page output after validation. Do not use this to edit an existing multi-page or multi-asset project: pull_site, edit and validate locally, then deploy the complete folder with its slug.",
|
|
326
362
|
inputSchema: z.object({
|
|
327
363
|
html: z.string().describe("The HTML content to deploy"),
|
|
328
364
|
slug: z.string().optional().describe("Existing site slug to update (omit to create new)"),
|
|
329
365
|
title: z.string().optional().describe("Title for the site"),
|
|
330
366
|
ttl: z.string().optional().describe("Time-to-live: 1d, 7d, 30d, or never"),
|
|
367
|
+
isDraft: z.boolean().optional().describe("Save files as a draft without publishing. Does not unpublish a live site."),
|
|
368
|
+
confirmPublish: z.boolean().optional().describe("Set true ONLY after the user explicitly agrees to make an unpublished website public. Ordinary editing or deploy instructions are not confirmation."),
|
|
331
369
|
}),
|
|
332
370
|
},
|
|
333
|
-
wrap(async ({ html, slug, title, ttl }) => {
|
|
371
|
+
wrap(async ({ html, slug, title, ttl, isDraft, confirmPublish }) => {
|
|
334
372
|
if (!isLoggedIn()) return NOT_SIGNED_IN;
|
|
335
373
|
|
|
336
|
-
const body = { html };
|
|
374
|
+
const body = { html, isDraft, confirmPublish };
|
|
337
375
|
if (slug) body.slug = slug;
|
|
338
376
|
if (title) body.title = title;
|
|
339
377
|
if (ttl) body.ttl = ttl;
|
|
@@ -341,7 +379,9 @@ export function createServer() {
|
|
|
341
379
|
const data = await apiPost("/api/sites", body);
|
|
342
380
|
|
|
343
381
|
return {
|
|
344
|
-
message: "Deployed successfully",
|
|
382
|
+
message: data.isPublished === false ? "Saved as an unpublished draft" : isDraft ? "Draft saved; live site unchanged" : "Deployed successfully",
|
|
383
|
+
isPublished: data.isPublished,
|
|
384
|
+
draftVersion: data.draftVersion,
|
|
345
385
|
url: `https://${data.url}`,
|
|
346
386
|
slug: data.slug,
|
|
347
387
|
version: data.version,
|
|
@@ -372,6 +412,16 @@ export function createServer() {
|
|
|
372
412
|
})
|
|
373
413
|
);
|
|
374
414
|
|
|
415
|
+
server.registerTool("unpublish_site", {
|
|
416
|
+
title: "Unpublish a website",
|
|
417
|
+
description: "Owner only: stop public serving while retaining files, saved drafts and history. Obtain the user's explicit confirmation first.",
|
|
418
|
+
inputSchema: z.object({ slug: z.string(), confirm: z.boolean().describe("True only after the user agrees to take this website offline") }),
|
|
419
|
+
}, wrap(async ({ slug, confirm }) => {
|
|
420
|
+
if (!isLoggedIn()) return NOT_SIGNED_IN;
|
|
421
|
+
if (confirm !== true) return "Unpublish not confirmed.";
|
|
422
|
+
return apiPost(`/api/sites/${encodeURIComponent(slug.replace(/\.htmlhost\.co$/, ""))}/unpublish`, {});
|
|
423
|
+
}));
|
|
424
|
+
|
|
375
425
|
// -------------------------------------------------------------------
|
|
376
426
|
// Tool: restore_version
|
|
377
427
|
// -------------------------------------------------------------------
|
|
@@ -386,13 +436,14 @@ export function createServer() {
|
|
|
386
436
|
slug: z.string().describe("The site slug"),
|
|
387
437
|
version: z.number().int().positive().describe("The version number to restore (from list_versions)"),
|
|
388
438
|
confirm: z.boolean().describe("Must be true to confirm replacing the live site"),
|
|
439
|
+
confirmPublish: z.boolean().optional().describe("For unpublished sites, set true only after the user explicitly agrees to make the website public again."),
|
|
389
440
|
}),
|
|
390
441
|
},
|
|
391
|
-
wrap(async ({ slug, version, confirm }) => {
|
|
442
|
+
wrap(async ({ slug, version, confirm, confirmPublish }) => {
|
|
392
443
|
if (!isLoggedIn()) return NOT_SIGNED_IN;
|
|
393
444
|
if (!confirm) return "Restore not confirmed. Set confirm to true to proceed.";
|
|
394
445
|
const cleanSlug = slug.replace(/\.htmlhost\.co$/, "");
|
|
395
|
-
const data = await apiPost(`/api/sites/${encodeURIComponent(cleanSlug)}/versions/${version}/restore`, {});
|
|
446
|
+
const data = await apiPost(`/api/sites/${encodeURIComponent(cleanSlug)}/versions/${version}/restore`, { confirmPublish });
|
|
396
447
|
return {
|
|
397
448
|
message: `Restored v${version}; it's live as v${data.version}.`,
|
|
398
449
|
url: data.url ? (String(data.url).startsWith("http") ? data.url : `https://${data.url}`) : `https://${cleanSlug}.htmlhost.co`,
|