@cheerful/mcp-server 0.1.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 ADDED
@@ -0,0 +1,68 @@
1
+ # @cheerful/mcp-server
2
+
3
+ MCP server for the [Cheerful](https://cheerful.ai) platform. Manage campaigns, search creators, send emails, and monitor your inbox from AI coding assistants like Claude Code, Cursor, and Windsurf.
4
+
5
+ ## Setup
6
+
7
+ 1. Get your API key at [cheerful.ai/cc](https://cheerful.ai/cc)
8
+ 2. Add the server to your AI assistant (see below)
9
+
10
+ ### Claude Code
11
+
12
+ ```bash
13
+ claude mcp add cheerful -e CHEERFUL_API_KEY=chrl_your_key_here -- npx @cheerful/mcp-server
14
+ ```
15
+
16
+ ### Cursor / Windsurf / VS Code
17
+
18
+ Add to `.cursor/mcp.json` (or your IDE's MCP config):
19
+
20
+ ```json
21
+ {
22
+ "mcpServers": {
23
+ "cheerful": {
24
+ "command": "npx",
25
+ "args": ["@cheerful/mcp-server"],
26
+ "env": {
27
+ "CHEERFUL_API_KEY": "chrl_your_key_here"
28
+ }
29
+ }
30
+ }
31
+ }
32
+ ```
33
+
34
+ ## Environment Variables
35
+
36
+ | Variable | Required | Description |
37
+ |---|---|---|
38
+ | `CHEERFUL_API_KEY` | Yes | Your `chrl_`-prefixed API key |
39
+ | `CHEERFUL_API_URL` | No | API base URL (defaults to production) |
40
+
41
+ ## Available Tools
42
+
43
+ ### Campaigns
44
+ - **list_campaigns** -- List all your campaigns
45
+ - **get_campaign** -- Get details for a specific campaign
46
+ - **create_campaign** -- Create a new campaign with email templates
47
+ - **add_recipients_to_campaign** -- Add creators to a campaign by email or social handle
48
+ - **set_campaign_sender** -- Change the sender email account
49
+ - **launch_campaign** -- Start sending emails for a campaign
50
+ - **get_dashboard_analytics** -- Aggregated analytics across campaigns
51
+
52
+ ### Creators
53
+ - **search_creators** -- Search for creators by keyword
54
+ - **get_creator_profile** -- Get a creator's profile by handle
55
+ - **list_creator_lists** -- List your saved creator lists
56
+
57
+ ### Email
58
+ - **get_draft** -- Get the current draft for a thread
59
+ - **update_draft** -- Edit a draft
60
+ - **send_email** -- Send an email via a connected account
61
+
62
+ ### Inbox
63
+ - **list_inbox** -- List and search email threads
64
+ - **list_automations** -- List campaign automation rules
65
+
66
+ ## License
67
+
68
+ MIT
@@ -0,0 +1,25 @@
1
+ import type { Config } from "./config.js";
2
+ export interface ApiError {
3
+ status: number;
4
+ statusText: string;
5
+ body: string;
6
+ }
7
+ export declare class CheerfulApiClient {
8
+ private baseUrl;
9
+ private apiKey;
10
+ constructor(config: Config);
11
+ private headers;
12
+ get<T>(path: string, params?: Record<string, string>): Promise<T>;
13
+ post<T>(path: string, body?: unknown): Promise<T>;
14
+ put<T>(path: string, body?: unknown): Promise<T>;
15
+ patch<T>(path: string, body?: unknown): Promise<T>;
16
+ delete(path: string): Promise<void>;
17
+ private toApiError;
18
+ }
19
+ export declare function formatError(error: unknown): {
20
+ content: Array<{
21
+ type: "text";
22
+ text: string;
23
+ }>;
24
+ isError: true;
25
+ };
@@ -0,0 +1,88 @@
1
+ export class CheerfulApiClient {
2
+ baseUrl;
3
+ apiKey;
4
+ constructor(config) {
5
+ this.baseUrl = config.apiBaseUrl;
6
+ this.apiKey = config.apiKey;
7
+ }
8
+ headers() {
9
+ return {
10
+ "Content-Type": "application/json",
11
+ Accept: "application/json",
12
+ "X-Cheerful-Api-Key": this.apiKey,
13
+ };
14
+ }
15
+ async get(path, params) {
16
+ const url = new URL(`/api${path}`, this.baseUrl);
17
+ if (params) {
18
+ for (const [k, v] of Object.entries(params)) {
19
+ if (v !== undefined && v !== "")
20
+ url.searchParams.set(k, v);
21
+ }
22
+ }
23
+ const res = await fetch(url.toString(), { headers: this.headers() });
24
+ if (!res.ok)
25
+ throw await this.toApiError(res);
26
+ return (await res.json());
27
+ }
28
+ async post(path, body) {
29
+ const url = new URL(`/api${path}`, this.baseUrl);
30
+ const res = await fetch(url.toString(), {
31
+ method: "POST",
32
+ headers: this.headers(),
33
+ body: body ? JSON.stringify(body) : undefined,
34
+ });
35
+ if (!res.ok)
36
+ throw await this.toApiError(res);
37
+ return (await res.json());
38
+ }
39
+ async put(path, body) {
40
+ const url = new URL(`/api${path}`, this.baseUrl);
41
+ const res = await fetch(url.toString(), {
42
+ method: "PUT",
43
+ headers: this.headers(),
44
+ body: body ? JSON.stringify(body) : undefined,
45
+ });
46
+ if (!res.ok)
47
+ throw await this.toApiError(res);
48
+ return (await res.json());
49
+ }
50
+ async patch(path, body) {
51
+ const url = new URL(`/api${path}`, this.baseUrl);
52
+ const res = await fetch(url.toString(), {
53
+ method: "PATCH",
54
+ headers: this.headers(),
55
+ body: body ? JSON.stringify(body) : undefined,
56
+ });
57
+ if (!res.ok)
58
+ throw await this.toApiError(res);
59
+ return (await res.json());
60
+ }
61
+ async delete(path) {
62
+ const url = new URL(`/api${path}`, this.baseUrl);
63
+ const res = await fetch(url.toString(), {
64
+ method: "DELETE",
65
+ headers: this.headers(),
66
+ });
67
+ if (!res.ok)
68
+ throw await this.toApiError(res);
69
+ }
70
+ async toApiError(res) {
71
+ const body = await res.text();
72
+ return { status: res.status, statusText: res.statusText, body };
73
+ }
74
+ }
75
+ export function formatError(error) {
76
+ let message;
77
+ if (typeof error === "object" && error !== null && "status" in error) {
78
+ const e = error;
79
+ message = `API error ${e.status} ${e.statusText}: ${e.body}`;
80
+ }
81
+ else if (error instanceof Error) {
82
+ message = error.message;
83
+ }
84
+ else {
85
+ message = String(error);
86
+ }
87
+ return { content: [{ type: "text", text: message }], isError: true };
88
+ }
@@ -0,0 +1,5 @@
1
+ export interface Config {
2
+ apiKey: string;
3
+ apiBaseUrl: string;
4
+ }
5
+ export declare function loadConfig(): Config;
package/dist/config.js ADDED
@@ -0,0 +1,9 @@
1
+ export function loadConfig() {
2
+ const apiKey = process.env.CHEERFUL_API_KEY ?? "";
3
+ const apiBaseUrl = process.env.CHEERFUL_API_URL ?? "https://prd-cheerful.fly.dev";
4
+ if (!apiKey) {
5
+ throw new Error("CHEERFUL_API_KEY environment variable is required. " +
6
+ "Get your key at https://cheerful.ai/cc and configure it in your MCP client.");
7
+ }
8
+ return { apiKey, apiBaseUrl };
9
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "module";
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { loadConfig } from "./config.js";
6
+ import { CheerfulApiClient } from "./api-client.js";
7
+ import { registerCampaignTools } from "./tools/campaigns.js";
8
+ import { registerCreatorTools } from "./tools/creators.js";
9
+ import { registerEmailTools } from "./tools/email.js";
10
+ import { registerInboxTools } from "./tools/inbox.js";
11
+ const require = createRequire(import.meta.url);
12
+ const { version } = require("../package.json");
13
+ async function main() {
14
+ const config = loadConfig();
15
+ const api = new CheerfulApiClient(config);
16
+ const server = new McpServer({
17
+ name: "cheerful",
18
+ version,
19
+ });
20
+ registerCampaignTools(server, api);
21
+ registerCreatorTools(server, api);
22
+ registerEmailTools(server, api);
23
+ registerInboxTools(server, api);
24
+ const transport = new StdioServerTransport();
25
+ await server.connect(transport);
26
+ console.error("Cheerful MCP server running on stdio");
27
+ }
28
+ main().catch((error) => {
29
+ console.error("Fatal error:", error.message ?? error);
30
+ process.exit(1);
31
+ });
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { CheerfulApiClient } from "../api-client.js";
3
+ export declare function registerCampaignTools(server: McpServer, api: CheerfulApiClient): void;
@@ -0,0 +1,195 @@
1
+ import { z } from "zod";
2
+ import { formatError } from "../api-client.js";
3
+ export function registerCampaignTools(server, api) {
4
+ server.registerTool("list_campaigns", {
5
+ title: "List Campaigns",
6
+ description: "List all campaigns for the authenticated user. Returns campaign id, name, status, and type.",
7
+ inputSchema: {},
8
+ }, async () => {
9
+ try {
10
+ const campaigns = await api.get("/campaigns/");
11
+ if (campaigns.length === 0) {
12
+ return { content: [{ type: "text", text: "No campaigns found." }] };
13
+ }
14
+ const formatted = campaigns
15
+ .map((c) => `- **${c.name}** (${c.status})\n ID: ${c.id} | Type: ${c.campaign_type ?? "N/A"} | Created: ${c.created_at}`)
16
+ .join("\n");
17
+ return {
18
+ content: [
19
+ { type: "text", text: `Found ${campaigns.length} campaigns:\n\n${formatted}` },
20
+ ],
21
+ };
22
+ }
23
+ catch (error) {
24
+ return formatError(error);
25
+ }
26
+ });
27
+ server.registerTool("get_campaign", {
28
+ title: "Get Campaign",
29
+ description: "Get detailed information about a specific campaign by ID.",
30
+ inputSchema: {
31
+ campaign_id: z.string().uuid().describe("The campaign UUID"),
32
+ },
33
+ }, async ({ campaign_id }) => {
34
+ try {
35
+ const campaign = await api.get(`/campaigns/${campaign_id}`);
36
+ return {
37
+ content: [{ type: "text", text: JSON.stringify(campaign, null, 2) }],
38
+ };
39
+ }
40
+ catch (error) {
41
+ return formatError(error);
42
+ }
43
+ });
44
+ server.registerTool("create_campaign", {
45
+ title: "Create Campaign",
46
+ description: "Create a new campaign. Requires name, campaign_type, subject_template, and body_template.",
47
+ inputSchema: {
48
+ name: z.string().min(1).max(255).describe("Campaign name"),
49
+ campaign_type: z
50
+ .enum(["paid_promotion", "creator", "gifting", "sales", "other"])
51
+ .describe("Campaign type"),
52
+ subject_template: z.string().min(1).describe("Email subject template"),
53
+ body_template: z.string().min(1).describe("Email body template"),
54
+ description: z.string().optional().describe("Campaign description"),
55
+ },
56
+ }, async ({ name, campaign_type, subject_template, body_template, description }) => {
57
+ try {
58
+ const campaign = await api.post("/campaigns/", {
59
+ name,
60
+ campaign_type,
61
+ subject_template,
62
+ body_template,
63
+ description,
64
+ });
65
+ return {
66
+ content: [
67
+ {
68
+ type: "text",
69
+ text: `Campaign created!\n\nID: ${campaign.id}\nName: ${campaign.name}\nStatus: ${campaign.status}`,
70
+ },
71
+ ],
72
+ };
73
+ }
74
+ catch (error) {
75
+ return formatError(error);
76
+ }
77
+ });
78
+ server.registerTool("get_dashboard_analytics", {
79
+ title: "Get Dashboard Analytics",
80
+ description: "Get aggregated analytics across all campaigns for the authenticated user. Includes campaign counts, opt-in rates, recent opt-ins, and active campaign stats.",
81
+ inputSchema: {},
82
+ }, async () => {
83
+ try {
84
+ const analytics = await api.get(`/dashboard/analytics`);
85
+ return {
86
+ content: [{ type: "text", text: JSON.stringify(analytics, null, 2) }],
87
+ };
88
+ }
89
+ catch (error) {
90
+ return formatError(error);
91
+ }
92
+ });
93
+ server.registerTool("add_recipients_to_campaign", {
94
+ title: "Add Recipients to Campaign",
95
+ description: "Add recipients (creators) to a campaign. Provide email and/or social media handles. If no email is provided, enrichment will attempt to find it automatically.",
96
+ inputSchema: {
97
+ campaign_id: z.string().uuid().describe("The campaign UUID"),
98
+ recipients: z
99
+ .array(z.object({
100
+ email: z.string().email().optional().describe("Creator's email"),
101
+ name: z.string().optional().describe("Creator's name"),
102
+ social_media_handles: z
103
+ .array(z.object({
104
+ platform: z.string().describe("Social media platform"),
105
+ handle: z.string().describe("Handle on the platform"),
106
+ url: z.string().optional().describe("Profile URL"),
107
+ }))
108
+ .optional()
109
+ .default([])
110
+ .describe("Social media handles"),
111
+ custom_fields: z
112
+ .record(z.string(), z.any())
113
+ .optional()
114
+ .default({})
115
+ .describe("Custom fields for the recipient"),
116
+ }))
117
+ .min(1)
118
+ .describe("List of recipients to add"),
119
+ },
120
+ }, async ({ campaign_id, recipients }) => {
121
+ try {
122
+ const results = await api.post(`/campaigns/${campaign_id}/recipients-from-search`, recipients);
123
+ const formatted = results
124
+ .map((r) => `- Recipient: ${r.recipient_id} | Creator: ${r.creator_id} | Email: ${r.email ?? "pending enrichment"} | ${r.already_existed ? "Already existed" : "New"}`)
125
+ .join("\n");
126
+ return {
127
+ content: [
128
+ {
129
+ type: "text",
130
+ text: `Added ${results.length} recipient(s):\n\n${formatted}`,
131
+ },
132
+ ],
133
+ };
134
+ }
135
+ catch (error) {
136
+ return formatError(error);
137
+ }
138
+ });
139
+ server.registerTool("set_campaign_sender", {
140
+ title: "Set Campaign Sender",
141
+ description: "Change the sender email account for a campaign. Both email addresses must be connected Gmail or SMTP accounts owned by the campaign owner.",
142
+ inputSchema: {
143
+ campaign_id: z.string().uuid().describe("The campaign UUID"),
144
+ old_sender_email: z.string().email().describe("Current sender email to replace"),
145
+ new_sender_email: z.string().email().describe("New sender email"),
146
+ },
147
+ }, async ({ campaign_id, old_sender_email, new_sender_email }) => {
148
+ try {
149
+ const result = await api.patch(`/campaigns/${campaign_id}/senders`, {
150
+ old_sender_email,
151
+ new_sender_email,
152
+ });
153
+ return {
154
+ content: [
155
+ {
156
+ type: "text",
157
+ text: `Success: ${result.success}\nAffected emails: ${result.affected_emails}\nMessage: ${result.message}`,
158
+ },
159
+ ],
160
+ };
161
+ }
162
+ catch (error) {
163
+ return formatError(error);
164
+ }
165
+ });
166
+ server.registerTool("launch_campaign", {
167
+ title: "Launch Campaign",
168
+ description: "Populate the outbound queue for a campaign and start sending. Emails are distributed round-robin across configured senders and sent automatically every 5 minutes. This is idempotent — only creates queue entries for recipients that don't already have one.",
169
+ inputSchema: {
170
+ campaign_id: z.string().uuid().describe("The campaign UUID"),
171
+ cc_emails: z
172
+ .array(z.string().email())
173
+ .optional()
174
+ .describe("Optional CC email addresses"),
175
+ },
176
+ }, async ({ campaign_id, cc_emails }) => {
177
+ try {
178
+ const body = {};
179
+ if (cc_emails)
180
+ body.cc_emails = cc_emails;
181
+ const result = await api.post(`/campaigns/${campaign_id}/outbound`, body);
182
+ return {
183
+ content: [
184
+ {
185
+ type: "text",
186
+ text: `Status: ${result.status}\nMessage: ${result.message}\nEntries created: ${result.entries_created}`,
187
+ },
188
+ ],
189
+ };
190
+ }
191
+ catch (error) {
192
+ return formatError(error);
193
+ }
194
+ });
195
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { CheerfulApiClient } from "../api-client.js";
3
+ export declare function registerCreatorTools(server: McpServer, api: CheerfulApiClient): void;
@@ -0,0 +1,54 @@
1
+ import { z } from "zod";
2
+ import { formatError } from "../api-client.js";
3
+ export function registerCreatorTools(server, api) {
4
+ server.registerTool("search_creators", {
5
+ title: "Search Creators",
6
+ description: "Search for creators by keyword. Returns matching creator profiles. Private and inactive (no posts in 90 days) profiles are excluded by default.",
7
+ inputSchema: {
8
+ query: z.string().min(1).describe("Search query (keyword or handle)"),
9
+ limit: z
10
+ .number()
11
+ .int()
12
+ .min(1)
13
+ .max(50)
14
+ .optional()
15
+ .describe("Max number of creators to return (default 50, max 50)"),
16
+ },
17
+ }, async ({ query, limit }) => {
18
+ try {
19
+ const results = await api.post("/v1/creator-search/keyword", { keyword: query, limit: limit ?? 50 });
20
+ return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
21
+ }
22
+ catch (error) {
23
+ return formatError(error);
24
+ }
25
+ });
26
+ server.registerTool("get_creator_profile", {
27
+ title: "Get Creator Profile",
28
+ description: "Get detailed profile for a creator by their handle.",
29
+ inputSchema: {
30
+ handle: z.string().min(1).describe("Creator social media handle"),
31
+ },
32
+ }, async ({ handle }) => {
33
+ try {
34
+ const profile = await api.get(`/v1/creators/profiles/${encodeURIComponent(handle)}`);
35
+ return { content: [{ type: "text", text: JSON.stringify(profile, null, 2) }] };
36
+ }
37
+ catch (error) {
38
+ return formatError(error);
39
+ }
40
+ });
41
+ server.registerTool("list_creator_lists", {
42
+ title: "List Creator Lists",
43
+ description: "List all saved creator lists for the authenticated user.",
44
+ inputSchema: {},
45
+ }, async () => {
46
+ try {
47
+ const lists = await api.get("/v1/lists/");
48
+ return { content: [{ type: "text", text: JSON.stringify(lists, null, 2) }] };
49
+ }
50
+ catch (error) {
51
+ return formatError(error);
52
+ }
53
+ });
54
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { CheerfulApiClient } from "../api-client.js";
3
+ export declare function registerEmailTools(server: McpServer, api: CheerfulApiClient): void;
@@ -0,0 +1,62 @@
1
+ import { z } from "zod";
2
+ import { formatError } from "../api-client.js";
3
+ export function registerEmailTools(server, api) {
4
+ server.registerTool("get_draft", {
5
+ title: "Get Draft",
6
+ description: "Get the current draft for a specific email thread.",
7
+ inputSchema: { thread_id: z.string().min(1).describe("The Gmail/SMTP/Instagram thread ID") },
8
+ }, async ({ thread_id }) => {
9
+ try {
10
+ const draft = await api.get(`/threads/${thread_id}/draft`);
11
+ return { content: [{ type: "text", text: JSON.stringify(draft, null, 2) }] };
12
+ }
13
+ catch (error) {
14
+ return formatError(error);
15
+ }
16
+ });
17
+ server.registerTool("update_draft", {
18
+ title: "Update Draft",
19
+ description: "Update the draft content for a specific email thread.",
20
+ inputSchema: {
21
+ thread_id: z.string().min(1).describe("The Gmail/SMTP/Instagram thread ID"),
22
+ gmail_thread_state_id: z.string().uuid().describe("Thread state UUID for optimistic concurrency (from get_draft response)"),
23
+ draft_subject: z.string().optional().describe("Email subject line"),
24
+ draft_body_text: z.string().optional().describe("Email body as plain text"),
25
+ },
26
+ }, async ({ thread_id, gmail_thread_state_id, draft_subject, draft_body_text }) => {
27
+ try {
28
+ const draft = await api.put(`/threads/${thread_id}/draft`, { gmail_thread_state_id, draft_subject, draft_body_text });
29
+ return {
30
+ content: [{ type: "text", text: `Draft updated.\n\n${JSON.stringify(draft, null, 2)}` }],
31
+ };
32
+ }
33
+ catch (error) {
34
+ return formatError(error);
35
+ }
36
+ });
37
+ server.registerTool("send_email", {
38
+ title: "Send Email",
39
+ description: "Send an email via a connected Gmail or SMTP account. Requires the sender account email, recipient, subject, and body.",
40
+ inputSchema: {
41
+ account_email: z.string().email().describe("Sender account email address (must be a connected Gmail or SMTP account)"),
42
+ to: z.array(z.string().email()).min(1).describe("Recipient email addresses"),
43
+ subject: z.string().min(1).describe("Email subject"),
44
+ body_html: z.string().optional().describe("Email body as HTML"),
45
+ body_text: z.string().optional().describe("Email body as plain text (provide body_html or body_text)"),
46
+ cc: z.array(z.string().email()).optional().describe("CC email addresses"),
47
+ thread_id: z.string().optional().describe("Gmail thread ID for replies"),
48
+ in_reply_to: z.string().optional().describe("Message-ID header of the message being replied to"),
49
+ references: z.string().optional().describe("References header for threading"),
50
+ },
51
+ }, async ({ account_email, to, subject, body_html, body_text, cc, thread_id, in_reply_to, references }) => {
52
+ try {
53
+ const result = await api.post("/emails/send", { account_email, to, subject, body_html, body_text, cc, thread_id, in_reply_to, references });
54
+ return {
55
+ content: [{ type: "text", text: `Email sent to ${to.join(", ")}.\n\n${JSON.stringify(result, null, 2)}` }],
56
+ };
57
+ }
58
+ catch (error) {
59
+ return formatError(error);
60
+ }
61
+ });
62
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { CheerfulApiClient } from "../api-client.js";
3
+ export declare function registerInboxTools(server: McpServer, api: CheerfulApiClient): void;
@@ -0,0 +1,50 @@
1
+ import { z } from "zod";
2
+ import { formatError } from "../api-client.js";
3
+ export function registerInboxTools(server, api) {
4
+ server.registerTool("list_inbox", {
5
+ title: "List Inbox",
6
+ description: "List email threads. Optionally filter by campaign ID, search text, or limit results.",
7
+ inputSchema: {
8
+ campaign_id: z.string().uuid().optional().describe("Filter by campaign UUID"),
9
+ search: z.string().optional().describe("Search in sender, recipient, or subject"),
10
+ limit: z.number().int().min(1).max(100).default(50).describe("Max threads to return"),
11
+ offset: z.number().int().min(0).default(0).describe("Pagination offset"),
12
+ },
13
+ }, async ({ campaign_id, search, limit, offset }) => {
14
+ try {
15
+ const params = {};
16
+ if (campaign_id)
17
+ params.campaign_id = campaign_id;
18
+ if (search)
19
+ params.search = search;
20
+ if (limit !== undefined)
21
+ params.limit = String(limit);
22
+ if (offset !== undefined)
23
+ params.offset = String(offset);
24
+ const threads = await api.get(`/threads/`, params);
25
+ return {
26
+ content: [
27
+ { type: "text", text: `Found ${threads.length} threads:\n\n${JSON.stringify(threads, null, 2)}` },
28
+ ],
29
+ };
30
+ }
31
+ catch (error) {
32
+ return formatError(error);
33
+ }
34
+ });
35
+ server.registerTool("list_automations", {
36
+ title: "List Automations",
37
+ description: "List campaign automation rules.",
38
+ inputSchema: {
39
+ campaign_id: z.string().uuid().describe("Campaign UUID"),
40
+ },
41
+ }, async ({ campaign_id }) => {
42
+ try {
43
+ const automations = await api.get(`/v1/campaigns/${campaign_id}/workflows`);
44
+ return { content: [{ type: "text", text: JSON.stringify(automations, null, 2) }] };
45
+ }
46
+ catch (error) {
47
+ return formatError(error);
48
+ }
49
+ });
50
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@cheerful/mcp-server",
3
+ "version": "0.1.0",
4
+ "description": "Cheerful MCP server — manage campaigns, creators, and more from AI coding assistants",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "homepage": "https://cheerful.ai/cc",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/nuts-and-bolts-ai/cheerful.git",
11
+ "directory": "packages/mcp-server"
12
+ },
13
+ "bin": {
14
+ "cheerful-mcp-server": "./dist/index.js"
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc",
21
+ "dev": "tsc --watch",
22
+ "prepublishOnly": "npm run build",
23
+ "inspect": "npx @modelcontextprotocol/inspector dist/index.js"
24
+ },
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "engines": {
29
+ "node": ">=18"
30
+ },
31
+ "dependencies": {
32
+ "@modelcontextprotocol/sdk": "^1.28.0",
33
+ "zod": "^3.24.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^22.19.0",
37
+ "typescript": "^5.9.0"
38
+ }
39
+ }