@fil-technology/appmate-mcp 0.5.0 → 0.6.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.
Files changed (3) hide show
  1. package/README.md +8 -1
  2. package/dist/tools.js +221 -1
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -88,8 +88,15 @@ staging or self-hosted instances.
88
88
  | `get_referral_flow` | Read published + draft referral program config. |
89
89
  | `update_referral_draft` | Replace the referral draft (rewards, share text, cap, landing). |
90
90
  | `publish_referral_flow` | Promote the referral draft live. |
91
- | `list_referrals` | Paginated referral graph (status, referee, reward flags). |
91
+ | `list_referrals` | Paginated referral graph (status, `source` [link or typed-code], referee, reward flags). |
92
92
  | `export_referrals_csv` | Return the full referral graph as a CSV string. |
93
+ | `get_link_page_flow` | Read published + draft link-page (link-in-bio) config. |
94
+ | `update_link_page_draft` | Replace the link-page draft (header, icon links, link list, theme). |
95
+ | `publish_link_page_flow` | Promote the link-page draft live (appmate.cloud/p/{appSlug}). |
96
+
97
+ Each referrer gets a unique link **and** a short, human-readable code (e.g.
98
+ `K7Q4-R9XP`); a friend redeems by tapping the link or **typing the code** (no
99
+ clipboard needed). `source` on each referral row records which path was used.
93
100
 
94
101
  Tools that accept an app reference (`get_app`, `update_cancel_draft`,
95
102
  etc.) accept either the cuid `id` or the human-readable `slug` — use
package/dist/tools.js CHANGED
@@ -175,7 +175,7 @@ export const updateWaitlistDraft = {
175
175
  " theme?: 'minimal' | 'gradient' | 'dark' | 'side_by_side',",
176
176
  " eyebrow?: string, // short chip above title, e.g. 'Coming soon · Q1 2026'",
177
177
  " accentColor?: string, // hex '#rrggbb'; tints button + chip + gradient blob",
178
- " bullets?: [ // 0–5 value-prop cards under the form",
178
+ " bullets?: [ // 0–5 value-prop cards, stacked one per row under the form",
179
179
  " { icon?: '✨', title: 'Fast', body?: 'Sub-second responses' }",
180
180
  " ],",
181
181
  " showCount?: boolean, // renders '{N} on the waitlist' pill (hides if <3 signups)",
@@ -250,6 +250,7 @@ export const updateFeedbackDraft = {
250
250
  "Shape:",
251
251
  " {",
252
252
  " type: 'feedback',",
253
+ " layout: 'steps' | 'single', // optional, default 'steps' (multi-step: rating+message, then email). 'single' = one screen.",
253
254
  " intro: {",
254
255
  " title, subtitle,",
255
256
  " messagePlaceholder, // textarea placeholder",
@@ -320,6 +321,7 @@ export const updateReportDraft = {
320
321
  "Shape:",
321
322
  " {",
322
323
  " type: 'report',",
324
+ " layout: 'steps' | 'single', // optional, default 'steps' (multi-step: category, then message+email). 'single' = one screen.",
323
325
  " intro: {",
324
326
  " title, subtitle,",
325
327
  " messagePlaceholder, // textarea placeholder",
@@ -385,6 +387,7 @@ export const updateContactDraft = {
385
387
  "Shape:",
386
388
  " {",
387
389
  " type: 'contact',",
390
+ " layout: 'steps' | 'single', // optional, default 'steps' (multi-step: message, then name+email). 'single' = one screen.",
388
391
  " intro: {",
389
392
  " title, subtitle,",
390
393
  " submitLabel,",
@@ -424,6 +427,46 @@ export const publishContactFlow = {
424
427
  inputSchema: z.object({ appIdOrSlug: z.string().min(1) }),
425
428
  handler: (input, cfg) => apiFetch(cfg, "POST", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/flows/contact/publish`),
426
429
  };
430
+ // ─── Link page (link-in-bio) ────────────────────────────────────────────────
431
+ export const getLinkPageFlow = {
432
+ name: "get_link_page_flow",
433
+ description: "Read the published and draft link-page configs for an app. A link page is a shareable 'link-in-bio' page (app logo + title/description, a row of icon links, and a list of labeled links) at appmate.cloud/p/{appSlug} — drop it in an Instagram/TikTok bio.",
434
+ inputSchema: z.object({ appIdOrSlug: z.string().min(1) }),
435
+ handler: (input, cfg) => apiFetch(cfg, "GET", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/flows/link-page`),
436
+ };
437
+ export const updateLinkPageDraft = {
438
+ name: "update_link_page_draft",
439
+ description: [
440
+ "Replace the draft link-page config. Body MUST be a full link_page config object (type: 'link_page').",
441
+ "",
442
+ "Shape:",
443
+ " {",
444
+ " type: 'link_page',",
445
+ " header: { title, description? }, // logo comes from the app record",
446
+ " iconLinks: [ // top row of compact icon buttons (<=6)",
447
+ " { icon: 'ri:instagram', label: 'Instagram', url: 'https://…' } // icon = a brand 'ri:<key>' (instagram/tiktok/x/youtube/…), a lucide name (e.g. 'Globe'), or an emoji",
448
+ " ],",
449
+ " links: [ // main list of labeled links (<=25)",
450
+ " { label: 'Download on the App Store', sublabel?: 'iPhone & iPad', url: 'https://…' }",
451
+ " ],",
452
+ " legal?: string, // small print at the bottom",
453
+ " hero?: { // visual treatment",
454
+ " theme?: 'minimal' | 'gradient' | 'dark' | 'side_by_side',",
455
+ " eyebrow?, accentColor?, titleFont?",
456
+ " }",
457
+ " }",
458
+ "",
459
+ "No submissions — a link page is a published, versioned page. Publish with publish_link_page_flow.",
460
+ ].join("\n"),
461
+ inputSchema: updateDraftInput,
462
+ handler: (input, cfg) => apiFetch(cfg, "PUT", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/flows/link-page`, input.config),
463
+ };
464
+ export const publishLinkPageFlow = {
465
+ name: "publish_link_page_flow",
466
+ description: "Promote the draft link-page config to the live published version. Visitors at appmate.cloud/p/{appSlug} see the new version immediately.",
467
+ inputSchema: z.object({ appIdOrSlug: z.string().min(1) }),
468
+ handler: (input, cfg) => apiFetch(cfg, "POST", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/flows/link-page/publish`),
469
+ };
427
470
  export const listContactSubmissions = {
428
471
  name: "list_contact_submissions",
429
472
  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.",
@@ -542,6 +585,7 @@ export const updateReferralDraft = {
542
585
  " landing: { // the invite page a friend sees at /r/{code}",
543
586
  " eyebrow?, title, subtitle, ctaLabel,",
544
587
  " appStoreUrl?, // REQUIRED before go-live — the install button target",
588
+ " fallback?: 'app_store' | 'website', // where a not-installed friend goes (default app_store; 'website' uses the app's websiteUrl)",
545
589
  " legal?",
546
590
  " },",
547
591
  " share: { messageTemplate }, // the referrer's share text; the link is appended automatically",
@@ -595,20 +639,188 @@ export const exportReferralsCsv = {
595
639
  return { csv };
596
640
  },
597
641
  };
642
+ // ─── Wishlist flow (feature-request board) ──────────────────────────────────
643
+ //
644
+ // Unlike every other flow (write-only), the wishlist is an INTERACTIVE board:
645
+ // ideas are read back, upvoted, commented on, and moderated. Beyond the usual
646
+ // get/update/publish trio it exposes moderation tools (set status, delete idea,
647
+ // reply, delete comment) — the first MUTATING admin tools in the set.
648
+ export const getWishlistFlow = {
649
+ name: "get_wishlist_flow",
650
+ description: "Read the published and draft wishlist (feature-request board) configs for an app. The board lives at appmate.cloud/wishlist/{appSlug}; visitors submit ideas, upvote, and comment, and the owner moderates status.",
651
+ inputSchema: z.object({ appIdOrSlug: z.string().min(1) }),
652
+ handler: (input, cfg) => apiFetch(cfg, "GET", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/flows/wishlist`),
653
+ };
654
+ export const updateWishlistDraft = {
655
+ name: "update_wishlist_draft",
656
+ description: [
657
+ "Replace the draft wishlist config. Body MUST be a full wishlist config object (type: 'wishlist').",
658
+ "",
659
+ "Shape:",
660
+ " {",
661
+ " type: 'wishlist',",
662
+ " intro: { title, subtitle, submitLabel, legal? },",
663
+ " identity?: { // who can participate + vote dedup",
664
+ " mode: 'anonymous' | 'email', // default 'anonymous' (dedup by cookie+IP)",
665
+ " requireEmailToSubmit?, requireEmailToVote?, requireEmailToComment? // email mode only",
666
+ " },",
667
+ " categories?: [ // OPTIONAL tags (0–12)",
668
+ " { id: 'feature', label: 'New feature', emoji?: '✨', hint?: '…' }",
669
+ " ],",
670
+ " statusLabels?: { // rename lifecycle stages on the board",
671
+ " open?, planned?, in_progress?, done?, declined?",
672
+ " },",
673
+ " submitForm?: { titlePlaceholder?, bodyPlaceholder? },",
674
+ " moderation?: { autoApprove?: false }, // false (default) = new ideas start pending/hidden",
675
+ " success: { title, body, ctaLabel?, ctaUrl? },",
676
+ " hero?: { theme?, eyebrow?, accentColor?, titleFont? }",
677
+ " }",
678
+ "",
679
+ "Server returns { ok:true, warnings: [] }.",
680
+ ].join("\n"),
681
+ inputSchema: updateDraftInput,
682
+ handler: (input, cfg) => apiFetch(cfg, "PUT", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/flows/wishlist`, input.config),
683
+ };
684
+ export const publishWishlistFlow = {
685
+ name: "publish_wishlist_flow",
686
+ description: "Promote the draft wishlist config to the live published version. Visitors at appmate.cloud/wishlist/{appSlug} see the new version immediately.",
687
+ inputSchema: z.object({ appIdOrSlug: z.string().min(1) }),
688
+ handler: (input, cfg) => apiFetch(cfg, "POST", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/flows/wishlist/publish`),
689
+ };
690
+ export const listWishlistIdeas = {
691
+ name: "list_wishlist_ideas",
692
+ description: "Paginated list of wishlist ideas for an app — ALL statuses (including pending review). Each row: { id, title, body, category, status, voteCount, commentCount, pinned, author, email, source, country, createdAt }. Optional status ('pending'|'open'|'planned'|'in_progress'|'done'|'declined') + category filters; sort 'votes' (default 'new'). limit max 200, default 50; pass nextCursor back for the next page.",
693
+ inputSchema: z.object({
694
+ appIdOrSlug: z.string().min(1),
695
+ status: z.string().optional(),
696
+ category: z.string().optional(),
697
+ sort: z.enum(["votes", "new"]).optional(),
698
+ limit: z.number().int().min(1).max(200).optional(),
699
+ cursor: z.string().optional(),
700
+ }),
701
+ handler: (input, cfg) => {
702
+ const qs = new URLSearchParams();
703
+ if (input.status)
704
+ qs.set("status", input.status);
705
+ if (input.category)
706
+ qs.set("category", input.category);
707
+ if (input.sort)
708
+ qs.set("sort", input.sort);
709
+ if (input.limit !== undefined)
710
+ qs.set("limit", String(input.limit));
711
+ if (input.cursor)
712
+ qs.set("cursor", input.cursor);
713
+ const tail = qs.toString() ? `?${qs.toString()}` : "";
714
+ return apiFetch(cfg, "GET", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/wishlist/ideas${tail}`);
715
+ },
716
+ };
717
+ export const createWishlistIdea = {
718
+ name: "create_wishlist_idea",
719
+ description: "Seed an idea onto the board as the app owner (so it isn't empty for early visitors — they can then upvote/comment). Defaults to status 'open' (visible). Use a category id from the flow config when set.",
720
+ inputSchema: z.object({
721
+ appIdOrSlug: z.string().min(1),
722
+ title: z.string().min(1).max(120),
723
+ body: z.string().max(4000).optional(),
724
+ category: z.string().optional(),
725
+ status: z
726
+ .enum(["pending", "open", "planned", "in_progress", "done", "declined"])
727
+ .optional(),
728
+ }),
729
+ handler: (input, cfg) => apiFetch(cfg, "POST", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/wishlist/ideas`, {
730
+ title: input.title,
731
+ body: input.body,
732
+ category: input.category,
733
+ status: input.status,
734
+ }),
735
+ };
736
+ export const setWishlistIdeaStatus = {
737
+ name: "set_wishlist_idea_status",
738
+ description: "Moderate an idea: set its status and/or pin it. 'pending' hides it from the public board; 'open' approves it. Statuses: pending | open | planned | in_progress | done | declined.",
739
+ inputSchema: z.object({
740
+ appIdOrSlug: z.string().min(1),
741
+ ideaId: z.string().min(1),
742
+ status: z.enum(["pending", "open", "planned", "in_progress", "done", "declined"]),
743
+ pinned: z.boolean().optional(),
744
+ }),
745
+ handler: (input, cfg) => apiFetch(cfg, "PATCH", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/wishlist/ideas/${encodeURIComponent(input.ideaId)}`, { status: input.status, ...(input.pinned !== undefined ? { pinned: input.pinned } : {}) }),
746
+ };
747
+ export const deleteWishlistIdea = {
748
+ name: "delete_wishlist_idea",
749
+ description: "Permanently delete an idea AND all of its votes and comments. IRREVERSIBLE — there is no undo. Prefer set_wishlist_idea_status with 'declined' to reject an idea while keeping it (and the decision) visible.",
750
+ inputSchema: z.object({
751
+ appIdOrSlug: z.string().min(1),
752
+ ideaId: z.string().min(1),
753
+ }),
754
+ handler: (input, cfg) => apiFetch(cfg, "DELETE", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/wishlist/ideas/${encodeURIComponent(input.ideaId)}`),
755
+ };
756
+ export const listWishlistComments = {
757
+ name: "list_wishlist_comments",
758
+ description: "Paginated comments on an idea (oldest-first, excludes deleted). Each row: { id, body, author, isOwner, isOfficial, createdAt }. limit max 200, default 50; pass nextCursor back for more.",
759
+ inputSchema: z.object({
760
+ appIdOrSlug: z.string().min(1),
761
+ ideaId: z.string().min(1),
762
+ limit: z.number().int().min(1).max(200).optional(),
763
+ cursor: z.string().optional(),
764
+ }),
765
+ handler: (input, cfg) => {
766
+ const qs = new URLSearchParams();
767
+ if (input.limit !== undefined)
768
+ qs.set("limit", String(input.limit));
769
+ if (input.cursor)
770
+ qs.set("cursor", input.cursor);
771
+ const tail = qs.toString() ? `?${qs.toString()}` : "";
772
+ return apiFetch(cfg, "GET", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/wishlist/ideas/${encodeURIComponent(input.ideaId)}/comments${tail}`);
773
+ },
774
+ };
775
+ export const postWishlistComment = {
776
+ name: "post_wishlist_comment",
777
+ description: "Post an OWNER reply on an idea (shown with a Team badge). Set official:true to mark it an authoritative response. Use this to answer or expand on a user's idea.",
778
+ inputSchema: z.object({
779
+ appIdOrSlug: z.string().min(1),
780
+ ideaId: z.string().min(1),
781
+ body: z.string().min(1).max(2000),
782
+ official: z.boolean().optional(),
783
+ }),
784
+ handler: (input, cfg) => apiFetch(cfg, "POST", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/wishlist/ideas/${encodeURIComponent(input.ideaId)}/comments`, { body: input.body, official: input.official ?? false }),
785
+ };
786
+ export const deleteWishlistComment = {
787
+ name: "delete_wishlist_comment",
788
+ description: "Soft-delete a comment (hidden from the public board, thread continuity preserved). Use for moderation.",
789
+ inputSchema: z.object({
790
+ appIdOrSlug: z.string().min(1),
791
+ commentId: z.string().min(1),
792
+ }),
793
+ handler: (input, cfg) => apiFetch(cfg, "DELETE", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/wishlist/comments/${encodeURIComponent(input.commentId)}`),
794
+ };
795
+ export const exportWishlistCsv = {
796
+ name: "export_wishlist_csv",
797
+ description: "Return all wishlist ideas for an app as a CSV string (one row per idea, every status: created_at, status, title, votes, comments, category, email, author, country, body).",
798
+ inputSchema: z.object({ appIdOrSlug: z.string().min(1) }),
799
+ handler: async (input, cfg) => {
800
+ const csv = await apiFetchText(cfg, "GET", `/api/v1/apps/${encodeURIComponent(input.appIdOrSlug)}/wishlist/ideas.csv`);
801
+ return { csv };
802
+ },
803
+ };
598
804
  // Registered alphabetically so `list_tools` reads predictably.
599
805
  export const ALL_TOOLS = [
600
806
  createApp,
807
+ createWishlistIdea,
808
+ deleteWishlistComment,
809
+ deleteWishlistIdea,
601
810
  exportOnboardingCsv,
602
811
  exportReferralsCsv,
603
812
  exportWaitlistCsv,
813
+ exportWishlistCsv,
604
814
  getApp,
605
815
  getCancelFlow,
606
816
  getContactFlow,
607
817
  getFeedbackFlow,
818
+ getLinkPageFlow,
608
819
  getOnboardingFlow,
609
820
  getReferralFlow,
610
821
  getReportFlow,
611
822
  getWaitlistFlow,
823
+ getWishlistFlow,
612
824
  listApps,
613
825
  listContactSubmissions,
614
826
  listFeedbackSubmissions,
@@ -616,18 +828,26 @@ export const ALL_TOOLS = [
616
828
  listReferrals,
617
829
  listReportSubmissions,
618
830
  listWaitlistSignups,
831
+ listWishlistComments,
832
+ listWishlistIdeas,
833
+ postWishlistComment,
619
834
  publishCancelFlow,
620
835
  publishContactFlow,
621
836
  publishFeedbackFlow,
837
+ publishLinkPageFlow,
622
838
  publishOnboardingFlow,
623
839
  publishReferralFlow,
624
840
  publishReportFlow,
625
841
  publishWaitlistFlow,
842
+ publishWishlistFlow,
843
+ setWishlistIdeaStatus,
626
844
  updateCancelDraft,
627
845
  updateContactDraft,
628
846
  updateFeedbackDraft,
847
+ updateLinkPageDraft,
629
848
  updateOnboardingDraft,
630
849
  updateReferralDraft,
631
850
  updateReportDraft,
632
851
  updateWaitlistDraft,
852
+ updateWishlistDraft,
633
853
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fil-technology/appmate-mcp",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Model Context Protocol server for AppMate — lets Claude / Cursor / Codex drive your retention flows via API tokens.",
5
5
  "type": "module",
6
6
  "bin": {