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 +35 -3
- package/package.json +1 -1
- package/src/api.mjs +11 -4
- package/src/server.mjs +93 -2
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
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
// -------------------------------------------------------------------
|