htmlhost-mcp 1.0.0 → 1.2.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 +25 -0
- package/package.json +1 -1
- package/src/api.mjs +11 -4
- package/src/server.mjs +61 -2
package/README.md
CHANGED
|
@@ -83,6 +83,8 @@ htmlhost-mcp
|
|
|
83
83
|
| `pull_site` | Download a site's files to a local directory |
|
|
84
84
|
| `delete_site` | Permanently delete a site |
|
|
85
85
|
| `whoami` | Show the current authenticated user |
|
|
86
|
+
| `list_versions` | List a site's published versions and what each changed |
|
|
87
|
+
| `restore_version` | Make an earlier version live again (Pro) |
|
|
86
88
|
|
|
87
89
|
### `deploy`
|
|
88
90
|
|
|
@@ -95,6 +97,11 @@ title (optional) Title for the site
|
|
|
95
97
|
ttl (optional) Time-to-live: 1d, 7d, 30d, or never
|
|
96
98
|
```
|
|
97
99
|
|
|
100
|
+
A `_redirects` file at the root of a deployed directory sets the site's redirects,
|
|
101
|
+
one per line: `/old-page /new-page`, optionally followed by `301` (default) or `302`.
|
|
102
|
+
It isn't published. Each deploy with the file replaces the redirects it set before;
|
|
103
|
+
redirects added in the dashboard stay. `pull_site` writes the file back.
|
|
104
|
+
|
|
98
105
|
### `deploy_html`
|
|
99
106
|
|
|
100
107
|
Deploy generated HTML directly — no files on disk needed. Perfect for when an AI assistant generates a webpage and wants to make it live immediately.
|
|
@@ -138,6 +145,24 @@ confirm (required) Must be true — safety check
|
|
|
138
145
|
|
|
139
146
|
Returns the authenticated user's email, name, handle, and plan.
|
|
140
147
|
|
|
148
|
+
### `list_versions`
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
slug (required) The site slug
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Returns the site's versions, newest first, with time, source, size, what changed, and which one is live. `usable: false` means the owner's plan can't restore them.
|
|
155
|
+
|
|
156
|
+
### `restore_version`
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
slug (required) The site slug
|
|
160
|
+
version (required) Version number from list_versions
|
|
161
|
+
confirm (required) Must be true — safety check
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Publishes that version again as a new version, so it can be undone the same way. Needs the site owner to be on Pro.
|
|
165
|
+
|
|
141
166
|
## Architecture
|
|
142
167
|
|
|
143
168
|
```
|
package/package.json
CHANGED
package/src/api.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Reuses the same ~/.htmlhostrc token and API base as the CLI,
|
|
5
5
|
* so `htmlhost login` is the only setup step needed.
|
|
6
6
|
*/
|
|
7
|
-
import { readFileSync, writeFileSync, statSync, readdirSync, mkdirSync } from "node:fs";
|
|
7
|
+
import { readFileSync, writeFileSync, statSync, readdirSync, mkdirSync, existsSync } from "node:fs";
|
|
8
8
|
import { join, basename, dirname, resolve } from "node:path";
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { createHash } from "node:crypto";
|
|
@@ -48,7 +48,7 @@ class ApiError extends Error {
|
|
|
48
48
|
function headers(extra = {}) {
|
|
49
49
|
const token = getToken();
|
|
50
50
|
if (!token) throw new ApiError("Not logged in. Run `htmlhost login` in a terminal first.", 0);
|
|
51
|
-
return { Authorization: `Bearer ${token}`, ...extra };
|
|
51
|
+
return { Authorization: `Bearer ${token}`, "X-HTMLHost-Client": "mcp", ...extra };
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
async function withRetry(fn, label = "request", retries = 3) {
|
|
@@ -132,6 +132,8 @@ const DEFAULT_IGNORE = [
|
|
|
132
132
|
".gitignore", ".npmrc", "package-lock.json", "yarn.lock", "pnpm-lock.yaml",
|
|
133
133
|
"AGENTS.md", "CLAUDE.md", "GEMINI.md", ".claude", ".cursor", ".cursorrules",
|
|
134
134
|
".windsurfrules", ".github",
|
|
135
|
+
// Redirect rules: sent with the deploy and applied by htmlhost, never published as a file.
|
|
136
|
+
"_redirects",
|
|
135
137
|
];
|
|
136
138
|
|
|
137
139
|
function shouldIgnore(name) {
|
|
@@ -184,6 +186,8 @@ export async function deployDirectory(dirPath, { slug = null, title = null, ttl
|
|
|
184
186
|
|
|
185
187
|
const htmlFiles = allFiles.filter((f) => f.relativePath.endsWith(".html"));
|
|
186
188
|
const assetFiles = allFiles.filter((f) => !f.relativePath.endsWith(".html"));
|
|
189
|
+
// Decided before upload-url may hand back a generated slug for the new site.
|
|
190
|
+
const isNewSite = !slug;
|
|
187
191
|
|
|
188
192
|
// Build page payloads
|
|
189
193
|
const pagePayloads = [];
|
|
@@ -232,12 +236,15 @@ export async function deployDirectory(dirPath, { slug = null, title = null, ttl
|
|
|
232
236
|
}
|
|
233
237
|
}
|
|
234
238
|
|
|
235
|
-
// Final deploy
|
|
239
|
+
// Final deploy. A root `_redirects` file (Netlify style) replaces the site's
|
|
240
|
+
// redirects from this file; rules added in the dashboard stay. No file, no change.
|
|
236
241
|
const body = { pages: pagePayloads, assets: assetPayloads };
|
|
242
|
+
const redirectsPath = join(resolvedDir, "_redirects");
|
|
243
|
+
if (existsSync(redirectsPath)) body.redirects = readFileSync(redirectsPath, "utf8");
|
|
237
244
|
if (ttl) body.ttl = ttl;
|
|
238
245
|
if (slug) body.slug = slug;
|
|
239
246
|
if (title) body.title = title;
|
|
240
|
-
if (
|
|
247
|
+
if (isNewSite) body.isNewSite = true;
|
|
241
248
|
|
|
242
249
|
return await apiPost("/api/sites", body);
|
|
243
250
|
}
|
package/src/server.mjs
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
mimeFromExt,
|
|
27
27
|
} from "./api.mjs";
|
|
28
28
|
|
|
29
|
-
const VERSION = "1.
|
|
29
|
+
const VERSION = "1.2.0";
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* Helper: wraps a handler to return proper MCP content and catch errors.
|
|
@@ -59,7 +59,8 @@ export function createServer() {
|
|
|
59
59
|
description:
|
|
60
60
|
"Deploy a file or directory to htmlhost.co. " +
|
|
61
61
|
"Pass an absolute path to a directory (deploys all files) or a single HTML file. " +
|
|
62
|
-
"Optionally set slug (to update an existing site), title, and ttl."
|
|
62
|
+
"Optionally set slug (to update an existing site), title, and ttl. " +
|
|
63
|
+
"A `_redirects` file at the directory's root (one `/old /new [301|302]` per line) sets the site's redirects and isn't published.",
|
|
63
64
|
inputSchema: z.object({
|
|
64
65
|
path: z.string().describe("Absolute path to the file or directory to deploy"),
|
|
65
66
|
slug: z.string().optional().describe("Existing site slug to update (omit to create a new site)"),
|
|
@@ -92,6 +93,7 @@ export function createServer() {
|
|
|
92
93
|
pages: pc,
|
|
93
94
|
ttl: data.ttl,
|
|
94
95
|
expiresAt: data.expiresAt || null,
|
|
96
|
+
...(data.redirects ? { redirects: data.redirects } : {}),
|
|
95
97
|
};
|
|
96
98
|
})
|
|
97
99
|
);
|
|
@@ -219,6 +221,12 @@ export function createServer() {
|
|
|
219
221
|
written.push(asset.path);
|
|
220
222
|
}
|
|
221
223
|
|
|
224
|
+
// Redirects that came from a deployed _redirects file
|
|
225
|
+
if (data.redirectsFile) {
|
|
226
|
+
writeFileSync(join(destDir, "_redirects"), data.redirectsFile, "utf8");
|
|
227
|
+
written.push("_redirects");
|
|
228
|
+
}
|
|
229
|
+
|
|
222
230
|
return {
|
|
223
231
|
message: `Pulled ${written.length} files from ${cleanSlug}.htmlhost.co`,
|
|
224
232
|
directory: destDir,
|
|
@@ -314,5 +322,56 @@ export function createServer() {
|
|
|
314
322
|
})
|
|
315
323
|
);
|
|
316
324
|
|
|
325
|
+
// -------------------------------------------------------------------
|
|
326
|
+
// Tool: list_versions
|
|
327
|
+
// -------------------------------------------------------------------
|
|
328
|
+
server.registerTool(
|
|
329
|
+
"list_versions",
|
|
330
|
+
{
|
|
331
|
+
title: "List site versions",
|
|
332
|
+
description:
|
|
333
|
+
"List the published versions of a site, newest first, with what each one changed. " +
|
|
334
|
+
"`usable: false` means the owner's plan can't restore them (Pro feature).",
|
|
335
|
+
inputSchema: z.object({
|
|
336
|
+
slug: z.string().describe("The site slug"),
|
|
337
|
+
}),
|
|
338
|
+
},
|
|
339
|
+
wrap(async ({ slug }) => {
|
|
340
|
+
if (!isLoggedIn()) return "Not logged in. Run `htmlhost login` in a terminal first.";
|
|
341
|
+
const cleanSlug = slug.replace(/\.htmlhost\.co$/, "");
|
|
342
|
+
return apiGet(`/api/sites/${encodeURIComponent(cleanSlug)}/versions`);
|
|
343
|
+
})
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
// -------------------------------------------------------------------
|
|
347
|
+
// Tool: restore_version
|
|
348
|
+
// -------------------------------------------------------------------
|
|
349
|
+
server.registerTool(
|
|
350
|
+
"restore_version",
|
|
351
|
+
{
|
|
352
|
+
title: "Restore a site version",
|
|
353
|
+
description:
|
|
354
|
+
"Make an earlier version of a site live again. It's published as a new version " +
|
|
355
|
+
"(the current one stays in history), so this can be undone by restoring again. Needs Pro.",
|
|
356
|
+
inputSchema: z.object({
|
|
357
|
+
slug: z.string().describe("The site slug"),
|
|
358
|
+
version: z.number().int().positive().describe("The version number to restore (from list_versions)"),
|
|
359
|
+
confirm: z.boolean().describe("Must be true to confirm replacing the live site"),
|
|
360
|
+
}),
|
|
361
|
+
},
|
|
362
|
+
wrap(async ({ slug, version, confirm }) => {
|
|
363
|
+
if (!isLoggedIn()) return "Not logged in. Run `htmlhost login` in a terminal first.";
|
|
364
|
+
if (!confirm) return "Restore not confirmed. Set confirm to true to proceed.";
|
|
365
|
+
const cleanSlug = slug.replace(/\.htmlhost\.co$/, "");
|
|
366
|
+
const data = await apiPost(`/api/sites/${encodeURIComponent(cleanSlug)}/versions/${version}/restore`, {});
|
|
367
|
+
return {
|
|
368
|
+
message: `Restored v${version}; it's live as v${data.version}.`,
|
|
369
|
+
url: data.url ? (String(data.url).startsWith("http") ? data.url : `https://${data.url}`) : `https://${cleanSlug}.htmlhost.co`,
|
|
370
|
+
version: data.version,
|
|
371
|
+
restoredFrom: version,
|
|
372
|
+
};
|
|
373
|
+
})
|
|
374
|
+
);
|
|
375
|
+
|
|
317
376
|
return server;
|
|
318
377
|
}
|