@awesomate/hosting-mcp 0.21.0 → 0.22.1

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/dist/index.js CHANGED
@@ -40196,6 +40196,41 @@ async function knowledgeSources(config3, args) {
40196
40196
  const page = await hubGet(config3, `/api/knowledge/sources${qs ? `?${qs}` : ""}`);
40197
40197
  return describeSourcePage(page);
40198
40198
  }
40199
+ if (args.action === "sync_rules") {
40200
+ const r = await hubGet(config3, "/api/knowledge/files-sync/rules");
40201
+ return { ...r, note: "Each rule keeps a File Manager folder synced into the knowledge base: files under path_prefix (matching include_globs, if any) are ingested with the rule visibility/tags/collection, re-ingested when they change, and handled per on_file_removed when they disappear. A worker sweeps every 15 min; uploads under an auto rule sync immediately; sync_run sweeps now." };
40202
+ }
40203
+ if (args.action === "sync_state") {
40204
+ const q = new URLSearchParams();
40205
+ if (args.ruleId !== void 0) q.set("rule_id", String(args.ruleId));
40206
+ if (args.status) q.set("status", args.status);
40207
+ if (args.limit !== void 0) q.set("limit", String(args.limit));
40208
+ const qs = q.toString();
40209
+ return await hubGet(config3, `/api/knowledge/files-sync/state${qs ? `?${qs}` : ""}`);
40210
+ }
40211
+ if (args.action === "sync_rule_create") {
40212
+ if (!args.pathPrefix) return { error: "invalid_request", note: "sync_rule_create needs pathPrefix (a File Manager folder such as 'public/reports')" };
40213
+ if (args.visibility === "public" && !args.confirm) {
40214
+ return { error: "confirmation_required", note: "A rule that makes every file in a folder PUBLIC is a standing publication. Explain that, then call again with confirm set to the account slug only after the user agrees in words." };
40215
+ }
40216
+ return await hubPost(config3, "/api/knowledge/files-sync/rules", {
40217
+ path_prefix: args.pathPrefix,
40218
+ ...args.includeGlobs?.length ? { include_globs: args.includeGlobs } : {},
40219
+ ...args.collectionIds?.[0] ? { collection_id: args.collectionIds[0] } : {},
40220
+ ...args.visibility ? { visibility: args.visibility } : {},
40221
+ ...args.tags?.length ? { tags: args.tags } : {},
40222
+ ...args.onFileRemoved ? { on_file_removed: args.onFileRemoved } : {},
40223
+ ...args.confirm ? { confirm: args.confirm } : {}
40224
+ });
40225
+ }
40226
+ if (args.action === "sync_rule_delete") {
40227
+ if (args.ruleId === void 0) return { error: "invalid_request", note: "sync_rule_delete needs ruleId (from sync_rules)" };
40228
+ return await hubDelete(config3, `/api/knowledge/files-sync/rules/${args.ruleId}`);
40229
+ }
40230
+ if (args.action === "sync_run") {
40231
+ const r = await hubPost(config3, "/api/knowledge/files-sync/run", {});
40232
+ return { ...r, note: 'Files ingested this run are queued on the platform; verify with awesomate_knowledge_sources {action:"jobs"} and a search for a phrase from the file, never with the run summary alone.' };
40233
+ }
40199
40234
  if (args.action === "search") {
40200
40235
  const q = new URLSearchParams();
40201
40236
  if (args.q) q.set("q", args.q);
@@ -41189,7 +41224,7 @@ server.registerTool(
41189
41224
  server.registerTool(
41190
41225
  "awesomate_site_create",
41191
41226
  {
41192
- description: "Create a WordPress site on the user's hosting. Plan limits are enforced server-side (429/403 with an upgrade hint if they're at their cap \u2014 relay it). Ask 'live or dev?' first per the awesomate-hosting skill. Returns the new site's admin details.",
41227
+ description: "Create a WordPress site on the user's hosting. Plan limits are enforced server-side (429/403 with an upgrade hint if they're at their cap \u2014 relay it; 409 means WordPress already exists at that domain). Ask 'live or dev?' first per the awesomate-hosting skill. Omit `domain` and the next free siteN.{primary} subdomain is chosen for you. ASYNC and returns only {success:true} \u2014 no domain, no admin details: poll awesomate_list_sites to learn the name, and expect HTTPS to refuse connections for a minute or two while AutoSSL issues even though the site is already serving over http. See 'After creating a site' in the skill.",
41193
41228
  inputSchema: {
41194
41229
  domain: external_exports.string().optional().describe("Custom domain if they have one; omit for a default *.awesomate.site subdomain"),
41195
41230
  siteTitle: external_exports.string().optional(),
@@ -41636,9 +41671,13 @@ server.registerTool(
41636
41671
  server.registerTool(
41637
41672
  "awesomate_knowledge_sources",
41638
41673
  {
41639
- description: "The knowledge base's content sources \u2014 the LIBRARY. action 'search' {q?, kind?, visibility?, tags?, collectionIds?, cursor?, limit?} \u2014 find sources by title/tag substring and metadata filters (instant, free; for CONTENT search use awesomate_knowledge_search); each row carries visibility (private|internal|public \u2014 who may retrieve it, ENFORCED), tags and collections. 'tag' {sourceId, tags} \u2014 REPLACE a source's free-form tags. 'set_visibility' {sourceId|sourceIds, visibility, confirm?} \u2014 change who may retrieve the source(s); 'public' is irreversible once fetched, so it needs the user's explicit agreement and their account slug as confirm. 'move' {sourceId|sourceIds, collectionIds?, removeCollectionIds?} \u2014 add to / remove from collections. 'list' \u2014 ONE page of sources, most recently ingested first (default 50, max 200 via limit): read `page.has_more`/`next_cursor` and pass cursor to continue \u2014 a page is never the whole library. 'summary' \u2014 exact whole-library counts {total, by_kind, chunks, indexed_chunks, failed_sources, failed_jobs_7d}: use THIS to say what the knowledge base contains, and if failed_jobs_7d > 0 say so \u2014 those ingests are missing from every other count. 'jobs' \u2014 ingest job statuses (optional status filter: queued|running|succeeded|failed). 'add' \u2014 ingest a public page {url} or a whole site {sitemap, since?}, optionally with visibility (default private), tags and collectionIds; ALWAYS get explicit approval first (ingest costs money and counts against quota), for a local FILE on the user's machine use awesomate_knowledge_upload instead (it streams the file from disk; this tool takes URLs only). A pack_required response means the allowance is exhausted: NOTHING was purchased \u2014 present the pack price (1 credit = $100) and let the user buy from the hub if they want it. 'remove' {sourceId} \u2014 deletes the source AND its indexed content; explicit approval required.",
41674
+ description: "The knowledge base's content sources \u2014 the LIBRARY. action 'search' {q?, kind?, visibility?, tags?, collectionIds?, cursor?, limit?} \u2014 find sources by title/tag substring and metadata filters (instant, free; for CONTENT search use awesomate_knowledge_search); each row carries visibility (private|internal|public \u2014 who may retrieve it, ENFORCED), tags and collections. 'tag' {sourceId, tags} \u2014 REPLACE a source's free-form tags. 'set_visibility' {sourceId|sourceIds, visibility, confirm?} \u2014 change who may retrieve the source(s); 'public' is irreversible once fetched, so it needs the user's explicit agreement and their account slug as confirm. 'move' {sourceId|sourceIds, collectionIds?, removeCollectionIds?} \u2014 add to / remove from collections. 'sync_rules' / 'sync_rule_create' {pathPrefix, includeGlobs?, visibility?, tags?, collectionIds?, onFileRemoved?} / 'sync_rule_delete' {ruleId} / 'sync_run' / 'sync_state' \u2014 keep a folder of the account's File Manager (their n8n file system: public/, private/, temp/) synced into the knowledge base, so files their workflows write become searchable; a public rule needs confirm. 'list' \u2014 ONE page of sources, most recently ingested first (default 50, max 200 via limit): read `page.has_more`/`next_cursor` and pass cursor to continue \u2014 a page is never the whole library. 'summary' \u2014 exact whole-library counts {total, by_kind, chunks, indexed_chunks, failed_sources, failed_jobs_7d}: use THIS to say what the knowledge base contains, and if failed_jobs_7d > 0 say so \u2014 those ingests are missing from every other count. 'jobs' \u2014 ingest job statuses (optional status filter: queued|running|succeeded|failed). 'add' \u2014 ingest a public page {url} or a whole site {sitemap, since?}, optionally with visibility (default private), tags and collectionIds; ALWAYS get explicit approval first (ingest costs money and counts against quota), for a local FILE on the user's machine use awesomate_knowledge_upload instead (it streams the file from disk; this tool takes URLs only). A pack_required response means the allowance is exhausted: NOTHING was purchased \u2014 present the pack price (1 credit = $100) and let the user buy from the hub if they want it. 'remove' {sourceId} \u2014 deletes the source AND its indexed content; explicit approval required.",
41640
41675
  inputSchema: {
41641
- action: external_exports.enum(["list", "summary", "add", "remove", "jobs", "search", "tag", "set_visibility", "move"]),
41676
+ action: external_exports.enum(["list", "summary", "add", "remove", "jobs", "search", "tag", "set_visibility", "move", "sync_rules", "sync_rule_create", "sync_rule_delete", "sync_run", "sync_state"]),
41677
+ pathPrefix: external_exports.string().max(512).optional().describe("sync_rule_create: a File Manager folder to keep synced, e.g. 'public/reports'"),
41678
+ includeGlobs: external_exports.array(external_exports.string().max(120)).max(20).optional().describe("sync_rule_create: only files matching these globs (relative to the folder), e.g. ['**/*.pdf']"),
41679
+ onFileRemoved: external_exports.enum(["keep", "demote_to_private", "delete"]).optional().describe("sync_rule_create: what happens to the knowledge source when the file is removed (default demote_to_private for internal/public rules, keep for private)"),
41680
+ ruleId: external_exports.number().int().optional().describe("sync_rule_delete / sync_state: rule id from sync_rules"),
41642
41681
  q: external_exports.string().max(100).optional().describe("search: title/tag substring"),
41643
41682
  kind: external_exports.string().max(32).optional().describe("search: source kind filter (web, document, video, audio, image, book, dataset)"),
41644
41683
  visibility: external_exports.enum(["private", "internal", "public"]).optional().describe("add: the audience the new source is cleared for (default private); set_visibility: the level to apply; search: filter"),
@@ -41656,10 +41695,10 @@ server.registerTool(
41656
41695
  limit: external_exports.number().int().min(1).max(200).optional().describe("list only: page size (default 50, max 200)")
41657
41696
  }
41658
41697
  },
41659
- async ({ action, url, sitemap, since, sourceId, status, cursor, limit, q, kind, visibility, tags, collectionIds, removeCollectionIds, sourceIds, confirm }) => {
41698
+ async ({ action, url, sitemap, since, sourceId, status, cursor, limit, q, kind, visibility, tags, collectionIds, removeCollectionIds, sourceIds, confirm, pathPrefix, includeGlobs, onFileRemoved, ruleId }) => {
41660
41699
  try {
41661
41700
  return knowledgeResult(
41662
- await knowledgeSources(requireConfig(), { action, url, sitemap, since, sourceId, status, cursor, limit, q, kind, visibility, tags, collectionIds, removeCollectionIds, sourceIds, confirm })
41701
+ await knowledgeSources(requireConfig(), { action, url, sitemap, since, sourceId, status, cursor, limit, q, kind, visibility, tags, collectionIds, removeCollectionIds, sourceIds, confirm, pathPrefix, includeGlobs, onFileRemoved, ruleId })
41663
41702
  );
41664
41703
  } catch (err) {
41665
41704
  return knowledgeError(err);
@@ -42097,15 +42136,26 @@ server.registerTool(
42097
42136
  {
42098
42137
  description: "Ingest ONE file from the user's own computer into their Knowledge Base (Pro+). Pass a LOCAL PATH \u2014 this server runs on their machine and streams the file to the hub itself, so the file contents never pass through the conversation. Handles documents (pdf, md, txt), business data (csv, tsv, xls, xlsx, json), audio and video (transcribed), and images. Word/PowerPoint/RTF/EPUB files are NOT parseable yet \u2014 the tool refuses them with the workaround (export to PDF, or save as .md/.txt). Max 100 MB per file; bigger media goes through the hub's Knowledge \u2192 Sources page. INGESTING COSTS MONEY and counts against the monthly allowance, so ALWAYS get explicit approval for the specific file(s) first and say what it will consume. For several files, call once per file and report progress \u2014 do not loop silently. Returns a job with its live status; poll awesomate_knowledge_sources {action:'jobs'} until it succeeds (a duplicate upload replays the earlier job \u2014 a response already reading succeeded needs no polling), then probe the content with awesomate_knowledge_ask before building anything on it. A pack_required response means the allowance is exhausted: nothing was ingested and nothing was purchased.",
42099
42138
  inputSchema: {
42100
- path: external_exports.string().min(1).max(4096).describe("Path to the file on the user's machine (~ is expanded)"),
42101
- title: external_exports.string().max(300).optional().describe("Display title; defaults to the filename"),
42139
+ path: external_exports.string().min(1).max(4096).optional().describe("Path to the file on the user's machine (~ is expanded). Omit when using fromFilesPath."),
42140
+ fromFilesPath: external_exports.string().max(1024).optional().describe("Instead of a local file: a path in the account's File Manager (their n8n file system), relative to files/, e.g. 'public/reports/weekly.md'. The hub streams it from their storage \u2014 nothing leaves this machine."),
42141
+ title: external_exports.string().max(300).optional().describe("Display title; defaults to the filename (local uploads only)"),
42102
42142
  visibility: external_exports.enum(["private", "internal", "public"]).optional().describe("Who may retrieve this source (default private). public = customers and public chatbots; irreversible once fetched \u2014 ask first."),
42103
42143
  tags: external_exports.array(external_exports.string().max(64)).max(64).optional().describe("Free-form tags for the new source"),
42104
42144
  collectionIds: external_exports.array(external_exports.string().max(64)).max(16).optional().describe("Put the new source in these collections (awesomate_knowledge_collections list)")
42105
42145
  }
42106
42146
  },
42107
- async ({ path: rawPath, title, visibility, tags, collectionIds }) => {
42147
+ async ({ path: rawPath, fromFilesPath, title, visibility, tags, collectionIds }) => {
42108
42148
  try {
42149
+ if (fromFilesPath) {
42150
+ const out = await hubPost(requireConfig(), "/api/knowledge/sources/from-files", {
42151
+ path: fromFilesPath,
42152
+ ...visibility ? { visibility } : {},
42153
+ ...tags?.length ? { tags } : {},
42154
+ ...collectionIds?.length ? { collection_ids: collectionIds } : {}
42155
+ });
42156
+ return knowledgeResult({ ...out, note: 'Queued from the File Manager. Poll awesomate_knowledge_sources {action:"jobs"} until succeeded, then verify with a search for a phrase from the file.' });
42157
+ }
42158
+ if (!rawPath) return errorResult(new Error("Give either path (a local file) or fromFilesPath (a File Manager path)."));
42109
42159
  const { statSync, existsSync: existsSync3 } = await import("node:fs");
42110
42160
  const { resolve: resolve2, basename, extname } = await import("node:path");
42111
42161
  const expanded = rawPath.startsWith("~") ? join3(homedir3(), rawPath.slice(1).replace(/^[/\\]/, "")) : rawPath;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awesomate/hosting-mcp",
3
- "version": "0.21.0",
3
+ "version": "0.22.1",
4
4
  "description": "Awesomate MCP server — lets Claude manage your Awesomate WordPress hosting, plan, limits, n8n automations, and build Node/static apps + databases",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -89,6 +89,21 @@ proxy failures, resuming a connect, support report) ·
89
89
  `references/rest-fallback.md` (no `awesomate_*` tools — serve over REST) ·
90
90
  `references/voice.md` (how to talk to a non-technical owner).
91
91
 
92
+ ## After creating a site
93
+
94
+ Creation is queued, not instant, and the certificate lags the install. For a
95
+ minute or two afterwards `https://` refuses the connection while `http://`
96
+ already returns real WordPress HTML — and `sslActive` in the site list may
97
+ already read `true`, because it reports intent rather than the live cert.
98
+
99
+ That combination is the expected intermediate state, not a contradiction and
100
+ not a failed build. Confirm over plain HTTP, say the certificate is still
101
+ issuing, and poll until HTTPS answers. Never delete and recreate the site or
102
+ start debugging DNS on the strength of it.
103
+
104
+ New sites come up on the host's default theme (`hello-elementor`). A fresh
105
+ install is not evidence that a custom theme shipped.
106
+
92
107
  ## 0a. Tool map — reach for it when
93
108
 
94
109
  | Tool | Reach for it when |
@@ -100,7 +115,7 @@ proxy failures, resuming a connect, support report) ·
100
115
  | `awesomate_get_hosting_status` | Is hosting provisioned at all — eligible/provisioned flags, in-progress step, primary domain, DNS targets. Check before suggesting any site action |
101
116
  | `awesomate_get_hosting_account` | cPanel account details: package, server, provisioned-at, masked username |
102
117
  | `awesomate_list_sites` | What WordPress sites exist on this account |
103
- | `awesomate_site_create` | Spin up a new WordPress site (check limits first) |
118
+ | `awesomate_site_create` | Spin up a new WordPress site (check limits first). Async — poll `awesomate_list_sites` for the name, and expect HTTPS to fail for a minute or two while AutoSSL issues; see **After creating a site** below |
104
119
  | `awesomate_list_domains` | What domains are attached, and to which site |
105
120
  | `awesomate_domain_add` | Attach a custom domain to a site |
106
121
  | `awesomate_dns_check` | Live answer to "where does this domain actually point" — before and after any DNS change |
@@ -26,5 +26,50 @@ GET with `Authorization: Bearer <pat>`:
26
26
  | `awesomate_list_domains` | `GET {apiBase}/api/client-hosting/domains` |
27
27
  | `awesomate_get_limits` | `GET {apiBase}/api/client-hosting/limits` |
28
28
 
29
+ ## Creating a site over REST
30
+
31
+ `POST {apiBase}/api/client-hosting/sites` (JSON, same bearer) is the fallback
32
+ for `awesomate_site_create`. Three things about it trip people up:
33
+
34
+ - **The body field is `siteTitle`, not `title`.** An unknown key is ignored
35
+ silently, so the site ends up titled after its domain. `domain`, `themeSlug`,
36
+ `adminEmail` and `intent` are the other accepted keys; all are optional.
37
+ - **Omit `domain` and the server picks the next free `siteN.{primary}`** —
38
+ the first install takes the bare primary domain, then `site2`, `site3`, and
39
+ so on. Pass `domain` explicitly if the user named one.
40
+ - **The response is bare `{"success":true}`** — no domain, no id. It does not
41
+ tell you what got built, so read the name back from `GET /sites`.
42
+
43
+ Refusals are specific and worth relaying verbatim: `403` means the plan's site
44
+ cap is reached (staging clones are excluded from the count), `409` means
45
+ WordPress already exists at that domain.
46
+
47
+ ## Provisioning is async — do not call it broken
48
+
49
+ The POST returns as soon as the install is queued. Poll
50
+ `GET {apiBase}/api/client-hosting/sites` until the new hostname appears rather
51
+ than assuming the call failed.
52
+
53
+ Then expect a **gap of a minute or two where HTTPS does not answer at all**
54
+ while AutoSSL issues the certificate. During that window the symptom looks
55
+ alarming but is normal and self-resolving:
56
+
57
+ | Check | During issuance | Meaning |
58
+ |---|---|---|
59
+ | `curl https://…` | `000` / connection failed | Cert not issued **yet** |
60
+ | `curl http://…` | `200`, real WordPress HTML | The site is genuinely built |
61
+ | `dig +short` | resolves to the hosting IP | DNS is fine |
62
+ | `/sites` `sslActive` | may already say `true` | Reports intent, not the live cert |
63
+
64
+ So: an HTTPS failure straight after creation is not a failed build. Confirm
65
+ over plain HTTP, tell the user the certificate is still issuing, and poll
66
+ HTTPS until it answers. Do not delete and recreate the site, and do not go
67
+ hunting through DNS or Cloudflare — `sslActive: true` alongside a dead HTTPS
68
+ port is the expected intermediate state, not a contradiction.
69
+
70
+ New sites come up on the host's default theme (currently
71
+ `hello-elementor`). If the user asked for a custom theme, that is still ahead
72
+ of you — the fresh install is not evidence their theme shipped.
73
+
29
74
  Serve the request with these now, then suggest restarting Claude Code at a
30
75
  natural break so the richer MCP integration loads.
@@ -60,6 +60,7 @@ platform's answer beats anything you remember.
60
60
  | Enable the knowledge base (idempotent; 202 = keep polling status) | `awesomate_knowledge_provision` |
61
61
  | The LIBRARY: search by title/tag/collection/visibility, list, add url·sitemap (with visibility, tags, collections), tag, set visibility (public needs the slug typed), move between collections, remove, ingest jobs | `awesomate_knowledge_sources` |
62
62
  | Collections — named sets of sources a chatbot is scoped to: list / create / update / delete / add / remove members | `awesomate_knowledge_collections` |
63
+ | Keep a File Manager folder (their n8n file system) synced into the knowledge base: `sync_rules` / `sync_rule_create` / `sync_rule_delete` / `sync_run` / `sync_state` on the sources tool; one file at a time via `awesomate_knowledge_upload {fromFilesPath}` | `awesomate_knowledge_sources`, `awesomate_knowledge_upload` |
63
64
  | Instant search with facet counts (find WHAT is in there: moments, pages, datasets; free) | `awesomate_knowledge_search` |
64
65
  | Ask a question → verified answer + numbered sources (accepts the same facet `filters` to ask within a slice) | `awesomate_knowledge_ask` |
65
66
  | Agent persona / fallback message / model tier — get & set (the single workspace default) | `awesomate_knowledge_agent` |
@@ -37,6 +37,15 @@ contents never pass through the conversation and a 100 MB PDF costs no context.
37
37
  - A folder path is rejected on purpose — list it, agree the files, then upload.
38
38
  - Public URLs and whole sitemaps stay on `awesomate_knowledge_sources`
39
39
  `{action:'add'}`. Use that for their website; use upload for their disk.
40
+ - **Files already in their File Manager** (the n8n file system at hub
41
+ Files — `public/`, `private/`, `temp/`, where their workflows write
42
+ outputs): `awesomate_knowledge_upload {fromFilesPath: 'public/reports/x.md'}`
43
+ for one file, or a folder rule with `awesomate_knowledge_sources
44
+ {action:'sync_rule_create', pathPrefix, visibility, collectionIds}` so
45
+ every file a workflow drops there is ingested automatically (uploads sync
46
+ at once, everything else on a 15-minute sweep). A `public/` folder being
47
+ web-served does NOT make its knowledge sources public — say which
48
+ visibility the rule should use, default private.
40
49
  - **What parses:** pdf, md, txt (documents); csv, tsv, xls, xlsx, json (business data);
41
50
  audio, video, images. Word/PowerPoint/RTF/EPUB/HTML files are refused with a
42
51
  workaround — export to PDF or save as .md/.txt; a web page goes in by URL.