@fil-technology/appmate-mcp 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/dist/index.js +12 -0
- package/dist/tools.js +455 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -68,6 +68,28 @@ staging or self-hosted instances.
|
|
|
68
68
|
| `publish_waitlist_flow` | Promote the waitlist draft live. |
|
|
69
69
|
| `list_waitlist_signups` | Paginated list (cursor + nextCursor). |
|
|
70
70
|
| `export_waitlist_csv` | Return the full waitlist as a CSV string. |
|
|
71
|
+
| `get_feedback_flow` | Read published + draft feedback config. |
|
|
72
|
+
| `update_feedback_draft` | Replace the feedback draft. |
|
|
73
|
+
| `publish_feedback_flow` | Promote the feedback draft live. |
|
|
74
|
+
| `list_feedback_submissions` | Paginated list of feedback rows (rating + message + email). |
|
|
75
|
+
| `get_report_flow` | Read published + draft report config. |
|
|
76
|
+
| `update_report_draft` | Replace the report draft (categorised). |
|
|
77
|
+
| `publish_report_flow` | Promote the report draft live. |
|
|
78
|
+
| `list_report_submissions` | Paginated, optional `category` filter. |
|
|
79
|
+
| `get_contact_flow` | Read published + draft contact config. |
|
|
80
|
+
| `update_contact_draft` | Replace the contact draft. |
|
|
81
|
+
| `publish_contact_flow` | Promote the contact draft live. |
|
|
82
|
+
| `list_contact_submissions` | Paginated list of contact rows (name + email + message). |
|
|
83
|
+
| `get_onboarding_flow` | Read published + draft onboarding (web-to-app funnel) config. |
|
|
84
|
+
| `update_onboarding_draft` | Replace the onboarding draft (quiz / info / email-capture steps + handoff). |
|
|
85
|
+
| `publish_onboarding_flow` | Promote the onboarding draft live. |
|
|
86
|
+
| `list_onboarding_submissions` | Paginated list of funnel completions (answers + email + claim status). |
|
|
87
|
+
| `export_onboarding_csv` | Return all onboarding completions as a CSV string. |
|
|
88
|
+
| `get_referral_flow` | Read published + draft referral program config. |
|
|
89
|
+
| `update_referral_draft` | Replace the referral draft (rewards, share text, cap, landing). |
|
|
90
|
+
| `publish_referral_flow` | Promote the referral draft live. |
|
|
91
|
+
| `list_referrals` | Paginated referral graph (status, referee, reward flags). |
|
|
92
|
+
| `export_referrals_csv` | Return the full referral graph as a CSV string. |
|
|
71
93
|
|
|
72
94
|
Tools that accept an app reference (`get_app`, `update_cancel_draft`,
|
|
73
95
|
etc.) accept either the cuid `id` or the human-readable `slug` — use
|
|
@@ -83,6 +105,10 @@ whichever you have. The full REST shape is documented at
|
|
|
83
105
|
> *"Export the waitlist for `appmate-pro` as CSV and save it to
|
|
84
106
|
> `~/Downloads/waitlist.csv`."*
|
|
85
107
|
|
|
108
|
+
> *"Build a 3-question onboarding funnel for `ledgr` that asks the user's
|
|
109
|
+
> goal, captures their email, and hands off to the App Store, then publish
|
|
110
|
+
> it."*
|
|
111
|
+
|
|
86
112
|
> *"Compare the published and draft cancel configs for `quakemate` and
|
|
87
113
|
> tell me what changed."*
|
|
88
114
|
|
package/dist/index.js
CHANGED
|
@@ -124,6 +124,18 @@ function zodToJsonSchema(schema) {
|
|
|
124
124
|
if (schema instanceof z.ZodBoolean) {
|
|
125
125
|
return { type: "boolean" };
|
|
126
126
|
}
|
|
127
|
+
// z.preprocess / z.transform wrap the real schema in ZodEffects — emit the
|
|
128
|
+
// inner schema's shape so e.g. a preprocessed record still advertises
|
|
129
|
+
// `type: object` to the host.
|
|
130
|
+
if (schema instanceof z.ZodEffects) {
|
|
131
|
+
return zodToJsonSchema(schema._def.schema);
|
|
132
|
+
}
|
|
133
|
+
// A record (open-ended object, e.g. a flow `config`) MUST advertise
|
|
134
|
+
// `type: object` — otherwise hosts may serialize the value to a JSON string
|
|
135
|
+
// and the server rejects it ("expected object, received string").
|
|
136
|
+
if (schema instanceof z.ZodRecord) {
|
|
137
|
+
return { type: "object", additionalProperties: true };
|
|
138
|
+
}
|
|
127
139
|
if (schema instanceof z.ZodUnknown || schema instanceof z.ZodAny) {
|
|
128
140
|
return {};
|
|
129
141
|
}
|
package/dist/tools.js
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { apiFetch, apiFetchText } from "./api-client.js";
|
|
3
|
+
// `config` for the update_*_draft tools is a full flow-config OBJECT. We
|
|
4
|
+
// must advertise it as `type: object` in the JSON schema (see zodToJsonSchema
|
|
5
|
+
// in index.ts) — otherwise an untyped field leads some MCP hosts to serialize
|
|
6
|
+
// it to a JSON string, which the server then rejects with
|
|
7
|
+
// `422: expected object, received string`.
|
|
8
|
+
//
|
|
9
|
+
// Belt-and-braces: we ALSO accept a JSON string and parse it here (via
|
|
10
|
+
// z.preprocess), since a few hosts stringify nested args regardless. Either
|
|
11
|
+
// way the server receives a real object, never `"{...}"`.
|
|
12
|
+
function parseJsonStringConfig(v) {
|
|
13
|
+
if (typeof v === "string") {
|
|
14
|
+
const trimmed = v.trim();
|
|
15
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(trimmed);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// Not valid JSON — leave it; record validation reports a clear error.
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return v;
|
|
25
|
+
}
|
|
26
|
+
// Shared input for every update_*_draft tool: an app reference + the full
|
|
27
|
+
// config object. The inner record makes `config` advertise `type: object`;
|
|
28
|
+
// the preprocess tolerates a stringified body.
|
|
29
|
+
const updateDraftInput = z.object({
|
|
30
|
+
appIdOrSlug: z.string().min(1),
|
|
31
|
+
config: z.preprocess(parseJsonStringConfig, z.record(z.string(), z.unknown())),
|
|
32
|
+
});
|
|
3
33
|
// ─── Apps ───────────────────────────────────────────────────────────────────
|
|
4
34
|
export const listApps = {
|
|
5
35
|
name: "list_apps",
|
|
@@ -46,10 +76,11 @@ export const getCancelFlow = {
|
|
|
46
76
|
inputSchema: z.object({ appIdOrSlug: z.string().min(1) }),
|
|
47
77
|
handler: (input, cfg) => apiFetch(cfg, "GET", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/flows/cancel`),
|
|
48
78
|
};
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
// mismatches (e.g. showThanksScreen + a
|
|
79
|
+
// `config` is the full flow-config object (advertised as type:object via
|
|
80
|
+
// updateDraftInput so hosts pass structured JSON). The server holds the
|
|
81
|
+
// canonical Zod schema: it returns 422 with paths on validation errors AND a
|
|
82
|
+
// `warnings` array on success for soft mismatches (e.g. showThanksScreen + a
|
|
83
|
+
// "Contact support" label).
|
|
53
84
|
export const updateCancelDraft = {
|
|
54
85
|
name: "update_cancel_draft",
|
|
55
86
|
description: [
|
|
@@ -101,10 +132,7 @@ export const updateCancelDraft = {
|
|
|
101
132
|
"",
|
|
102
133
|
"See https://docs.appmate.cloud/ai-agents for the full do/don't guide and examples.",
|
|
103
134
|
].join("\n"),
|
|
104
|
-
inputSchema:
|
|
105
|
-
appIdOrSlug: z.string().min(1),
|
|
106
|
-
config: z.unknown(),
|
|
107
|
-
}),
|
|
135
|
+
inputSchema: updateDraftInput,
|
|
108
136
|
handler: (input, cfg) => apiFetch(cfg, "PUT", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/flows/cancel`, input.config),
|
|
109
137
|
};
|
|
110
138
|
export const publishCancelFlow = {
|
|
@@ -125,27 +153,52 @@ export const updateWaitlistDraft = {
|
|
|
125
153
|
description: [
|
|
126
154
|
"Replace the draft waitlist config. Body MUST be a full waitlist config object (type: 'waitlist').",
|
|
127
155
|
"",
|
|
128
|
-
"
|
|
156
|
+
"The public URL at signup.appmate.cloud/{slug} renders a FULL landing",
|
|
157
|
+
"page — not just a form. The `hero` block drives the visual treatment;",
|
|
158
|
+
"omit it for the minimal/legacy look.",
|
|
159
|
+
"",
|
|
160
|
+
"Full shape:",
|
|
129
161
|
" {",
|
|
130
162
|
" type: 'waitlist',",
|
|
131
163
|
" intro: {",
|
|
132
|
-
" title,
|
|
133
|
-
"
|
|
134
|
-
"
|
|
164
|
+
" title, // h1 on the landing",
|
|
165
|
+
" subtitle, // lede paragraph",
|
|
166
|
+
" emailPlaceholder, // input placeholder, e.g. 'you@example.com'",
|
|
167
|
+
" submitLabel, // button text, e.g. 'Notify me'",
|
|
168
|
+
" legal? // small print under the form (optional)",
|
|
135
169
|
" },",
|
|
136
170
|
" success: {",
|
|
137
|
-
" title, body,",
|
|
138
|
-
" ctaLabel?, ctaUrl?
|
|
139
|
-
" }",
|
|
171
|
+
" title, body, // shown after signup",
|
|
172
|
+
" ctaLabel?, ctaUrl? // both-or-neither — partial pair renders nothing",
|
|
173
|
+
" },",
|
|
174
|
+
" hero?: { // ALL fields optional; omit `hero` entirely for minimal",
|
|
175
|
+
" theme?: 'minimal' | 'gradient' | 'dark' | 'side_by_side',",
|
|
176
|
+
" eyebrow?: string, // short chip above title, e.g. 'Coming soon · Q1 2026'",
|
|
177
|
+
" accentColor?: string, // hex '#rrggbb'; tints button + chip + gradient blob",
|
|
178
|
+
" bullets?: [ // 0–5 value-prop cards under the form",
|
|
179
|
+
" { icon?: '✨', title: 'Fast', body?: 'Sub-second responses' }",
|
|
180
|
+
" ],",
|
|
181
|
+
" showCount?: boolean, // renders '{N} on the waitlist' pill (hides if <3 signups)",
|
|
182
|
+
" heroImage?: string, // optional URL of a hero image",
|
|
183
|
+
" },",
|
|
184
|
+
" templateId?: string // analytics tag if seeded from a template",
|
|
140
185
|
" }",
|
|
141
186
|
"",
|
|
187
|
+
"Theme picker guide — pick from intent:",
|
|
188
|
+
" - minimal → no brand, conservative B2B. Default.",
|
|
189
|
+
" - gradient → marketing-launch energy. Pastel blobs + accent color.",
|
|
190
|
+
" - dark → premium / product-reveal vibe. Dark hero + accent glow.",
|
|
191
|
+
" - side_by_side → desktop-first 2-column (story left, form right).",
|
|
192
|
+
" Collapses to minimal on phone — fine for landing-pageiness.",
|
|
193
|
+
"",
|
|
194
|
+
"Starter templates available (see /examples?kind=waitlist):",
|
|
195
|
+
" minimal_email_only, feature_tease, launching_soon, pro_upsell,",
|
|
196
|
+
" private_beta, early_access_referral.",
|
|
197
|
+
"",
|
|
142
198
|
"Server returns { ok:true, warnings: [...] } — check warnings before publishing.",
|
|
143
|
-
"Common: partial_cta (label without url, or vice versa).",
|
|
199
|
+
"Common warnings: partial_cta (label without url, or vice versa).",
|
|
144
200
|
].join("\n"),
|
|
145
|
-
inputSchema:
|
|
146
|
-
appIdOrSlug: z.string().min(1),
|
|
147
|
-
config: z.unknown(),
|
|
148
|
-
}),
|
|
201
|
+
inputSchema: updateDraftInput,
|
|
149
202
|
handler: (input, cfg) => apiFetch(cfg, "PUT", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/flows/waitlist`, input.config),
|
|
150
203
|
};
|
|
151
204
|
export const publishWaitlistFlow = {
|
|
@@ -182,17 +235,399 @@ export const exportWaitlistCsv = {
|
|
|
182
235
|
return { csv };
|
|
183
236
|
},
|
|
184
237
|
};
|
|
238
|
+
// ─── Feedback flow ──────────────────────────────────────────────────────────
|
|
239
|
+
export const getFeedbackFlow = {
|
|
240
|
+
name: "get_feedback_flow",
|
|
241
|
+
description: "Read the published and draft feedback flow configs for an app. Feedback flows host an open-ended message form (optional 1–5 star rating + optional reply email) at appmate.cloud/feedback/{appSlug}.",
|
|
242
|
+
inputSchema: z.object({ appIdOrSlug: z.string().min(1) }),
|
|
243
|
+
handler: (input, cfg) => apiFetch(cfg, "GET", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/flows/feedback`),
|
|
244
|
+
};
|
|
245
|
+
export const updateFeedbackDraft = {
|
|
246
|
+
name: "update_feedback_draft",
|
|
247
|
+
description: [
|
|
248
|
+
"Replace the draft feedback config. Body MUST be a full feedback config object (type: 'feedback').",
|
|
249
|
+
"",
|
|
250
|
+
"Shape:",
|
|
251
|
+
" {",
|
|
252
|
+
" type: 'feedback',",
|
|
253
|
+
" intro: {",
|
|
254
|
+
" title, subtitle,",
|
|
255
|
+
" messagePlaceholder, // textarea placeholder",
|
|
256
|
+
" submitLabel,",
|
|
257
|
+
" legal? // small print under the form (optional)",
|
|
258
|
+
" },",
|
|
259
|
+
" rating?: { // OPTIONAL 1–5 star widget",
|
|
260
|
+
" enabled: true,",
|
|
261
|
+
" prompt?: 'How would you rate your experience?',",
|
|
262
|
+
" required?: false // when true, blocks submit until picked",
|
|
263
|
+
" },",
|
|
264
|
+
" emailField?: { // OPTIONAL reply-to email field",
|
|
265
|
+
" enabled: true,",
|
|
266
|
+
" placeholder?: 'you@example.com (optional)',",
|
|
267
|
+
" required?: false",
|
|
268
|
+
" },",
|
|
269
|
+
" success: {",
|
|
270
|
+
" title, body,",
|
|
271
|
+
" ctaLabel?, ctaUrl? // both-or-neither follow-up CTA",
|
|
272
|
+
" },",
|
|
273
|
+
" hero?: { // visual treatment, matches waitlist hero",
|
|
274
|
+
" theme?: 'minimal' | 'gradient' | 'dark' | 'side_by_side',",
|
|
275
|
+
" eyebrow?, accentColor?, titleFont?",
|
|
276
|
+
" }",
|
|
277
|
+
" }",
|
|
278
|
+
"",
|
|
279
|
+
"Server returns { ok:true, warnings: [] }. Warning rules will be added later — for now treat any non-empty array as advisory.",
|
|
280
|
+
].join("\n"),
|
|
281
|
+
inputSchema: updateDraftInput,
|
|
282
|
+
handler: (input, cfg) => apiFetch(cfg, "PUT", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/flows/feedback`, input.config),
|
|
283
|
+
};
|
|
284
|
+
export const publishFeedbackFlow = {
|
|
285
|
+
name: "publish_feedback_flow",
|
|
286
|
+
description: "Promote the draft feedback config to the live published version. Visitors at appmate.cloud/feedback/{appSlug} see the new version immediately.",
|
|
287
|
+
inputSchema: z.object({ appIdOrSlug: z.string().min(1) }),
|
|
288
|
+
handler: (input, cfg) => apiFetch(cfg, "POST", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/flows/feedback/publish`),
|
|
289
|
+
};
|
|
290
|
+
export const listFeedbackSubmissions = {
|
|
291
|
+
name: "list_feedback_submissions",
|
|
292
|
+
description: "Paginated list of feedback submissions for an app. Each row: { id, message, rating, email, source, createdAt }. limit max 200, default 50; pass nextCursor back for the next page.",
|
|
293
|
+
inputSchema: z.object({
|
|
294
|
+
appIdOrSlug: z.string().min(1),
|
|
295
|
+
limit: z.number().int().min(1).max(200).optional(),
|
|
296
|
+
cursor: z.string().optional(),
|
|
297
|
+
}),
|
|
298
|
+
handler: (input, cfg) => {
|
|
299
|
+
const qs = new URLSearchParams();
|
|
300
|
+
if (input.limit !== undefined)
|
|
301
|
+
qs.set("limit", String(input.limit));
|
|
302
|
+
if (input.cursor)
|
|
303
|
+
qs.set("cursor", input.cursor);
|
|
304
|
+
const tail = qs.toString() ? `?${qs.toString()}` : "";
|
|
305
|
+
return apiFetch(cfg, "GET", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/feedback/submissions${tail}`);
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
// ─── Report flow ────────────────────────────────────────────────────────────
|
|
309
|
+
export const getReportFlow = {
|
|
310
|
+
name: "get_report_flow",
|
|
311
|
+
description: "Read the published and draft report flow configs for an app. Report flows host a categorised bug/abuse/spam form (required category picker + message + optional reply email) at appmate.cloud/report/{appSlug}.",
|
|
312
|
+
inputSchema: z.object({ appIdOrSlug: z.string().min(1) }),
|
|
313
|
+
handler: (input, cfg) => apiFetch(cfg, "GET", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/flows/report`),
|
|
314
|
+
};
|
|
315
|
+
export const updateReportDraft = {
|
|
316
|
+
name: "update_report_draft",
|
|
317
|
+
description: [
|
|
318
|
+
"Replace the draft report config. Body MUST be a full report config object (type: 'report').",
|
|
319
|
+
"",
|
|
320
|
+
"Shape:",
|
|
321
|
+
" {",
|
|
322
|
+
" type: 'report',",
|
|
323
|
+
" intro: {",
|
|
324
|
+
" title, subtitle,",
|
|
325
|
+
" messagePlaceholder, // textarea placeholder",
|
|
326
|
+
" submitLabel,",
|
|
327
|
+
" legal? // optional small print",
|
|
328
|
+
" },",
|
|
329
|
+
" categories: [ // REQUIRED 1–10 entries",
|
|
330
|
+
" { id: 'bug', label: 'Bug or crash', emoji?: '🐞', hint?: '…' },",
|
|
331
|
+
" { id: 'abuse', label: 'Harassment or abuse', emoji?: '🚫' },",
|
|
332
|
+
" { id: 'spam', label: 'Spam', emoji?: '🧹' },",
|
|
333
|
+
" { id: 'privacy', label: 'Privacy concern', emoji?: '🔒' },",
|
|
334
|
+
" { id: 'other', label: 'Something else', emoji?: '💬' }",
|
|
335
|
+
" ],",
|
|
336
|
+
" emailField?: { enabled, placeholder?, required? },",
|
|
337
|
+
" success: { title, body, ctaLabel?, ctaUrl? },",
|
|
338
|
+
" hero?: { theme?, eyebrow?, accentColor?, titleFont? }",
|
|
339
|
+
" }",
|
|
340
|
+
"",
|
|
341
|
+
"Category ids must be snake_case ([a-z][a-z0-9_]*). The public submit endpoint validates posted category against this list — unknown ids return 422.",
|
|
342
|
+
].join("\n"),
|
|
343
|
+
inputSchema: updateDraftInput,
|
|
344
|
+
handler: (input, cfg) => apiFetch(cfg, "PUT", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/flows/report`, input.config),
|
|
345
|
+
};
|
|
346
|
+
export const publishReportFlow = {
|
|
347
|
+
name: "publish_report_flow",
|
|
348
|
+
description: "Promote the draft report config to the live published version. Visitors at appmate.cloud/report/{appSlug} see the new version immediately.",
|
|
349
|
+
inputSchema: z.object({ appIdOrSlug: z.string().min(1) }),
|
|
350
|
+
handler: (input, cfg) => apiFetch(cfg, "POST", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/flows/report/publish`),
|
|
351
|
+
};
|
|
352
|
+
export const listReportSubmissions = {
|
|
353
|
+
name: "list_report_submissions",
|
|
354
|
+
description: "Paginated list of report submissions for an app. Each row: { id, message, category, email, source, createdAt }. Pass `category` to scope to one bucket (e.g. 'bug') for triage.",
|
|
355
|
+
inputSchema: z.object({
|
|
356
|
+
appIdOrSlug: z.string().min(1),
|
|
357
|
+
limit: z.number().int().min(1).max(200).optional(),
|
|
358
|
+
cursor: z.string().optional(),
|
|
359
|
+
category: z.string().optional(),
|
|
360
|
+
}),
|
|
361
|
+
handler: (input, cfg) => {
|
|
362
|
+
const qs = new URLSearchParams();
|
|
363
|
+
if (input.limit !== undefined)
|
|
364
|
+
qs.set("limit", String(input.limit));
|
|
365
|
+
if (input.cursor)
|
|
366
|
+
qs.set("cursor", input.cursor);
|
|
367
|
+
if (input.category)
|
|
368
|
+
qs.set("category", input.category);
|
|
369
|
+
const tail = qs.toString() ? `?${qs.toString()}` : "";
|
|
370
|
+
return apiFetch(cfg, "GET", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/report/submissions${tail}`);
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
// ─── Contact flow ───────────────────────────────────────────────────────────
|
|
374
|
+
export const getContactFlow = {
|
|
375
|
+
name: "get_contact_flow",
|
|
376
|
+
description: "Read the published and draft contact flow configs for an app. Contact flows host a minimal inquiry form (optional name + required/optional email + optional message text) at appmate.cloud/contact/{appSlug}.",
|
|
377
|
+
inputSchema: z.object({ appIdOrSlug: z.string().min(1) }),
|
|
378
|
+
handler: (input, cfg) => apiFetch(cfg, "GET", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/flows/contact`),
|
|
379
|
+
};
|
|
380
|
+
export const updateContactDraft = {
|
|
381
|
+
name: "update_contact_draft",
|
|
382
|
+
description: [
|
|
383
|
+
"Replace the draft contact config. Body MUST be a full contact config object (type: 'contact').",
|
|
384
|
+
"",
|
|
385
|
+
"Shape:",
|
|
386
|
+
" {",
|
|
387
|
+
" type: 'contact',",
|
|
388
|
+
" intro: {",
|
|
389
|
+
" title, subtitle,",
|
|
390
|
+
" submitLabel,",
|
|
391
|
+
" legal? // optional small print under the form",
|
|
392
|
+
" },",
|
|
393
|
+
" nameField?: { // OPTIONAL name widget",
|
|
394
|
+
" enabled: boolean,",
|
|
395
|
+
" placeholder?: 'Your name',",
|
|
396
|
+
" required?: boolean",
|
|
397
|
+
" },",
|
|
398
|
+
" emailField?: { // OPTIONAL/REQUIRED email input",
|
|
399
|
+
" enabled: boolean,",
|
|
400
|
+
" placeholder?: 'you@example.com',",
|
|
401
|
+
" required?: boolean",
|
|
402
|
+
" },",
|
|
403
|
+
" messageField?: { // OPTIONAL message textarea",
|
|
404
|
+
" enabled: boolean,",
|
|
405
|
+
" placeholder?: 'What is on your mind?',",
|
|
406
|
+
" required?: boolean",
|
|
407
|
+
" },",
|
|
408
|
+
" success: {",
|
|
409
|
+
" title, body,",
|
|
410
|
+
" ctaLabel?, ctaUrl? // optional follow-up button pair",
|
|
411
|
+
" },",
|
|
412
|
+
" hero?: { // dynamic landing theme config",
|
|
413
|
+
" theme?: 'minimal' | 'gradient' | 'dark' | 'side_by_side',",
|
|
414
|
+
" eyebrow?, accentColor?, titleFont?",
|
|
415
|
+
" }",
|
|
416
|
+
" }",
|
|
417
|
+
].join("\n"),
|
|
418
|
+
inputSchema: updateDraftInput,
|
|
419
|
+
handler: (input, cfg) => apiFetch(cfg, "PUT", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/flows/contact`, input.config),
|
|
420
|
+
};
|
|
421
|
+
export const publishContactFlow = {
|
|
422
|
+
name: "publish_contact_flow",
|
|
423
|
+
description: "Promote the draft contact config to the live published version. Visitors at appmate.cloud/contact/{appSlug} see the new version live immediately.",
|
|
424
|
+
inputSchema: z.object({ appIdOrSlug: z.string().min(1) }),
|
|
425
|
+
handler: (input, cfg) => apiFetch(cfg, "POST", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/flows/contact/publish`),
|
|
426
|
+
};
|
|
427
|
+
export const listContactSubmissions = {
|
|
428
|
+
name: "list_contact_submissions",
|
|
429
|
+
description: "Paginated list of contact submissions for an app. Each row: { id, name, email, message, source, country, createdAt }. limit max 200, default 50; pass nextCursor back for next page.",
|
|
430
|
+
inputSchema: z.object({
|
|
431
|
+
appIdOrSlug: z.string().min(1),
|
|
432
|
+
limit: z.number().int().min(1).max(200).optional(),
|
|
433
|
+
cursor: z.string().optional(),
|
|
434
|
+
}),
|
|
435
|
+
handler: (input, cfg) => {
|
|
436
|
+
const qs = new URLSearchParams();
|
|
437
|
+
if (input.limit !== undefined)
|
|
438
|
+
qs.set("limit", String(input.limit));
|
|
439
|
+
if (input.cursor)
|
|
440
|
+
qs.set("cursor", input.cursor);
|
|
441
|
+
const tail = qs.toString() ? `?${qs.toString()}` : "";
|
|
442
|
+
return apiFetch(cfg, "GET", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/contact/submissions${tail}`);
|
|
443
|
+
},
|
|
444
|
+
};
|
|
445
|
+
// ─── Onboarding flow (web-to-app funnel) ─────────────────────────────────────
|
|
446
|
+
export const getOnboardingFlow = {
|
|
447
|
+
name: "get_onboarding_flow",
|
|
448
|
+
description: "Read the published and draft onboarding flow configs for an app. Onboarding flows are web-to-app funnels (intro → quiz/info/email-capture steps → App Store handoff) hosted at appmate.cloud/onboarding/{appSlug}. Answers + email are captured server-side; the iOS SDK recovers them on first launch via a claim token.",
|
|
449
|
+
inputSchema: z.object({ appIdOrSlug: z.string().min(1) }),
|
|
450
|
+
handler: (input, cfg) => apiFetch(cfg, "GET", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/flows/onboarding`),
|
|
451
|
+
};
|
|
452
|
+
export const updateOnboardingDraft = {
|
|
453
|
+
name: "update_onboarding_draft",
|
|
454
|
+
description: [
|
|
455
|
+
"Replace the draft onboarding config. Body MUST be a full onboarding config object (type: 'onboarding').",
|
|
456
|
+
"",
|
|
457
|
+
"Shape:",
|
|
458
|
+
" {",
|
|
459
|
+
" type: 'onboarding',",
|
|
460
|
+
" intro: { title, subtitle, startLabel, eyebrow? },",
|
|
461
|
+
" steps: [ // REQUIRED 1–20, ordered",
|
|
462
|
+
" // question step — pick one or several options",
|
|
463
|
+
" {",
|
|
464
|
+
" kind: 'question', id: 'goal',",
|
|
465
|
+
" prompt: 'What brings you here?', subtitle?: '…',",
|
|
466
|
+
" selectMode?: 'single' | 'multi', // omit → single",
|
|
467
|
+
" autoAdvance?: true, // single only: tap advances",
|
|
468
|
+
" required?: true,",
|
|
469
|
+
" options: [ { id: 'save_time', label: 'Save time', emoji?: '⚡' } ]",
|
|
470
|
+
" },",
|
|
471
|
+
" // info step — copy/image screen, no input",
|
|
472
|
+
" { kind: 'info', id: 'value', title: '…', body: '…', imageUrl?, continueLabel? },",
|
|
473
|
+
" // email_capture step — the lead-capture moment",
|
|
474
|
+
" {",
|
|
475
|
+
" kind: 'email_capture', id: 'email',",
|
|
476
|
+
" title: '…', subtitle?, placeholder?, submitLabel?,",
|
|
477
|
+
" required?: true, // omit → required",
|
|
478
|
+
" legal?: '…'",
|
|
479
|
+
" }",
|
|
480
|
+
" ],",
|
|
481
|
+
" handoff: {",
|
|
482
|
+
" title, body, ctaLabel,",
|
|
483
|
+
" appStoreUrl?, // App Store / TestFlight URL; omit → no button",
|
|
484
|
+
" legal?",
|
|
485
|
+
" },",
|
|
486
|
+
" hero?: { theme?, eyebrow?, accentColor?, titleFont? }",
|
|
487
|
+
" }",
|
|
488
|
+
"",
|
|
489
|
+
"Step + option ids must be snake_case ([a-z][a-z0-9_]*) and unique. The server returns { ok:true, warnings:[...] } — warnings flag a missing email_capture step, a handoff with no appStoreUrl, duplicate ids, etc. Treat warnings as advisory.",
|
|
490
|
+
].join("\n"),
|
|
491
|
+
inputSchema: updateDraftInput,
|
|
492
|
+
handler: (input, cfg) => apiFetch(cfg, "PUT", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/flows/onboarding`, input.config),
|
|
493
|
+
};
|
|
494
|
+
export const publishOnboardingFlow = {
|
|
495
|
+
name: "publish_onboarding_flow",
|
|
496
|
+
description: "Promote the draft onboarding config to the live published version. Visitors at appmate.cloud/onboarding/{appSlug} see the new funnel immediately.",
|
|
497
|
+
inputSchema: z.object({ appIdOrSlug: z.string().min(1) }),
|
|
498
|
+
handler: (input, cfg) => apiFetch(cfg, "POST", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/flows/onboarding/publish`),
|
|
499
|
+
};
|
|
500
|
+
export const listOnboardingSubmissions = {
|
|
501
|
+
name: "list_onboarding_submissions",
|
|
502
|
+
description: "Paginated list of completed onboarding funnels for an app. Each row: { id, answers, email, source, claimed, claimedAt, country, createdAt }. `claimed` is true once the install was matched back to the completion. limit max 200, default 50; pass nextCursor back for the next page.",
|
|
503
|
+
inputSchema: z.object({
|
|
504
|
+
appIdOrSlug: z.string().min(1),
|
|
505
|
+
limit: z.number().int().min(1).max(200).optional(),
|
|
506
|
+
cursor: z.string().optional(),
|
|
507
|
+
}),
|
|
508
|
+
handler: (input, cfg) => {
|
|
509
|
+
const qs = new URLSearchParams();
|
|
510
|
+
if (input.limit !== undefined)
|
|
511
|
+
qs.set("limit", String(input.limit));
|
|
512
|
+
if (input.cursor)
|
|
513
|
+
qs.set("cursor", input.cursor);
|
|
514
|
+
const tail = qs.toString() ? `?${qs.toString()}` : "";
|
|
515
|
+
return apiFetch(cfg, "GET", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/onboarding/submissions${tail}`);
|
|
516
|
+
},
|
|
517
|
+
};
|
|
518
|
+
export const exportOnboardingCsv = {
|
|
519
|
+
name: "export_onboarding_csv",
|
|
520
|
+
description: "Return all completed onboarding funnels for an app as a CSV string (header row + one row per completion, with email + answers JSON + claim status). Useful for hand-off to a spreadsheet or mail merge.",
|
|
521
|
+
inputSchema: z.object({ appIdOrSlug: z.string().min(1) }),
|
|
522
|
+
handler: async (input, cfg) => {
|
|
523
|
+
const csv = await apiFetchText(cfg, "GET", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/onboarding/submissions.csv`);
|
|
524
|
+
return { csv };
|
|
525
|
+
},
|
|
526
|
+
};
|
|
527
|
+
// ─── Referral flow ───────────────────────────────────────────────────────────
|
|
528
|
+
export const getReferralFlow = {
|
|
529
|
+
name: "get_referral_flow",
|
|
530
|
+
description: "Read the published and draft referral program config for an app. Referral is a share-with-a-friend loop: each user gets a link (appmate.cloud/r/{code}) AND a short human-readable code (e.g. K7Q4-R9XP); a friend who taps the link or types the code, then installs, triggers a reward for both sides. Codes + the referral graph live server-side; the iOS SDK mints links/codes, attributes installs on first launch (clipboard handoff) or via a typed code, and reports rewards owed.",
|
|
531
|
+
inputSchema: z.object({ appIdOrSlug: z.string().min(1) }),
|
|
532
|
+
handler: (input, cfg) => apiFetch(cfg, "GET", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/flows/referral`),
|
|
533
|
+
};
|
|
534
|
+
export const updateReferralDraft = {
|
|
535
|
+
name: "update_referral_draft",
|
|
536
|
+
description: [
|
|
537
|
+
"Replace the draft referral config. Body MUST be a full referral config object (type: 'referral').",
|
|
538
|
+
"",
|
|
539
|
+
"Shape:",
|
|
540
|
+
" {",
|
|
541
|
+
" type: 'referral',",
|
|
542
|
+
" landing: { // the invite page a friend sees at /r/{code}",
|
|
543
|
+
" eyebrow?, title, subtitle, ctaLabel,",
|
|
544
|
+
" appStoreUrl?, // REQUIRED before go-live — the install button target",
|
|
545
|
+
" legal?",
|
|
546
|
+
" },",
|
|
547
|
+
" share: { messageTemplate }, // the referrer's share text; the link is appended automatically",
|
|
548
|
+
" rewards: {",
|
|
549
|
+
" referrerWeeks: 1, // free weeks the referrer earns per installed friend (1–8)",
|
|
550
|
+
" refereeEnabled: true, // also reward the NEW user on install?",
|
|
551
|
+
" refereeWeeks?: 1, // required when refereeEnabled (1–8)",
|
|
552
|
+
" referrerLabel?, refereeLabel? // human copy shown on the landing + returned to the SDK",
|
|
553
|
+
" },",
|
|
554
|
+
" maxRewardsPerReferrer?: 10 // lifetime cap per referrer; 0/omit = unlimited (farming risk)",
|
|
555
|
+
" }",
|
|
556
|
+
"",
|
|
557
|
+
"Reward trigger is the friend's INSTALL (attributed on first launch). The server returns { ok:true, warnings:[...] } — warnings flag a missing/placeholder appStoreUrl, refereeEnabled without refereeWeeks, and an absent cap. Treat warnings as advisory but fix them before publishing.",
|
|
558
|
+
].join("\n"),
|
|
559
|
+
inputSchema: updateDraftInput,
|
|
560
|
+
handler: (input, cfg) => apiFetch(cfg, "PUT", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/flows/referral`, input.config),
|
|
561
|
+
};
|
|
562
|
+
export const publishReferralFlow = {
|
|
563
|
+
name: "publish_referral_flow",
|
|
564
|
+
description: "Promote the draft referral config to the live published version. New share links + the invite landing use the new config immediately.",
|
|
565
|
+
inputSchema: z.object({ appIdOrSlug: z.string().min(1) }),
|
|
566
|
+
handler: (input, cfg) => apiFetch(cfg, "POST", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/flows/referral/publish`),
|
|
567
|
+
};
|
|
568
|
+
export const listReferrals = {
|
|
569
|
+
name: "list_referrals",
|
|
570
|
+
description: "Paginated list of referrals for an app. Each row: { id, code, referrerUserId, status, source, refereeUserId, attributedAt, referrerRewarded, refereeRewarded, country, createdAt }. status='attributed' means the friend installed; source is 'link' (tapped share link) or 'code' (typed the referrer's code). Optional `status` filter; limit max 200, default 50.",
|
|
571
|
+
inputSchema: z.object({
|
|
572
|
+
appIdOrSlug: z.string().min(1),
|
|
573
|
+
status: z.enum(["pending", "attributed", "expired"]).optional(),
|
|
574
|
+
limit: z.number().int().min(1).max(200).optional(),
|
|
575
|
+
cursor: z.string().optional(),
|
|
576
|
+
}),
|
|
577
|
+
handler: (input, cfg) => {
|
|
578
|
+
const qs = new URLSearchParams();
|
|
579
|
+
if (input.status)
|
|
580
|
+
qs.set("status", input.status);
|
|
581
|
+
if (input.limit !== undefined)
|
|
582
|
+
qs.set("limit", String(input.limit));
|
|
583
|
+
if (input.cursor)
|
|
584
|
+
qs.set("cursor", input.cursor);
|
|
585
|
+
const tail = qs.toString() ? `?${qs.toString()}` : "";
|
|
586
|
+
return apiFetch(cfg, "GET", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/referrals${tail}`);
|
|
587
|
+
},
|
|
588
|
+
};
|
|
589
|
+
export const exportReferralsCsv = {
|
|
590
|
+
name: "export_referrals_csv",
|
|
591
|
+
description: "Return the full referral graph for an app as a CSV string (one row per invite, with code, status, source (link/typed-code), referee, and reward flags).",
|
|
592
|
+
inputSchema: z.object({ appIdOrSlug: z.string().min(1) }),
|
|
593
|
+
handler: async (input, cfg) => {
|
|
594
|
+
const csv = await apiFetchText(cfg, "GET", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/referrals.csv`);
|
|
595
|
+
return { csv };
|
|
596
|
+
},
|
|
597
|
+
};
|
|
185
598
|
// Registered alphabetically so `list_tools` reads predictably.
|
|
186
599
|
export const ALL_TOOLS = [
|
|
187
600
|
createApp,
|
|
601
|
+
exportOnboardingCsv,
|
|
602
|
+
exportReferralsCsv,
|
|
188
603
|
exportWaitlistCsv,
|
|
189
604
|
getApp,
|
|
190
605
|
getCancelFlow,
|
|
606
|
+
getContactFlow,
|
|
607
|
+
getFeedbackFlow,
|
|
608
|
+
getOnboardingFlow,
|
|
609
|
+
getReferralFlow,
|
|
610
|
+
getReportFlow,
|
|
191
611
|
getWaitlistFlow,
|
|
192
612
|
listApps,
|
|
613
|
+
listContactSubmissions,
|
|
614
|
+
listFeedbackSubmissions,
|
|
615
|
+
listOnboardingSubmissions,
|
|
616
|
+
listReferrals,
|
|
617
|
+
listReportSubmissions,
|
|
193
618
|
listWaitlistSignups,
|
|
194
619
|
publishCancelFlow,
|
|
620
|
+
publishContactFlow,
|
|
621
|
+
publishFeedbackFlow,
|
|
622
|
+
publishOnboardingFlow,
|
|
623
|
+
publishReferralFlow,
|
|
624
|
+
publishReportFlow,
|
|
195
625
|
publishWaitlistFlow,
|
|
196
626
|
updateCancelDraft,
|
|
627
|
+
updateContactDraft,
|
|
628
|
+
updateFeedbackDraft,
|
|
629
|
+
updateOnboardingDraft,
|
|
630
|
+
updateReferralDraft,
|
|
631
|
+
updateReportDraft,
|
|
197
632
|
updateWaitlistDraft,
|
|
198
633
|
];
|
package/package.json
CHANGED