htmlhost-mcp 1.1.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 CHANGED
@@ -97,6 +97,11 @@ title (optional) Title for the site
97
97
  ttl (optional) Time-to-live: 1d, 7d, 30d, or never
98
98
  ```
99
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
+
100
105
  ### `deploy_html`
101
106
 
102
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "htmlhost-mcp",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "MCP server for htmlhost.co — deploy, list, and manage sites from AI coding tools",
5
5
  "type": "module",
6
6
  "bin": {
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";
@@ -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 (!slug) body.isNewSite = true;
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.1.0";
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,