htmlhost-mcp 1.7.0 → 1.8.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,13 +97,15 @@ htmlhost-mcp
97
97
  | `deploy` | Deploy a local file or directory to htmlhost.co |
98
98
  | `deploy_html` | Deploy raw HTML content directly (no local files needed) |
99
99
  | `list_sites` | List your sites and the ones shared with you |
100
- | `get_site` | Get details about a specific site (pages, assets, version) |
100
+ | `get_site` | Get details about a specific site (pages, assets, version, keys, secure calls, collections) |
101
101
  | `pull_site` | Download a site's files to a local directory |
102
102
  | `delete_site` | Permanently delete a site |
103
103
  | `login` | Sign in: opens an approval page in your browser |
104
104
  | `whoami` | Show the current authenticated user |
105
105
  | `list_versions` | List a site's published versions and what each changed |
106
106
  | `restore_version` | Make an earlier version live again (Pro) |
107
+ | `set_site_key` | Save a key the user gave you to a site's Keys (public or secret) |
108
+ | `remove_site_key` | Remove a key from a site's Keys |
107
109
 
108
110
  ### `deploy`
109
111
 
@@ -142,7 +144,37 @@ Returns all your sites, their URLs, sizes, TTLs, and plan usage, plus `shared`:
142
144
  slug (required) The site slug
143
145
  ```
144
146
 
145
- Returns version, page routes, asset paths and sizes.
147
+ Returns version, page routes, asset paths and sizes. When the site has them, also:
148
+
149
+ - `keys`: the keys its owner saved for the services it uses, each with its `name`, `kind` (`public` or `secret`) and note. Only public keys carry a `value`; a secret key's value is never sent.
150
+ - `secureCalls`: how a page uses a secret key. Each has a `name`, the `path` a page POSTs to (`/__hh/call/<name>`), the `fields` it may send and the parts of the answer it `returns`.
151
+ - `collections`: the site's own data. Each has a `name`, its `path` (`/__hh/data/<name>`), its `fields` and its `read`/`add` rules. Records are never listed.
152
+
153
+ For questions about sign-in, members or private pages, the [agent guide](https://htmlhost.co/agents.md) has a Sign-in and access control section: htmlhost has a whole-site password (Pro) and team accounts, and individual visitor accounts use a browser sign-in service.
154
+
155
+ Secure calls and collections are set up in the site's dashboard (Keys and Data tabs). Keys can also be saved from your AI tool (below). See [Site keys](https://htmlhost.co/agents.md) in the agent guide, and the guides on [payments](https://htmlhost.co/guides/accept-payments-on-your-website), [API keys](https://htmlhost.co/guides/use-api-keys-safely-on-your-website) and [collections](https://htmlhost.co/guides/add-a-database-to-your-website).
156
+
157
+ ### `set_site_key`
158
+
159
+ ```
160
+ slug (required) The site slug
161
+ name (required) The key's name, e.g. STRIPE_SECRET_KEY
162
+ value (required) The key's value, exactly as the user gave it
163
+ kind (optional) public or secret: only if the user said which
164
+ note (optional) What it's for
165
+ confirmPublic (optional) Only after the user confirms a secret-looking value should be public
166
+ ```
167
+
168
+ Adds the key, or replaces the value of a key with that name. Without `kind`, htmlhost works it out from the value (`sk_…` secret, `pk_…` public, and similar formats) or the name. When it can't tell, it refuses with `KEY_KIND_UNKNOWN`, and the tool asks the user. Saving a secret-looking value as public is refused with `KEY_LOOKS_SECRET` until the user confirms. The site's owner or an Admin only, on Pro. The result never contains the value.
169
+
170
+ ### `remove_site_key`
171
+
172
+ ```
173
+ slug (required) The site slug
174
+ name (required) The key's name
175
+ ```
176
+
177
+ Removes the key. Refused while a secure call uses it.
146
178
 
147
179
  ### `pull_site`
148
180
 
@@ -151,7 +183,7 @@ slug (required) The site slug to pull
151
183
  directory (required) Absolute path to write files into
152
184
  ```
153
185
 
154
- Downloads all pages and assets into the specified directory.
186
+ Downloads all pages and assets into the specified directory. The result includes the same `keys`, `secureCalls` and `collections` as `get_site`.
155
187
 
156
188
  ### `delete_site`
157
189
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "htmlhost-mcp",
3
- "version": "1.7.0",
3
+ "version": "1.8.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
@@ -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.7.0";
17
+ export const CLIENT_VERSION = "1.8.0";
18
18
  const DEFAULT_API = "https://htmlhost.co";
19
19
 
20
20
  export function readConfig() {
@@ -54,6 +54,13 @@ class ApiError extends Error {
54
54
  }
55
55
  }
56
56
 
57
+ /** An error's message for the AI tool: a key refusal carries its code (KEY_KIND_UNKNOWN…), which the tool descriptions name. */
58
+ function errorText(data, status) {
59
+ if (data.code === "PUBLICATION_CONFIRMATION_REQUIRED") return `${data.error} Ask the user for explicit permission before retrying with confirmPublish: true, or use isDraft: true.`;
60
+ if (typeof data.code === "string" && /^KEYS?_/.test(data.code)) return `${data.code}: ${data.error}`;
61
+ return data.error || `HTTP ${status}`;
62
+ }
63
+
57
64
  function headers(extra = {}) {
58
65
  const token = getToken();
59
66
  if (!token) throw new ApiError("Not signed in to htmlhost. Call the `login` tool: it opens an approval page in the user's browser.", 0);
@@ -79,7 +86,7 @@ export async function apiGet(path) {
79
86
  return withRetry(async () => {
80
87
  const res = await fetch(`${getApi()}${path}`, { headers: headers() });
81
88
  const data = await res.json();
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);
89
+ if (!res.ok) throw new ApiError(errorText(data, res.status), res.status);
83
90
  return data;
84
91
  }, `GET ${path}`);
85
92
  }
@@ -92,7 +99,7 @@ export async function apiPost(path, body) {
92
99
  body: JSON.stringify(body),
93
100
  });
94
101
  const data = await res.json();
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);
102
+ if (!res.ok) throw new ApiError(errorText(data, res.status), res.status);
96
103
  return data;
97
104
  }, `POST ${path}`, body?.confirmPublish === true ? 1 : 3);
98
105
  }
@@ -104,7 +111,7 @@ export async function apiDelete(path) {
104
111
  headers: headers(),
105
112
  });
106
113
  const data = await res.json();
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);
114
+ if (!res.ok) throw new ApiError(errorText(data, res.status), res.status);
108
115
  return data;
109
116
  }, `DELETE ${path}`);
110
117
  }
package/src/server.mjs CHANGED
@@ -41,6 +41,37 @@ const VERSION = CLIENT_VERSION;
41
41
  let updateNotice = null;
42
42
  let updateNoticeShown = false;
43
43
 
44
+ /**
45
+ * The keys a site's owner saved for the services it uses, as the server sends
46
+ * them: every key's name and kind, and the value of public keys only. A
47
+ * secret's value is never sent. Nothing is added for a site without keys.
48
+ */
49
+ export function siteKeys(data) {
50
+ const keys = Array.isArray(data?.keys) ? data.keys : [];
51
+ const calls = Array.isArray(data?.calls) ? data.calls : [];
52
+ const collections = Array.isArray(data?.collections) ? data.collections : [];
53
+ if (!keys.length && !calls.length && !collections.length) return {};
54
+ return {
55
+ keys,
56
+ ...(calls.length ? {
57
+ secureCalls: calls,
58
+ secureCallsNote:
59
+ "A secure call is how page code uses a secret key: htmlhost makes the call. From a page of the site, fetch(call.path, { method: \"POST\", headers: { \"Content-Type\": \"application/json\" }, body: JSON.stringify({ ...its fields }) }) " +
60
+ "and read { ok, status, data, error }. Calls work on the published site only. See https://htmlhost.co/agents.md (Secure calls).",
61
+ } : {}),
62
+ ...(collections.length ? {
63
+ collections,
64
+ collectionsNote:
65
+ "The site's own data. A page reads a collection whose read rule is public with fetch(collection.path) → { ok, records, next }, and adds to one whose add rule is public with " +
66
+ "POST JSON of its fields → { ok, record, error }. Records are never listed here. Collections work on the published site only. See https://htmlhost.co/agents.md (Collections).",
67
+ } : {}),
68
+ keysNote:
69
+ "A public key's value may be written into the site's pages exactly as given. " +
70
+ "Never write, guess or ask the user to paste a secret key's value into any site file: every visitor can read a site's files. Refer to a secret key by its name only, " +
71
+ "and say plainly when a request needs a step that can't run from the page. When the user gives you a key, save it with set_site_key (htmlhost works out public or secret, and asks you to check with the user only when it can't).",
72
+ };
73
+ }
74
+
44
75
  function wrap(fn) {
45
76
  return async (args) => {
46
77
  try {
@@ -70,7 +101,9 @@ export function createServer() {
70
101
  "When resuming staged work, verify the original project is unchanged and retain validation failures; never treat a pause as successful completion. " +
71
102
  "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
103
  "Check references, forms and desktop/mobile rendering with your own tools, and disclose unavailable checks. A frozen static render cannot verify animation playback. 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.",
104
+ "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. " +
105
+ "get_site and pull_site list the site's keys (payment, mailing-list and other service keys its owner saved) and its secure calls: use a public key's value as given, never put a secret key's value in a site file, use a secret key only through a secure call, and use the site's collections only as their read/add rules allow. " +
106
+ "For questions about login, members or private pages, answer from the agent guide's Sign-in and access control section: htmlhost has a whole-site password (Pro) and team accounts, not individual visitor accounts; those use a browser sign-in service such as Clerk, Auth0, Firebase Authentication or Supabase Auth.",
74
107
  });
75
108
 
76
109
  // -------------------------------------------------------------------
@@ -189,7 +222,7 @@ export function createServer() {
189
222
  {
190
223
  title: "Get site details",
191
224
  description:
192
- "Get detailed information about a specific site including its pages and assets.",
225
+ "Get detailed information about a specific site including its pages and assets, the keys its owner saved for the services it uses (names, and the values of public keys only), its secure calls, and its collections (shapes and rules, never records).",
193
226
  inputSchema: z.object({
194
227
  slug: z.string().describe("The site slug (e.g. 'my-site')"),
195
228
  }),
@@ -217,6 +250,7 @@ export function createServer() {
217
250
  mimeType: a.mimeType,
218
251
  sizeBytes: a.sizeBytes,
219
252
  })),
253
+ ...siteKeys(data),
220
254
  };
221
255
  })
222
256
  );
@@ -289,6 +323,7 @@ export function createServer() {
289
323
  directory: destDir,
290
324
  version: data.version,
291
325
  files: written,
326
+ ...siteKeys(data),
292
327
  };
293
328
  })
294
329
  );
@@ -318,6 +353,62 @@ export function createServer() {
318
353
  })
319
354
  );
320
355
 
356
+ // -------------------------------------------------------------------
357
+ // Tools: set_site_key, remove_site_key (the site's Keys; site-keys.ts)
358
+ // -------------------------------------------------------------------
359
+ server.registerTool(
360
+ "set_site_key",
361
+ {
362
+ title: "Save site key",
363
+ description:
364
+ "Save a key the user gave you (a payment gateway's, a mailing list's, an analytics ID) to a site's Keys, by name, so its pages and secure calls can use it. " +
365
+ "Adds it, or replaces the value of a key with that name. Leave kind out unless the user said which it is: htmlhost works it out from the value " +
366
+ "(sk_… secret, pk_… public, and similar) and, when it can't, refuses with KEY_KIND_UNKNOWN: then ask the user whether it's public or secret and call again with kind. " +
367
+ "A secret-looking value saved as public is refused with KEY_LOOKS_SECRET unless the user confirms (confirmPublic: true). " +
368
+ "The site's owner or an Admin only, on Pro. The result never holds the value: never repeat a secret back to the user, and never write one into a site file.",
369
+ inputSchema: z.object({
370
+ slug: z.string().describe("The site slug"),
371
+ name: z.string().describe("The key's name, e.g. STRIPE_SECRET_KEY (stored in UPPER_SNAKE_CASE)"),
372
+ value: z.string().describe("The key's value, exactly as the user gave it"),
373
+ kind: z.enum(["public", "secret"]).optional().describe("Only if the user said which it is; otherwise leave out and htmlhost decides or asks you to check"),
374
+ note: z.string().optional().describe("What the key is for, in a few words"),
375
+ confirmPublic: z.boolean().optional().describe("Only after the user confirms a secret-looking value should be public"),
376
+ }),
377
+ },
378
+ wrap(async ({ slug, name, value, kind, note, confirmPublic }) => {
379
+ if (!isLoggedIn()) return NOT_SIGNED_IN;
380
+ const cleanSlug = slug.replace(/\.htmlhost\.co$/, "");
381
+ const data = await apiPost(`/api/sites/${encodeURIComponent(cleanSlug)}/keys`, { name, value, kind, note, confirmPublic });
382
+ return {
383
+ slug: cleanSlug,
384
+ name: data.key.name,
385
+ kind: data.key.kind,
386
+ created: data.created,
387
+ message: data.key.kind === "secret"
388
+ ? `Saved ${data.key.name} as a secret key. Its value is never shown again; pages use it only through a secure call.`
389
+ : `Saved ${data.key.name} as a public key. Pages may use its value as given.`,
390
+ };
391
+ })
392
+ );
393
+
394
+ server.registerTool(
395
+ "remove_site_key",
396
+ {
397
+ title: "Remove site key",
398
+ description: "Remove a key from a site's Keys by name, when the user asks. Refused while a secure call uses it. The site's owner or an Admin only.",
399
+ inputSchema: z.object({
400
+ slug: z.string().describe("The site slug"),
401
+ name: z.string().describe("The key's name"),
402
+ }),
403
+ },
404
+ wrap(async ({ slug, name }) => {
405
+ if (!isLoggedIn()) return NOT_SIGNED_IN;
406
+ const cleanSlug = slug.replace(/\.htmlhost\.co$/, "");
407
+ const data = await apiDelete(`/api/sites/${encodeURIComponent(cleanSlug)}/keys?name=${encodeURIComponent(name)}`);
408
+ return { slug: cleanSlug, name, removed: data.removed, message: data.removed ? `Removed ${name}.` : `${cleanSlug} has no key named ${name}.` };
409
+ })
410
+ );
411
+
321
412
  // -------------------------------------------------------------------
322
413
  // Tool: whoami
323
414
  // -------------------------------------------------------------------