@awesomate/hosting-mcp 0.49.0 → 0.51.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
@@ -41217,6 +41217,7 @@ var TITLES = {
41217
41217
  awesomate_crm_apps: "Set up sign-in for your own apps",
41218
41218
  awesomate_crm_app_users: "Manage the people who sign in to your apps",
41219
41219
  awesomate_crm_assistant: "Set up the AI in your app's conversations",
41220
+ awesomate_crm_hooks: "Send record changes to your n8n",
41220
41221
  awesomate_crm_email: "Draft an email to a contact list"
41221
41222
  };
41222
41223
  function annotate(name, kind) {
@@ -42886,13 +42887,14 @@ server.registerTool(
42886
42887
  "awesomate_crm_apps",
42887
42888
  {
42888
42889
  annotations: annotate("awesomate_crm_apps", MUTATING),
42889
- 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. 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. Confirm origins and sign-up mode with the user: open sign-up lets strangers in under the default role. 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, or the n8n credential) and never into browser code, a repository or a reply to the user; 'revoke_key' {app_id, key_id} stops it at once. 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.",
42890
+ 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. 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. 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, or the n8n credential) and never into browser code, a repository or a reply to the user; 'revoke_key' {app_id, key_id} stops it at once. 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.",
42890
42891
  inputSchema: {
42891
- action: external_exports.enum(["list", "create", "update", "keys", "create_key", "revoke_key"]),
42892
- app_id: external_exports.string().max(48).optional().describe("update, keys, create_key, revoke_key: the app"),
42892
+ action: external_exports.enum(["list", "create", "update", "keys", "create_key", "revoke_key", "customer_lookup"]),
42893
+ app_id: external_exports.string().max(48).optional().describe("update, keys, create_key, revoke_key, customer_lookup: the app"),
42893
42894
  access: external_exports.enum(["read", "write"]).optional().describe("create_key"),
42894
42895
  kinds: external_exports.array(external_exports.string().max(41)).max(50).optional().describe("create_key: only these kinds (contact to read contacts); leave out for every kind"),
42895
42896
  key_id: external_exports.string().max(64).optional().describe("revoke_key: from keys"),
42897
+ roles: external_exports.array(external_exports.string().max(31)).max(10).optional().describe("customer_lookup: the roles that may look customers up; [] switches it off; leave out to read"),
42896
42898
  name: external_exports.string().max(80).optional(),
42897
42899
  allowed_origins: external_exports.array(external_exports.string().max(200)).max(10).optional(),
42898
42900
  sign_up: external_exports.enum(["invite", "open"]).optional(),
@@ -42900,10 +42902,15 @@ server.registerTool(
42900
42902
  status: external_exports.enum(["active", "disabled"]).optional().describe("update only")
42901
42903
  }
42902
42904
  },
42903
- async ({ action, app_id, name, allowed_origins, sign_up, default_role, status, access, kinds, key_id }) => {
42905
+ async ({ action, app_id, name, allowed_origins, sign_up, default_role, status, access, kinds, key_id, roles }) => {
42904
42906
  try {
42905
42907
  const cfg = requireConfig();
42906
42908
  if (action === "list") return textResult(await hubGet(cfg, "/api/my-crm/v1/apps"));
42909
+ if (action === "customer_lookup") {
42910
+ if (!app_id) return errorResult(new Error("customer_lookup needs app_id, from list."));
42911
+ const path = `/api/my-crm/v1/apps/${encodeURIComponent(app_id)}/customer-lookup`;
42912
+ return textResult(roles === void 0 ? await hubGet(cfg, path) : await hubPut(cfg, path, { roles }));
42913
+ }
42907
42914
  if (action === "keys" || action === "create_key" || action === "revoke_key") {
42908
42915
  if (!app_id) return errorResult(new Error(`${action} needs app_id, from list.`));
42909
42916
  const keys = `/api/my-crm/v1/apps/${encodeURIComponent(app_id)}/keys`;
@@ -42992,6 +42999,39 @@ server.registerTool(
42992
42999
  }
42993
43000
  }
42994
43001
  );
43002
+ server.registerTool(
43003
+ "awesomate_crm_hooks",
43004
+ {
43005
+ annotations: annotate("awesomate_crm_hooks", MUTATING),
43006
+ description: "Send changes to an app's records to the account's own n8n: when a record of a named kind is created, updated or archived, the hub POSTs an event to a Webhook node on THEIR n8n (no other host is accepted), in order, at least once, retrying while n8n is down. Every writer counts: the app's people, its server key, Claude, a recipe, the assistant. Use it for 'when a job is requested, draft a quote', then write the result back with an app server key (awesomate_crm_apps create_key) from the same workflow. Event body: {id (stable: drop repeats), event: created|updated|archived, kind, record_id, changed: [attributes and links that changed], by, at, record (the record as it is when sent; null once archived), app}. Pro and above to change; reading is every plan. action 'list' {app_id}: each hook with delivered/missed counts and its last error. 'create' {app_id, name, url, kinds, events?}: url is the PRODUCTION webhook URL of an active workflow on their n8n (create and activate the workflow first with the awesomate_n8n tools); returns a secret ONCE: put it straight into an n8n Header Auth credential on that Webhook node (header X-Awesomate-Webhook-Secret), never into a reply. A new hook starts from now. 'update' {app_id, hook_id, name?, url?, kinds?, events?, enabled?}: enabled true retries at once. 'remove' {app_id, hook_id}. 'test' {app_id, hook_id}: one test event now ({event: 'test'}), and what n8n answered. Confirm the kinds and what the workflow will do with the user before creating one.",
43007
+ inputSchema: {
43008
+ action: external_exports.enum(["list", "create", "update", "remove", "test"]),
43009
+ app_id: external_exports.string().max(48).describe("the app, from awesomate_crm_apps list"),
43010
+ hook_id: external_exports.string().max(64).optional().describe("update, remove, test: from list"),
43011
+ name: external_exports.string().max(80).optional(),
43012
+ url: external_exports.string().max(500).optional().describe("create/update: the production URL of a Webhook node on the account's own n8n"),
43013
+ kinds: external_exports.array(external_exports.string().max(41)).max(20).optional(),
43014
+ events: external_exports.array(external_exports.enum(["created", "updated", "archived"])).max(3).optional().describe("default all three"),
43015
+ enabled: external_exports.boolean().optional().describe("update only")
43016
+ }
43017
+ },
43018
+ async ({ action, app_id, hook_id, name, url, kinds, events, enabled }) => {
43019
+ try {
43020
+ const cfg = requireConfig();
43021
+ const path = `/api/my-crm/v1/apps/${encodeURIComponent(app_id)}/hooks`;
43022
+ if (action === "list") return textResult(await hubGet(cfg, path));
43023
+ const fields = Object.fromEntries(Object.entries({ name, url, kinds, events }).filter(([, v]) => v !== void 0));
43024
+ if (action === "create") return textResult(await hubPost(cfg, path, fields));
43025
+ if (!hook_id) return errorResult(new Error(`${action} needs hook_id, from list.`));
43026
+ const one = `${path}/${encodeURIComponent(hook_id)}`;
43027
+ if (action === "remove") return textResult(await hubDelete(cfg, one));
43028
+ if (action === "test") return textResult(await hubPost(cfg, `${one}/test`, {}));
43029
+ return textResult(await hubPatch(cfg, one, { ...fields, ...enabled === void 0 ? {} : { enabled } }));
43030
+ } catch (err) {
43031
+ return errorResult(err);
43032
+ }
43033
+ }
43034
+ );
42995
43035
  var crmParam = external_exports.object({
42996
43036
  name: external_exports.string().max(41).describe("lower_case"),
42997
43037
  type: external_exports.enum(["text", "number", "date", "datetime", "boolean", "uuid", "text_list", "number_list"]).optional().describe("default text"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awesomate/hosting-mcp",
3
- "version": "0.49.0",
3
+ "version": "0.51.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",
@@ -1,5 +1,21 @@
1
1
  {
2
2
  "versions": [
3
+ {
4
+ "version": "0.51.0",
5
+ "highlights": [
6
+ "Your app's staff can now find a customer and put a job under them, straight from the portal. Switch it on per app for the roles that need it (awesomate_crm_apps, customer_lookup). Pro and above",
7
+ "They see name, email and phone only, never a field you marked sensitive, and never the role your customers sign in with: customers still see only themselves"
8
+ ]
9
+ },
10
+ {
11
+ "version": "0.50.0",
12
+ "highlights": [
13
+ "Your n8n can now hear about your app's records (awesomate_crm_hooks): when a job is requested, changed or archived, a workflow runs straight away, with the record attached",
14
+ "Events arrive in order and keep coming back while n8n is down, so a quiet weekend doesn't lose anything. Each has an id, so a workflow can ignore a repeat",
15
+ "Only your own n8n can receive them, protected by a secret your Webhook node checks. Pair it with an app server key to write the result back, such as a drafted quote",
16
+ "New skill, awesomate-portals: ask Claude for a customer portal and it designs the records, who sees what, sign-in by email link and the page, step by step"
17
+ ]
18
+ },
3
19
  {
4
20
  "version": "0.49.0",
5
21
  "highlights": [
@@ -0,0 +1,150 @@
1
+ ---
2
+ name: awesomate-portals
3
+ description: Build a portal or members' app on the account's own data with @awesomate/sdk. Customers sign in by email link and see only their own records, staff see everything, lists update live, and optionally an AI assistant answers in conversations. Use when the user says "customer portal", "client login", "members' area", "let my customers see their jobs / bookings / orders", "staff dashboard", "chat with customers", "portal", or wants people outside the business to sign in to something of theirs. Companion to awesomate-app-builder (hosting the page), awesomate-database and awesomate-n8n: same connection, same token.
4
+ ---
5
+
6
+ # Awesomate Portals: people sign in and see their own records
7
+
8
+ A portal is three things the account already has, put together: **kinds** (the app's own tables,
9
+ in the account's own database), **access rules** on each kind (who sees and changes what, enforced
10
+ by the database itself), and an **app** (where it runs, how people sign in). The page uses
11
+ `@awesomate/sdk` straight from the browser. There is no app server to write.
12
+
13
+ Work in plain words with the owner. They describe their business ("customers request jobs, we
14
+ quote, they see the status"), you turn that into kinds and rules, show them the design in a
15
+ sentence or two, and only then create anything. Kinds, rules and apps are part of their data,
16
+ not scratch space.
17
+
18
+ ## 0. First, every time
19
+
20
+ 1. `awesomate_whoami`: everything below acts on that account. Fix the connection if the slug is
21
+ not the one the user means.
22
+ 2. `awesomate_get_context`: check the plan. **Sign-in for an app's people is Pro and above.**
23
+ Creating kinds and writing data is Support Plus and above. Reading is every plan. A 403
24
+ carries the upgrade path: relay it honestly, never retry it.
25
+
26
+ ## 1. Understand who signs in and what they do
27
+
28
+ Ask, one or two at a time:
29
+ - Who signs in? Usually **customers** (role `member`) and **staff** (role `staff`).
30
+ - What does a customer see? ("their own jobs, and the messages on them")
31
+ - What can a customer do? ("request a job, send a message")
32
+ - What do staff do? ("see every job, set status and quote, reply")
33
+ - Who may sign up: only people the owner adds (`invite`, the default and the safe choice), or
34
+ anyone with the link (`open`, everyone gets the default role)?
35
+
36
+ ## 2. Design the kinds
37
+
38
+ `awesomate_crm_kinds {action:'define'}`. Typical portal:
39
+
40
+ | Kind | Attributes | Links |
41
+ |---|---|---|
42
+ | `job` | `title` (text, required), `status` (choice), `quote` (money) | `customer` to `contact` |
43
+ | `message` | `body` (long_text, required) | `job` to `job` (required) |
44
+
45
+ - **Link the thing to the customer's contact.** That link is what "their own" means.
46
+ - Mark an attribute `readable_by_ai` only when the owner wants Claude and AI assistants to see
47
+ it. Never for anything sensitive. App people see every attribute of a record they may read;
48
+ `readable_by_ai` is only about AI.
49
+ - Show the owner the design ("A job has a title, a status and a quote, and belongs to a
50
+ customer. Messages hang off a job.") and get a yes first.
51
+
52
+ ## 3. Set the access rules
53
+
54
+ `awesomate_crm_kinds {action:'set_access'}`. Rules are per kind and per role:
55
+ `all`, `none`, `own` (records they created), or `linked:<path ending at a contact>`.
56
+
57
+ ```json
58
+ job: {"read": {"member": "linked:customer", "staff": "all"}, "write": {"member": "linked:customer", "staff": "all"}}
59
+ message: {"read": {"member": "linked:job.customer", "staff": "all"}, "write": {"member": "linked:job.customer", "staff": "all"}}
60
+ ```
61
+
62
+ - A role with no rule gets nothing. The database applies these to every read and every write, so
63
+ a mistake in the page can never show anyone more than their role allows.
64
+ - A person can only link a record to something they can see: a customer can point a job at their
65
+ own contact, never someone else's.
66
+ - **Staff putting a job under a customer** needs one more switch, per app: app people read no
67
+ contacts but their own, so by default staff can only link to themselves. Ask the owner, then
68
+ `awesomate_crm_apps {action:'customer_lookup', app_id, roles:['staff']}`. Those roles can then
69
+ search customers (name, email, phone; never a sensitive field) and link a record to any of
70
+ them. Never the app's default role (the one customers sign in with); the hub refuses it.
71
+
72
+ ## 4. Create the app and add people
73
+
74
+ 1. `awesomate_crm_apps {action:'create', name, allowed_origins, sign_up}`. `allowed_origins` are
75
+ the exact addresses the page runs on (`https://portal.example.com.au`, plus
76
+ `http://localhost:5173` while building). The name appears in the sign-in email. The answer
77
+ has the **publishable key** (`pk_...`): it is not a secret and goes in the page.
78
+ 2. `awesomate_crm_app_users {action:'add', email, role}` for each person. A customer is matched to
79
+ their contact by email. Staff get `role: 'staff'`. No email is sent; they ask for a link on the
80
+ page.
81
+
82
+ ## 5. Build the page
83
+
84
+ Any static page works (host it with the awesomate-app-builder skill, `kind: 'static'`). The SDK
85
+ loads from a CDN, or from npm in a Vite app:
86
+
87
+ ```html
88
+ <script type="module">
89
+ import { createAppClient } from 'https://cdn.jsdelivr.net/npm/@awesomate/sdk@0.8/+esm';
90
+ const app = createAppClient({ publishableKey: 'pk_...' });
91
+
92
+ // The email link signs them in by itself (also when it opens in a tab already on this page).
93
+ app.auth.onChange((user) => (user ? showPortal(user) : showSignIn()));
94
+ app.auth.onSignInError((err) => showMessage(err.message));
95
+ const user = await app.auth.user();
96
+ user ? showPortal(user) : showSignIn();
97
+
98
+ // Sign-in form
99
+ await app.auth.signInWithLink(email, { redirectTo: location.href });
100
+
101
+ // Data: the same calls for everyone; the rules decide what each person gets.
102
+ const stop = app.live('job', { orderBy: ['created_at', 'desc'], limit: 100 }, (rows) => renderJobs(rows));
103
+ await app.write('job', { title: 'Leaking tap', status: 'open' }, { links: { customer: user.contact_id } });
104
+ await app.write('message', { body }, { links: { job: jobId } });
105
+
106
+ // Staff, once customer_lookup is on for their role: find a customer, file a job for them.
107
+ const [customer] = await app.lookupCustomers({ q: 'jo' });
108
+ await app.write('job', { title: 'Gutters', status: 'open' }, { links: { customer: customer.id } });
109
+ </script>
110
+ ```
111
+
112
+ - Choose the view from `user.role` (`staff` sees everything and edit controls; `member` sees
113
+ their own records and a request form).
114
+ - `live()` keeps a list current: anything changed anywhere (staff, Claude, n8n) shows at once.
115
+ - To test customer and staff side by side in one browser, give the staff view its own
116
+ `storageKey` (for example when the address has `?staff`).
117
+ - Serve the HTML with `Cache-Control: no-cache` (an `.htaccess` rule on Awesomate hosting), or
118
+ people keep an old copy of the page after you change it.
119
+ - **Never put the account's token (`amt_pat_...`) or a server key (`ak_...`) in a page.** Only
120
+ the `pk_` key belongs in the browser.
121
+
122
+ ## 6. Optional: an AI in the conversations
123
+
124
+ `awesomate_crm_assistant` adds an assistant (for example "Bree") to the message threads. Each
125
+ conversation is Off (people only, the default), Draft (a suggested reply only staff see) or Auto
126
+ (she replies, marked as AI, when she is sure). She reads as the customer she is answering, so
127
+ the access rules bound her, and only `readable_by_ai` attributes reach her. The tool description
128
+ lists the two staff-only kinds it needs (`assistant_thread`, `assistant_draft`) and the
129
+ `from_assistant` attribute on the message kind. Give her a published, public, strict Knowledge
130
+ agent (awesomate-knowledge skill) so she can answer questions about the business. Confirm the
131
+ name and the default mode with the owner: switching it on lets an AI reply to their customers.
132
+
133
+ ## 7. Optional: automation
134
+
135
+ - **Server key** (`awesomate_crm_apps {action:'create_key'}`): for the app's own server or an n8n
136
+ workflow to read and write records. Read or write, only the kinds it needs. Shown once: put it
137
+ straight into the server's environment or an n8n credential, never in a reply or a repository.
138
+ - **Record events** (`awesomate_crm_hooks`): when a record of a kind is created, updated or
139
+ archived, the account's own n8n gets an event at once. Build and activate the workflow first
140
+ (awesomate-n8n skill), then create the hook with its production webhook URL and put the secret
141
+ in the Webhook node's Header Auth credential. Use `test` to check it. Example: a job is
142
+ requested, the workflow drafts a quote and writes it back with a write key for `job`.
143
+
144
+ ## 8. Check it works
145
+
146
+ - Sign in as a customer and as staff (two windows). The customer must not see another
147
+ customer's records; try it.
148
+ - Write as one, watch the other update live.
149
+ - With an assistant: set one job to Draft, ask a question as the customer, see the draft as staff.
150
+ - Tell the owner what you built in their terms, and what is not built yet.