@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/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
- // Initialize Bento client from environment variables
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
- const publishableKey = process.env.BENTO_PUBLISHABLE_KEY;
9
- const secretKey = process.env.BENTO_SECRET_KEY;
10
- const siteUuid = process.env.BENTO_SITE_UUID;
11
- if (!publishableKey || !secretKey || !siteUuid) {
12
- throw new Error("Missing required environment variables: BENTO_PUBLISHABLE_KEY, BENTO_SECRET_KEY, BENTO_SITE_UUID");
13
- }
14
- return new Analytics({
15
- authentication: {
16
- publishableKey,
17
- secretKey,
18
- },
19
- siteUuid,
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
- // Create MCP server
23
- const server = new McpServer({
24
- name: "bento",
25
- version: "1.0.0",
26
- });
27
- // Helper to format responses
28
- function formatResponse(data) {
29
- if (data === null || data === undefined) {
30
- return "No data returned";
31
- }
32
- if (typeof data === "boolean") {
33
- return data ? "Success" : "Operation failed";
34
- }
35
- if (typeof data === "number") {
36
- return `Count: ${data}`;
37
- }
38
- return JSON.stringify(data, null, 2);
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
- // Helper for error handling
41
- function handleError(error) {
42
- if (error instanceof Error) {
43
- return `Error: ${error.message}`;
44
- }
45
- return `Error: ${String(error)}`;
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
- // SUBSCRIBER TOOLS
49
- // =============================================================================
50
- server.tool("bento_get_subscriber", "Look up a Bento subscriber by email or UUID. Returns subscriber details including tags, fields, and subscription status.", {
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
- }, async ({ email, uuid }) => {
54
- try {
55
- if (!email && !uuid) {
56
- return {
57
- content: [{ type: "text", text: "Either email or uuid is required" }],
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
- const bento = getBentoClient();
77
- const subscriber = await bento.V1.Subscribers.createSubscriber({ email });
78
- return {
79
- content: [{ type: "text", text: formatResponse(subscriber) }],
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
- const bento = getBentoClient();
105
- const subscriber = await bento.V1.upsertSubscriber({
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
- return {
112
- content: [{ type: "text", text: formatResponse(subscriber) }],
113
- };
114
- }
115
- catch (error) {
116
- return {
117
- content: [{ type: "text", text: handleError(error) }],
118
- };
119
- }
120
- });
121
- server.tool("bento_add_subscriber", "Subscribe a user to your Bento account. This triggers automations and is processed via the batch API (1-3 min delay).", {
122
- email: z.string().email().describe("Subscriber email address"),
123
- fields: z
124
- .record(z.unknown())
125
- .optional()
126
- .describe("Custom fields to set on the subscriber"),
127
- }, async ({ email, fields }) => {
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
- const bento = getBentoClient();
183
- const result = await bento.V1.Commands.removeTag({ email, tagName });
184
- return {
185
- content: [{ type: "text", text: formatResponse(result) }],
186
- };
187
- }
188
- catch (error) {
189
- return {
190
- content: [{ type: "text", text: handleError(error) }],
191
- };
192
- }
193
- });
194
- server.tool("bento_list_tags", "List all tags in your Bento account.", {}, async () => {
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
- const bento = getBentoClient();
197
- const tags = await bento.V1.Tags.getTags();
198
- return {
199
- content: [{ type: "text", text: formatResponse(tags) }],
200
- };
201
- }
202
- catch (error) {
203
- return {
204
- content: [{ type: "text", text: handleError(error) }],
205
- };
206
- }
207
- });
208
- server.tool("bento_create_tag", "Create a new tag in your Bento account.", {
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
- const bento = getBentoClient();
213
- const tags = await bento.V1.Tags.createTag({ name });
214
- return {
215
- content: [{ type: "text", text: formatResponse(tags) }],
216
- };
217
- }
218
- catch (error) {
219
- return {
220
- content: [{ type: "text", text: handleError(error) }],
221
- };
222
- }
223
- });
224
- // =============================================================================
225
- // EVENT TRACKING TOOLS
226
- // =============================================================================
227
- server.tool("bento_track_event", "Track a custom event for a subscriber. Events can trigger automations. Common event types: $pageView, $signup, $login, or any custom event name.", {
228
- email: z.string().email().describe("Subscriber email address"),
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
- const bento = getBentoClient();
289
- const result = await bento.V1.trackPurchase({
290
- email,
291
- purchaseDetails: {
292
- unique: { key: orderId },
293
- value: { currency, amount },
294
- cart: cart
295
- ? {
296
- abandoned_checkout_url: cart.abandonedCheckoutUrl,
297
- items: cart.items?.map((item) => ({
298
- product_id: item.productId,
299
- product_sku: item.productSku,
300
- product_name: item.productName,
301
- quantity: item.quantity,
302
- product_price: item.productPrice,
303
- })),
304
- }
305
- : undefined,
306
- },
307
- });
308
- return {
309
- content: [{ type: "text", text: formatResponse(result) }],
310
- };
311
- }
312
- catch (error) {
313
- return {
314
- content: [{ type: "text", text: handleError(error) }],
315
- };
316
- }
317
- });
318
- // =============================================================================
319
- // FIELD TOOLS
320
- // =============================================================================
321
- server.tool("bento_update_fields", "Update custom fields on a subscriber. Triggers automations.", {
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
- fields: z
324
- .record(z.unknown())
325
- .describe("Fields to update (e.g., { firstName: 'John', company: 'Acme', plan: 'pro' })"),
326
- }, async ({ email, fields }) => {
327
- try {
328
- const bento = getBentoClient();
329
- const result = await bento.V1.updateFields({ email, fields });
330
- return {
331
- content: [{ type: "text", text: formatResponse(result) }],
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
- const bento = getBentoClient();
378
- const stats = await bento.V1.Stats.getSiteStats();
379
- return {
380
- content: [{ type: "text", text: formatResponse(stats) }],
381
- };
382
- }
383
- catch (error) {
384
- return {
385
- content: [{ type: "text", text: handleError(error) }],
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
- const bento = getBentoClient();
614
- const template = await bento.V1.EmailTemplates.updateEmailTemplate({
615
- id,
616
- subject,
617
- html,
618
- });
619
- return {
620
- content: [{ type: "text", text: formatResponse(template) }],
621
- };
622
- }
623
- catch (error) {
624
- return {
625
- content: [{ type: "text", text: handleError(error) }],
626
- };
627
- }
628
- });
629
- // =============================================================================
630
- // BATCH TOOLS
631
- // =============================================================================
632
- server.tool("bento_batch_import_subscribers", "Import multiple subscribers at once (up to 1000). Does NOT trigger automations - use for bulk imports only.", {
633
- subscribers: z
634
- .array(z.object({
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
- if (subscribers.length > 1000) {
644
- return {
645
- content: [
646
- { type: "text", text: "Error: Maximum 1000 subscribers per batch" },
647
- ],
648
- };
649
- }
650
- const bento = getBentoClient();
651
- const count = await bento.V1.Batch.importSubscribers({
652
- subscribers: subscribers.map((s) => {
653
- const { firstName, lastName, ...rest } = s;
654
- return {
655
- ...rest,
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
- const bento = getBentoClient();
684
- const isValid = await bento.V1.Experimental.validateEmail({
685
- email,
686
- name,
687
- ip,
688
- userAgent,
689
- });
690
- return {
691
- content: [
692
- {
693
- type: "text",
694
- text: isValid
695
- ? "Email is valid"
696
- : "Email appears to be invalid or risky",
697
- },
698
- ],
699
- };
700
- }
701
- catch (error) {
702
- return {
703
- content: [{ type: "text", text: handleError(error) }],
704
- };
705
- }
706
- });
707
- server.tool("bento_guess_gender", "Guess the gender based on a first name. Returns gender and confidence score.", {
708
- name: z.string().describe("First name to analyze"),
709
- }, async ({ name }) => {
710
- try {
711
- const bento = getBentoClient();
712
- const result = await bento.V1.Experimental.guessGender({ name });
713
- return {
714
- content: [{ type: "text", text: formatResponse(result) }],
715
- };
716
- }
717
- catch (error) {
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
- const bento = getBentoClient();
728
- const location = await bento.V1.Experimental.geoLocateIP(ip);
729
- return {
730
- content: [{ type: "text", text: formatResponse(location) }],
731
- };
732
- }
733
- catch (error) {
734
- return {
735
- content: [{ type: "text", text: handleError(error) }],
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
- const bento = getBentoClient();
750
- const result = await bento.V1.Experimental.getBlacklistStatus(domain ? { domain } : { ipAddress: ip });
751
- return {
752
- content: [{ type: "text", text: formatResponse(result) }],
753
- };
754
- }
755
- catch (error) {
756
- return {
757
- content: [{ type: "text", text: handleError(error) }],
758
- };
759
- }
760
- });
761
- server.tool("bento_moderate_content", "Check content for potential issues using AI content moderation.", {
762
- content: z.string().describe("Content to moderate"),
763
- }, async ({ content }) => {
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
- const bento = getBentoClient();
766
- const result = await bento.V1.Experimental.getContentModeration(content);
767
- return {
768
- content: [{ type: "text", text: formatResponse(result) }],
769
- };
770
- }
771
- catch (error) {
772
- return {
773
- content: [{ type: "text", text: handleError(error) }],
774
- };
775
- }
776
- });
777
- // =============================================================================
778
- // FORM TOOLS
779
- // =============================================================================
780
- server.tool("bento_get_form_responses", "Get all responses for a specific Bento form.", {
781
- formId: z.string().describe("The form ID to get responses for"),
782
- }, async ({ formId }) => {
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
- const bento = getBentoClient();
785
- const responses = await bento.V1.Forms.getResponses(formId);
786
- return {
787
- content: [{ type: "text", text: formatResponse(responses) }],
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
- catch (error) {
791
- return {
792
- content: [{ type: "text", text: handleError(error) }],
793
- };
794
- }
795
- });
796
- // =============================================================================
797
- // RUN SERVER
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
- const transport = new StdioServerTransport();
801
- await server.connect(transport);
802
- console.error("Bento MCP Server running on stdio");
520
+ const transport = new StdioServerTransport();
521
+ await server.connect(transport);
522
+ console.error(`Bento MCP Server v${VERSION} running on stdio`);
803
523
  }
804
- main().catch((error) => {
805
- console.error("Fatal error:", error);
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