@awesomate/hosting-mcp 0.67.0 → 0.68.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/dist/index.js CHANGED
@@ -40110,6 +40110,227 @@ function hubPatch(config3, path, jsonBody = {}) {
40110
40110
  function hubDelete(config3, path, jsonBody) {
40111
40111
  return hubRequest(config3, "DELETE", path, jsonBody);
40112
40112
  }
40113
+ function serverMessage(body, status, fallback) {
40114
+ const msg = body && typeof body === "object" && "message" in body && typeof body.message === "string" ? body.message : body && typeof body === "object" && "error" in body && typeof body.error === "string" ? body.error : `${fallback} (${status})`;
40115
+ return /[.!?]$/.test(msg) ? msg : `${msg}.`;
40116
+ }
40117
+ async function readBody(res) {
40118
+ const text = await res.text();
40119
+ try {
40120
+ return text ? JSON.parse(text) : null;
40121
+ } catch {
40122
+ return text;
40123
+ }
40124
+ }
40125
+ async function hubUploadRaw(config3, path, file) {
40126
+ const { openAsBlob } = await import("node:fs");
40127
+ const blob = typeof openAsBlob === "function" ? await openAsBlob(file.localPath, { type: file.contentType ?? "application/octet-stream" }) : new Blob([await (await import("node:fs/promises")).readFile(file.localPath)], {
40128
+ type: file.contentType ?? "application/octet-stream"
40129
+ });
40130
+ const controller = new AbortController();
40131
+ const timer = setTimeout(() => controller.abort(), 6e5);
40132
+ let res;
40133
+ try {
40134
+ res = await (0, import_undici.fetch)(`${config3.apiBase}${path}`, {
40135
+ method: "POST",
40136
+ headers: {
40137
+ Authorization: `Bearer ${config3.pat}`,
40138
+ "Content-Type": blob.type || "application/octet-stream",
40139
+ "Content-Length": String(blob.size)
40140
+ },
40141
+ body: blob,
40142
+ signal: controller.signal,
40143
+ dispatcher: proxyDispatcher
40144
+ });
40145
+ } catch (err) {
40146
+ const aborted2 = err instanceof Error && err.name === "AbortError";
40147
+ throw new HubApiError(
40148
+ aborted2 ? "The upload timed out after 10 minutes. Try a smaller file, or upload it in the hub at Knowledge, Files." : `Could not reach the Awesomate hub at ${config3.apiBase} to upload. This is a connectivity issue, not an auth problem.`,
40149
+ 0,
40150
+ null
40151
+ );
40152
+ } finally {
40153
+ clearTimeout(timer);
40154
+ }
40155
+ const body = await readBody(res);
40156
+ if (!res.ok) throw new HubApiError(serverMessage(body, res.status, "Upload failed"), res.status, body);
40157
+ return body;
40158
+ }
40159
+ async function hubDownloadToFile(config3, path, localPath) {
40160
+ const controller = new AbortController();
40161
+ const timer = setTimeout(() => controller.abort(), 6e5);
40162
+ try {
40163
+ let res;
40164
+ try {
40165
+ res = await (0, import_undici.fetch)(`${config3.apiBase}${path}`, {
40166
+ method: "GET",
40167
+ headers: { Authorization: `Bearer ${config3.pat}` },
40168
+ signal: controller.signal,
40169
+ dispatcher: proxyDispatcher
40170
+ });
40171
+ } catch (err) {
40172
+ const aborted2 = err instanceof Error && err.name === "AbortError";
40173
+ throw new HubApiError(
40174
+ aborted2 ? "The download timed out after 10 minutes." : `Could not reach the Awesomate hub at ${config3.apiBase} to download. This is a connectivity issue, not an auth problem.`,
40175
+ 0,
40176
+ null
40177
+ );
40178
+ }
40179
+ if (!res.ok || !res.body) {
40180
+ const body = await readBody(res);
40181
+ throw new HubApiError(serverMessage(body, res.status, "Download failed"), res.status, body);
40182
+ }
40183
+ const { createWriteStream } = await import("node:fs");
40184
+ const { rename, rm } = await import("node:fs/promises");
40185
+ const { Readable } = await import("node:stream");
40186
+ const { pipeline } = await import("node:stream/promises");
40187
+ const tmp = `${localPath}.part-${process.pid}-${Date.now()}`;
40188
+ let bytes = 0;
40189
+ const counted = Readable.fromWeb(res.body);
40190
+ counted.on("data", (chunk) => {
40191
+ bytes += chunk.length;
40192
+ });
40193
+ try {
40194
+ await pipeline(counted, createWriteStream(tmp, { flags: "wx" }));
40195
+ await rename(tmp, localPath);
40196
+ } catch (err) {
40197
+ await rm(tmp, { force: true });
40198
+ throw err;
40199
+ }
40200
+ return bytes;
40201
+ } finally {
40202
+ clearTimeout(timer);
40203
+ }
40204
+ }
40205
+
40206
+ // src/files.ts
40207
+ var FILES_ACTIONS = [
40208
+ "status",
40209
+ "list",
40210
+ "search",
40211
+ "download",
40212
+ "upload",
40213
+ "mkdir",
40214
+ "move",
40215
+ "make_public",
40216
+ "make_private",
40217
+ "delete"
40218
+ ];
40219
+ var FILES_MAX_UPLOAD_BYTES = 50 * 1024 * 1024;
40220
+ var TOP = ["public", "private", "temp"];
40221
+ function filesPath(raw, what = "path") {
40222
+ const p = String(raw ?? "").trim().replace(/^\/+/, "").replace(/^files\//, "");
40223
+ const top = p.split("/")[0];
40224
+ if (!p || !TOP.includes(top)) {
40225
+ throw new Error(`${what} must start with public/, private/ or temp/ (got "${raw ?? ""}"). Paths are relative to the account's files folder.`);
40226
+ }
40227
+ return p;
40228
+ }
40229
+ function swapTopFolder(path, to) {
40230
+ const rest = path.split("/").slice(1).join("/");
40231
+ if (!rest) throw new Error(`"${path}" is a top folder. Name the file or folder inside it.`);
40232
+ return `${to}/${rest}`;
40233
+ }
40234
+ function uploadTarget(dest, localName) {
40235
+ const d = filesPath(dest, "path");
40236
+ if (d.endsWith("/") || TOP.includes(d)) return `${d.replace(/\/$/, "")}/${localName}`;
40237
+ return d;
40238
+ }
40239
+ var q = (params) => Object.entries(params).filter(([, v]) => v !== void 0 && v !== "").map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
40240
+ var PUBLIC_NOTE = "A file in public/ can be read by anyone who has its public_url, with no login. Files in private/ and temp/ are never served publicly.";
40241
+ async function filesAction(config3, args) {
40242
+ switch (args.action) {
40243
+ case "status": {
40244
+ const caps = await hubGet(config3, "/api/files/capabilities");
40245
+ if (!caps.enabled) {
40246
+ const why = !caps.planEligible ? "Files comes with Support Plus, Pro and Embedded. Relay that honestly (hub.awesomate.ai/billing) and let the user decide." : !caps.variantEligible ? "Files needs a one-time switch of their automation account, which the owner starts in the hub at Knowledge, Files (a short restart). Never start it for them." : `The owner has not switched on "Open your automation account's file folders" in Settings, Privacy. Only they can switch it on; never do it for them.`;
40247
+ return { ...caps, note: why };
40248
+ }
40249
+ const usage = await hubGet(config3, "/api/files/usage");
40250
+ return { ...caps, usage, note: PUBLIC_NOTE };
40251
+ }
40252
+ case "list": {
40253
+ const path = args.path ? filesPath(args.path) : "";
40254
+ return { ...await hubGet(config3, `/api/files?${q({ path })}`), note: PUBLIC_NOTE };
40255
+ }
40256
+ case "search": {
40257
+ const out = await hubGet(
40258
+ config3,
40259
+ `/api/files/search?${q({ q: args.q ?? "", top: args.top, exts: args.exts?.join(",") })}`
40260
+ );
40261
+ return out.truncated ? { ...out, note: "Only the first 200 matches came back. Narrow the search (a longer name, a top folder or file types) before saying something is not there." } : out;
40262
+ }
40263
+ case "download": {
40264
+ const path = filesPath(args.path);
40265
+ const { resolve: resolve3, basename, join: join4 } = await import("node:path");
40266
+ const { existsSync: existsSync3, statSync } = await import("node:fs");
40267
+ const { homedir: homedir4 } = await import("node:os");
40268
+ const name = basename(path);
40269
+ const raw = args.to ? args.to.startsWith("~") ? join4(homedir4(), args.to.slice(1)) : args.to : name;
40270
+ let target = resolve3(raw);
40271
+ if (existsSync3(target) && statSync(target).isDirectory()) target = join4(target, name);
40272
+ if (existsSync3(target) && !args.overwrite) {
40273
+ throw new Error(`${target} already exists on this computer. Ask the user, then pass overwrite: true or a different "to".`);
40274
+ }
40275
+ const bytes = await hubDownloadToFile(config3, `/api/files/stream?${q({ path, disposition: "attachment" })}`, target);
40276
+ return { downloaded: path, savedTo: target, bytes };
40277
+ }
40278
+ case "upload": {
40279
+ if (!args.localPath) throw new Error('upload needs localPath (the file on this computer) and path (where it goes, e.g. "public/images/").');
40280
+ const { resolve: resolve3, basename, join: join4 } = await import("node:path");
40281
+ const { existsSync: existsSync3, statSync } = await import("node:fs");
40282
+ const { homedir: homedir4 } = await import("node:os");
40283
+ const abs = resolve3(args.localPath.startsWith("~") ? join4(homedir4(), args.localPath.slice(1)) : args.localPath);
40284
+ if (!existsSync3(abs)) throw new Error(`No file at ${abs}. Prefer an absolute path: this server's working directory may differ from the user's shell.`);
40285
+ const st = statSync(abs);
40286
+ if (!st.isFile()) throw new Error(`${abs} is not a file. Upload one file per call.`);
40287
+ if (st.size > FILES_MAX_UPLOAD_BYTES) {
40288
+ throw new Error(`${basename(abs)} is ${(st.size / 1024 / 1024).toFixed(1)} MB, over the 50 MB limit for one upload. Compress or split it.`);
40289
+ }
40290
+ const target = uploadTarget(args.path ?? "", basename(abs));
40291
+ const out = await hubUploadRaw(
40292
+ config3,
40293
+ `/api/files?${q({ path: target, overwrite: args.overwrite ? "1" : void 0 })}`,
40294
+ { localPath: abs }
40295
+ );
40296
+ return target.startsWith("public/") ? { ...out, note: PUBLIC_NOTE } : out;
40297
+ }
40298
+ case "mkdir":
40299
+ return hubPost(config3, "/api/files/mkdir", { path: filesPath(args.path) });
40300
+ case "move": {
40301
+ const from = filesPath(args.path, "path");
40302
+ const to = filesPath(args.to, "to");
40303
+ if (to.startsWith("public/") && !from.startsWith("public/") && !args.confirm) {
40304
+ throw new Error(`Moving into public/ makes the file readable by anyone with its address. Ask the user first, then call again with confirm: true.`);
40305
+ }
40306
+ return hubPost(config3, "/api/files/move", { from, to });
40307
+ }
40308
+ case "make_public": {
40309
+ const from = filesPath(args.path);
40310
+ if (from.startsWith("public/")) throw new Error(`${from} is already public.`);
40311
+ if (!args.confirm) {
40312
+ throw new Error("Making a file public means anyone with its address can read it, with no login. Ask the user first, then call again with confirm: true.");
40313
+ }
40314
+ const out = await hubPost(config3, "/api/files/move", { from, to: swapTopFolder(from, "public") });
40315
+ return { ...out, note: `Moved from ${from}. Any workflow or page that used the old path needs the new one.` };
40316
+ }
40317
+ case "make_private": {
40318
+ const from = filesPath(args.path);
40319
+ if (!from.startsWith("public/")) throw new Error(`${from} is not in public/, so it is already private.`);
40320
+ const out = await hubPost(config3, "/api/files/move", { from, to: swapTopFolder(from, "private") });
40321
+ return {
40322
+ ...out,
40323
+ note: `Moved from ${from}. Its public address no longer serves it, but Cloudflare can keep showing the last copy for up to 4 hours, and a copy someone already downloaded is not recalled. Any workflow, page or app that used it needs changing.`
40324
+ };
40325
+ }
40326
+ case "delete": {
40327
+ const path = filesPath(args.path);
40328
+ if (!args.confirm) throw new Error(`Delete moves ${path} to the trash. Ask the user first, then call again with confirm: true.`);
40329
+ const out = await hubDelete(config3, `/api/files?${q({ path })}`);
40330
+ return { ...out, note: "Moved to the trash folder (trashPath), not erased, and no longer counted toward the Files allowance." };
40331
+ }
40332
+ }
40333
+ }
40113
40334
 
40114
40335
  // src/skills.ts
40115
40336
  import { cpSync, existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "node:fs";
@@ -40346,10 +40567,10 @@ function describeSourcePage(page) {
40346
40567
  async function knowledgeSources(config3, args) {
40347
40568
  try {
40348
40569
  if (args.action === "list") {
40349
- const q = new URLSearchParams();
40350
- if (args.cursor) q.set("cursor", args.cursor);
40351
- if (args.limit !== void 0) q.set("limit", String(args.limit));
40352
- const qs = q.toString();
40570
+ const q2 = new URLSearchParams();
40571
+ if (args.cursor) q2.set("cursor", args.cursor);
40572
+ if (args.limit !== void 0) q2.set("limit", String(args.limit));
40573
+ const qs = q2.toString();
40353
40574
  const page = await hubGet(config3, `/api/knowledge/sources${qs ? `?${qs}` : ""}`);
40354
40575
  return describeSourcePage(page);
40355
40576
  }
@@ -40358,11 +40579,11 @@ async function knowledgeSources(config3, args) {
40358
40579
  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." };
40359
40580
  }
40360
40581
  if (args.action === "sync_state") {
40361
- const q = new URLSearchParams();
40362
- if (args.ruleId !== void 0) q.set("rule_id", String(args.ruleId));
40363
- if (args.status) q.set("status", args.status);
40364
- if (args.limit !== void 0) q.set("limit", String(args.limit));
40365
- const qs = q.toString();
40582
+ const q2 = new URLSearchParams();
40583
+ if (args.ruleId !== void 0) q2.set("rule_id", String(args.ruleId));
40584
+ if (args.status) q2.set("status", args.status);
40585
+ if (args.limit !== void 0) q2.set("limit", String(args.limit));
40586
+ const qs = q2.toString();
40366
40587
  return await hubGet(config3, `/api/knowledge/files-sync/state${qs ? `?${qs}` : ""}`);
40367
40588
  }
40368
40589
  if (args.action === "sync_rule_create") {
@@ -40389,15 +40610,15 @@ async function knowledgeSources(config3, args) {
40389
40610
  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.' };
40390
40611
  }
40391
40612
  if (args.action === "search") {
40392
- const q = new URLSearchParams();
40393
- if (args.q) q.set("q", args.q);
40394
- if (args.kind) q.set("kind", args.kind);
40395
- if (args.visibility) q.set("visibility", args.visibility);
40396
- for (const t of args.tags ?? []) q.append("tag", t);
40397
- for (const c of args.collectionIds ?? []) q.append("collection", c);
40398
- if (args.cursor) q.set("cursor", args.cursor);
40399
- if (args.limit !== void 0) q.set("limit", String(args.limit));
40400
- const qs = q.toString();
40613
+ const q2 = new URLSearchParams();
40614
+ if (args.q) q2.set("q", args.q);
40615
+ if (args.kind) q2.set("kind", args.kind);
40616
+ if (args.visibility) q2.set("visibility", args.visibility);
40617
+ for (const t of args.tags ?? []) q2.append("tag", t);
40618
+ for (const c of args.collectionIds ?? []) q2.append("collection", c);
40619
+ if (args.cursor) q2.set("cursor", args.cursor);
40620
+ if (args.limit !== void 0) q2.set("limit", String(args.limit));
40621
+ const qs = q2.toString();
40401
40622
  const page = await hubGet(config3, `/api/knowledge/library${qs ? `?${qs}` : ""}`);
40402
40623
  return { ...describeSourcePage(page), note_visibility: "Each source carries visibility (private|internal|public \u2014 who may retrieve it; enforced) and collections. Nothing marked public = a public chatbot answers nothing." };
40403
40624
  }
@@ -40457,8 +40678,8 @@ async function knowledgeSources(config3, args) {
40457
40678
  }
40458
40679
  }
40459
40680
  if (args.action === "jobs") {
40460
- const q = args.status ? `?status=${encodeURIComponent(args.status)}` : "";
40461
- return await hubGet(config3, `/api/knowledge/jobs${q}`);
40681
+ const q2 = args.status ? `?status=${encodeURIComponent(args.status)}` : "";
40682
+ return await hubGet(config3, `/api/knowledge/jobs${q2}`);
40462
40683
  }
40463
40684
  if (args.action === "remove") {
40464
40685
  if (!args.sourceId) throw new Error("sourceId is required for remove");
@@ -40622,10 +40843,10 @@ async function knowledgePeople(config3, args) {
40622
40843
  const probe = await hubGet(config3, "/api/knowledge/entities");
40623
40844
  if (!probe.available) return entityLayerUnavailablePayload();
40624
40845
  if (args.action === "list") {
40625
- const q = new URLSearchParams();
40626
- if (args.status) q.set("status", args.status);
40627
- if (args.cursor) q.set("cursor", args.cursor);
40628
- const qs = q.toString();
40846
+ const q2 = new URLSearchParams();
40847
+ if (args.status) q2.set("status", args.status);
40848
+ if (args.cursor) q2.set("cursor", args.cursor);
40849
+ const qs = q2.toString();
40629
40850
  const page = await hubGet(config3, `/api/knowledge/people${qs ? `?${qs}` : ""}`);
40630
40851
  return {
40631
40852
  ...page,
@@ -40636,10 +40857,10 @@ async function knowledgePeople(config3, args) {
40636
40857
  };
40637
40858
  }
40638
40859
  if (args.action === "aliases") {
40639
- const q = new URLSearchParams({ status: "pending" });
40640
- if (args.kind) q.set("kind", args.kind);
40641
- if (args.cursor) q.set("cursor", args.cursor);
40642
- const page = await hubGet(config3, `/api/knowledge/entities/aliases?${q.toString()}`);
40860
+ const q2 = new URLSearchParams({ status: "pending" });
40861
+ if (args.kind) q2.set("kind", args.kind);
40862
+ if (args.cursor) q2.set("cursor", args.cursor);
40863
+ const page = await hubGet(config3, `/api/knowledge/entities/aliases?${q2.toString()}`);
40643
40864
  return {
40644
40865
  ...page,
40645
40866
  note: "Each suggestion is a text name the content uses that probably refers to a known entity. Show the user the alias, the suggested match and the confidence, then use decide with the (kind, alias_norm) pair: only after they explicitly say accept, reject, or create."
@@ -40763,9 +40984,9 @@ function plainSnippet(text) {
40763
40984
  }
40764
40985
  async function knowledgeSearch(config3, args) {
40765
40986
  try {
40766
- const q = new URLSearchParams();
40767
- if (args.q) q.set("q", args.q);
40768
- if (args.kind) q.set("kind", args.kind);
40987
+ const q2 = new URLSearchParams();
40988
+ if (args.q) q2.set("q", args.q);
40989
+ if (args.kind) q2.set("kind", args.kind);
40769
40990
  for (const [name, values] of [
40770
40991
  ["topic", args.topic],
40771
40992
  ["person", args.person],
@@ -40775,12 +40996,12 @@ async function knowledgeSearch(config3, args) {
40775
40996
  ["year", args.year],
40776
40997
  ["doc", args.doc]
40777
40998
  ]) {
40778
- for (const v of values ?? []) q.append(name, v);
40999
+ for (const v of values ?? []) q2.append(name, v);
40779
41000
  }
40780
- if (args.limit !== void 0) q.set("limit", String(args.limit));
40781
- if (args.offset !== void 0) q.set("offset", String(args.offset));
40782
- if (args.include_media) q.set("include", "media");
40783
- const qs = q.toString();
41001
+ if (args.limit !== void 0) q2.set("limit", String(args.limit));
41002
+ if (args.offset !== void 0) q2.set("offset", String(args.offset));
41003
+ if (args.include_media) q2.set("include", "media");
41004
+ const qs = q2.toString();
40784
41005
  const result = await hubGet(config3, `/api/knowledge/explore${qs ? `?${qs}` : ""}`);
40785
41006
  const hits = Array.isArray(result.hits) ? result.hits.map((h) => ({
40786
41007
  ...h,
@@ -41336,6 +41557,7 @@ var TITLES = {
41336
41557
  awesomate_knowledge_ask: "Ask the Knowledge Base",
41337
41558
  awesomate_knowledge_sources: "Manage knowledge sources",
41338
41559
  awesomate_knowledge_upload: "Upload file to Knowledge Base",
41560
+ awesomate_files: "Manage Files (public and private file folders)",
41339
41561
  awesomate_knowledge_agent: "Configure knowledge agent",
41340
41562
  awesomate_knowledge_agents: "Manage knowledge agents",
41341
41563
  awesomate_knowledge_collections: "Manage knowledge collections",
@@ -41534,8 +41756,8 @@ server.registerTool(
41534
41756
  return textResult(await hubGet(cfg, "/api/my-n8n/machine/agents/status"));
41535
41757
  }
41536
41758
  if (action === "describe") {
41537
- const q = tool ? `?name=${encodeURIComponent(tool)}` : "";
41538
- return textResult(await hubGet(cfg, `/api/my-n8n/machine/agents/tools${q}`));
41759
+ const q2 = tool ? `?name=${encodeURIComponent(tool)}` : "";
41760
+ return textResult(await hubGet(cfg, `/api/my-n8n/machine/agents/tools${q2}`));
41539
41761
  }
41540
41762
  if (action === "result") {
41541
41763
  if (!call_id) return errorResult(new Error('call_id is required for action "result"'));
@@ -41610,10 +41832,10 @@ server.registerTool(
41610
41832
  if (action === "mine") return textResult(await hubGet(cfg, "/api/bookings"));
41611
41833
  if (!session_type) return errorResult(new Error(`session_type is required for ${action}`));
41612
41834
  if (action === "slots") {
41613
- const q = new URLSearchParams({ session_type });
41614
- if (from) q.set("from", String(Date.parse(from)));
41615
- if (to) q.set("to", String(Date.parse(to)));
41616
- return textResult(await hubGet(cfg, `/api/bookings/slots?${q.toString()}`));
41835
+ const q2 = new URLSearchParams({ session_type });
41836
+ if (from) q2.set("from", String(Date.parse(from)));
41837
+ if (to) q2.set("to", String(Date.parse(to)));
41838
+ return textResult(await hubGet(cfg, `/api/bookings/slots?${q2.toString()}`));
41617
41839
  }
41618
41840
  if (!starts_at || !host_team_member_id) return errorResult(new Error("starts_at and host_team_member_id are required for book"));
41619
41841
  return textResult(
@@ -41664,8 +41886,8 @@ server.registerTool(
41664
41886
  if (active) qs.set("active", active);
41665
41887
  if (limit) qs.set("limit", String(limit));
41666
41888
  if (offset) qs.set("offset", String(offset));
41667
- const q = qs.toString();
41668
- return textResult(await hubGet(requireConfig(), `/api/my-n8n/machine/workflows${q ? `?${q}` : ""}`));
41889
+ const q2 = qs.toString();
41890
+ return textResult(await hubGet(requireConfig(), `/api/my-n8n/machine/workflows${q2 ? `?${q2}` : ""}`));
41669
41891
  }
41670
41892
  const full = await hubGet(requireConfig(), `/api/my-n8n/workflows/${encodeURIComponent(id)}`);
41671
41893
  if (detail === "structure" && full?.workflow?.nodes) {
@@ -41696,8 +41918,8 @@ server.registerTool(
41696
41918
  const cfg = requireConfig();
41697
41919
  if (what === "datatable_rows") {
41698
41920
  if (!datatableId) return errorResult(new Error("datatableId is required for datatable_rows"));
41699
- const q = limit ? `?limit=${limit}` : "";
41700
- return textResult(await hubGet(cfg, `/api/my-n8n/machine/datatables/${encodeURIComponent(datatableId)}/rows${q}`));
41921
+ const q2 = limit ? `?limit=${limit}` : "";
41922
+ return textResult(await hubGet(cfg, `/api/my-n8n/machine/datatables/${encodeURIComponent(datatableId)}/rows${q2}`));
41701
41923
  }
41702
41924
  const path = {
41703
41925
  nodes: "/api/my-n8n/machine/nodes",
@@ -41850,19 +42072,19 @@ server.registerTool(
41850
42072
  )
41851
42073
  }
41852
42074
  },
41853
- async ({ action, q, subject, message, category }) => {
42075
+ async ({ action, q: q2, subject, message, category }) => {
41854
42076
  try {
41855
42077
  const cfg = requireConfig();
41856
42078
  if (action === "ask") {
41857
- if (!q) return errorResult(new Error("ask needs q"));
41858
- return textResult(await hubGet(cfg, `/api/help/ask?q=${encodeURIComponent(q)}`));
42079
+ if (!q2) return errorResult(new Error("ask needs q"));
42080
+ return textResult(await hubGet(cfg, `/api/help/ask?q=${encodeURIComponent(q2)}`));
41859
42081
  }
41860
42082
  if (action === "faq") {
41861
- return textResult(await hubGet(cfg, `/api/help/faq${q ? `?q=${encodeURIComponent(q)}` : ""}`));
42083
+ return textResult(await hubGet(cfg, `/api/help/faq${q2 ? `?q=${encodeURIComponent(q2)}` : ""}`));
41862
42084
  }
41863
42085
  if (action === "help_docs") {
41864
- if (!q) return errorResult(new Error("help_docs needs q"));
41865
- return textResult(await hubGet(cfg, `/api/help/docs?q=${encodeURIComponent(q)}`));
42086
+ if (!q2) return errorResult(new Error("help_docs needs q"));
42087
+ return textResult(await hubGet(cfg, `/api/help/docs?q=${encodeURIComponent(q2)}`));
41866
42088
  }
41867
42089
  if (action === "list_tickets") {
41868
42090
  return textResult(await hubGet(cfg, "/api/support/tickets"));
@@ -42564,10 +42786,10 @@ server.registerTool(
42564
42786
  limit: external_exports.number().int().min(1).max(200).optional().describe("list only: page size (default 50, max 200)")
42565
42787
  }
42566
42788
  },
42567
- async ({ action, url, sitemap, since, sourceId, status, cursor, limit, q, kind, visibility, tags, collectionIds, removeCollectionIds, sourceIds, confirm, pathPrefix, includeGlobs, onFileRemoved, ruleId }) => {
42789
+ async ({ action, url, sitemap, since, sourceId, status, cursor, limit, q: q2, kind, visibility, tags, collectionIds, removeCollectionIds, sourceIds, confirm, pathPrefix, includeGlobs, onFileRemoved, ruleId }) => {
42568
42790
  try {
42569
42791
  return knowledgeResult(
42570
- await knowledgeSources(requireConfig(), { action, url, sitemap, since, sourceId, status, cursor, limit, q, kind, visibility, tags, collectionIds, removeCollectionIds, sourceIds, confirm, pathPrefix, includeGlobs, onFileRemoved, ruleId })
42792
+ await knowledgeSources(requireConfig(), { action, url, sitemap, since, sourceId, status, cursor, limit, q: q2, kind, visibility, tags, collectionIds, removeCollectionIds, sourceIds, confirm, pathPrefix, includeGlobs, onFileRemoved, ruleId })
42571
42793
  );
42572
42794
  } catch (err) {
42573
42795
  return knowledgeError(err);
@@ -42762,6 +42984,31 @@ server.registerTool(
42762
42984
  }
42763
42985
  }
42764
42986
  );
42987
+ server.registerTool(
42988
+ "awesomate_files",
42989
+ {
42990
+ annotations: annotate("awesomate_files", DESTRUCTIVE),
42991
+ description: `The account's Files: the folders on their automation account (n8n) that workflows read and write, and that the hub shows at Knowledge, Files. Three top folders: public/ (every file has a public_url anyone can open with no login: images for a site or email, downloads, media a workflow made), private/ (only the account, its workflows and the hub) and temp/ (scratch space). Support Plus, Pro and Embedded; the owner must also have switched on "Open your automation account's file folders" in Settings, Privacy. Paths are relative to the files folder and start with the top folder, e.g. public/images/logo.png. action 'status': whether Files is on, why not, and storage used against the plan's limit: call this first. 'list' {path?}: one folder (no path = the top folders); files in public/ carry public_url. 'search' {q?, top?, exts?}: find files by name across the folders (first 200 matches; truncated:true means narrow it). 'download' {path, to?, overwrite?}: save a file to THIS computer (to = a local file or folder, default the working directory); never overwrites without overwrite:true. 'upload' {localPath, path, overwrite?}: send a file from this computer (max 50 MB each); path is the target file, or a folder ending in / to keep the name. Upload into public/ only when the user wants it publicly reachable. 'mkdir' {path}. 'move' {path, to}: rename or move; moving into public/ needs confirm:true. 'make_public' {path, confirm:true}: moves private/ or temp/ to the same place under public/ and returns its public_url: ASK the user first, anyone with the address can read it. 'make_private' {path}: moves it out of public/, so its public address stops serving it; Cloudflare can keep showing the last copy for up to 4 hours (also after a delete or a replace of a public file), so never promise it is gone at once. 'delete' {path, confirm:true}: moves to the trash (not erased); ask first. Moving or renaming changes the path, so tell the user any workflow, page or app using the old path or address needs the new one. Contents stream between this computer and the hub: never paste a file's contents into the conversation to move it. A 507 files_storage_full means the plan's Files allowance is used: say how much, offer to find large or old files to remove, and leave any plan change to the user.`,
42992
+ inputSchema: {
42993
+ action: external_exports.enum(FILES_ACTIONS),
42994
+ path: external_exports.string().max(1024).optional().describe("A path in Files, starting public/, private/ or temp/. For upload, the target (a folder ending in / keeps the local name)."),
42995
+ to: external_exports.string().max(4096).optional().describe("move: the new Files path. download: where to save on this computer (file or folder; ~ expands)."),
42996
+ localPath: external_exports.string().max(4096).optional().describe("upload: the file on this computer (~ expands; prefer an absolute path)."),
42997
+ q: external_exports.string().max(200).optional().describe("search: part of a file name (* and ? work as wildcards)."),
42998
+ top: external_exports.enum(["public", "private", "temp"]).optional().describe("search: only this top folder."),
42999
+ exts: external_exports.array(external_exports.string().regex(/^\.[a-z0-9]+$/i)).max(20).optional().describe('search: only these extensions, e.g. [".png", ".jpg"].'),
43000
+ overwrite: external_exports.boolean().optional().describe("upload: replace a file already at path. download: replace a local file."),
43001
+ confirm: external_exports.boolean().optional().describe("true only after the user agreed: make_public, delete, and a move into public/.")
43002
+ }
43003
+ },
43004
+ async (args) => {
43005
+ try {
43006
+ return textResult(await filesAction(requireConfig(), args));
43007
+ } catch (err) {
43008
+ return errorResult(err);
43009
+ }
43010
+ }
43011
+ );
42765
43012
  server.registerTool(
42766
43013
  "awesomate_knowledge_people",
42767
43014
  {
@@ -43035,7 +43282,7 @@ server.registerTool(
43035
43282
  "awesomate_crm_apps",
43036
43283
  {
43037
43284
  annotations: annotate("awesomate_crm_apps", MUTATING),
43038
- description: "The account's own apps (a client portal, a members' app) whose users sign in by email link and then read and change only what each kind's access rules allow them (awesomate_crm_kinds set_access). Pro and above to create or change; reading is every plan. action 'list': each app with its publishable key, allowed origins, sign-up mode and default role. action 'create' {name, allowed_origins, sign_up?, default_role?}: name appears in the sign-in email; allowed_origins are the exact origins the app runs on (https://portal.example.com; http://localhost:5173 for development; capacitor://localhost for the mobile shell), with no path and no wildcard; sign_up 'invite' (default: only people added with awesomate_crm_app_users) or 'open' (anyone who uses the email link gets the default role). action 'update' {app_id, ...any of those, status?}: status 'disabled' signs everyone out of that app and stops its server keys; voice_agent_id names the account's agent that signed-in people talk to by voice (Talk to the agent: @awesomate/sdk voiceAgent() and voiceSession() with the hub's AwesomateAgent widget; the agent greets them by name and, when the app's assistant is set up, knows the 10 most recent conversations they can see, read as them; the app's origin must also be listed under the agent's 'Where your app runs'), null takes it away. The publishable key (pk_...) is not a secret: it goes in the app's browser code, with @awesomate/sdk. Never put the account's own token (amt_pat_...) in browser code. SDK docs, with a tutorial that builds a customer portal: https://hub.awesomate.ai/docs/sdk/ (as one text file for you: https://hub.awesomate.ai/docs/sdk/llms-full.txt). Confirm origins and sign-up mode with the user: open sign-up lets strangers in under the default role. action 'customer_lookup' {app_id, roles?}: which roles in that app may look customers up (name, email and phone, by search or id, with @awesomate/sdk lookupCustomers) and so put a record under any customer, not only themselves. Without roles it reads the setting; roles ['staff'] switches it on for staff; [] switches it off. Never the app's default role (the one customers sign in with): that would show every customer to every other customer, and the hub refuses it. Off until the owner asks for it. SERVER KEYS (ak_...), for the app's own server or an n8n workflow, used with @awesomate/sdk createClient({ token: <key> }) or as a Bearer token on https://hub.awesomate.ai/api/sdk/v1/server: action 'keys' {app_id} lists them (never the secrets); 'create_key' {app_id, name, access: 'read'|'write', kinds?} makes one (kinds leaves it to those kinds, 'contact' to read contacts; leave it out for every kind) and returns the key ONCE: put it straight into the app server's environment (awesomate_app_set_env for an app hosted with us) and never into browser code, a repository or a reply to the user. For an n8n workflow pass n8n_credential: true instead (prefer it): the hub puts the key into a Header Auth credential 'Awesomate app key: <name>' on their n8n (Authorization: Bearer, usable only towards the hub) and returns only its id and name, so the key never passes through you. 'revoke_key' {app_id, key_id} stops it at once, and removes a credential the hub made. A key reads every attribute of an app kind, contacts only as you read them, and writes need Support Plus and above. Prefer the narrowest key: read unless the server writes, and only the kinds it uses.",
43285
+ description: "The account's own apps (a client portal, a members' app) whose users sign in by email link and then read and change only what each kind's access rules allow them (awesomate_crm_kinds set_access). Pro and above to create or change; reading is every plan. action 'list': each app with its publishable key, allowed origins, sign-up mode and default role. action 'create' {name, allowed_origins, sign_up?, default_role?}: name appears in the sign-in email; allowed_origins are the exact origins the app runs on (https://portal.example.com; http://localhost:5173 for development; capacitor://localhost for the mobile shell), with no path and no wildcard; sign_up 'invite' (default: only people added with awesomate_crm_app_users) or 'open' (anyone who uses the email link gets the default role). action 'update' {app_id, ...any of those, status?}: status 'disabled' signs everyone out of that app and stops its server keys; voice_agent_id names the account's agent that signed-in people talk to by voice (Talk to the agent: @awesomate/sdk voiceAgent() and voiceSession() with the hub's AwesomateAgent widget; the agent greets them by name and, when the app's assistant is set up, knows the 10 most recent conversations they can see, read as them; the app's origin must also be listed under the agent's 'Where your app runs'), null takes it away; file_uploads 'off' (default) | 'private' | 'public' lets signed-in people upload files from the app (@awesomate/sdk files.upload, up to 10 MB each) into the account's Files under private/apps/<app_id>/ or public/apps/<app_id>/ (public gives each a public address; page and script types are refused there), counted toward the plan's Files space: with open sign-up that means anyone, so confirm with the user. The publishable key (pk_...) is not a secret: it goes in the app's browser code, with @awesomate/sdk. Never put the account's own token (amt_pat_...) in browser code. SDK docs, with a tutorial that builds a customer portal: https://hub.awesomate.ai/docs/sdk/ (as one text file for you: https://hub.awesomate.ai/docs/sdk/llms-full.txt). Confirm origins and sign-up mode with the user: open sign-up lets strangers in under the default role. action 'customer_lookup' {app_id, roles?}: which roles in that app may look customers up (name, email and phone, by search or id, with @awesomate/sdk lookupCustomers) and so put a record under any customer, not only themselves. Without roles it reads the setting; roles ['staff'] switches it on for staff; [] switches it off. Never the app's default role (the one customers sign in with): that would show every customer to every other customer, and the hub refuses it. Off until the owner asks for it. SERVER KEYS (ak_...), for the app's own server or an n8n workflow, used with @awesomate/sdk createClient({ token: <key> }) or as a Bearer token on https://hub.awesomate.ai/api/sdk/v1/server: action 'keys' {app_id} lists them (never the secrets); 'create_key' {app_id, name, access: 'read'|'write', kinds?} makes one (kinds leaves it to those kinds, 'contact' to read contacts; leave it out for every kind) and returns the key ONCE: put it straight into the app server's environment (awesomate_app_set_env for an app hosted with us) and never into browser code, a repository or a reply to the user. For an n8n workflow pass n8n_credential: true instead (prefer it): the hub puts the key into a Header Auth credential 'Awesomate app key: <name>' on their n8n (Authorization: Bearer, usable only towards the hub) and returns only its id and name, so the key never passes through you. 'revoke_key' {app_id, key_id} stops it at once, and removes a credential the hub made. A key reads every attribute of an app kind, contacts only as you read them, and writes need Support Plus and above. Prefer the narrowest key: read unless the server writes, and only the kinds it uses.",
43039
43286
  inputSchema: {
43040
43287
  action: external_exports.enum(["list", "create", "update", "keys", "create_key", "revoke_key", "customer_lookup"]),
43041
43288
  app_id: external_exports.string().max(48).optional().describe("update, keys, create_key, revoke_key, customer_lookup: the app"),
@@ -43049,10 +43296,11 @@ server.registerTool(
43049
43296
  sign_up: external_exports.enum(["invite", "open"]).optional(),
43050
43297
  default_role: external_exports.string().max(31).optional().describe("lower case; the role open sign-up gives"),
43051
43298
  status: external_exports.enum(["active", "disabled"]).optional().describe("update only"),
43052
- voice_agent_id: external_exports.string().uuid().nullable().optional().describe("update only: the account's own agent (awesomate_knowledge_agents list) that signed-in people talk to by voice in this app; null takes voice away")
43299
+ voice_agent_id: external_exports.string().uuid().nullable().optional().describe("update only: the account's own agent (awesomate_knowledge_agents list) that signed-in people talk to by voice in this app; null takes voice away"),
43300
+ file_uploads: external_exports.enum(["off", "private", "public"]).optional().describe("update only: whether signed-in people may upload files from the app, and whether each gets a public address")
43053
43301
  }
43054
43302
  },
43055
- async ({ action, app_id, name, allowed_origins, sign_up, default_role, status, voice_agent_id, access, kinds, key_id, roles, n8n_credential }) => {
43303
+ async ({ action, app_id, name, allowed_origins, sign_up, default_role, status, voice_agent_id, file_uploads, access, kinds, key_id, roles, n8n_credential }) => {
43056
43304
  try {
43057
43305
  const cfg = requireConfig();
43058
43306
  if (action === "list") return textResult(await hubGet(cfg, "/api/my-crm/v1/apps"));
@@ -43078,7 +43326,8 @@ server.registerTool(
43078
43326
  return textResult(await hubPatch(cfg, `/api/my-crm/v1/apps/${encodeURIComponent(app_id)}`, {
43079
43327
  ...fields,
43080
43328
  ...status ? { status } : {},
43081
- ...voice_agent_id === void 0 ? {} : { voice_agent_id }
43329
+ ...voice_agent_id === void 0 ? {} : { voice_agent_id },
43330
+ ...file_uploads ? { file_uploads } : {}
43082
43331
  }));
43083
43332
  } catch (err) {
43084
43333
  return errorResult(err);
@@ -43658,8 +43907,8 @@ server.registerTool(
43658
43907
  async ({ domain, path }) => {
43659
43908
  try {
43660
43909
  const dom = encodeURIComponent(domain.toLowerCase());
43661
- const q = path && path !== "/" ? `?path=${encodeURIComponent(path)}` : "";
43662
- return textResult(await hubGet(requireConfig(), `/api/client-hosting/sites/${dom}/audit${q}`));
43910
+ const q2 = path && path !== "/" ? `?path=${encodeURIComponent(path)}` : "";
43911
+ return textResult(await hubGet(requireConfig(), `/api/client-hosting/sites/${dom}/audit${q2}`));
43663
43912
  } catch (err) {
43664
43913
  return errorResult(err);
43665
43914
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awesomate/hosting-mcp",
3
- "version": "0.67.0",
3
+ "version": "0.68.0",
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",
@@ -19,7 +19,7 @@
19
19
  "scripts": {
20
20
  "build": "esbuild src/index.ts --bundle --platform=node --target=node18 --format=esm --outfile=dist/index.js --banner:js='#!/usr/bin/env node\nimport { createRequire as __awmCreateRequire } from \"node:module\"; const require = __awmCreateRequire(import.meta.url);' --external:node:*",
21
21
  "typecheck": "tsc --noEmit",
22
- "test": "npm run build && esbuild src/config.ts --bundle --platform=node --target=node18 --format=esm --outfile=test/.build/config.mjs --external:node:* && esbuild src/knowledge.ts --bundle --platform=node --target=node18 --format=esm --outfile=test/.build/knowledge.mjs --banner:js='import { createRequire as __awmCreateRequire } from \"node:module\"; const require = __awmCreateRequire(import.meta.url);' --external:node:* && esbuild src/skills.ts --bundle --platform=node --target=node18 --format=esm --outfile=test/.build/skills.mjs --external:node:* && esbuild src/features.ts --bundle --platform=node --target=node18 --format=esm --outfile=test/.build/features.mjs --external:node:* && esbuild src/http.ts --bundle --platform=node --target=node18 --format=esm --outfile=test/.build/http.mjs --banner:js='import { createRequire as __awmCreateRequire } from \"node:module\"; const require = __awmCreateRequire(import.meta.url);' --external:node:* && node --test test/*.test.mjs",
22
+ "test": "npm run build && esbuild src/config.ts --bundle --platform=node --target=node18 --format=esm --outfile=test/.build/config.mjs --external:node:* && esbuild src/knowledge.ts --bundle --platform=node --target=node18 --format=esm --outfile=test/.build/knowledge.mjs --banner:js='import { createRequire as __awmCreateRequire } from \"node:module\"; const require = __awmCreateRequire(import.meta.url);' --external:node:* && esbuild src/skills.ts --bundle --platform=node --target=node18 --format=esm --outfile=test/.build/skills.mjs --external:node:* && esbuild src/features.ts --bundle --platform=node --target=node18 --format=esm --outfile=test/.build/features.mjs --external:node:* && esbuild src/http.ts --bundle --platform=node --target=node18 --format=esm --outfile=test/.build/http.mjs --banner:js='import { createRequire as __awmCreateRequire } from \"node:module\"; const require = __awmCreateRequire(import.meta.url);' --external:node:* && esbuild src/files.ts --bundle --platform=node --target=node18 --format=esm --outfile=test/.build/files.mjs --banner:js='import { createRequire as __awmCreateRequire } from \"node:module\"; const require = __awmCreateRequire(import.meta.url);' --external:node:* && node --test test/*.test.mjs",
23
23
  "prepublishOnly": "npm run typecheck && npm run test"
24
24
  },
25
25
  "dependencies": {
@@ -1,5 +1,12 @@
1
1
  {
2
2
  "versions": [
3
+ {
4
+ "version": "0.68.0",
5
+ "highlights": [
6
+ "Claude can work with your Files: the folders your workflows use on your automation account. Put an image up and get its public address, fetch a report a workflow saved, make a file public or private, and tidy up, with storage counted against your plan's Files space",
7
+ "Apps you build can let signed-in people attach files, such as a photo of a job, when you switch uploads on for the app"
8
+ ]
9
+ },
3
10
  {
4
11
  "version": "0.67.0",
5
12
  "highlights": [
@@ -224,6 +224,7 @@ MCP, so say that rather than stopping.
224
224
  | `awesomate_dns_check` | Live answer to "where does this domain actually point" — before and after any DNS change |
225
225
  | `awesomate_site_uptime` | 30-day availability, incidents, downtime per domain (cached, free) |
226
226
  | `awesomate_site_audit` | SEO + AEO build gates for one page, fetched as GPTBot — content present without JS, retrieval bots reachable, canonical, schema, structure, speed. Not plan-gated. Run it after any public-page change, and see the awesomate-seo skill for the fixes |
227
+ | `awesomate_files` | Files on their automation account (public/, private/, temp/): put an image or download up and hand back its `public_url`, fetch what a workflow wrote, tidy a folder. Ask before make_public, a move into public/, or delete. Support Plus+, owner's file-folders switch on (`status` says what's missing). Guide: https://hub.awesomate.ai/docs/mcp/guides/files/ |
227
228
  | `awesomate_snapshot_site` | Before ANY change to a live site — files + DB restore point |
228
229
  | `awesomate_list_snapshots` | What restore points a site has |
229
230
  | `awesomate_rollback_site` | Restore a snapshot after something went wrong (confirm first) |
@@ -178,6 +178,17 @@ https://hub.awesomate.ai/docs/sdk/guides/customer-actions/
178
178
  `agentId: await app.voiceAgent()` and `session: () => app.voiceSession()`. Guide:
179
179
  https://hub.awesomate.ai/docs/sdk/guides/voice/
180
180
 
181
+ ## 6b2. Optional: file uploads
182
+
183
+ People signed in to the app can attach files (a photo of a job, a signed form) when the owner
184
+ switches it on: `awesomate_crm_apps {action:'update', app_id, file_uploads:'private'}` ('public'
185
+ also gives each file a public address; web pages and scripts are refused there). Off by default;
186
+ with open sign-up anyone could upload, so ask the owner. Give the kind a `file` attribute
187
+ (`awesomate_crm_kinds {action:'add_attribute'}`), then the page does
188
+ `const f = await app.files.upload(file)` (10 MB each) and stores `f.handle` in that attribute;
189
+ `app.files.download(handle)` gives the file back to anyone in the app who can read the record.
190
+ Uploads count toward the account's Files space. Guide: https://hub.awesomate.ai/docs/sdk/guides/files/
191
+
181
192
  ## 6c. Optional: reply emails
182
193
 
183
194
  `awesomate_crm_notifications {action:'set', app_id, notifications: {enabled: true, thread_link: 'job'}}`