htmlhost-mcp 1.2.0 → 1.3.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
@@ -12,6 +12,14 @@ Works with **Cursor**, **Claude Desktop**, **Windsurf**, **Cline**, **Zed**, and
12
12
  npm install -g htmlhost-mcp
13
13
  ```
14
14
 
15
+ Or skip installing and use `npx -y htmlhost-mcp@latest` as the command, which fetches the newest version each
16
+ time your AI tool starts the server.
17
+
18
+ **Updates:** the server checks npm once a day. A global install updates itself in the background (restart your
19
+ AI tool to load it), and the first tool result of a session tells your AI tool when a newer version exists. With
20
+ npx, keep `@latest` in the command: a bare `npx -y htmlhost-mcp` keeps reusing its cached copy. Set
21
+ `HTMLHOST_NO_UPDATE_CHECK=1` to turn the check off.
22
+
15
23
  ### 2. Authenticate
16
24
 
17
25
  The MCP server reuses the same token as the CLI. If you've already run `htmlhost login`, you're set.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "htmlhost-mcp",
3
- "version": "1.2.0",
3
+ "version": "1.3.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
@@ -13,9 +13,11 @@ import { createHash } from "node:crypto";
13
13
  // Config (mirrors packages/cli/src/config.mjs)
14
14
  // ---------------------------------------------------------------------------
15
15
  const CONFIG_PATH = join(homedir(), ".htmlhostrc");
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.3.0";
16
18
  const DEFAULT_API = "https://htmlhost.co";
17
19
 
18
- function readConfig() {
20
+ export function readConfig() {
19
21
  try {
20
22
  return JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
21
23
  } catch {
@@ -23,6 +25,13 @@ function readConfig() {
23
25
  }
24
26
  }
25
27
 
28
+ /** Merge keys into ~/.htmlhostrc (shared with the CLI), keeping it readable by its owner only. */
29
+ export function writeConfig(data) {
30
+ try {
31
+ writeFileSync(CONFIG_PATH, JSON.stringify({ ...readConfig(), ...data }, null, 2) + "\n", { encoding: "utf8", mode: 0o600 });
32
+ } catch { /* a read-only home: the check just runs again next time */ }
33
+ }
34
+
26
35
  export function getToken() {
27
36
  return readConfig().token || null;
28
37
  }
@@ -48,7 +57,7 @@ class ApiError extends Error {
48
57
  function headers(extra = {}) {
49
58
  const token = getToken();
50
59
  if (!token) throw new ApiError("Not logged in. Run `htmlhost login` in a terminal first.", 0);
51
- return { Authorization: `Bearer ${token}`, "X-HTMLHost-Client": "mcp", ...extra };
60
+ return { Authorization: `Bearer ${token}`, "X-HTMLHost-Client": "mcp", "X-HTMLHost-Client-Version": CLIENT_VERSION, ...extra };
52
61
  }
53
62
 
54
63
  async function withRetry(fn, label = "request", retries = 3) {
package/src/server.mjs CHANGED
@@ -24,18 +24,28 @@ import {
24
24
  findAllFiles,
25
25
  pagePathForFile,
26
26
  mimeFromExt,
27
+ CLIENT_VERSION,
27
28
  } from "./api.mjs";
29
+ import { checkForUpdate } from "./update.mjs";
28
30
 
29
- const VERSION = "1.2.0";
31
+ const VERSION = CLIENT_VERSION;
30
32
 
31
33
  /**
32
34
  * Helper: wraps a handler to return proper MCP content and catch errors.
33
35
  */
36
+ /** An update notice (update.mjs), shown once: with the first tool result after the check finishes. */
37
+ let updateNotice = null;
38
+ let updateNoticeShown = false;
39
+
34
40
  function wrap(fn) {
35
41
  return async (args) => {
36
42
  try {
37
43
  const result = await fn(args);
38
- const text = typeof result === "string" ? result : JSON.stringify(result, null, 2);
44
+ let text = typeof result === "string" ? result : JSON.stringify(result, null, 2);
45
+ if (updateNotice && !updateNoticeShown) {
46
+ updateNoticeShown = true;
47
+ text += `\n\nNote for the user: ${updateNotice}`;
48
+ }
39
49
  return { content: [{ type: "text", text }] };
40
50
  } catch (e) {
41
51
  return { content: [{ type: "text", text: `Error: ${e.message}` }], isError: true };
@@ -44,6 +54,8 @@ function wrap(fn) {
44
54
  }
45
55
 
46
56
  export function createServer() {
57
+ // In the background: never delays starting up or a tool call.
58
+ void checkForUpdate(VERSION).then((notice) => { updateNotice = notice; }).catch(() => {});
47
59
  const server = new McpServer({
48
60
  name: "htmlhost",
49
61
  version: VERSION,
package/src/update.mjs ADDED
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Keeping htmlhost-mcp current, like the CLI's update check.
3
+ *
4
+ * An MCP server is a child process of the AI tool, talking over stdio, so it
5
+ * can't swap itself out while running and must never print to stdout. On
6
+ * start it checks the npm registry (at most once a day, cached in the shared
7
+ * ~/.htmlhostrc). When a newer version exists:
8
+ *
9
+ * - installed globally (`htmlhost-mcp` command): it installs the new version
10
+ * in the background, so the next start of the AI tool uses it;
11
+ * - run with `npx -y htmlhost-mcp@latest`: npx fetches it on the next start;
12
+ * - run with a bare `npx -y htmlhost-mcp`: npx keeps reusing its cached copy,
13
+ * so the notice asks for `@latest` in the MCP config.
14
+ *
15
+ * The first tool result of the session carries a one-line notice, so the AI
16
+ * tells the user. Set HTMLHOST_NO_UPDATE_CHECK to turn all of this off.
17
+ */
18
+ import { spawn } from "node:child_process";
19
+ import { realpathSync } from "node:fs";
20
+ import { sep } from "node:path";
21
+ import { readConfig, writeConfig } from "./api.mjs";
22
+
23
+ const PACKAGE_NAME = "htmlhost-mcp";
24
+ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
25
+
26
+ /** True when a is a newer x.y.z than b. */
27
+ export function isNewer(a, b) {
28
+ const pa = String(a).split(".").map(Number);
29
+ const pb = String(b).split(".").map(Number);
30
+ for (let i = 0; i < 3; i++) {
31
+ if ((pa[i] || 0) > (pb[i] || 0)) return true;
32
+ if ((pa[i] || 0) < (pb[i] || 0)) return false;
33
+ }
34
+ return false;
35
+ }
36
+
37
+ /** How this server was started: "npx" (from npx's cache), "global", or "local" (a project's node_modules, or a checkout). */
38
+ export function installKind(entry = process.argv[1] || "", globalRoot = null) {
39
+ let real = entry;
40
+ try { real = realpathSync(entry); } catch { /* keep the path as given */ }
41
+ if (real.includes(`${sep}_npx${sep}`)) return "npx";
42
+ if (globalRoot && real.startsWith(globalRoot + sep)) return "global";
43
+ return "local";
44
+ }
45
+
46
+ /** `npm root -g`, or null. */
47
+ function npmGlobalRoot() {
48
+ return new Promise((resolve) => {
49
+ try {
50
+ const child = spawn(process.platform === "win32" ? "npm.cmd" : "npm", ["root", "-g"], { stdio: ["ignore", "pipe", "ignore"] });
51
+ let out = "";
52
+ child.stdout.on("data", (d) => (out += d));
53
+ child.on("error", () => resolve(null));
54
+ child.on("close", () => resolve(out.trim() || null));
55
+ setTimeout(() => { child.kill(); resolve(null); }, 5000).unref();
56
+ } catch {
57
+ resolve(null);
58
+ }
59
+ });
60
+ }
61
+
62
+ /** The newest published version, from the cache when it's less than a day old. */
63
+ async function latestVersion() {
64
+ const config = readConfig();
65
+ if (config.mcpLatestVersion && Date.now() - (config.mcpLastUpdateCheck || 0) < CHECK_INTERVAL_MS) {
66
+ return config.mcpLatestVersion;
67
+ }
68
+ const controller = new AbortController();
69
+ const timer = setTimeout(() => controller.abort(), 5000);
70
+ try {
71
+ const res = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, { signal: controller.signal, headers: { Accept: "application/json" } });
72
+ if (!res.ok) return null;
73
+ const data = await res.json();
74
+ if (!data?.version) return null;
75
+ writeConfig({ mcpLastUpdateCheck: Date.now(), mcpLatestVersion: data.version });
76
+ return data.version;
77
+ } catch {
78
+ return null;
79
+ } finally {
80
+ clearTimeout(timer);
81
+ }
82
+ }
83
+
84
+ /** Install the latest version globally, detached and silent (stdout belongs to MCP). */
85
+ function installInBackground() {
86
+ try {
87
+ const child = spawn(process.platform === "win32" ? "npm.cmd" : "npm", ["install", "-g", `${PACKAGE_NAME}@latest`], {
88
+ stdio: "ignore",
89
+ detached: true,
90
+ });
91
+ child.on("error", () => {});
92
+ child.unref();
93
+ return true;
94
+ } catch {
95
+ return false;
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Check for a newer version and act on it. Resolves to a one-line notice for
101
+ * the AI to pass on, or null. Never throws, never writes to stdout.
102
+ */
103
+ export async function checkForUpdate(currentVersion) {
104
+ if (process.env.HTMLHOST_NO_UPDATE_CHECK || process.env.CI) return null;
105
+ const latest = await latestVersion();
106
+ if (!latest || !isNewer(latest, currentVersion)) return null;
107
+ const kind = installKind(process.argv[1], await npmGlobalRoot());
108
+ const head = `htmlhost-mcp ${latest} is available (this is ${currentVersion}).`;
109
+ if (kind === "global") {
110
+ return installInBackground()
111
+ ? `${head} It's being installed now; restart your AI tool (or its MCP servers) to use it.`
112
+ : `${head} Update with: npm install -g ${PACKAGE_NAME}@latest, then restart your AI tool.`;
113
+ }
114
+ if (kind === "npx") {
115
+ return `${head} Restart your AI tool to load it. If it still reports ${currentVersion}, change the MCP config to run "npx -y ${PACKAGE_NAME}@latest" so npx always fetches the newest version.`;
116
+ }
117
+ return `${head} Update the htmlhost-mcp package this server runs from, then restart your AI tool.`;
118
+ }