@bentonow/bento-mcp 1.0.0 → 1.0.2
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 +55 -43
- package/build/index.js +505 -773
- package/build/index.js.map +1 -1
- package/package.json +9 -4
package/build/index.js
CHANGED
|
@@ -1,808 +1,540 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
2
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
6
|
import { z } from "zod";
|
|
5
7
|
import { Analytics } from "@bentonow/bento-node-sdk";
|
|
6
|
-
|
|
8
|
+
import { createRequire } from "node:module";
|
|
9
|
+
var require2 = createRequire(import.meta.url);
|
|
10
|
+
var packageJson = require2("../package.json");
|
|
11
|
+
var VERSION = packageJson.version;
|
|
7
12
|
function getBentoClient() {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
13
|
+
const publishableKey = process.env.BENTO_PUBLISHABLE_KEY;
|
|
14
|
+
const secretKey = process.env.BENTO_SECRET_KEY;
|
|
15
|
+
const siteUuid = process.env.BENTO_SITE_UUID;
|
|
16
|
+
if (!publishableKey || !secretKey || !siteUuid) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
"Missing required environment variables: BENTO_PUBLISHABLE_KEY, BENTO_SECRET_KEY, BENTO_SITE_UUID"
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
return new Analytics({
|
|
22
|
+
authentication: {
|
|
23
|
+
publishableKey,
|
|
24
|
+
secretKey
|
|
25
|
+
},
|
|
26
|
+
siteUuid
|
|
27
|
+
});
|
|
21
28
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
29
|
+
var server = new McpServer({
|
|
30
|
+
name: "bento",
|
|
31
|
+
version: VERSION
|
|
32
|
+
});
|
|
33
|
+
function successResponse(data, context) {
|
|
34
|
+
let text;
|
|
35
|
+
if (data === null || data === void 0) {
|
|
36
|
+
text = context ? `${context}: No data returned` : "No data returned";
|
|
37
|
+
} else if (typeof data === "boolean") {
|
|
38
|
+
text = data ? context ? `${context}: Success` : "Operation completed successfully" : context ? `${context}: Operation failed` : "Operation failed";
|
|
39
|
+
} else if (typeof data === "number") {
|
|
40
|
+
text = context ? `${context}: ${data}` : `Result: ${data}`;
|
|
41
|
+
} else if (Array.isArray(data)) {
|
|
42
|
+
if (data.length === 0) {
|
|
43
|
+
text = context ? `${context}: No items found` : "No items found";
|
|
44
|
+
} else {
|
|
45
|
+
text = context ? `${context} (${data.length} items):
|
|
46
|
+
${JSON.stringify(data, null, 2)}` : `Found ${data.length} items:
|
|
47
|
+
${JSON.stringify(data, null, 2)}`;
|
|
48
|
+
}
|
|
49
|
+
} else if (typeof data === "object") {
|
|
50
|
+
text = context ? `${context}:
|
|
51
|
+
${JSON.stringify(data, null, 2)}` : JSON.stringify(data, null, 2);
|
|
52
|
+
} else {
|
|
53
|
+
text = String(data);
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
content: [{ type: "text", text }]
|
|
57
|
+
};
|
|
39
58
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
59
|
+
function errorResponse(error, operation) {
|
|
60
|
+
let message;
|
|
61
|
+
if (error instanceof Error) {
|
|
62
|
+
message = error.message;
|
|
63
|
+
if (message.includes("Missing required environment variables")) {
|
|
64
|
+
message = `Configuration error: ${message}. Please ensure BENTO_PUBLISHABLE_KEY, BENTO_SECRET_KEY, and BENTO_SITE_UUID are set.`;
|
|
65
|
+
} else if (message.includes("401") || message.toLowerCase().includes("unauthorized")) {
|
|
66
|
+
message = "Authentication failed: Invalid API credentials. Please check your BENTO_PUBLISHABLE_KEY and BENTO_SECRET_KEY.";
|
|
67
|
+
} else if (message.includes("404") || message.toLowerCase().includes("not found")) {
|
|
68
|
+
message = `Resource not found: ${message}`;
|
|
69
|
+
} else if (message.includes("429")) {
|
|
70
|
+
message = "Rate limit exceeded: Too many requests. Please wait before trying again.";
|
|
71
|
+
} else if (message.includes("500") || message.includes("502")) {
|
|
72
|
+
message = "Bento API error: The service is temporarily unavailable. Please try again later.";
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
message = String(error);
|
|
76
|
+
}
|
|
77
|
+
const text = operation ? `Failed to ${operation}: ${message}` : `Error: ${message}`;
|
|
78
|
+
return {
|
|
79
|
+
content: [{ type: "text", text }],
|
|
80
|
+
isError: true
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function validationError(message) {
|
|
84
|
+
return {
|
|
85
|
+
content: [{ type: "text", text: `Validation error: ${message}` }],
|
|
86
|
+
isError: true
|
|
87
|
+
};
|
|
46
88
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
89
|
+
server.tool(
|
|
90
|
+
"get_subscriber",
|
|
91
|
+
"Look up a Bento subscriber by email or UUID. Returns subscriber details including tags, fields, and subscription status.",
|
|
92
|
+
{
|
|
51
93
|
email: z.string().email().optional().describe("Subscriber email address"),
|
|
52
|
-
uuid: z.string().optional().describe("Subscriber UUID")
|
|
53
|
-
},
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
const bento = getBentoClient();
|
|
61
|
-
const subscriber = await bento.V1.Subscribers.getSubscribers(email ? { email } : { uuid: uuid });
|
|
62
|
-
return {
|
|
63
|
-
content: [{ type: "text", text: formatResponse(subscriber) }],
|
|
64
|
-
};
|
|
94
|
+
uuid: z.string().optional().describe("Subscriber UUID")
|
|
95
|
+
},
|
|
96
|
+
async ({ email, uuid }) => {
|
|
97
|
+
if (!email && !uuid) {
|
|
98
|
+
return validationError(
|
|
99
|
+
"Either email or uuid is required to look up a subscriber"
|
|
100
|
+
);
|
|
65
101
|
}
|
|
66
|
-
catch (error) {
|
|
67
|
-
return {
|
|
68
|
-
content: [{ type: "text", text: handleError(error) }],
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
server.tool("bento_create_subscriber", "Create a new subscriber in Bento. If the subscriber already exists, returns the existing subscriber.", {
|
|
73
|
-
email: z.string().email().describe("Email address for the new subscriber"),
|
|
74
|
-
}, async ({ email }) => {
|
|
75
102
|
try {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
103
|
+
const bento = getBentoClient();
|
|
104
|
+
const subscriber = await bento.V1.Subscribers.getSubscribers(
|
|
105
|
+
email ? { email } : { uuid }
|
|
106
|
+
);
|
|
107
|
+
if (!subscriber) {
|
|
108
|
+
return successResponse(
|
|
109
|
+
null,
|
|
110
|
+
`Subscriber ${email || uuid} not found in Bento`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return successResponse(
|
|
114
|
+
subscriber,
|
|
115
|
+
`Subscriber details for ${email || uuid}`
|
|
116
|
+
);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
return errorResponse(error, `get subscriber ${email || uuid}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
server.tool(
|
|
123
|
+
"batch_import_subscribers",
|
|
124
|
+
"Import or update multiple subscribers at once (up to 1000). Supports custom fields and tags. Does NOT trigger automations - use for bulk imports only.",
|
|
125
|
+
{
|
|
126
|
+
subscribers: z.array(
|
|
127
|
+
z.object({
|
|
128
|
+
email: z.string().email().describe("Subscriber email address"),
|
|
129
|
+
firstName: z.string().optional().describe("First name"),
|
|
130
|
+
lastName: z.string().optional().describe("Last name"),
|
|
131
|
+
tags: z.string().optional().describe("Comma-separated tags to add"),
|
|
132
|
+
removeTags: z.string().optional().describe("Comma-separated tags to remove")
|
|
133
|
+
}).passthrough()
|
|
134
|
+
).describe("Array of subscribers to import (max 1000)")
|
|
135
|
+
},
|
|
136
|
+
async ({ subscribers }) => {
|
|
137
|
+
if (subscribers.length === 0) {
|
|
138
|
+
return validationError("At least one subscriber is required");
|
|
139
|
+
}
|
|
140
|
+
if (subscribers.length > 1e3) {
|
|
141
|
+
return validationError(
|
|
142
|
+
`Maximum 1000 subscribers per batch, received ${subscribers.length}`
|
|
143
|
+
);
|
|
81
144
|
}
|
|
82
|
-
catch (error) {
|
|
83
|
-
return {
|
|
84
|
-
content: [{ type: "text", text: handleError(error) }],
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
server.tool("bento_upsert_subscriber", "Create or update a subscriber with custom fields and tags. This is the most flexible way to manage subscribers.", {
|
|
89
|
-
email: z.string().email().describe("Subscriber email address"),
|
|
90
|
-
fields: z
|
|
91
|
-
.record(z.unknown())
|
|
92
|
-
.optional()
|
|
93
|
-
.describe("Custom fields to set on the subscriber (e.g., { firstName: 'John', company: 'Acme' })"),
|
|
94
|
-
tags: z
|
|
95
|
-
.string()
|
|
96
|
-
.optional()
|
|
97
|
-
.describe("Comma-separated list of tags to add (e.g., 'lead,newsletter')"),
|
|
98
|
-
removeTags: z
|
|
99
|
-
.string()
|
|
100
|
-
.optional()
|
|
101
|
-
.describe("Comma-separated list of tags to remove"),
|
|
102
|
-
}, async ({ email, fields, tags, removeTags }) => {
|
|
103
145
|
try {
|
|
104
|
-
|
|
105
|
-
|
|
146
|
+
const bento = getBentoClient();
|
|
147
|
+
const count = await bento.V1.Batch.importSubscribers({
|
|
148
|
+
subscribers: subscribers.map((s) => {
|
|
149
|
+
const {
|
|
150
|
+
firstName,
|
|
151
|
+
lastName,
|
|
152
|
+
removeTags,
|
|
153
|
+
email,
|
|
154
|
+
tags,
|
|
155
|
+
...customFields
|
|
156
|
+
} = s;
|
|
157
|
+
return {
|
|
106
158
|
email,
|
|
107
|
-
fields,
|
|
108
159
|
tags,
|
|
160
|
+
first_name: firstName,
|
|
161
|
+
last_name: lastName,
|
|
109
162
|
remove_tags: removeTags,
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
try {
|
|
129
|
-
const bento = getBentoClient();
|
|
130
|
-
const result = await bento.V1.addSubscriber({ email, fields });
|
|
131
|
-
return {
|
|
132
|
-
content: [{ type: "text", text: formatResponse(result) }],
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
catch (error) {
|
|
136
|
-
return {
|
|
137
|
-
content: [{ type: "text", text: handleError(error) }],
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
server.tool("bento_remove_subscriber", "Unsubscribe a user from your Bento account. This triggers automations.", {
|
|
142
|
-
email: z.string().email().describe("Subscriber email address to unsubscribe"),
|
|
143
|
-
}, async ({ email }) => {
|
|
144
|
-
try {
|
|
145
|
-
const bento = getBentoClient();
|
|
146
|
-
const result = await bento.V1.removeSubscriber({ email });
|
|
147
|
-
return {
|
|
148
|
-
content: [{ type: "text", text: formatResponse(result) }],
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
catch (error) {
|
|
152
|
-
return {
|
|
153
|
-
content: [{ type: "text", text: handleError(error) }],
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
});
|
|
157
|
-
// =============================================================================
|
|
158
|
-
// TAGGING TOOLS
|
|
159
|
-
// =============================================================================
|
|
160
|
-
server.tool("bento_tag_subscriber", "Add a tag to a subscriber. Creates the tag and/or subscriber if they don't exist. Triggers automations (1-3 min delay).", {
|
|
161
|
-
email: z.string().email().describe("Subscriber email address"),
|
|
162
|
-
tagName: z.string().describe("Name of the tag to add"),
|
|
163
|
-
}, async ({ email, tagName }) => {
|
|
164
|
-
try {
|
|
165
|
-
const bento = getBentoClient();
|
|
166
|
-
const result = await bento.V1.tagSubscriber({ email, tagName });
|
|
167
|
-
return {
|
|
168
|
-
content: [{ type: "text", text: formatResponse(result) }],
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
catch (error) {
|
|
172
|
-
return {
|
|
173
|
-
content: [{ type: "text", text: handleError(error) }],
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
});
|
|
177
|
-
server.tool("bento_remove_tag", "Remove a tag from a subscriber.", {
|
|
178
|
-
email: z.string().email().describe("Subscriber email address"),
|
|
179
|
-
tagName: z.string().describe("Name of the tag to remove"),
|
|
180
|
-
}, async ({ email, tagName }) => {
|
|
163
|
+
...customFields
|
|
164
|
+
};
|
|
165
|
+
})
|
|
166
|
+
});
|
|
167
|
+
return successResponse(
|
|
168
|
+
{ imported: count, total: subscribers.length },
|
|
169
|
+
`Successfully imported ${count} of ${subscribers.length} subscribers`
|
|
170
|
+
);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
return errorResponse(error, "import subscribers");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
);
|
|
176
|
+
server.tool(
|
|
177
|
+
"list_tags",
|
|
178
|
+
"List all tags in your Bento account.",
|
|
179
|
+
{},
|
|
180
|
+
async () => {
|
|
181
181
|
try {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
182
|
+
const bento = getBentoClient();
|
|
183
|
+
const tags = await bento.V1.Tags.getTags();
|
|
184
|
+
return successResponse(tags, "Tags in your Bento account");
|
|
185
|
+
} catch (error) {
|
|
186
|
+
return errorResponse(error, "list tags");
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
);
|
|
190
|
+
server.tool(
|
|
191
|
+
"create_tag",
|
|
192
|
+
"Create a new tag in your Bento account.",
|
|
193
|
+
{
|
|
194
|
+
name: z.string().min(1).describe("Tag name to create")
|
|
195
|
+
},
|
|
196
|
+
async ({ name }) => {
|
|
195
197
|
try {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
name: z.string().describe("Name of the tag to create"),
|
|
210
|
-
}, async ({ name }) => {
|
|
198
|
+
const bento = getBentoClient();
|
|
199
|
+
const tag = await bento.V1.Tags.createTag({ name });
|
|
200
|
+
return successResponse(tag, `Created tag "${name}"`);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
return errorResponse(error, `create tag "${name}"`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
);
|
|
206
|
+
server.tool(
|
|
207
|
+
"list_fields",
|
|
208
|
+
"List all custom fields defined in your Bento account.",
|
|
209
|
+
{},
|
|
210
|
+
async () => {
|
|
211
211
|
try {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
type: z
|
|
230
|
-
.string()
|
|
231
|
-
.describe("Event type/name (e.g., '$pageView', 'signup_completed', 'feature_used')"),
|
|
232
|
-
fields: z
|
|
233
|
-
.record(z.unknown())
|
|
234
|
-
.optional()
|
|
235
|
-
.describe("Custom fields to update on the subscriber"),
|
|
236
|
-
details: z
|
|
237
|
-
.record(z.unknown())
|
|
238
|
-
.optional()
|
|
239
|
-
.describe("Additional event details (e.g., { url: '/pricing', source: 'campaign' })"),
|
|
240
|
-
}, async ({ email, type, fields, details }) => {
|
|
241
|
-
try {
|
|
242
|
-
const bento = getBentoClient();
|
|
243
|
-
const result = await bento.V1.track({
|
|
244
|
-
email,
|
|
245
|
-
type,
|
|
246
|
-
fields: fields,
|
|
247
|
-
details,
|
|
248
|
-
});
|
|
249
|
-
return {
|
|
250
|
-
content: [{ type: "text", text: formatResponse(result) }],
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
catch (error) {
|
|
254
|
-
return {
|
|
255
|
-
content: [{ type: "text", text: handleError(error) }],
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
});
|
|
259
|
-
server.tool("bento_track_purchase", "Track a purchase event for a subscriber. Used for calculating LTV (Lifetime Value). Amount should be in cents (e.g., 9999 = $99.99).", {
|
|
260
|
-
email: z.string().email().describe("Subscriber email address"),
|
|
261
|
-
orderId: z
|
|
262
|
-
.string()
|
|
263
|
-
.describe("Unique order/transaction ID to prevent duplicates"),
|
|
264
|
-
amount: z
|
|
265
|
-
.number()
|
|
266
|
-
.describe("Purchase amount in cents (e.g., 9999 for $99.99)"),
|
|
267
|
-
currency: z
|
|
268
|
-
.string()
|
|
269
|
-
.default("USD")
|
|
270
|
-
.describe("Currency code (default: USD)"),
|
|
271
|
-
cart: z
|
|
272
|
-
.object({
|
|
273
|
-
abandonedCheckoutUrl: z.string().optional(),
|
|
274
|
-
items: z
|
|
275
|
-
.array(z.object({
|
|
276
|
-
productId: z.string().optional(),
|
|
277
|
-
productSku: z.string().optional(),
|
|
278
|
-
productName: z.string().optional(),
|
|
279
|
-
quantity: z.number().optional(),
|
|
280
|
-
productPrice: z.number().optional(),
|
|
281
|
-
}))
|
|
282
|
-
.optional(),
|
|
283
|
-
})
|
|
284
|
-
.optional()
|
|
285
|
-
.describe("Optional cart details including items"),
|
|
286
|
-
}, async ({ email, orderId, amount, currency, cart }) => {
|
|
212
|
+
const bento = getBentoClient();
|
|
213
|
+
const fields = await bento.V1.Fields.getFields();
|
|
214
|
+
return successResponse(fields, "Custom fields in your Bento account");
|
|
215
|
+
} catch (error) {
|
|
216
|
+
return errorResponse(error, "list custom fields");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
);
|
|
220
|
+
server.tool(
|
|
221
|
+
"create_field",
|
|
222
|
+
"Create a new custom field in your Bento account. The key is automatically converted to a display name (e.g., 'firstName' becomes 'First Name').",
|
|
223
|
+
{
|
|
224
|
+
key: z.string().min(1).describe(
|
|
225
|
+
"Field key in camelCase or snake_case (e.g., 'firstName', 'company_name')"
|
|
226
|
+
)
|
|
227
|
+
},
|
|
228
|
+
async ({ key }) => {
|
|
287
229
|
try {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
230
|
+
const bento = getBentoClient();
|
|
231
|
+
const field = await bento.V1.Fields.createField({ key });
|
|
232
|
+
return successResponse(field, `Created custom field "${key}"`);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
return errorResponse(error, `create custom field "${key}"`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
);
|
|
238
|
+
var PURCHASE_EVENT_TYPES = [
|
|
239
|
+
"$purchase",
|
|
240
|
+
"purchase",
|
|
241
|
+
"order",
|
|
242
|
+
"order_complete",
|
|
243
|
+
"event_sale"
|
|
244
|
+
];
|
|
245
|
+
var CartItemSchema = z.object({
|
|
246
|
+
product_sku: z.string().optional().describe("Product SKU"),
|
|
247
|
+
product_name: z.string().optional().describe("Product name"),
|
|
248
|
+
quantity: z.number().optional().describe("Quantity purchased")
|
|
249
|
+
}).passthrough();
|
|
250
|
+
var CartSchema = z.object({
|
|
251
|
+
items: z.array(CartItemSchema).optional().describe("Array of cart items"),
|
|
252
|
+
abandoned_checkout_url: z.string().url().optional().describe("URL to abandoned checkout")
|
|
253
|
+
}).passthrough();
|
|
254
|
+
var PurchaseDetailsSchema = z.object({
|
|
255
|
+
unique: z.object({
|
|
256
|
+
key: z.string().min(1).describe("Unique key to prevent double-counting (e.g., order ID)")
|
|
257
|
+
}),
|
|
258
|
+
value: z.object({
|
|
259
|
+
currency: z.string().length(3).describe("ISO 4217 currency code (e.g., 'USD', 'EUR')"),
|
|
260
|
+
amount: z.number().min(0).describe("Amount in cents (e.g., 4000 for $40.00)")
|
|
261
|
+
}),
|
|
262
|
+
cart: CartSchema.optional().describe("Optional cart details with items")
|
|
263
|
+
}).passthrough();
|
|
264
|
+
function generateUniqueKey() {
|
|
265
|
+
return `mcp_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
|
266
|
+
}
|
|
267
|
+
function validatePurchaseDetails(details) {
|
|
268
|
+
if (!details) {
|
|
269
|
+
return {
|
|
270
|
+
valid: false,
|
|
271
|
+
error: "Purchase events require 'details' with 'unique.key' and 'value' (currency + amount)"
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
const value = details.value;
|
|
275
|
+
if (!value) {
|
|
276
|
+
return {
|
|
277
|
+
valid: false,
|
|
278
|
+
error: "Purchase events require 'details.value' with 'currency' (ISO 4217 code) and 'amount' (in cents)"
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
if (typeof value.currency !== "string" || value.currency.length !== 3) {
|
|
282
|
+
return {
|
|
283
|
+
valid: false,
|
|
284
|
+
error: "Purchase events require 'details.value.currency' as a 3-letter ISO 4217 code (e.g., 'USD', 'EUR')"
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
if (typeof value.amount !== "number" || value.amount < 0) {
|
|
288
|
+
return {
|
|
289
|
+
valid: false,
|
|
290
|
+
error: "Purchase events require 'details.value.amount' as a positive number in cents (e.g., 4000 for $40.00)"
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
const unique = details.unique;
|
|
294
|
+
if (!unique || !unique.key) {
|
|
295
|
+
const generatedKey = generateUniqueKey();
|
|
296
|
+
return {
|
|
297
|
+
valid: true,
|
|
298
|
+
details: {
|
|
299
|
+
...details,
|
|
300
|
+
unique: { key: generatedKey }
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
return { valid: true, details };
|
|
305
|
+
}
|
|
306
|
+
server.tool(
|
|
307
|
+
"track_event",
|
|
308
|
+
`Track a custom event for a subscriber. Events can trigger automations.
|
|
309
|
+
|
|
310
|
+
Common event types: $pageView, $signup, $login, or any custom event name.
|
|
311
|
+
|
|
312
|
+
**IMPORTANT: For purchase events (${PURCHASE_EVENT_TYPES.join(", ")}), the details object MUST include:**
|
|
313
|
+
- \`unique.key\`: A unique identifier to prevent double-counting (e.g., order ID). Auto-generated if not provided.
|
|
314
|
+
- \`value.currency\`: ISO 4217 currency code (e.g., "USD", "EUR")
|
|
315
|
+
- \`value.amount\`: Amount in cents (e.g., 4000 for $40.00)
|
|
316
|
+
- \`cart\` (optional): Cart details with items array
|
|
317
|
+
|
|
318
|
+
Example purchase event details:
|
|
319
|
+
{
|
|
320
|
+
"unique": { "key": "order_12345" },
|
|
321
|
+
"value": { "currency": "USD", "amount": 4000 },
|
|
322
|
+
"cart": {
|
|
323
|
+
"items": [{ "product_sku": "SKU123", "product_name": "Widget", "quantity": 2 }]
|
|
324
|
+
}
|
|
325
|
+
}`,
|
|
326
|
+
{
|
|
322
327
|
email: z.string().email().describe("Subscriber email address"),
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
catch (error) {
|
|
335
|
-
return {
|
|
336
|
-
content: [{ type: "text", text: handleError(error) }],
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
});
|
|
340
|
-
server.tool("bento_list_fields", "List all custom fields defined in your Bento account.", {}, async () => {
|
|
341
|
-
try {
|
|
342
|
-
const bento = getBentoClient();
|
|
343
|
-
const fields = await bento.V1.Fields.getFields();
|
|
344
|
-
return {
|
|
345
|
-
content: [{ type: "text", text: formatResponse(fields) }],
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
catch (error) {
|
|
349
|
-
return {
|
|
350
|
-
content: [{ type: "text", text: handleError(error) }],
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
});
|
|
354
|
-
server.tool("bento_create_field", "Create a new custom field in your Bento account. The key is automatically converted to a display name (e.g., 'firstName' becomes 'First Name').", {
|
|
355
|
-
key: z
|
|
356
|
-
.string()
|
|
357
|
-
.describe("Field key in camelCase or snake_case (e.g., 'firstName', 'company_name')"),
|
|
358
|
-
}, async ({ key }) => {
|
|
359
|
-
try {
|
|
360
|
-
const bento = getBentoClient();
|
|
361
|
-
const fields = await bento.V1.Fields.createField({ key });
|
|
362
|
-
return {
|
|
363
|
-
content: [{ type: "text", text: formatResponse(fields) }],
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
catch (error) {
|
|
367
|
-
return {
|
|
368
|
-
content: [{ type: "text", text: handleError(error) }],
|
|
369
|
-
};
|
|
370
|
-
}
|
|
371
|
-
});
|
|
372
|
-
// =============================================================================
|
|
373
|
-
// STATISTICS TOOLS
|
|
374
|
-
// =============================================================================
|
|
375
|
-
server.tool("bento_get_site_stats", "Get overall statistics for your Bento site including subscriber counts, broadcast counts, and engagement rates.", {}, async () => {
|
|
328
|
+
type: z.string().min(1).describe(
|
|
329
|
+
"Event type/name (e.g., '$pageView', 'signup_completed', '$purchase')"
|
|
330
|
+
),
|
|
331
|
+
fields: z.record(z.unknown()).optional().describe("Custom fields to update on the subscriber"),
|
|
332
|
+
details: z.record(z.unknown()).optional().describe(
|
|
333
|
+
"Additional event details. For purchase events, must include: unique.key, value.currency, value.amount"
|
|
334
|
+
)
|
|
335
|
+
},
|
|
336
|
+
async ({ email, type, fields, details }) => {
|
|
376
337
|
try {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
};
|
|
387
|
-
}
|
|
388
|
-
});
|
|
389
|
-
server.tool("bento_get_segment_stats", "Get statistics for a specific segment including subscriber count and engagement metrics.", {
|
|
390
|
-
segmentId: z.string().describe("The segment ID to get stats for"),
|
|
391
|
-
}, async ({ segmentId }) => {
|
|
392
|
-
try {
|
|
393
|
-
const bento = getBentoClient();
|
|
394
|
-
const stats = await bento.V1.Stats.getSegmentStats(segmentId);
|
|
395
|
-
return {
|
|
396
|
-
content: [{ type: "text", text: formatResponse(stats) }],
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
|
-
catch (error) {
|
|
400
|
-
return {
|
|
401
|
-
content: [{ type: "text", text: handleError(error) }],
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
});
|
|
405
|
-
server.tool("bento_get_report_stats", "Get statistics for a specific email report/broadcast including opens, clicks, and unsubscribes.", {
|
|
406
|
-
reportId: z.string().describe("The report/broadcast ID to get stats for"),
|
|
407
|
-
}, async ({ reportId }) => {
|
|
408
|
-
try {
|
|
409
|
-
const bento = getBentoClient();
|
|
410
|
-
const stats = await bento.V1.Stats.getReportStats(reportId);
|
|
411
|
-
return {
|
|
412
|
-
content: [{ type: "text", text: formatResponse(stats) }],
|
|
413
|
-
};
|
|
414
|
-
}
|
|
415
|
-
catch (error) {
|
|
416
|
-
return {
|
|
417
|
-
content: [{ type: "text", text: handleError(error) }],
|
|
418
|
-
};
|
|
419
|
-
}
|
|
420
|
-
});
|
|
421
|
-
// =============================================================================
|
|
422
|
-
// EMAIL TOOLS
|
|
423
|
-
// =============================================================================
|
|
424
|
-
server.tool("bento_send_email", "Send a transactional email to a subscriber. The 'from' address must be an authorized Author in your Bento account.", {
|
|
425
|
-
to: z.string().email().describe("Recipient email address"),
|
|
426
|
-
from: z
|
|
427
|
-
.string()
|
|
428
|
-
.email()
|
|
429
|
-
.describe("Sender email address (must be an authorized Author in Bento)"),
|
|
430
|
-
subject: z
|
|
431
|
-
.string()
|
|
432
|
-
.describe("Email subject line (can include {{ personalization }} tags)"),
|
|
433
|
-
htmlBody: z
|
|
434
|
-
.string()
|
|
435
|
-
.describe("HTML content of the email (can include {{ personalization }} tags)"),
|
|
436
|
-
transactional: z
|
|
437
|
-
.boolean()
|
|
438
|
-
.default(true)
|
|
439
|
-
.describe("If true, sends even to unsubscribed users (use for receipts, password resets, etc.)"),
|
|
440
|
-
personalizations: z
|
|
441
|
-
.record(z.string())
|
|
442
|
-
.optional()
|
|
443
|
-
.describe("Key-value pairs for personalization tags (e.g., { name: 'John', orderNumber: '12345' })"),
|
|
444
|
-
}, async ({ to, from, subject, htmlBody, transactional, personalizations }) => {
|
|
445
|
-
try {
|
|
446
|
-
const bento = getBentoClient();
|
|
447
|
-
const result = await bento.V1.Batch.sendTransactionalEmails({
|
|
448
|
-
emails: [
|
|
449
|
-
{
|
|
450
|
-
to,
|
|
451
|
-
from,
|
|
452
|
-
subject,
|
|
453
|
-
html_body: htmlBody,
|
|
454
|
-
transactional,
|
|
455
|
-
personalizations,
|
|
456
|
-
},
|
|
457
|
-
],
|
|
458
|
-
});
|
|
459
|
-
return {
|
|
460
|
-
content: [
|
|
461
|
-
{
|
|
462
|
-
type: "text",
|
|
463
|
-
text: result > 0 ? "Email sent successfully" : "Failed to send email",
|
|
464
|
-
},
|
|
465
|
-
],
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
|
-
catch (error) {
|
|
469
|
-
return {
|
|
470
|
-
content: [{ type: "text", text: handleError(error) }],
|
|
471
|
-
};
|
|
472
|
-
}
|
|
473
|
-
});
|
|
474
|
-
// =============================================================================
|
|
475
|
-
// BROADCAST TOOLS
|
|
476
|
-
// =============================================================================
|
|
477
|
-
server.tool("bento_list_broadcasts", "List all email broadcasts/campaigns in your Bento account.", {}, async () => {
|
|
478
|
-
try {
|
|
479
|
-
const bento = getBentoClient();
|
|
480
|
-
const broadcasts = await bento.V1.Broadcasts.getBroadcasts();
|
|
481
|
-
return {
|
|
482
|
-
content: [{ type: "text", text: formatResponse(broadcasts) }],
|
|
483
|
-
};
|
|
484
|
-
}
|
|
485
|
-
catch (error) {
|
|
486
|
-
return {
|
|
487
|
-
content: [{ type: "text", text: handleError(error) }],
|
|
488
|
-
};
|
|
489
|
-
}
|
|
490
|
-
});
|
|
491
|
-
server.tool("bento_create_broadcast", "Create a new email broadcast/campaign. The broadcast will be created as a draft.", {
|
|
492
|
-
name: z.string().describe("Internal name for the broadcast"),
|
|
493
|
-
subject: z
|
|
494
|
-
.string()
|
|
495
|
-
.describe("Email subject line (can include {{ personalization }} tags)"),
|
|
496
|
-
content: z.string().describe("Email content (HTML, plain text, or markdown)"),
|
|
497
|
-
type: z
|
|
498
|
-
.enum(["plain", "html", "markdown"])
|
|
499
|
-
.default("html")
|
|
500
|
-
.describe("Content type"),
|
|
501
|
-
fromName: z.string().describe("Sender name"),
|
|
502
|
-
fromEmail: z
|
|
503
|
-
.string()
|
|
504
|
-
.email()
|
|
505
|
-
.describe("Sender email (must be an authorized Author)"),
|
|
506
|
-
inclusiveTags: z
|
|
507
|
-
.string()
|
|
508
|
-
.optional()
|
|
509
|
-
.describe("Comma-separated tags - subscribers must have at least one"),
|
|
510
|
-
exclusiveTags: z
|
|
511
|
-
.string()
|
|
512
|
-
.optional()
|
|
513
|
-
.describe("Comma-separated tags - subscribers with these tags are excluded"),
|
|
514
|
-
segmentId: z.string().optional().describe("Target a specific segment"),
|
|
515
|
-
batchSizePerHour: z
|
|
516
|
-
.number()
|
|
517
|
-
.optional()
|
|
518
|
-
.describe("Limit sending rate (emails per hour)"),
|
|
519
|
-
}, async ({ name, subject, content, type, fromName, fromEmail, inclusiveTags, exclusiveTags, segmentId, batchSizePerHour, }) => {
|
|
520
|
-
try {
|
|
521
|
-
const bento = getBentoClient();
|
|
522
|
-
const broadcasts = await bento.V1.Broadcasts.createBroadcast([
|
|
523
|
-
{
|
|
524
|
-
name,
|
|
525
|
-
subject,
|
|
526
|
-
content,
|
|
527
|
-
type,
|
|
528
|
-
from: { name: fromName, email: fromEmail },
|
|
529
|
-
inclusive_tags: inclusiveTags,
|
|
530
|
-
exclusive_tags: exclusiveTags,
|
|
531
|
-
segment_id: segmentId,
|
|
532
|
-
batch_size_per_hour: batchSizePerHour ?? 1000,
|
|
533
|
-
},
|
|
534
|
-
]);
|
|
535
|
-
return {
|
|
536
|
-
content: [{ type: "text", text: formatResponse(broadcasts) }],
|
|
537
|
-
};
|
|
538
|
-
}
|
|
539
|
-
catch (error) {
|
|
540
|
-
return {
|
|
541
|
-
content: [{ type: "text", text: handleError(error) }],
|
|
542
|
-
};
|
|
543
|
-
}
|
|
544
|
-
});
|
|
545
|
-
// =============================================================================
|
|
546
|
-
// SEQUENCE & WORKFLOW TOOLS
|
|
547
|
-
// =============================================================================
|
|
548
|
-
server.tool("bento_list_sequences", "List all email sequences in your Bento account. Returns each sequence with its name, ID, and email templates (id, subject, stats). Use this to discover what automated email sequences exist and get template IDs for reading/editing content.", {}, async () => {
|
|
549
|
-
try {
|
|
550
|
-
const bento = getBentoClient();
|
|
551
|
-
const sequences = await bento.V1.Sequences.getSequences();
|
|
552
|
-
return {
|
|
553
|
-
content: [{ type: "text", text: formatResponse(sequences) }],
|
|
554
|
-
};
|
|
555
|
-
}
|
|
556
|
-
catch (error) {
|
|
557
|
-
return {
|
|
558
|
-
content: [{ type: "text", text: handleError(error) }],
|
|
559
|
-
};
|
|
560
|
-
}
|
|
561
|
-
});
|
|
562
|
-
server.tool("bento_list_workflows", "List all workflows (automation flows) in your Bento account. Returns each workflow with its name, ID, and email templates (id, subject, stats). Use this to discover what automated workflows exist and get template IDs for reading/editing content.", {}, async () => {
|
|
563
|
-
try {
|
|
564
|
-
const bento = getBentoClient();
|
|
565
|
-
const workflows = await bento.V1.Workflows.getWorkflows();
|
|
566
|
-
return {
|
|
567
|
-
content: [{ type: "text", text: formatResponse(workflows) }],
|
|
568
|
-
};
|
|
569
|
-
}
|
|
570
|
-
catch (error) {
|
|
571
|
-
return {
|
|
572
|
-
content: [{ type: "text", text: handleError(error) }],
|
|
573
|
-
};
|
|
574
|
-
}
|
|
575
|
-
});
|
|
576
|
-
server.tool("bento_get_email_template", "Get the full content of an email template by ID. Returns the template's name, subject, HTML content, and stats. Use this after listing sequences/workflows to read the actual email content for review or editing.", {
|
|
577
|
-
id: z
|
|
578
|
-
.number()
|
|
579
|
-
.describe("The email template ID (numeric ID from the email_templates array in sequences or workflows)"),
|
|
580
|
-
}, async ({ id }) => {
|
|
581
|
-
try {
|
|
582
|
-
const bento = getBentoClient();
|
|
583
|
-
const template = await bento.V1.EmailTemplates.getEmailTemplate({ id });
|
|
584
|
-
return {
|
|
585
|
-
content: [{ type: "text", text: formatResponse(template) }],
|
|
586
|
-
};
|
|
587
|
-
}
|
|
588
|
-
catch (error) {
|
|
589
|
-
return {
|
|
590
|
-
content: [{ type: "text", text: handleError(error) }],
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
});
|
|
594
|
-
server.tool("bento_update_email_template", "Update an email template's subject line and/or HTML content. Use this to improve email copy, fix typos, update designs, or make any changes to emails in sequences or workflows. Changes take effect immediately for future sends.", {
|
|
595
|
-
id: z.number().describe("The email template ID to update"),
|
|
596
|
-
subject: z
|
|
597
|
-
.string()
|
|
598
|
-
.optional()
|
|
599
|
-
.describe("New subject line for the email (can include {{ liquid }} personalization tags)"),
|
|
600
|
-
html: z
|
|
601
|
-
.string()
|
|
602
|
-
.optional()
|
|
603
|
-
.describe("New HTML content for the email body (can include {{ liquid }} personalization tags). Must include {{ visitor.unsubscribe_url }} for compliance."),
|
|
604
|
-
}, async ({ id, subject, html }) => {
|
|
605
|
-
try {
|
|
606
|
-
if (!subject && !html) {
|
|
607
|
-
return {
|
|
608
|
-
content: [
|
|
609
|
-
{ type: "text", text: "Either subject or html (or both) is required to update" },
|
|
610
|
-
],
|
|
611
|
-
};
|
|
338
|
+
const bento = getBentoClient();
|
|
339
|
+
const isPurchaseEvent = PURCHASE_EVENT_TYPES.some(
|
|
340
|
+
(purchaseType) => type.toLowerCase() === purchaseType.toLowerCase()
|
|
341
|
+
);
|
|
342
|
+
let finalDetails = details;
|
|
343
|
+
if (isPurchaseEvent) {
|
|
344
|
+
const validation = validatePurchaseDetails(details);
|
|
345
|
+
if (!validation.valid) {
|
|
346
|
+
return validationError(validation.error);
|
|
612
347
|
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
email: z.string().email(),
|
|
636
|
-
firstName: z.string().optional(),
|
|
637
|
-
lastName: z.string().optional(),
|
|
638
|
-
tags: z.string().optional(),
|
|
639
|
-
}).passthrough())
|
|
640
|
-
.describe("Array of subscribers to import (max 1000)"),
|
|
641
|
-
}, async ({ subscribers }) => {
|
|
348
|
+
finalDetails = validation.details;
|
|
349
|
+
}
|
|
350
|
+
const result = await bento.V1.track({
|
|
351
|
+
email,
|
|
352
|
+
type,
|
|
353
|
+
fields,
|
|
354
|
+
details: finalDetails
|
|
355
|
+
});
|
|
356
|
+
return successResponse(
|
|
357
|
+
result,
|
|
358
|
+
`Tracked event "${type}" for subscriber ${email}`
|
|
359
|
+
);
|
|
360
|
+
} catch (error) {
|
|
361
|
+
return errorResponse(error, `track event "${type}" for ${email}`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
);
|
|
365
|
+
server.tool(
|
|
366
|
+
"get_site_stats",
|
|
367
|
+
"Get overall statistics for your Bento site including subscriber counts, broadcast counts, and engagement rates.",
|
|
368
|
+
{},
|
|
369
|
+
async () => {
|
|
642
370
|
try {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
first_name: firstName,
|
|
657
|
-
last_name: lastName,
|
|
658
|
-
};
|
|
659
|
-
}),
|
|
660
|
-
});
|
|
661
|
-
return {
|
|
662
|
-
content: [
|
|
663
|
-
{ type: "text", text: `Successfully imported ${count} subscribers` },
|
|
664
|
-
],
|
|
665
|
-
};
|
|
666
|
-
}
|
|
667
|
-
catch (error) {
|
|
668
|
-
return {
|
|
669
|
-
content: [{ type: "text", text: handleError(error) }],
|
|
670
|
-
};
|
|
671
|
-
}
|
|
672
|
-
});
|
|
673
|
-
// =============================================================================
|
|
674
|
-
// EXPERIMENTAL TOOLS
|
|
675
|
-
// =============================================================================
|
|
676
|
-
server.tool("bento_validate_email", "Validate an email address using Bento's email validation service. Checks for syntax, deliverability, and spam traps.", {
|
|
677
|
-
email: z.string().email().describe("Email address to validate"),
|
|
678
|
-
name: z.string().optional().describe("Name associated with the email"),
|
|
679
|
-
ip: z.string().optional().describe("IP address of the user"),
|
|
680
|
-
userAgent: z.string().optional().describe("User agent string"),
|
|
681
|
-
}, async ({ email, name, ip, userAgent }) => {
|
|
371
|
+
const bento = getBentoClient();
|
|
372
|
+
const stats = await bento.V1.Stats.getSiteStats();
|
|
373
|
+
return successResponse(stats, "Bento site statistics");
|
|
374
|
+
} catch (error) {
|
|
375
|
+
return errorResponse(error, "get site statistics");
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
);
|
|
379
|
+
server.tool(
|
|
380
|
+
"list_broadcasts",
|
|
381
|
+
"List all email broadcasts/campaigns in your Bento account.",
|
|
382
|
+
{},
|
|
383
|
+
async () => {
|
|
682
384
|
try {
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
return {
|
|
719
|
-
content: [{ type: "text", text: handleError(error) }],
|
|
720
|
-
};
|
|
721
|
-
}
|
|
722
|
-
});
|
|
723
|
-
server.tool("bento_geolocate_ip", "Get geographic location data for an IP address.", {
|
|
724
|
-
ip: z.string().describe("IP address to geolocate"),
|
|
725
|
-
}, async ({ ip }) => {
|
|
385
|
+
const bento = getBentoClient();
|
|
386
|
+
const broadcasts = await bento.V1.Broadcasts.getBroadcasts();
|
|
387
|
+
return successResponse(broadcasts, "Broadcasts in your Bento account");
|
|
388
|
+
} catch (error) {
|
|
389
|
+
return errorResponse(error, "list broadcasts");
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
);
|
|
393
|
+
server.tool(
|
|
394
|
+
"create_broadcast",
|
|
395
|
+
"Create a new email broadcast/campaign as a draft. The broadcast will need to be sent manually from the Bento dashboard.",
|
|
396
|
+
{
|
|
397
|
+
name: z.string().min(1).describe("Internal name for the broadcast"),
|
|
398
|
+
subject: z.string().min(1).describe("Email subject line"),
|
|
399
|
+
content: z.string().min(1).describe("Email content (HTML, plain text, or markdown)"),
|
|
400
|
+
type: z.enum(["plain", "html", "markdown"]).default("html").describe("Content type"),
|
|
401
|
+
fromName: z.string().min(1).describe("Sender name"),
|
|
402
|
+
fromEmail: z.string().email().describe("Sender email (must be an authorized Author in Bento)"),
|
|
403
|
+
inclusiveTags: z.string().optional().describe("Comma-separated tags - subscribers must have at least one"),
|
|
404
|
+
exclusiveTags: z.string().optional().describe("Comma-separated tags - subscribers with these are excluded"),
|
|
405
|
+
segmentId: z.string().optional().describe("Target a specific segment ID"),
|
|
406
|
+
batchSizePerHour: z.number().positive().optional().describe("Sending rate limit (emails per hour, default: 1000)")
|
|
407
|
+
},
|
|
408
|
+
async ({
|
|
409
|
+
name,
|
|
410
|
+
subject,
|
|
411
|
+
content,
|
|
412
|
+
type,
|
|
413
|
+
fromName,
|
|
414
|
+
fromEmail,
|
|
415
|
+
inclusiveTags,
|
|
416
|
+
exclusiveTags,
|
|
417
|
+
segmentId,
|
|
418
|
+
batchSizePerHour
|
|
419
|
+
}) => {
|
|
726
420
|
try {
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
server.tool("bento_check_blacklist", "Check if a domain or IP address is on any email blacklists.", {
|
|
740
|
-
domain: z.string().optional().describe("Domain to check"),
|
|
741
|
-
ip: z.string().optional().describe("IP address to check"),
|
|
742
|
-
}, async ({ domain, ip }) => {
|
|
743
|
-
try {
|
|
744
|
-
if (!domain && !ip) {
|
|
745
|
-
return {
|
|
746
|
-
content: [{ type: "text", text: "Either domain or ip is required" }],
|
|
747
|
-
};
|
|
421
|
+
const bento = getBentoClient();
|
|
422
|
+
const broadcast = await bento.V1.Broadcasts.createBroadcast([
|
|
423
|
+
{
|
|
424
|
+
name,
|
|
425
|
+
subject,
|
|
426
|
+
content,
|
|
427
|
+
type,
|
|
428
|
+
from: { name: fromName, email: fromEmail },
|
|
429
|
+
inclusive_tags: inclusiveTags,
|
|
430
|
+
exclusive_tags: exclusiveTags,
|
|
431
|
+
segment_id: segmentId,
|
|
432
|
+
batch_size_per_hour: batchSizePerHour ?? 1e3
|
|
748
433
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
434
|
+
]);
|
|
435
|
+
return successResponse(
|
|
436
|
+
broadcast,
|
|
437
|
+
`Created draft broadcast "${name}" with subject "${subject}"`
|
|
438
|
+
);
|
|
439
|
+
} catch (error) {
|
|
440
|
+
return errorResponse(error, `create broadcast "${name}"`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
);
|
|
444
|
+
server.tool(
|
|
445
|
+
"list_automations",
|
|
446
|
+
"List email sequences and/or workflows in your Bento account with their templates.",
|
|
447
|
+
{
|
|
448
|
+
type: z.enum(["sequences", "workflows", "all"]).default("all").describe("Filter by automation type")
|
|
449
|
+
},
|
|
450
|
+
async ({ type }) => {
|
|
764
451
|
try {
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
}
|
|
776
|
-
});
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
server.tool(
|
|
781
|
-
|
|
782
|
-
|
|
452
|
+
const bento = getBentoClient();
|
|
453
|
+
const results = {};
|
|
454
|
+
if (type === "sequences" || type === "all") {
|
|
455
|
+
results.sequences = await bento.V1.Sequences.getSequences();
|
|
456
|
+
}
|
|
457
|
+
if (type === "workflows" || type === "all") {
|
|
458
|
+
results.workflows = await bento.V1.Workflows.getWorkflows();
|
|
459
|
+
}
|
|
460
|
+
const context = type === "all" ? "Sequences and workflows" : type === "sequences" ? "Email sequences" : "Workflows";
|
|
461
|
+
return successResponse(results, `${context} in your Bento account`);
|
|
462
|
+
} catch (error) {
|
|
463
|
+
return errorResponse(error, `list ${type}`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
);
|
|
467
|
+
server.tool(
|
|
468
|
+
"get_email_template",
|
|
469
|
+
"Get the full content of an email template by ID. Returns the template's name, subject, HTML content, and stats.",
|
|
470
|
+
{
|
|
471
|
+
id: z.number().positive().describe("Email template ID")
|
|
472
|
+
},
|
|
473
|
+
async ({ id }) => {
|
|
783
474
|
try {
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
475
|
+
const bento = getBentoClient();
|
|
476
|
+
const template = await bento.V1.EmailTemplates.getEmailTemplate({ id });
|
|
477
|
+
if (!template) {
|
|
478
|
+
return successResponse(null, `Email template with ID ${id} not found`);
|
|
479
|
+
}
|
|
480
|
+
return successResponse(template, `Email template (ID: ${id})`);
|
|
481
|
+
} catch (error) {
|
|
482
|
+
return errorResponse(error, `get email template ${id}`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
);
|
|
486
|
+
server.tool(
|
|
487
|
+
"update_email_template",
|
|
488
|
+
"Update an email template's subject line and/or HTML content. Changes take effect immediately for future sends.",
|
|
489
|
+
{
|
|
490
|
+
id: z.number().positive().describe("Email template ID to update"),
|
|
491
|
+
subject: z.string().optional().describe("New subject line"),
|
|
492
|
+
html: z.string().optional().describe(
|
|
493
|
+
"New HTML content (must include {{ visitor.unsubscribe_url }} for compliance)"
|
|
494
|
+
)
|
|
495
|
+
},
|
|
496
|
+
async ({ id, subject, html }) => {
|
|
497
|
+
if (!subject && !html) {
|
|
498
|
+
return validationError(
|
|
499
|
+
"Either subject or html (or both) is required to update a template"
|
|
500
|
+
);
|
|
789
501
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
502
|
+
try {
|
|
503
|
+
const bento = getBentoClient();
|
|
504
|
+
const template = await bento.V1.EmailTemplates.updateEmailTemplate({
|
|
505
|
+
id,
|
|
506
|
+
subject,
|
|
507
|
+
html
|
|
508
|
+
});
|
|
509
|
+
const updated = [subject && "subject", html && "content"].filter(Boolean).join(" and ");
|
|
510
|
+
return successResponse(
|
|
511
|
+
template,
|
|
512
|
+
`Updated email template ${id} (${updated})`
|
|
513
|
+
);
|
|
514
|
+
} catch (error) {
|
|
515
|
+
return errorResponse(error, `update email template ${id}`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
);
|
|
799
519
|
async function main() {
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
520
|
+
const transport = new StdioServerTransport();
|
|
521
|
+
await server.connect(transport);
|
|
522
|
+
console.error(`Bento MCP Server v${VERSION} running on stdio`);
|
|
803
523
|
}
|
|
804
|
-
|
|
805
|
-
|
|
524
|
+
function shutdown(signal) {
|
|
525
|
+
console.error(`
|
|
526
|
+
Received ${signal}, shutting down gracefully...`);
|
|
527
|
+
server.close().then(() => {
|
|
528
|
+
console.error("Server closed");
|
|
529
|
+
process.exit(0);
|
|
530
|
+
}).catch((err) => {
|
|
531
|
+
console.error("Error during shutdown:", err);
|
|
806
532
|
process.exit(1);
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
536
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
537
|
+
main().catch((error) => {
|
|
538
|
+
console.error("Fatal error:", error);
|
|
539
|
+
process.exit(1);
|
|
807
540
|
});
|
|
808
|
-
//# sourceMappingURL=index.js.map
|