@awesomate/hosting-mcp 0.22.2 → 0.23.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
@@ -40857,10 +40857,85 @@ ${JSON.stringify(details, null, 2)}`;
40857
40857
  }
40858
40858
  return { content: [{ type: "text", text }], isError: true };
40859
40859
  }
40860
- var READ_ONLY = { readOnlyHint: true };
40861
- var DESTRUCTIVE = { destructiveHint: true };
40860
+ var READ_ONLY = { readOnlyHint: true, destructiveHint: false };
40861
+ var MUTATING = { readOnlyHint: false, destructiveHint: false };
40862
+ var DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true };
40863
+ var TITLES = {
40864
+ awesomate_whoami: "Check connected account",
40865
+ awesomate_get_context: "Get account context",
40866
+ awesomate_get_hosting_status: "Get hosting status",
40867
+ awesomate_get_hosting_account: "Get hosting account",
40868
+ awesomate_list_sites: "List WordPress sites",
40869
+ awesomate_list_domains: "List domains",
40870
+ awesomate_get_limits: "Get plan limits",
40871
+ awesomate_get_plan_features: "Get plan features",
40872
+ awesomate_dashboard_metrics: "Get dashboard metrics",
40873
+ awesomate_account_report: "Get account report",
40874
+ awesomate_privacy_settings: "Get privacy settings",
40875
+ awesomate_app_list: "List apps",
40876
+ awesomate_app_context: "Get apps context",
40877
+ awesomate_n8n_context: "Get n8n context",
40878
+ awesomate_n8n_storage: "Get n8n storage usage",
40879
+ awesomate_dns_check: "Check domain DNS",
40880
+ awesomate_site_uptime: "Get site uptime",
40881
+ awesomate_notifications: "Account notifications",
40882
+ awesomate_support: "Get support help",
40883
+ // Destructive: rmSync/writeFileSync under ~/.claude/skills overwrites files
40884
+ // on the user's own machine, so it must always prompt.
40885
+ awesomate_skill_update: "Update Awesomate skills",
40886
+ awesomate_site_create: "Create WordPress site",
40887
+ awesomate_site_audit: "Audit site for SEO and AEO",
40888
+ awesomate_uninstall_site: "Delete WordPress site",
40889
+ awesomate_snapshot_site: "Snapshot site",
40890
+ awesomate_list_snapshots: "List site snapshots",
40891
+ awesomate_rollback_site: "Roll site back to snapshot",
40892
+ awesomate_site_staging_create: "Create staging site",
40893
+ awesomate_site_staging_promote: "Publish staging to live",
40894
+ awesomate_site_staging_discard: "Discard staging site",
40895
+ awesomate_domain_add: "Add custom domain",
40896
+ awesomate_run_wp_cli: "Run WP-CLI command",
40897
+ awesomate_wp_post: "Manage WordPress posts",
40898
+ awesomate_wp_media_import: "Import media to WordPress",
40899
+ awesomate_wp_settings: "Change WordPress settings",
40900
+ awesomate_app_create: "Create app",
40901
+ awesomate_app_get: "Get app details",
40902
+ awesomate_app_scaffold: "Get app scaffold guide",
40903
+ awesomate_app_deploy_info: "Get app deploy briefing",
40904
+ awesomate_app_health: "Check app health",
40905
+ awesomate_app_set_env: "Set app secret",
40906
+ awesomate_app_provision_db: "Add database to app",
40907
+ awesomate_n8n_workflows: "List n8n workflows",
40908
+ awesomate_n8n_inspect: "Inspect n8n workflow",
40909
+ awesomate_n8n_executions: "List n8n executions",
40910
+ awesomate_n8n_errors: "Get n8n errors",
40911
+ awesomate_n8n_findings: "Get n8n findings",
40912
+ awesomate_n8n_kpis: "Get n8n KPIs",
40913
+ awesomate_n8n_node_docs: "Get n8n node docs",
40914
+ awesomate_n8n_deploy: "Deploy n8n workflow",
40915
+ awesomate_n8n_test: "Test-fire n8n workflow",
40916
+ awesomate_n8n_datatable_write: "Write to n8n data table",
40917
+ awesomate_n8n_attach_to_app: "Attach n8n to app",
40918
+ awesomate_n8n_provision_pg: "Provision Postgres for n8n",
40919
+ awesomate_request_build: "Request a build from Awesomate",
40920
+ awesomate_knowledge_status: "Get Knowledge Base status",
40921
+ awesomate_knowledge_provision: "Enable Knowledge Base",
40922
+ awesomate_knowledge_search: "Search Knowledge Base",
40923
+ awesomate_knowledge_ask: "Ask the Knowledge Base",
40924
+ awesomate_knowledge_sources: "Manage knowledge sources",
40925
+ awesomate_knowledge_upload: "Upload file to Knowledge Base",
40926
+ awesomate_knowledge_agent: "Configure knowledge agent",
40927
+ awesomate_knowledge_agents: "Manage knowledge agents",
40928
+ awesomate_knowledge_collections: "Manage knowledge collections",
40929
+ awesomate_knowledge_people: "Manage knowledge entities",
40930
+ awesomate_knowledge_data: "Query knowledge data warehouse"
40931
+ };
40932
+ function annotate(name, kind) {
40933
+ const title = TITLES[name];
40934
+ if (!title) throw new Error(`No TITLES entry for tool "${name}" \u2014 add one before registering it.`);
40935
+ return { title, ...kind };
40936
+ }
40862
40937
  function readTool(name, description, path) {
40863
- server.registerTool(name, { description, inputSchema: {}, annotations: READ_ONLY }, async () => {
40938
+ server.registerTool(name, { description, inputSchema: {}, annotations: annotate(name, READ_ONLY) }, async () => {
40864
40939
  try {
40865
40940
  return textResult(await hubGet(requireConfig(), path));
40866
40941
  } catch (err) {
@@ -40871,7 +40946,7 @@ function readTool(name, description, path) {
40871
40946
  server.registerTool(
40872
40947
  "awesomate_whoami",
40873
40948
  {
40874
- annotations: READ_ONLY,
40949
+ annotations: annotate("awesomate_whoami", READ_ONLY),
40875
40950
  description: "Zero-network identity check: which Awesomate account this session is connected to (slug, plan, token expiry) and WHY (env var, project pin file, sole profile, or default). Call it after connecting, after switching folders, and any time the account in play matters \u2014 if the slug is not the account the user expects, STOP and fix the pin/connection before doing any work.",
40876
40951
  inputSchema: {}
40877
40952
  },
@@ -40895,7 +40970,7 @@ server.registerTool(
40895
40970
  server.registerTool(
40896
40971
  "awesomate_get_context",
40897
40972
  {
40898
- annotations: READ_ONLY,
40973
+ annotations: annotate("awesomate_get_context", READ_ONLY),
40899
40974
  description: "THE session entry point \u2014 call it FIRST, before any domain context. Returns the connected account: plan, capabilities, limits, scopes, token expiry, cPanel routing, PLUS `attention` ({unreadNotifications, erroringWorkflows7d, patExpiresInDays} \u2014 null means unknown, never zero; when something is non-zero, mention it to the user in one line before starting the asked task), `latestMcpVersion` (if serverVersion still lags it after a restart, the npx cache is stale \u2014 remedy: rm -rf ~/.npm/_npx, then restart), and `skill` ({updateAvailable, staleSkills, whatsNew} \u2014 if updateAvailable, mention ONCE with a whatsNew line, offer awesomate_skill_update, never mid-task). If the token is near expiry (patExpiresInDays < 7), re-running Connect Claude Code from hub.awesomate.ai/claude refreshes skills AND renews the token in one go.",
40900
40975
  inputSchema: {}
40901
40976
  },
@@ -40916,7 +40991,7 @@ readTool(
40916
40991
  server.registerTool(
40917
40992
  "awesomate_n8n_deploy",
40918
40993
  {
40919
- annotations: DESTRUCTIVE,
40994
+ annotations: annotate("awesomate_n8n_deploy", DESTRUCTIVE),
40920
40995
  description: "Workflow lifecycle writes on the client's n8n, all consent-gated and audited server-side. Actions: 'validate' (structural + n8n-mcp check of workflowJson \u2014 ALWAYS validate before create_draft), 'create_draft' (creates an INACTIVE '[CLI] ' workflow tagged awm:client-cli; returns its webhook URLs), 'activate'/'deactivate' (activation strips the [CLI] prefix; production webhooks respond only while active; agency-managed workflows are refused), 'promote' (swaps a TESTED draft into the live workflow IN PLACE \u2014 live id + webhookIds preserved so external callers keep working; pass workflowId=the LIVE id and draftId=the tested draft; the draft is archived '[promoted <date>]'; response includes operationId for rollback), 'rollback' (restore a promote's pre-swap state \u2014 pass operationId), 'update_draft' (replace an INACTIVE self-built draft's full JSON in place \u2014 pass workflowId + workflowJson; live workflows are refused, use the promote path), 'delete_draft' (inactive self-built drafts only). Get explicit user approval before activate, promote, rollback, and delete_draft. 429 quota_exceeded = daily plan limit; 403 consent_required \u2192 send user to settingsUrl; 409 n8n_credential_invalid = the user's OWN n8n rejected the stored API key (NOT a hub outage \u2014 reads and validate keep working because they never touch their instance): tell them to create a new API key in their n8n under Settings \u2192 n8n API, paste it into their Awesomate account at hub.awesomate.ai/n8n/settings, then retry.",
40921
40996
  inputSchema: {
40922
40997
  action: external_exports.enum(["validate", "create_draft", "update_draft", "activate", "deactivate", "promote", "rollback", "delete_draft"]),
@@ -40972,6 +41047,7 @@ server.registerTool(
40972
41047
  server.registerTool(
40973
41048
  "awesomate_n8n_test",
40974
41049
  {
41050
+ annotations: annotate("awesomate_n8n_test", MUTATING),
40975
41051
  description: "Fire a synthetic test payload at an ACTIVE self-built workflow's production webhook and get back the webhook response plus the executionId it caused (then poll GET /api/my-n8n/executions/:id for node-by-node results). Sends the X-Awesomate-Test header. Real side effects DO run (emails actually send) \u2014 warn the user, use their own address, prefix subjects with [TEST]. Only works on awm:client-cli-tagged workflows.",
40976
41052
  inputSchema: {
40977
41053
  workflowId: external_exports.string(),
@@ -40991,7 +41067,7 @@ server.registerTool(
40991
41067
  server.registerTool(
40992
41068
  "awesomate_n8n_workflows",
40993
41069
  {
40994
- annotations: READ_ONLY,
41070
+ annotations: annotate("awesomate_n8n_workflows", READ_ONLY),
40995
41071
  description: "The client's workflows. No id \u2192 EVERY workflow as a node-level summary in one call (nodeCount, nodeTypes, triggers, usesAi, communityNodes, dates; ?active filter + pagination) \u2014 use this for the session's world picture instead of fetching workflows one by one. With id \u2192 that workflow's JSON: detail 'full' (default \u2014 nodes with parameters, connections, settings) or 'structure' (nodes WITHOUT parameters + connections \u2014 cheap shape check for big workflows). 403 consent_required \u2192 send the user to settingsUrl and re-check.",
40996
41072
  inputSchema: {
40997
41073
  id: external_exports.string().optional().describe("Workflow id for a single-workflow read; omit for the all-workflows summary"),
@@ -41027,7 +41103,7 @@ server.registerTool(
41027
41103
  server.registerTool(
41028
41104
  "awesomate_n8n_inspect",
41029
41105
  {
41030
- annotations: READ_ONLY,
41106
+ annotations: annotate("awesomate_n8n_inspect", READ_ONLY),
41031
41107
  description: `Instance inventory reads, one tool: 'nodes' (distinct node types in use, counts, versions, community/AI flags), 'datatables' (tables + columns + row counts), 'datatable_rows' (pass datatableId; limit \u2264 100), 'possibilities' (facts for "what could I automate": connected services with live-usage cross-check, unused connections, top nodes, AI tools [null = unknown, not none], community packages, counts \u2014 YOU turn these into suggestions, grounded only in what's actually there), 'credentials' (names/types/inferred service \u2014 never secrets), 'variables' ($vars keys). All consent-gated server-side; 403 consent_required \u2192 settingsUrl.`,
41032
41108
  inputSchema: {
41033
41109
  what: external_exports.enum(["nodes", "datatables", "datatable_rows", "possibilities", "credentials", "variables"]),
@@ -41059,7 +41135,7 @@ server.registerTool(
41059
41135
  server.registerTool(
41060
41136
  "awesomate_n8n_executions",
41061
41137
  {
41062
- annotations: READ_ONLY,
41138
+ annotations: annotate("awesomate_n8n_executions", READ_ONLY),
41063
41139
  description: "Execution reads. workflowId \u2192 recent executions of that workflow. executionId \u2192 detail with errorSummary \u2014 trust errorSummary.failingNode only when confident:true (structural decode; also carries errorType/code/lastNodeExecuted); when confident:false it came from a heuristic and may name a node that does not exist \u2014 verify against the workflow before editing anything. executionId + debug:true \u2192 node-by-node decode (statuses, timings, errors, 2 example items per node) \u2014 the best failure-diagnosis view; needs the 'error content analysis' privacy toggle (403 consent_required \u2192 settingsUrl), and responses with tooLarge:true mean the payload exceeded 15MB \u2014 fall back to the non-debug detail. For 'what is failing across ALL my workflows', use awesomate_n8n_errors instead.",
41064
41140
  inputSchema: {
41065
41141
  workflowId: external_exports.string().optional(),
@@ -41088,7 +41164,7 @@ server.registerTool(
41088
41164
  server.registerTool(
41089
41165
  "awesomate_n8n_node_docs",
41090
41166
  {
41091
- annotations: READ_ONLY,
41167
+ annotations: annotate("awesomate_n8n_node_docs", READ_ONLY),
41092
41168
  description: "Live n8n documentation, 500+ nodes + 2,500+ community templates \u2014 ALWAYS prefer this over memory for node schemas and typeVersions. tools: search_nodes {query}, get_node {nodeType \u2014 full form like 'n8n-nodes-base.gmail' works, add detail:'full' for everything}, search_templates {query} / get_template {templateId} (real importable community workflows \u2014 great starting points), validate_node {nodeType, config}, tools_documentation {}. Works on every plan, no consent needed. 503 node_catalog_unavailable \u2192 use the skill's references/vendor/ files instead; 422 catalog_tool_error \u2192 YOUR args were wrong (message says why), the catalog is fine.",
41093
41169
  inputSchema: {
41094
41170
  tool: external_exports.enum(["search_nodes", "get_node", "search_templates", "get_template", "validate_node", "tools_documentation"]),
@@ -41106,7 +41182,7 @@ server.registerTool(
41106
41182
  server.registerTool(
41107
41183
  "awesomate_n8n_datatable_write",
41108
41184
  {
41109
- annotations: DESTRUCTIVE,
41185
+ annotations: annotate("awesomate_n8n_datatable_write", DESTRUCTIVE),
41110
41186
  description: "Datatable writes (Support Plus+, consent-gated, audited). 'create' {name, columns:[{name,type?}], workflowId?} \u2014 ALWAYS pass workflowId when the table serves a specific workflow (datatables resolve PER PROJECT at runtime; workflowId threads that workflow's project; a projectWarning in the response means pass it). 'add_column' {tableId, name, type?} \u2014 needs the 'direct database writes' privacy toggle; names: letters/digits/underscores only. 'insert' {tableId, rows:[...]} (\u2264100), 'update' {tableId, filter, data}, 'delete_rows' {tableId, filter \u2014 REQUIRED, there is no delete-all}. Writes only work on tables created through Claude Code (403 not_self_created otherwise \u2014 agency tables are off limits). Get explicit user approval before delete_rows.",
41111
41187
  inputSchema: {
41112
41188
  action: external_exports.enum(["create", "add_column", "insert", "update", "delete_rows"]),
@@ -41141,6 +41217,7 @@ server.registerTool(
41141
41217
  server.registerTool(
41142
41218
  "awesomate_skill_update",
41143
41219
  {
41220
+ annotations: annotate("awesomate_skill_update", DESTRUCTIVE),
41144
41221
  description: "The one-step updater for the locally installed Awesomate skills \u2014 run when any response stamp or awesomate_get_context reports an update ready. Refreshes ALL bundled skills in ~/.claude/skills from this (always-latest) server package, removes files newer bundles no longer ship, re-stamps versions, and returns per-skill from\u2192to plus what's-new lines and the exact restart step. New skill content applies from the NEXT Claude Code session; finish the current task first, then hand the user the restart instruction verbatim.",
41145
41222
  inputSchema: {}
41146
41223
  },
@@ -41173,6 +41250,7 @@ server.registerTool(
41173
41250
  server.registerTool(
41174
41251
  "awesomate_support",
41175
41252
  {
41253
+ annotations: annotate("awesomate_support", MUTATING),
41176
41254
  description: "Help the user with the service itself \u2014 questions, being stuck, or reaching a human. action 'faq' {q} searches the published help library (answer FROM the results; never invent policy or pricing). action 'help_docs' {q} searches the full Help Centre articles live \u2014 use when the FAQ has no good answer, and cite the returned article URLs. action 'create_ticket' {subject, message} opens a support ticket \u2014 draft it in the user's words, SHOW it, and get an explicit yes before sending; it emails the Awesomate team and returns a portal URL. action 'list_tickets' shows their tickets and status. Use the awesomate-support skill for the full flow. Not for building \u2014 route n8n work to awesomate-n8n and hosting to awesomate-hosting.",
41177
41255
  inputSchema: {
41178
41256
  action: external_exports.enum(["faq", "help_docs", "create_ticket", "list_tickets"]),
@@ -41204,7 +41282,7 @@ server.registerTool(
41204
41282
  server.registerTool(
41205
41283
  "awesomate_request_build",
41206
41284
  {
41207
- annotations: DESTRUCTIVE,
41285
+ annotations: annotate("awesomate_request_build", DESTRUCTIVE),
41208
41286
  description: "Submit a DONE-FOR-YOU automation request \u2014 the Awesomate team builds it, for clients who'd rather not build it themselves or whose request is beyond what you can build here. This SPENDS 1 CREDIT ($100). Two steps, always: call WITHOUT confirmCredit first \u2014 it returns the cost and the user's available balance and spends nothing; state both to the user in plain words, get an explicit yes, THEN call again with confirmCredit:true. Needs a wizard-enabled plan (Pro/Embedded) \u2014 a Support Plus user gets upgrade_required, relay it honestly. On success returns a tracking URL (progress shows on My Automations).",
41209
41287
  inputSchema: {
41210
41288
  title: external_exports.string().describe("Short name for the automation"),
@@ -41224,6 +41302,7 @@ server.registerTool(
41224
41302
  server.registerTool(
41225
41303
  "awesomate_site_create",
41226
41304
  {
41305
+ annotations: annotate("awesomate_site_create", MUTATING),
41227
41306
  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.",
41228
41307
  inputSchema: {
41229
41308
  domain: external_exports.string().optional().describe("Custom domain if they have one; omit for a default *.awesomate.site subdomain"),
@@ -41242,6 +41321,7 @@ server.registerTool(
41242
41321
  server.registerTool(
41243
41322
  "awesomate_domain_add",
41244
41323
  {
41324
+ annotations: annotate("awesomate_domain_add", MUTATING),
41245
41325
  description: "Add a custom domain to the user's hosting account. Returns the DNS steps they need to complete at their registrar. Plan-gated (some plans allow zero custom domains \u2014 relay the upgrade hint on 403).",
41246
41326
  inputSchema: { domain: external_exports.string().describe("The domain to add, e.g. example.com") }
41247
41327
  },
@@ -41256,6 +41336,7 @@ server.registerTool(
41256
41336
  server.registerTool(
41257
41337
  "awesomate_run_wp_cli",
41258
41338
  {
41339
+ annotations: annotate("awesomate_run_wp_cli", DESTRUCTIVE),
41259
41340
  description: "Run an allowlisted WP-CLI command on one of the user's WordPress sites (plugin/theme list+activate+update, cache flush, option get/update, post/media/menu/comment/user list). ARGUMENTS CANNOT CONTAIN SPACES (letters, digits and -_./=:@+, only) \u2014 so a site title, tagline or any multi-word value is impossible here: use awesomate_wp_settings for those, and awesomate_wp_post for post/page content. Installs accept wp.org SLUGS only \u2014 never URLs. args is the command as an array, e.g. ['plugin','list'] or ['plugin','install','wordpress-seo','--activate']. A 400 wp_cli_not_allowed means that command isn't permitted; a 502 wp_cli_unavailable is a temporary server-side issue, not your command.",
41260
41341
  inputSchema: {
41261
41342
  domain: external_exports.string().describe("The site domain"),
@@ -41273,7 +41354,7 @@ server.registerTool(
41273
41354
  server.registerTool(
41274
41355
  "awesomate_uninstall_site",
41275
41356
  {
41276
- annotations: DESTRUCTIVE,
41357
+ annotations: annotate("awesomate_uninstall_site", DESTRUCTIVE),
41277
41358
  description: "Permanently delete a WordPress site (files + database). IRREVERSIBLE \u2014 snapshot first if the user might want it back, and always get explicit confirmation. You MUST pass confirm equal to the exact domain, or the hub refuses. Support Plus+.",
41278
41359
  inputSchema: {
41279
41360
  domain: external_exports.string().describe("The site domain to delete"),
@@ -41291,6 +41372,7 @@ server.registerTool(
41291
41372
  server.registerTool(
41292
41373
  "awesomate_app_provision_db",
41293
41374
  {
41375
+ annotations: annotate("awesomate_app_provision_db", MUTATING),
41294
41376
  description: "Add a Postgres database to an EXISTING Node app that doesn't have one \u2014 one DB per environment, with DATABASE_URL injected into each app environment (restart/redeploy to pick it up). The password is set server-side and never returned. Support Plus+; static apps and apps that already have a DB are refused. Use the awesomate-database skill's decision tree first \u2014 a simple list an automation reads/writes is often better as an n8n data table.",
41295
41377
  inputSchema: { appId: external_exports.number().int().positive().describe("The app id from awesomate_app_list / _get") }
41296
41378
  },
@@ -41350,7 +41432,7 @@ var PLAN_LADDER_FALLBACK = {
41350
41432
  server.registerTool(
41351
41433
  "awesomate_get_plan_features",
41352
41434
  {
41353
- annotations: READ_ONLY,
41435
+ annotations: annotate("awesomate_get_plan_features", READ_ONLY),
41354
41436
  description: "The Awesomate plan ladder: what each plan includes for hosting (WordPress sites, custom domains, hosted apps, cPanel, shell/Claude Code access) and which plans are purchasable. Fetched live from the hub (single source of truth) with a static fallback. Use to explain what an upgrade unlocks; live per-account usage comes from awesomate_get_limits.",
41355
41437
  inputSchema: {}
41356
41438
  },
@@ -41372,6 +41454,7 @@ server.registerTool(
41372
41454
  server.registerTool(
41373
41455
  "awesomate_snapshot_site",
41374
41456
  {
41457
+ annotations: annotate("awesomate_snapshot_site", MUTATING),
41375
41458
  description: "Snapshot a WordPress site (files + database) BEFORE any risky change \u2014 an AI edit, a deploy, a plugin/theme/core update. Returns a snapshotId you can roll back to. ALWAYS snapshot before mutating a live site. Requires shell access (Support Plus+).",
41376
41459
  inputSchema: {
41377
41460
  domain: external_exports.string().describe("The site domain (as shown by awesomate_list_sites), e.g. mysite.awesomate.site"),
@@ -41391,7 +41474,7 @@ server.registerTool(
41391
41474
  server.registerTool(
41392
41475
  "awesomate_list_snapshots",
41393
41476
  {
41394
- annotations: READ_ONLY,
41477
+ annotations: annotate("awesomate_list_snapshots", READ_ONLY),
41395
41478
  description: "List a site\u2019s available snapshots (newest first) with their ids, timestamps, and reasons. Requires shell access (Support Plus+).",
41396
41479
  inputSchema: { domain: external_exports.string().describe("The site domain") }
41397
41480
  },
@@ -41406,7 +41489,7 @@ server.registerTool(
41406
41489
  server.registerTool(
41407
41490
  "awesomate_rollback_site",
41408
41491
  {
41409
- annotations: DESTRUCTIVE,
41492
+ annotations: annotate("awesomate_rollback_site", DESTRUCTIVE),
41410
41493
  description: "Restore a site to a previous snapshot (files + database). The current state is auto-snapshotted first, so a rollback is itself reversible (see preRollbackSnapshotId in the result). Confirm with the user before rolling back \u2014 it overwrites the live site. Requires shell access (Support Plus+).",
41411
41494
  inputSchema: {
41412
41495
  domain: external_exports.string().describe("The site domain"),
@@ -41426,6 +41509,7 @@ server.registerTool(
41426
41509
  server.registerTool(
41427
41510
  "awesomate_site_staging_create",
41428
41511
  {
41512
+ annotations: annotate("awesomate_site_staging_create", MUTATING),
41429
41513
  description: "Create a staging copy of a WordPress site on the client's private awesomate.dev address (files + database cloned, URLs rewritten). Use this BEFORE making user-facing changes so the user can review at the staging URL first \u2014 post to dev, review, then awesomate_site_staging_promote. The staging site is hidden from search engines and AI crawlers by policy; anyone with the link can view it. One staging copy per site \u2014 a 409 with code 'staging_exists' means promote or discard the existing one first. Cloning can take a few minutes on large sites. Requires shell access (Support Plus+).",
41430
41514
  inputSchema: {
41431
41515
  domain: external_exports.string().describe("The LIVE site domain (as shown by awesomate_list_sites), e.g. mysite.awesomate.site")
@@ -41444,7 +41528,7 @@ server.registerTool(
41444
41528
  server.registerTool(
41445
41529
  "awesomate_site_staging_promote",
41446
41530
  {
41447
- annotations: DESTRUCTIVE,
41531
+ annotations: annotate("awesomate_site_staging_promote", DESTRUCTIVE),
41448
41532
  description: "Publish the staging copy to the LIVE site (overwrites live files + database with staging). The live site is auto-snapshotted first \u2014 the result includes preSnapshotId, which awesomate_rollback_site can restore if anything looks wrong. **Confirm with the user before promoting \u2014 it replaces the live site.** Requires shell access (Support Plus+).",
41449
41533
  inputSchema: {
41450
41534
  domain: external_exports.string().describe("The LIVE site domain whose staging copy should go live")
@@ -41463,7 +41547,7 @@ server.registerTool(
41463
41547
  server.registerTool(
41464
41548
  "awesomate_site_staging_discard",
41465
41549
  {
41466
- annotations: DESTRUCTIVE,
41550
+ annotations: annotate("awesomate_site_staging_discard", DESTRUCTIVE),
41467
41551
  description: "Delete the staging copy of a site (staging WP install + its awesomate.dev address; the live site is untouched). **Confirm with the user before discarding \u2014 unpromoted staging changes are lost.** Requires shell access (Support Plus+).",
41468
41552
  inputSchema: {
41469
41553
  domain: external_exports.string().describe("The LIVE site domain whose staging copy should be discarded")
@@ -41492,7 +41576,7 @@ readTool(
41492
41576
  server.registerTool(
41493
41577
  "awesomate_app_get",
41494
41578
  {
41495
- annotations: READ_ONLY,
41579
+ annotations: annotate("awesomate_app_get", READ_ONLY),
41496
41580
  description: "Get one app plus its environments (subdomains, ports, db engine/name, deploy + health state). Poll this after awesomate_app_create \u2014 status goes provisioning \u2192 active | failed (read provision_error on failure).",
41497
41581
  inputSchema: { appId: external_exports.number().int().positive().describe("The app id from awesomate_app_list / _create") }
41498
41582
  },
@@ -41507,6 +41591,7 @@ server.registerTool(
41507
41591
  server.registerTool(
41508
41592
  "awesomate_app_create",
41509
41593
  {
41594
+ annotations: annotate("awesomate_app_create", MUTATING),
41510
41595
  description: "Provision a new app on the client's cPanel account. Choose the stack deliberately (see the awesomate-app-builder skill): kind 'static' = a single fast landing/lead page served straight from the docroot (no DB, no server process \u2014 pick this for brochure/landing/lead-capture); kind 'node' = a dynamic app with a backend + database (logins, custom logic, an API). Pick the template by job \u2014 'node-auth-sync' (user accounts/logins, MySQL), 'node-crud-postgres' (structured data without logins: trackers/dashboards \u2014 ready-made CRUD + UI, Postgres), 'node-api-only' (webhooks/integrations/glue, no DB wiring), 'static-landing' (marketing/lead page). If dbEngine is omitted the template's native engine is used (node-crud-postgres \u2192 postgres). Returns 202 immediately with an appId + subdomain(s); poll awesomate_app_get until status=active, then fetch the starter files with awesomate_app_scaffold. Requires apps:write (Support Plus+) \u2014 a 403 means offer an upgrade. Plans also cap the NUMBER of apps (Support Plus 5, Pro 20 \u2014 check awesomate_get_limits.dimensions.apps first): a 409 with code 'app_limit' means the cap is reached \u2014 do NOT retry; tell the user their allowance is full and surface the recommendedPlan/deepLink from the error. Get the user's confirmation on the stack + name before calling.",
41511
41596
  inputSchema: {
41512
41597
  appSlug: external_exports.string().regex(/^[a-z][a-z0-9]{1,15}$/).describe("2-16 chars, starts with a letter, lowercase alphanumeric \u2014 used for subdomains + db names"),
@@ -41528,7 +41613,7 @@ server.registerTool(
41528
41613
  server.registerTool(
41529
41614
  "awesomate_app_scaffold",
41530
41615
  {
41531
- annotations: READ_ONLY,
41616
+ annotations: annotate("awesomate_app_scaffold", READ_ONLY),
41532
41617
  description: "Fetch the app's starter files (its template rendered against the live app metadata \u2014 subdomains, control-plane URL, repo \u2014 with placeholders already substituted). Call after awesomate_app_create reaches status=active: write each returned file into a fresh local project folder at its relative path, then run npm install (Node apps) and follow the bundled CLAUDE.md. This is how the starter code gets onto the user's machine \u2014 don't reconstruct templates by hand.",
41533
41618
  inputSchema: { appId: external_exports.number().int().positive().describe("The app id from awesomate_app_create / _list") }
41534
41619
  },
@@ -41543,6 +41628,7 @@ server.registerTool(
41543
41628
  server.registerTool(
41544
41629
  "awesomate_app_deploy_info",
41545
41630
  {
41631
+ annotations: annotate("awesomate_app_deploy_info", READ_ONLY),
41546
41632
  description: "READ-ONLY deploy briefing \u2014 this never deploys anything (deploys happen via git push). Reports how to deploy an app, its per-env targets, last-deploy/health state, and how to promote (dev\u2192staging\u2192main) or roll back (git revert + push). Node apps deploy via git push (dev/staging/main \u2192 GitHub Actions \u2192 cPanel). Use the awesomate-github skill to wire push-to-deploy the first time.",
41547
41633
  inputSchema: { appId: external_exports.number().int().positive().describe("The app id") }
41548
41634
  },
@@ -41557,6 +41643,7 @@ server.registerTool(
41557
41643
  server.registerTool(
41558
41644
  "awesomate_app_set_env",
41559
41645
  {
41646
+ annotations: annotate("awesomate_app_set_env", MUTATING),
41560
41647
  description: "Store an API key / secret for a Node app environment: it's encrypted in the hub AND injected into the app's .env (0600) then the app restarts. ALWAYS use this instead of putting a secret in code, a committed file, or leaving it in the chat. NEVER echo the value back \u2014 confirm with the key name only. If the user pastes a key in chat, store it here and tell them (per the awesomate-credentials skill) it should be rotated since it passed through the transcript. Node apps only (static sites have no server env).",
41561
41648
  inputSchema: {
41562
41649
  appId: external_exports.number().int().positive().describe("The app id"),
@@ -41576,6 +41663,7 @@ server.registerTool(
41576
41663
  server.registerTool(
41577
41664
  "awesomate_n8n_attach_to_app",
41578
41665
  {
41666
+ annotations: annotate("awesomate_n8n_attach_to_app", MUTATING),
41579
41667
  description: "Wire an app to call the user's own n8n as a backend. Give it the app + a webhook URL you already created/tested/promoted on their n8n (via awesomate_n8n_deploy \u2014 create_draft returns the webhook URL, and promote preserves it). It stores N8N_WEBHOOK_URL + a generated N8N_WEBHOOK_SECRET on the app (encrypted + injected) and RETURNS the secret so you can add a matching `X-Awesomate-Webhook-Secret` header check to the n8n workflow (so the webhook isn't world-callable). The app then calls it via src/lib/n8n.ts callWorkflow(). Node apps only. Only reach for this when the job needs an external app/credential or AI the user already has in n8n \u2014 not for pure in-app logic.",
41580
41668
  inputSchema: {
41581
41669
  appId: external_exports.number().int().positive().describe("The app id"),
@@ -41594,7 +41682,7 @@ server.registerTool(
41594
41682
  server.registerTool(
41595
41683
  "awesomate_app_health",
41596
41684
  {
41597
- annotations: READ_ONLY,
41685
+ annotations: annotate("awesomate_app_health", READ_ONLY),
41598
41686
  description: "Probe an app's environments live and report health. Node envs hit /api/ready (200 = deployed + DB up + migrations applied); static hits the root. An env that isn't deployed yet reports unreachable \u2014 expected, not a failure. Use after a deploy to confirm it came up, or when the user says something's down.",
41599
41687
  inputSchema: { appId: external_exports.number().int().positive().describe("The app id") }
41600
41688
  },
@@ -41609,6 +41697,7 @@ server.registerTool(
41609
41697
  server.registerTool(
41610
41698
  "awesomate_n8n_provision_pg",
41611
41699
  {
41700
+ annotations: annotate("awesomate_n8n_provision_pg", MUTATING),
41612
41701
  description: "Provision a NEW Postgres database on the user's own Awesomate cPanel hosting and wire it into their n8n as a ready-to-use `postgres` credential their workflows can select. This is a WRITE that creates real infrastructure \u2014 get explicit user approval first. Requires: their n8n builder access (Support Plus+, allow_client_cli_builds) AND the allow_pg_writes consent (403 consent_required \u2192 send them to settingsUrl); AND an Awesomate cPanel hosting account (409 hosting_required otherwise). Limited to 2/day. Returns the new credential id + name, the database/user, and the password ONCE (it lives in the n8n credential, never stored by Awesomate \u2014 tell the user to note or rotate it). After this, build the workflow selecting the returned Postgres credential.",
41613
41702
  inputSchema: {}
41614
41703
  },
@@ -41642,7 +41731,7 @@ ${scrubKeyMaterial(message)}` }],
41642
41731
  server.registerTool(
41643
41732
  "awesomate_knowledge_status",
41644
41733
  {
41645
- annotations: READ_ONLY,
41734
+ annotations: annotate("awesomate_knowledge_status", READ_ONLY),
41646
41735
  description: "Call FIRST for any Knowledge Base work. The account's knowledge tenant state (provisioning/active/suspended), plan entitlement, consent flag, month-to-date usage vs included quota, and purchased packs. upgrade_required:true \u2192 relay the included upsell copy + billing link honestly, do NOT retry. available:false \u2192 this hub doesn't serve Knowledge Base yet (kill switch / old hub) \u2014 also not retryable. Reads work on every plan.",
41647
41736
  inputSchema: {}
41648
41737
  },
@@ -41657,6 +41746,7 @@ server.registerTool(
41657
41746
  server.registerTool(
41658
41747
  "awesomate_knowledge_provision",
41659
41748
  {
41749
+ annotations: annotate("awesomate_knowledge_provision", MUTATING),
41660
41750
  description: "Enable the Knowledge Base for this account (creates their isolated tenant on the Awesomate knowledge platform and wires the n8n credential). Idempotent \u2014 safe to re-call; a pending:true response means provisioning continues in the background: poll awesomate_knowledge_status. consent_required \u2192 send the user to Settings \u2192 Privacy (they must flip it themselves), re-check, then retry. upgrade_required \u2192 relay the upsell, don't retry. Get the user's explicit go-ahead before enabling.",
41661
41751
  inputSchema: {}
41662
41752
  },
@@ -41671,6 +41761,7 @@ server.registerTool(
41671
41761
  server.registerTool(
41672
41762
  "awesomate_knowledge_sources",
41673
41763
  {
41764
+ annotations: annotate("awesomate_knowledge_sources", MUTATING),
41674
41765
  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.",
41675
41766
  inputSchema: {
41676
41767
  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"]),
@@ -41708,7 +41799,7 @@ server.registerTool(
41708
41799
  server.registerTool(
41709
41800
  "awesomate_knowledge_search",
41710
41801
  {
41711
- annotations: READ_ONLY,
41802
+ annotations: annotate("awesomate_knowledge_search", READ_ONLY),
41712
41803
  description: "Instant search over the knowledge library with live facet counts: the fastest way to see WHAT is in there and to find the exact video moment, book page, dataset or web section. Returns hits (title, kind, locator like t=612-640 or p.42, snippet with **matched words**, score) plus facets {kind, year, category, author, people, places, topics} whose counts describe the current filters: repeat a facet value to OR within it, combine facets to AND. include_media adds presigned url/poster_url to hits: they expire in minutes, use immediately, never store. Keyword-only and free (no answer quota); for a verified ANSWER use awesomate_knowledge_ask, optionally with the same filters. Citations from awesomate_knowledge_ask carry NO media URLs \u2014 when a cited source is an image/video and the user wants to SEE it, re-query here with include_media (ideally filtered by its doc id). The returned url/poster_url expire in minutes: fine to show in chat, never safe to embed in a page \u2014 see the awesomate-knowledge skill's showing-media.md.",
41713
41804
  inputSchema: {
41714
41805
  q: external_exports.string().max(2e3).optional().describe("Search words; empty lists the library filtered by the facets"),
@@ -41736,6 +41827,7 @@ server.registerTool(
41736
41827
  server.registerTool(
41737
41828
  "awesomate_knowledge_ask",
41738
41829
  {
41830
+ annotations: annotate("awesomate_knowledge_ask", READ_ONLY),
41739
41831
  description: `Ask the account's knowledge base a question and get the VERIFIED answer with numbered sources (title, locator, url) \u2014 the test surface for 'is my content in there and answering well'. Read BOTH status and grounded. grounded:true \u2192 present the answer with its numbered sources. grounded:false (status ok but ZERO sources) \u2192 the agent answered from MODEL MEMORY, not their content: say their content does not cover it, never present it as an answer from their knowledge base, never build on it. The default workspace agent is not strict-grounded, so this is common \u2014 anything customer-facing should use a purpose-built agent (awesomate_knowledge_agents) with strict grounding. no_results / failed_validation (not_in_verified_content:true) \u2192 the verified content has no answer: relay that honestly (use configured_fallback), never fill the gap from memory \u2014 an honest "it doesn't know" is the feature working. error (platform_error:true) \u2192 the platform itself failed (model/API/infra): NOT a content gap \u2014 never tell the user their content lacks the answer; retry once, then awesomate_support. Counts against the monthly answers quota.`,
41740
41832
  inputSchema: {
41741
41833
  question: external_exports.string().min(1).max(2e3),
@@ -41763,6 +41855,7 @@ server.registerTool(
41763
41855
  server.registerTool(
41764
41856
  "awesomate_knowledge_agent",
41765
41857
  {
41858
+ annotations: annotate("awesomate_knowledge_agent", MUTATING),
41766
41859
  description: "The knowledge agent's configuration. action 'get' \u2014 persona, no-answer fallback message, model tier, allowed datasets, indexed counts. 'set' \u2014 change any of those on the LIVE agent that answers real customers: read the current values first, show the user exactly what will change, get explicit approval, then call; the response echoes the change \u2014 read it back to confirm. model_tier 'opus' is plan-gated (Embedded) \u2014 relay upgrade_required honestly.",
41767
41860
  inputSchema: {
41768
41861
  action: external_exports.enum(["get", "set"]),
@@ -41790,6 +41883,7 @@ server.registerTool(
41790
41883
  server.registerTool(
41791
41884
  "awesomate_knowledge_agents",
41792
41885
  {
41886
+ annotations: annotate("awesomate_knowledge_agents", MUTATING),
41793
41887
  description: "The agent builder (multi-agent; the older awesomate_knowledge_agent tool is the single workspace default). action 'list' \u2014 every agent with status (draft/published vN/suspended). 'get' {agentId} \u2014 full config incl. system message and scope. 'create' {goal} \u2014 AI drafts the whole setup (instructions, scope, tone, test questions) from the account's own content and saves it as a PRIVATE DRAFT (never live, nothing lost); or {name} for a blank draft. 'test' {agentId, message, sessionId?} \u2014 chat with the DRAFT config: free, unmetered, the right way to check behaviour before going live. 'publish' {agentId} \u2014 makes the draft LIVE immediately for every key bound to the agent: get the user's explicit approval first, and read the version back. Editing fields, policies, API keys and the request log live in the hub UI (Knowledge \u2192 Agents); keys are shown once there and never pass through this tool.",
41794
41888
  inputSchema: {
41795
41889
  action: external_exports.enum(["list", "get", "create", "publish", "test"]),
@@ -41812,6 +41906,7 @@ server.registerTool(
41812
41906
  server.registerTool(
41813
41907
  "awesomate_knowledge_collections",
41814
41908
  {
41909
+ annotations: annotate("awesomate_knowledge_collections", MUTATING),
41815
41910
  description: "Collections: named sets of knowledge sources with a default audience \u2014 the unit a chatbot is scoped to (agent scope.tags ['collection:<collection_id>']). action 'list' \u2014 every collection with source counts by visibility. 'create' {slug, name, description?, defaultVisibility?} \u2014 defaultVisibility only pre-fills NEW sources' visibility; each source keeps its own. 'get' {collectionId}. 'update' {collectionId, slug?|name?|description?|defaultVisibility?}. 'delete' {collectionId} \u2014 removes the collection; the sources survive (explicit approval first). 'add'/'remove' {collectionId, sourceIds} \u2014 membership. A source may be in several collections. A PUBLIC chatbot over a collection whose sources are all private answers nothing \u2014 check visibility (awesomate_knowledge_sources search) before building on one.",
41816
41911
  inputSchema: {
41817
41912
  action: external_exports.enum(["list", "create", "get", "update", "delete", "add", "remove"]),
@@ -41834,6 +41929,7 @@ server.registerTool(
41834
41929
  server.registerTool(
41835
41930
  "awesomate_knowledge_people",
41836
41931
  {
41932
+ annotations: annotate("awesomate_knowledge_people", MUTATING),
41837
41933
  description: "The people, places and topics the knowledge base has recognised \u2014 so 'everything about X' and 'who appears with X' answer with citations. action 'list' {status?: named|unknown|hidden|all, cursor?} \u2014 people with counts (unnamed rows carry an opaque handle, NEVER a name; do not guess who they are); 'get' {personId} \u2014 aliases, co-mentions and witness sources; 'aliases' \u2014 pending alias suggestions (text names that probably refer to a known entity); 'rename' {personId, displayName} \u2014 only a name the USER gave, after they confirm which cluster (face/mention counts + sources), then read the result back; 'hide'/'unhide' {personId}; 'merge' {personId, intoPersonId} \u2014 explicit approval first, faces and aliases move and the source entry is hidden; 'decide' {kind, aliasNorm, decision: accept|reject, entityId? | createPersonName?} \u2014 explicit approval first, alias identity is (kind, aliasNorm); 'resolve' \u2014 re-run entity resolution: counts toward the ingestion allowance, so ask first. list/aliases/resolve return available:false when the platform hasn't enabled the layer yet \u2014 relay that honestly, don't retry. Photos of people are only viewable on the hub Knowledge \u2192 People page.",
41838
41934
  inputSchema: {
41839
41935
  action: external_exports.enum(["list", "get", "rename", "hide", "unhide", "merge", "aliases", "decide", "resolve"]),
@@ -41860,6 +41956,7 @@ server.registerTool(
41860
41956
  server.registerTool(
41861
41957
  "awesomate_knowledge_data",
41862
41958
  {
41959
+ annotations: annotate("awesomate_knowledge_data", MUTATING),
41863
41960
  description: "The knowledge platform's business-data warehouse (Pro+). action 'metrics' \u2014 headline numbers for the data tab. 'datasets' \u2014 the datasets imported and their columns: read this FIRST, measures/dimensions must name real columns. 'imports' \u2014 import job statuses. 'query' {dataset, measures:[{column, agg}], plus optional dimensions/filters passed through} \u2014 answer QUANTITATIVE questions from the user's own imported business data (revenue by month, top customers); read-only, results come back as rows to present honestly. New data is imported in the hub UI, not here.",
41864
41961
  inputSchema: {
41865
41962
  action: external_exports.enum(["metrics", "datasets", "imports", "query"]),
@@ -41887,7 +41984,7 @@ server.registerTool(
41887
41984
  server.registerTool(
41888
41985
  "awesomate_n8n_kpis",
41889
41986
  {
41890
- annotations: READ_ONLY,
41987
+ annotations: annotate("awesomate_n8n_kpis", READ_ONLY),
41891
41988
  description: "Automation KPIs from the hub's monitoring: executions, error rate, avg + p95 duration, time saved, 7-day-vs-prior deltas. No workflowId \u2192 the whole instance (the headline numbers for any report). With workflowId \u2192 that workflow, plus last_error_at and clean_days. Needs the 'aggregate monitoring' privacy toggle (on by default). Pair with awesomate_n8n_errors for what is failing and awesomate_site_uptime for the hosting side.",
41892
41989
  inputSchema: { workflowId: external_exports.string().max(64).optional() }
41893
41990
  },
@@ -41904,7 +42001,7 @@ server.registerTool(
41904
42001
  server.registerTool(
41905
42002
  "awesomate_n8n_errors",
41906
42003
  {
41907
- annotations: READ_ONLY,
42004
+ annotations: annotate("awesomate_n8n_errors", READ_ONLY),
41908
42005
  description: "What is broken across the WHOLE instance \u2014 error events grouped by fingerprint (category, workflow, node, occurrences, first/last seen), newest first. THE first call when the user says 'something is failing' or at the start of an n8n session (a quick 7-day sweep; stay silent when it's clean). Counts and categories only \u2014 drill into a specific failure with awesomate_n8n_executions. days defaults to 30 (max 90).",
41909
42006
  inputSchema: {
41910
42007
  days: external_exports.number().int().min(1).max(90).optional(),
@@ -41930,7 +42027,7 @@ readTool(
41930
42027
  server.registerTool(
41931
42028
  "awesomate_n8n_findings",
41932
42029
  {
41933
- annotations: READ_ONLY,
42030
+ annotations: annotate("awesomate_n8n_findings", READ_ONLY),
41934
42031
  description: "Findings from Awesomate's automated error analyzer for THIS account (Pro/Embedded + the 'Enable AI Error Diagnosis' privacy toggle (n8n \u2192 Settings \u2192 Privacy)): severity, workflow, occurrences, a plain-language clientSummary, needsClientAction (something only the user can fix \u2014 expired logins, third-party quotas), and fixReady (a reviewed fix is prepared \u2014 raise it with awesomate_support to have it applied). Read-only. An empty list on a healthy instance is the good state, not an error.",
41935
42032
  inputSchema: { limit: external_exports.number().int().min(1).max(100).optional() }
41936
42033
  },
@@ -41971,6 +42068,7 @@ readTool(
41971
42068
  server.registerTool(
41972
42069
  "awesomate_notifications",
41973
42070
  {
42071
+ annotations: annotate("awesomate_notifications", MUTATING),
41974
42072
  description: "The account's hub notification bell \u2014 the only channel where Awesomate pushes to the client: knowledge quota warnings (80%/100%), 'your quote is ready', support-access events. action 'list' {limit?} \u2192 notifications + unread count (surface unread ones once per session, in one line). 'read' {id} / 'read_all' \u2192 mark seen after you've relayed them. Not for sending anything.",
41975
42073
  inputSchema: {
41976
42074
  action: external_exports.enum(["list", "read", "read_all"]),
@@ -41998,6 +42096,7 @@ server.registerTool(
41998
42096
  server.registerTool(
41999
42097
  "awesomate_wp_post",
42000
42098
  {
42099
+ annotations: annotate("awesomate_wp_post", MUTATING),
42001
42100
  description: "Create, update or read a WordPress post/page on the user's own Awesomate-hosted site \u2014 titles and content with spaces/HTML are fine (unlike awesomate_run_wp_cli). 'create' {domain, title, content?, status?, postType?: post|page, excerpt?, slug?} \u2014 lands as a DRAFT unless status:'publish' is explicit; never publish content the user hasn't seen or approved. 'update' {domain, postId, any of title/content/excerpt/slug/status}. 'get' {domain, postId, includeContent?}. Writes need Support Plus+ and are audited; snapshot the site first (awesomate_snapshot_site) before the session's first content change on a live site. Find post ids via awesomate_run_wp_cli ['post','list'].",
42002
42101
  inputSchema: {
42003
42102
  action: external_exports.enum(["create", "update", "get"]),
@@ -42046,7 +42145,7 @@ server.registerTool(
42046
42145
  domain: external_exports.string().min(3).max(253).describe("a domain on the user's own hosting account"),
42047
42146
  path: external_exports.string().max(400).optional().describe("page path to audit, default /")
42048
42147
  },
42049
- annotations: READ_ONLY
42148
+ annotations: annotate("awesomate_site_audit", READ_ONLY)
42050
42149
  },
42051
42150
  async ({ domain, path }) => {
42052
42151
  try {
@@ -42061,6 +42160,7 @@ server.registerTool(
42061
42160
  server.registerTool(
42062
42161
  "awesomate_wp_media_import",
42063
42162
  {
42163
+ annotations: annotate("awesomate_wp_media_import", MUTATING),
42064
42164
  description: "Import ONE media item into the user's WordPress media library by https URL (Support Plus+, audited). Returns the attachmentId to reference from posts. URLs only \u2014 this cannot read local files; for a local file, upload it somewhere reachable first or use wp-admin. Ask before importing anything the user didn't explicitly provide.",
42065
42165
  inputSchema: {
42066
42166
  domain: external_exports.string().min(3).max(253),
@@ -42080,6 +42180,7 @@ server.registerTool(
42080
42180
  server.registerTool(
42081
42181
  "awesomate_wp_settings",
42082
42182
  {
42183
+ annotations: annotate("awesomate_wp_settings", DESTRUCTIVE),
42083
42184
  description: `Change a WordPress site's core settings \u2014 THE tool for "change my site title" / tagline. Use this, never awesomate_run_wp_cli, for any value containing spaces: that tool's argument gate rejects spaces outright, so \`option update blogname "My Business Name"\` cannot work there. Fields (send only what you're changing): title, tagline, timezone (IANA, e.g. Australia/Sydney), dateFormat, timeFormat, postsPerPage (1-100), searchEngineVisible (false hides the site from search engines \u2014 confirm before setting it). Support Plus+ and audited; flushes the object cache so the change shows. Snapshot first with awesomate_snapshot_site if this is the session's first change to a live site.`,
42084
42185
  inputSchema: {
42085
42186
  domain: external_exports.string().min(3).max(253).describe("The site domain, e.g. mybusiness.awesomate.site"),
@@ -42134,6 +42235,7 @@ var KB_UNPARSEABLE = /* @__PURE__ */ new Set([".doc", ".docx", ".ppt", ".pptx",
42134
42235
  server.registerTool(
42135
42236
  "awesomate_knowledge_upload",
42136
42237
  {
42238
+ annotations: annotate("awesomate_knowledge_upload", MUTATING),
42137
42239
  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.",
42138
42240
  inputSchema: {
42139
42241
  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."),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awesomate/hosting-mcp",
3
- "version": "0.22.2",
3
+ "version": "0.23.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",
@@ -313,7 +313,7 @@ scripts/deploy.sh --from ~/Studio/mysite --domain mysite.awesomate.site --with-d
313
313
  Never deploy without confirming with the user first, and always report the
314
314
  snapshot id and the verified live status.
315
315
 
316
- **"Verified live status" means a real browser at 390px and 1440px, not a 200 from curl.**
316
+ **"Verified live status" means a real browser at 390, 768, 844×390, 1024 and 1440px, not a 200 from curl** — the in-between widths are where a header wraps and only the owner's iPad notices.
317
317
  Run the checklist in `references/wordpress-launch-checks.md` — full-bleed sections, gutters,
318
318
  logo and favicon present, fonts loaded, no broken images, a real 404 — before saying done. Every
319
319
  item on it was reported by a site owner after an agent had already said done.
@@ -92,9 +92,40 @@ theme's `Version:` header (`style.css`). A new file is not seen until that key c
92
92
  file. Verified: the same deploy with `0.1.0 → 0.2.0` registered the patterns instantly. Treat the
93
93
  version bump as part of the deploy, not a release ceremony.
94
94
 
95
+ ## 6. The header wraps between 600px and ~1000px (iPad, phone in landscape)
96
+
97
+ **Symptom:** the owner says the margins are "bad again" on an iPad or a phone turned sideways, but
98
+ every band measures full-bleed. What they are seeing is the **header**: logo and nav links on one
99
+ row, the buttons dropped to a second row on the left, and a slab of white under them.
100
+
101
+ **Cause:** core's navigation block collapses to the hamburger only below **600px**. Between 600 and
102
+ roughly 1000px the logo, the links and the header buttons all try to share one flex row, and the
103
+ row wraps. It looks fine on a phone (collapsed) and on a desktop (fits), which is exactly why phone
104
+ and desktop checks both pass while the owner's iPad does not.
105
+
106
+ **Fix (theme CSS, the block has no breakpoint setting):**
107
+ ```css
108
+ .site-header .nav-row { flex-wrap: nowrap; }
109
+ @media (max-width: 1023px) {
110
+ .site-header .wp-block-navigation { order: 3; } /* hamburger last */
111
+ .site-header .wp-block-navigation__responsive-container:not(.is-menu-open) { display: none !important; }
112
+ .site-header .wp-block-navigation__responsive-container-open:not(.always-shown) { display: flex !important; }
113
+ }
114
+ ```
115
+ Two faults travel with it at the same widths: core stacks **columns to a single column below
116
+ 782px**, so a three-card row becomes a tower on an iPad in portrait (make 3+ column rows two-up
117
+ between 600 and 781 with `display:grid` on `.wp-block-columns:has(> .wp-block-column:nth-child(3))`);
118
+ and a **fixed 96px section padding** is half the screen on a landscape phone — make the section
119
+ spacing preset fluid, e.g. `clamp(64px, 6.7vw, 96px)`.
120
+
121
+ **Verify at 390, 600, 768, 844×390, 1024 and 1440** — never just phone and desktop. The header
122
+ test: the logo and the primary button share a row (their `getBoundingClientRect().top` differ by
123
+ less than 24px), the hamburger is visible up to 1023 and the links from 1024.
124
+
95
125
  ## The launch checklist
96
126
 
97
- Run in a real browser against the LIVE domain, at 390 and 1440, before reporting done:
127
+ Run in a real browser against the LIVE domain at **390, 768, 844×390, 1024 and 1440** the in-between
128
+ widths are where headers wrap — before reporting done:
98
129
 
99
130
  1. Every coloured section is full-bleed (`getBoundingClientRect().left <= 1`) with text inset ≥16px.
100
131
  2. No horizontal overflow (`documentElement.scrollWidth <= clientWidth + 1`).
@@ -104,5 +135,6 @@ Run in a real browser against the LIVE domain, at 390 and 1440, before reporting
104
135
  6. A nonsense URL returns **404** with the theme's 404 template, not a 200 page.
105
136
  7. Section order matches the design (read the `h1,h2` sequence), and neither nav nor decorative
106
137
  marks appear inside page content.
107
- 8. If the deploy added pattern files, the theme `Version:` was bumped and every `wp:pattern`
138
+ 8. The header is one row at every width above, with the hamburger up to 1023 and the links from 1024.
139
+ 9. If the deploy added pattern files, the theme `Version:` was bumped and every `wp:pattern`
108
140
  in the parts actually rendered (an unregistered slug renders as empty, silently).