lettr-mcp 1.1.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -0
- package/dist/index.js +8 -1
- package/dist/lettr.js +10 -3
- package/dist/package.json +1 -1
- package/dist/tools/audience-contacts.js +256 -0
- package/dist/tools/audience-lists.js +141 -0
- package/dist/tools/audience-memberships.js +129 -0
- package/dist/tools/audience-properties.js +140 -0
- package/dist/tools/audience-segments.js +222 -0
- package/dist/tools/audience-topics.js +154 -0
- package/dist/tools/campaigns.js +270 -0
- package/dist/tools/index.js +7 -0
- package/dist/tools/templates.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,6 +19,7 @@ The official [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) se
|
|
|
19
19
|
- **Domains** — List, create, get, delete, and [verify sending domains](https://docs.lettr.com/learn/domains/sending-domains). View DNS records required for SPF, DKIM, and DMARC authentication.
|
|
20
20
|
- **Webhooks** — List, create, get, update, and delete [webhook configurations](https://docs.lettr.com/learn/webhooks/introduction) for real-time email event notifications.
|
|
21
21
|
- **Projects** — List the projects available to your team so you can target template and email tools at a specific project.
|
|
22
|
+
- **Audience** — Manage contacts, lists, subscription topics, custom properties, and segments. Create and update contacts (with double opt-in), attach contacts to lists and topics, bulk import, and build segments from match conditions.
|
|
22
23
|
- **System** — Health check and API key validation for client setup and diagnostics.
|
|
23
24
|
|
|
24
25
|
## Setup
|
|
@@ -139,6 +140,44 @@ Environment variables:
|
|
|
139
140
|
|------|-------------|
|
|
140
141
|
| `list-projects` | List projects owned by the team — useful for discovering project IDs |
|
|
141
142
|
|
|
143
|
+
### Audience
|
|
144
|
+
|
|
145
|
+
| Tool | Description |
|
|
146
|
+
|------|-------------|
|
|
147
|
+
| `list-audience-lists` | List audience (contact) lists with pagination |
|
|
148
|
+
| `create-audience-list` | Create a new audience list |
|
|
149
|
+
| `get-audience-list` | Get a single list and its contact count |
|
|
150
|
+
| `update-audience-list` | Rename an audience list |
|
|
151
|
+
| `delete-audience-list` | Delete an audience list |
|
|
152
|
+
| `bulk-delete-audience-lists` | Delete up to 50 lists in one call |
|
|
153
|
+
| `list-audience-contacts` | List contacts with search, status, list, and segment filters |
|
|
154
|
+
| `get-audience-contact` | Get a contact with its properties, lists, and topics |
|
|
155
|
+
| `create-audience-contact` | Create a contact, optionally with double opt-in |
|
|
156
|
+
| `bulk-create-audience-contacts` | Create many contacts from a list of emails |
|
|
157
|
+
| `update-audience-contact` | Update a contact's email, status, or properties |
|
|
158
|
+
| `delete-audience-contact` | Delete a contact |
|
|
159
|
+
| `attach-contact-to-list` | Add a contact to a list |
|
|
160
|
+
| `detach-contact-from-list` | Remove a contact from a list |
|
|
161
|
+
| `subscribe-contact-to-topic` | Subscribe a contact to a topic |
|
|
162
|
+
| `unsubscribe-contact-from-topic` | Unsubscribe a contact from a topic |
|
|
163
|
+
| `bulk-attach-contacts-to-lists` | Attach many contacts to many lists at once |
|
|
164
|
+
| `bulk-detach-contacts-from-lists` | Detach many contacts from many lists at once |
|
|
165
|
+
| `list-audience-topics` | List subscription topics with pagination |
|
|
166
|
+
| `create-audience-topic` | Create a subscription topic |
|
|
167
|
+
| `get-audience-topic` | Get a single topic |
|
|
168
|
+
| `update-audience-topic` | Update a topic's name, description, or visibility |
|
|
169
|
+
| `delete-audience-topic` | Delete a subscription topic |
|
|
170
|
+
| `list-audience-properties` | List custom contact properties |
|
|
171
|
+
| `create-audience-property` | Define a new custom property |
|
|
172
|
+
| `get-audience-property` | Get a single property |
|
|
173
|
+
| `update-audience-property` | Update a property's fallback value |
|
|
174
|
+
| `delete-audience-property` | Delete a custom property |
|
|
175
|
+
| `list-audience-segments` | List segments, optionally filtered by list |
|
|
176
|
+
| `create-audience-segment` | Create a segment from match conditions |
|
|
177
|
+
| `get-audience-segment` | Get a single segment and its conditions |
|
|
178
|
+
| `update-audience-segment` | Update a segment's name, list, or conditions |
|
|
179
|
+
| `delete-audience-segment` | Delete a segment |
|
|
180
|
+
|
|
142
181
|
### System
|
|
143
182
|
|
|
144
183
|
| Tool | Description |
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
4
4
|
import minimist from 'minimist';
|
|
5
5
|
import { LettrClient } from './lettr.js';
|
|
6
6
|
import packageJson from './package.json' with { type: 'json' };
|
|
7
|
-
import { addDomainTools, addEmailTools, addProjectTools, addSystemTools, addTemplateTools, addWebhookTools, } from './tools/index.js';
|
|
7
|
+
import { addAudienceContactTools, addAudienceListTools, addAudienceMembershipTools, addAudiencePropertyTools, addAudienceSegmentTools, addAudienceTopicTools, addCampaignTools, addDomainTools, addEmailTools, addProjectTools, addSystemTools, addTemplateTools, addWebhookTools, } from './tools/index.js';
|
|
8
8
|
const argv = minimist(process.argv.slice(2));
|
|
9
9
|
const apiKey = argv.key || process.env.LETTR_API_KEY;
|
|
10
10
|
const senderEmailAddress = argv.sender || process.env.SENDER_EMAIL_ADDRESS;
|
|
@@ -25,6 +25,13 @@ addTemplateTools(server, lettr);
|
|
|
25
25
|
addDomainTools(server, lettr);
|
|
26
26
|
addWebhookTools(server, lettr);
|
|
27
27
|
addProjectTools(server, lettr);
|
|
28
|
+
addAudienceListTools(server, lettr);
|
|
29
|
+
addAudienceContactTools(server, lettr);
|
|
30
|
+
addAudienceMembershipTools(server, lettr);
|
|
31
|
+
addAudienceTopicTools(server, lettr);
|
|
32
|
+
addAudiencePropertyTools(server, lettr);
|
|
33
|
+
addAudienceSegmentTools(server, lettr);
|
|
34
|
+
addCampaignTools(server, lettr);
|
|
28
35
|
addSystemTools(server, lettr);
|
|
29
36
|
async function main() {
|
|
30
37
|
const transport = new StdioServerTransport();
|
package/dist/lettr.js
CHANGED
|
@@ -18,7 +18,11 @@ export class LettrClient {
|
|
|
18
18
|
Accept: 'application/json',
|
|
19
19
|
};
|
|
20
20
|
const options = { method, headers };
|
|
21
|
-
if (body &&
|
|
21
|
+
if (body &&
|
|
22
|
+
(method === 'POST' ||
|
|
23
|
+
method === 'PUT' ||
|
|
24
|
+
method === 'PATCH' ||
|
|
25
|
+
method === 'DELETE')) {
|
|
22
26
|
headers['Content-Type'] = 'application/json';
|
|
23
27
|
options.body = JSON.stringify(body);
|
|
24
28
|
}
|
|
@@ -45,7 +49,10 @@ export class LettrClient {
|
|
|
45
49
|
async put(path, body, query) {
|
|
46
50
|
return this.request('PUT', path, body, query);
|
|
47
51
|
}
|
|
48
|
-
async
|
|
49
|
-
return this.request('
|
|
52
|
+
async patch(path, body, query) {
|
|
53
|
+
return this.request('PATCH', path, body, query);
|
|
54
|
+
}
|
|
55
|
+
async delete(path, body, query) {
|
|
56
|
+
return this.request('DELETE', path, body, query);
|
|
50
57
|
}
|
|
51
58
|
}
|
package/dist/package.json
CHANGED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
// The API stores every custom property value as a string (max 1000 chars).
|
|
3
|
+
// On update, a null value removes the property from the contact.
|
|
4
|
+
const createPropertyValue = z.string().max(1000);
|
|
5
|
+
const updatePropertyValue = z.string().max(1000).nullable();
|
|
6
|
+
function formatContact(c) {
|
|
7
|
+
const props = Object.entries(c.properties ?? {});
|
|
8
|
+
const lines = [
|
|
9
|
+
`ID: ${c.id}`,
|
|
10
|
+
`Email: ${c.email}`,
|
|
11
|
+
`Status: ${c.status}`,
|
|
12
|
+
`Created: ${c.created_at}`,
|
|
13
|
+
`Lists: ${c.lists.length > 0 ? c.lists.map((l) => `${l.name} (${l.id})`).join(', ') : 'none'}`,
|
|
14
|
+
`Topics: ${c.topics.length > 0 ? c.topics.map((t) => `${t.name} (${t.id})`).join(', ') : 'none'}`,
|
|
15
|
+
];
|
|
16
|
+
if (props.length > 0) {
|
|
17
|
+
lines.push(`Properties:\n${props.map(([k, v]) => ` ${k}: ${JSON.stringify(v)}`).join('\n')}`);
|
|
18
|
+
}
|
|
19
|
+
return lines.join('\n');
|
|
20
|
+
}
|
|
21
|
+
export function addAudienceContactTools(server, lettr) {
|
|
22
|
+
server.registerTool('list-audience-contacts', {
|
|
23
|
+
title: 'List Audience Contacts',
|
|
24
|
+
description: 'List audience contacts with pagination and optional filters. Filter by free-text search (email or name), status, a specific list, or a specific segment.',
|
|
25
|
+
inputSchema: {
|
|
26
|
+
per_page: z
|
|
27
|
+
.number()
|
|
28
|
+
.int()
|
|
29
|
+
.min(1)
|
|
30
|
+
.max(100)
|
|
31
|
+
.optional()
|
|
32
|
+
.describe('Results per page (1-100, default 20)'),
|
|
33
|
+
page: z
|
|
34
|
+
.number()
|
|
35
|
+
.int()
|
|
36
|
+
.min(1)
|
|
37
|
+
.optional()
|
|
38
|
+
.describe('Page number (default 1)'),
|
|
39
|
+
search: z
|
|
40
|
+
.string()
|
|
41
|
+
.max(255)
|
|
42
|
+
.optional()
|
|
43
|
+
.describe('Search by email address or contact name'),
|
|
44
|
+
status: z
|
|
45
|
+
.enum([
|
|
46
|
+
'subscribed',
|
|
47
|
+
'unsubscribed',
|
|
48
|
+
'bounced',
|
|
49
|
+
'complained',
|
|
50
|
+
'unverified',
|
|
51
|
+
])
|
|
52
|
+
.optional()
|
|
53
|
+
.describe('Filter by contact status'),
|
|
54
|
+
list_id: z
|
|
55
|
+
.string()
|
|
56
|
+
.optional()
|
|
57
|
+
.describe('Filter to contacts in a specific list (must belong to your team)'),
|
|
58
|
+
segment_id: z
|
|
59
|
+
.string()
|
|
60
|
+
.optional()
|
|
61
|
+
.describe('Filter to contacts matching a specific segment (must belong to your team)'),
|
|
62
|
+
},
|
|
63
|
+
}, async ({ per_page, page, search, status, list_id, segment_id }) => {
|
|
64
|
+
const query = {};
|
|
65
|
+
if (per_page)
|
|
66
|
+
query.per_page = per_page;
|
|
67
|
+
if (page)
|
|
68
|
+
query.page = page;
|
|
69
|
+
if (search)
|
|
70
|
+
query.search = search;
|
|
71
|
+
if (status)
|
|
72
|
+
query.status = status;
|
|
73
|
+
if (list_id)
|
|
74
|
+
query.list_id = list_id;
|
|
75
|
+
if (segment_id)
|
|
76
|
+
query.segment_id = segment_id;
|
|
77
|
+
const response = await lettr.get('/audience/contacts', query);
|
|
78
|
+
const { contacts, pagination } = response.data;
|
|
79
|
+
if (contacts.length === 0) {
|
|
80
|
+
return { content: [{ type: 'text', text: 'No contacts found.' }] };
|
|
81
|
+
}
|
|
82
|
+
const lines = contacts
|
|
83
|
+
.map((c) => `- ${c.email} (id: ${c.id}, status: ${c.status})`)
|
|
84
|
+
.join('\n');
|
|
85
|
+
return {
|
|
86
|
+
content: [
|
|
87
|
+
{
|
|
88
|
+
type: 'text',
|
|
89
|
+
text: `Found ${pagination.total} contact(s) — page ${pagination.current_page}/${pagination.last_page}:\n\n${lines}`,
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
server.registerTool('get-audience-contact', {
|
|
95
|
+
title: 'Get Audience Contact',
|
|
96
|
+
description: 'Retrieve a single contact by ID, including status, custom properties, and the lists and topics it belongs to.',
|
|
97
|
+
inputSchema: {
|
|
98
|
+
contactId: z.string().nonempty().describe('The contact ID'),
|
|
99
|
+
},
|
|
100
|
+
}, async ({ contactId }) => {
|
|
101
|
+
const response = await lettr.get(`/audience/contacts/${encodeURIComponent(contactId)}`);
|
|
102
|
+
return {
|
|
103
|
+
content: [
|
|
104
|
+
{ type: 'text', text: 'Contact details:' },
|
|
105
|
+
{ type: 'text', text: formatContact(response.data) },
|
|
106
|
+
],
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
server.registerTool('create-audience-contact', {
|
|
110
|
+
title: 'Create Audience Contact',
|
|
111
|
+
description: `Create a single audience contact.
|
|
112
|
+
|
|
113
|
+
- \`properties\` keys must match properties already defined for the team (use list-audience-properties).
|
|
114
|
+
- When \`double_opt_in\` is provided, the contact is created in \`unverified\` status and receives a confirmation email; all four of its fields (from, subject, template_slug, redirect_url) are required.`,
|
|
115
|
+
inputSchema: {
|
|
116
|
+
email: z.email().max(255).describe('Contact email address'),
|
|
117
|
+
list_id: z
|
|
118
|
+
.string()
|
|
119
|
+
.optional()
|
|
120
|
+
.describe('Optional list ID to add the contact to'),
|
|
121
|
+
properties: z
|
|
122
|
+
.record(z.string(), createPropertyValue)
|
|
123
|
+
.optional()
|
|
124
|
+
.describe('Custom property values, each as a string (max 1000 chars). Each key must match a property defined for the team.'),
|
|
125
|
+
double_opt_in: z
|
|
126
|
+
.object({
|
|
127
|
+
from: z
|
|
128
|
+
.email()
|
|
129
|
+
.max(255)
|
|
130
|
+
.describe('Sender email for the confirmation email'),
|
|
131
|
+
from_name: z
|
|
132
|
+
.string()
|
|
133
|
+
.max(255)
|
|
134
|
+
.optional()
|
|
135
|
+
.describe('Sender display name'),
|
|
136
|
+
subject: z
|
|
137
|
+
.string()
|
|
138
|
+
.max(998)
|
|
139
|
+
.describe('Subject line of the confirmation email'),
|
|
140
|
+
template_slug: z
|
|
141
|
+
.string()
|
|
142
|
+
.max(255)
|
|
143
|
+
.describe('Template slug used for the confirmation email'),
|
|
144
|
+
redirect_url: z
|
|
145
|
+
.url()
|
|
146
|
+
.max(2048)
|
|
147
|
+
.describe('URL the contact is sent to after confirming'),
|
|
148
|
+
})
|
|
149
|
+
.optional()
|
|
150
|
+
.describe('Double opt-in configuration. When set, the contact is created unverified until they click the confirmation link.'),
|
|
151
|
+
},
|
|
152
|
+
}, async ({ email, list_id, properties, double_opt_in }) => {
|
|
153
|
+
const body = { email };
|
|
154
|
+
if (list_id)
|
|
155
|
+
body.list_id = list_id;
|
|
156
|
+
if (properties)
|
|
157
|
+
body.properties = properties;
|
|
158
|
+
if (double_opt_in)
|
|
159
|
+
body.double_opt_in = double_opt_in;
|
|
160
|
+
const response = await lettr.post('/audience/contacts', body);
|
|
161
|
+
return {
|
|
162
|
+
content: [
|
|
163
|
+
{ type: 'text', text: 'Contact created successfully.' },
|
|
164
|
+
{ type: 'text', text: formatContact(response.data) },
|
|
165
|
+
],
|
|
166
|
+
};
|
|
167
|
+
});
|
|
168
|
+
server.registerTool('bulk-create-audience-contacts', {
|
|
169
|
+
title: 'Bulk Create Audience Contacts',
|
|
170
|
+
description: 'Create many contacts at once from a list of email addresses. Optionally add all of them to a list and/or apply the same custom properties to every contact in the batch. Already-existing emails are skipped (reported separately).',
|
|
171
|
+
inputSchema: {
|
|
172
|
+
emails: z
|
|
173
|
+
.array(z.email().max(255))
|
|
174
|
+
.min(1)
|
|
175
|
+
.max(1000)
|
|
176
|
+
.describe('Email addresses to create contacts for (max 1000)'),
|
|
177
|
+
list_id: z
|
|
178
|
+
.string()
|
|
179
|
+
.optional()
|
|
180
|
+
.describe('Optional list ID to add all contacts to'),
|
|
181
|
+
properties: z
|
|
182
|
+
.record(z.string(), createPropertyValue)
|
|
183
|
+
.optional()
|
|
184
|
+
.describe('Custom property values applied to every contact created in this batch, each as a string (max 1000 chars). Each key must match a property defined for the team.'),
|
|
185
|
+
},
|
|
186
|
+
}, async ({ emails, list_id, properties }) => {
|
|
187
|
+
const body = { emails };
|
|
188
|
+
if (list_id)
|
|
189
|
+
body.list_id = list_id;
|
|
190
|
+
if (properties)
|
|
191
|
+
body.properties = properties;
|
|
192
|
+
const response = await lettr.post('/audience/contacts/bulk', body);
|
|
193
|
+
const { created, already_existed } = response.data;
|
|
194
|
+
return {
|
|
195
|
+
content: [
|
|
196
|
+
{
|
|
197
|
+
type: 'text',
|
|
198
|
+
text: `Created ${created} contact(s); ${already_existed} already existed.`,
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
};
|
|
202
|
+
});
|
|
203
|
+
server.registerTool('update-audience-contact', {
|
|
204
|
+
title: 'Update Audience Contact',
|
|
205
|
+
description: `Update a contact. All fields are optional — only provided fields change.
|
|
206
|
+
|
|
207
|
+
- \`status\` may only be set to \`subscribed\` or \`unsubscribed\`.
|
|
208
|
+
- \`properties\` is a partial update: include only the keys you want to change. Setting a property to \`null\` removes it from the contact.`,
|
|
209
|
+
inputSchema: {
|
|
210
|
+
contactId: z.string().nonempty().describe('The contact ID to update'),
|
|
211
|
+
email: z
|
|
212
|
+
.email()
|
|
213
|
+
.max(255)
|
|
214
|
+
.optional()
|
|
215
|
+
.describe('New email address (must remain unique within the team)'),
|
|
216
|
+
status: z
|
|
217
|
+
.enum(['subscribed', 'unsubscribed'])
|
|
218
|
+
.optional()
|
|
219
|
+
.describe('New subscription status'),
|
|
220
|
+
properties: z
|
|
221
|
+
.record(z.string(), updatePropertyValue)
|
|
222
|
+
.optional()
|
|
223
|
+
.describe('Partial property update; each value is a string (max 1000 chars). A property set to null is removed from the contact.'),
|
|
224
|
+
},
|
|
225
|
+
}, async ({ contactId, ...rest }) => {
|
|
226
|
+
const body = {};
|
|
227
|
+
for (const [key, value] of Object.entries(rest)) {
|
|
228
|
+
if (value !== undefined)
|
|
229
|
+
body[key] = value;
|
|
230
|
+
}
|
|
231
|
+
const response = await lettr.patch(`/audience/contacts/${encodeURIComponent(contactId)}`, body);
|
|
232
|
+
return {
|
|
233
|
+
content: [
|
|
234
|
+
{ type: 'text', text: 'Contact updated successfully.' },
|
|
235
|
+
{ type: 'text', text: formatContact(response.data) },
|
|
236
|
+
],
|
|
237
|
+
};
|
|
238
|
+
});
|
|
239
|
+
server.registerTool('delete-audience-contact', {
|
|
240
|
+
title: 'Delete Audience Contact',
|
|
241
|
+
description: 'Delete a contact. Before using this tool, you MUST double-check with the user that they want to delete this contact. Warn them that this action is irreversible and removes the contact from all lists and topics.',
|
|
242
|
+
inputSchema: {
|
|
243
|
+
contactId: z.string().nonempty().describe('The contact ID to delete'),
|
|
244
|
+
},
|
|
245
|
+
}, async ({ contactId }) => {
|
|
246
|
+
await lettr.delete(`/audience/contacts/${encodeURIComponent(contactId)}`);
|
|
247
|
+
return {
|
|
248
|
+
content: [
|
|
249
|
+
{
|
|
250
|
+
type: 'text',
|
|
251
|
+
text: `Contact "${contactId}" deleted successfully.`,
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
};
|
|
255
|
+
});
|
|
256
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
function formatList(l) {
|
|
3
|
+
return `ID: ${l.id}\nName: ${l.name}\nContacts: ${l.contacts_count}`;
|
|
4
|
+
}
|
|
5
|
+
export function addAudienceListTools(server, lettr) {
|
|
6
|
+
server.registerTool('list-audience-lists', {
|
|
7
|
+
title: 'List Audience Lists',
|
|
8
|
+
description: 'List the audience (contact) lists for your team, with pagination. Use this to discover list IDs to pass to contact and segment tools.',
|
|
9
|
+
inputSchema: {
|
|
10
|
+
per_page: z
|
|
11
|
+
.number()
|
|
12
|
+
.int()
|
|
13
|
+
.min(1)
|
|
14
|
+
.max(100)
|
|
15
|
+
.optional()
|
|
16
|
+
.describe('Results per page (1-100, default 20)'),
|
|
17
|
+
page: z
|
|
18
|
+
.number()
|
|
19
|
+
.int()
|
|
20
|
+
.min(1)
|
|
21
|
+
.optional()
|
|
22
|
+
.describe('Page number (default 1)'),
|
|
23
|
+
},
|
|
24
|
+
}, async ({ per_page, page }) => {
|
|
25
|
+
const query = {};
|
|
26
|
+
if (per_page)
|
|
27
|
+
query.per_page = per_page;
|
|
28
|
+
if (page)
|
|
29
|
+
query.page = page;
|
|
30
|
+
const response = await lettr.get('/audience/lists', query);
|
|
31
|
+
const { lists, pagination } = response.data;
|
|
32
|
+
if (lists.length === 0) {
|
|
33
|
+
return {
|
|
34
|
+
content: [{ type: 'text', text: 'No audience lists found.' }],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const lines = lists
|
|
38
|
+
.map((l) => `- ${l.name} (id: ${l.id}, contacts: ${l.contacts_count})`)
|
|
39
|
+
.join('\n');
|
|
40
|
+
return {
|
|
41
|
+
content: [
|
|
42
|
+
{
|
|
43
|
+
type: 'text',
|
|
44
|
+
text: `Found ${pagination.total} list(s) — page ${pagination.current_page}/${pagination.last_page}:\n\n${lines}`,
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
server.registerTool('create-audience-list', {
|
|
50
|
+
title: 'Create Audience List',
|
|
51
|
+
description: 'Create a new audience list. The name must be unique within the team.',
|
|
52
|
+
inputSchema: {
|
|
53
|
+
name: z
|
|
54
|
+
.string()
|
|
55
|
+
.nonempty()
|
|
56
|
+
.max(255)
|
|
57
|
+
.describe('List name (must be unique within the team)'),
|
|
58
|
+
},
|
|
59
|
+
}, async ({ name }) => {
|
|
60
|
+
const response = await lettr.post('/audience/lists', { name });
|
|
61
|
+
return {
|
|
62
|
+
content: [
|
|
63
|
+
{ type: 'text', text: 'Audience list created successfully.' },
|
|
64
|
+
{ type: 'text', text: formatList(response.data) },
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
server.registerTool('get-audience-list', {
|
|
69
|
+
title: 'Get Audience List',
|
|
70
|
+
description: 'Retrieve a single audience list by its ID, including its current contact count.',
|
|
71
|
+
inputSchema: {
|
|
72
|
+
listId: z.string().nonempty().describe('The list ID'),
|
|
73
|
+
},
|
|
74
|
+
}, async ({ listId }) => {
|
|
75
|
+
const response = await lettr.get(`/audience/lists/${encodeURIComponent(listId)}`);
|
|
76
|
+
return {
|
|
77
|
+
content: [
|
|
78
|
+
{ type: 'text', text: 'Audience list details:' },
|
|
79
|
+
{ type: 'text', text: formatList(response.data) },
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
server.registerTool('update-audience-list', {
|
|
84
|
+
title: 'Update Audience List',
|
|
85
|
+
description: 'Rename an audience list. The new name must remain unique within the team.',
|
|
86
|
+
inputSchema: {
|
|
87
|
+
listId: z.string().nonempty().describe('The list ID to update'),
|
|
88
|
+
name: z
|
|
89
|
+
.string()
|
|
90
|
+
.nonempty()
|
|
91
|
+
.max(255)
|
|
92
|
+
.describe('New list name (must remain unique within the team)'),
|
|
93
|
+
},
|
|
94
|
+
}, async ({ listId, name }) => {
|
|
95
|
+
const response = await lettr.patch(`/audience/lists/${encodeURIComponent(listId)}`, { name });
|
|
96
|
+
return {
|
|
97
|
+
content: [
|
|
98
|
+
{ type: 'text', text: 'Audience list updated successfully.' },
|
|
99
|
+
{ type: 'text', text: formatList(response.data) },
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
server.registerTool('delete-audience-list', {
|
|
104
|
+
title: 'Delete Audience List',
|
|
105
|
+
description: 'Delete an audience list. Before using this tool, you MUST double-check with the user that they want to delete this list. Warn them that this action is irreversible — the list is removed and its contacts are detached from it.',
|
|
106
|
+
inputSchema: {
|
|
107
|
+
listId: z.string().nonempty().describe('The list ID to delete'),
|
|
108
|
+
},
|
|
109
|
+
}, async ({ listId }) => {
|
|
110
|
+
await lettr.delete(`/audience/lists/${encodeURIComponent(listId)}`);
|
|
111
|
+
return {
|
|
112
|
+
content: [
|
|
113
|
+
{
|
|
114
|
+
type: 'text',
|
|
115
|
+
text: `Audience list "${listId}" deleted successfully.`,
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
server.registerTool('bulk-delete-audience-lists', {
|
|
121
|
+
title: 'Bulk Delete Audience Lists',
|
|
122
|
+
description: 'Delete between 1 and 50 audience lists in a single call. Before using this tool, you MUST double-check with the user. Warn them that this action is irreversible — every listed list is removed and its contacts detached. All IDs must belong to your team.',
|
|
123
|
+
inputSchema: {
|
|
124
|
+
list_ids: z
|
|
125
|
+
.array(z.string().nonempty())
|
|
126
|
+
.min(1)
|
|
127
|
+
.max(50)
|
|
128
|
+
.describe('1–50 list IDs to delete (all must belong to your team)'),
|
|
129
|
+
},
|
|
130
|
+
}, async ({ list_ids }) => {
|
|
131
|
+
const response = await lettr.delete('/audience/lists/bulk', { list_ids });
|
|
132
|
+
return {
|
|
133
|
+
content: [
|
|
134
|
+
{
|
|
135
|
+
type: 'text',
|
|
136
|
+
text: `Deleted ${response.data.deleted} list(s).`,
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export function addAudienceMembershipTools(server, lettr) {
|
|
3
|
+
server.registerTool('attach-contact-to-list', {
|
|
4
|
+
title: 'Attach Contact to List',
|
|
5
|
+
description: 'Add a single contact to a single list. Both the contact and the list must belong to your team.',
|
|
6
|
+
inputSchema: {
|
|
7
|
+
contactId: z.string().nonempty().describe('The contact ID'),
|
|
8
|
+
listId: z.string().nonempty().describe('The list ID'),
|
|
9
|
+
},
|
|
10
|
+
}, async ({ contactId, listId }) => {
|
|
11
|
+
await lettr.post(`/audience/contacts/${encodeURIComponent(contactId)}/lists/${encodeURIComponent(listId)}`);
|
|
12
|
+
return {
|
|
13
|
+
content: [
|
|
14
|
+
{
|
|
15
|
+
type: 'text',
|
|
16
|
+
text: `Contact "${contactId}" attached to list "${listId}".`,
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
server.registerTool('detach-contact-from-list', {
|
|
22
|
+
title: 'Detach Contact from List',
|
|
23
|
+
description: 'Remove a single contact from a single list.',
|
|
24
|
+
inputSchema: {
|
|
25
|
+
contactId: z.string().nonempty().describe('The contact ID'),
|
|
26
|
+
listId: z.string().nonempty().describe('The list ID'),
|
|
27
|
+
},
|
|
28
|
+
}, async ({ contactId, listId }) => {
|
|
29
|
+
await lettr.delete(`/audience/contacts/${encodeURIComponent(contactId)}/lists/${encodeURIComponent(listId)}`);
|
|
30
|
+
return {
|
|
31
|
+
content: [
|
|
32
|
+
{
|
|
33
|
+
type: 'text',
|
|
34
|
+
text: `Contact "${contactId}" detached from list "${listId}".`,
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
server.registerTool('subscribe-contact-to-topic', {
|
|
40
|
+
title: 'Subscribe Contact to Topic',
|
|
41
|
+
description: 'Subscribe a single contact to a single topic. Both must belong to your team.',
|
|
42
|
+
inputSchema: {
|
|
43
|
+
contactId: z.string().nonempty().describe('The contact ID'),
|
|
44
|
+
topicId: z.string().nonempty().describe('The topic ID'),
|
|
45
|
+
},
|
|
46
|
+
}, async ({ contactId, topicId }) => {
|
|
47
|
+
await lettr.post(`/audience/contacts/${encodeURIComponent(contactId)}/topics/${encodeURIComponent(topicId)}`);
|
|
48
|
+
return {
|
|
49
|
+
content: [
|
|
50
|
+
{
|
|
51
|
+
type: 'text',
|
|
52
|
+
text: `Contact "${contactId}" subscribed to topic "${topicId}".`,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
server.registerTool('unsubscribe-contact-from-topic', {
|
|
58
|
+
title: 'Unsubscribe Contact from Topic',
|
|
59
|
+
description: 'Unsubscribe a single contact from a single topic.',
|
|
60
|
+
inputSchema: {
|
|
61
|
+
contactId: z.string().nonempty().describe('The contact ID'),
|
|
62
|
+
topicId: z.string().nonempty().describe('The topic ID'),
|
|
63
|
+
},
|
|
64
|
+
}, async ({ contactId, topicId }) => {
|
|
65
|
+
await lettr.delete(`/audience/contacts/${encodeURIComponent(contactId)}/topics/${encodeURIComponent(topicId)}`);
|
|
66
|
+
return {
|
|
67
|
+
content: [
|
|
68
|
+
{
|
|
69
|
+
type: 'text',
|
|
70
|
+
text: `Contact "${contactId}" unsubscribed from topic "${topicId}".`,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
server.registerTool('bulk-attach-contacts-to-lists', {
|
|
76
|
+
title: 'Bulk Attach Contacts to Lists',
|
|
77
|
+
description: 'Attach multiple contacts to multiple lists at once. Every contact is attached to every list (a cartesian product of contact_ids × list_ids). All IDs must belong to your team.',
|
|
78
|
+
inputSchema: {
|
|
79
|
+
contact_ids: z
|
|
80
|
+
.array(z.string().nonempty())
|
|
81
|
+
.min(1)
|
|
82
|
+
.max(1000)
|
|
83
|
+
.describe('Contact IDs to attach (max 1000)'),
|
|
84
|
+
list_ids: z
|
|
85
|
+
.array(z.string().nonempty())
|
|
86
|
+
.min(1)
|
|
87
|
+
.max(50)
|
|
88
|
+
.describe('List IDs to attach the contacts to (max 50)'),
|
|
89
|
+
},
|
|
90
|
+
}, async ({ contact_ids, list_ids }) => {
|
|
91
|
+
const response = await lettr.post('/audience/contacts/lists/bulk', { contact_ids, list_ids });
|
|
92
|
+
const { attached, already_attached, total_pairs } = response.data;
|
|
93
|
+
return {
|
|
94
|
+
content: [
|
|
95
|
+
{
|
|
96
|
+
type: 'text',
|
|
97
|
+
text: `Attached ${attached} of ${total_pairs} contact-list pair(s); ${already_attached} were already attached.`,
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
server.registerTool('bulk-detach-contacts-from-lists', {
|
|
103
|
+
title: 'Bulk Detach Contacts from Lists',
|
|
104
|
+
description: 'Detach multiple contacts from multiple lists at once (a cartesian product of contact_ids × list_ids). Before using this tool, you MUST double-check with the user, as it removes many memberships at once.',
|
|
105
|
+
inputSchema: {
|
|
106
|
+
contact_ids: z
|
|
107
|
+
.array(z.string().nonempty())
|
|
108
|
+
.min(1)
|
|
109
|
+
.max(1000)
|
|
110
|
+
.describe('Contact IDs to detach (max 1000)'),
|
|
111
|
+
list_ids: z
|
|
112
|
+
.array(z.string().nonempty())
|
|
113
|
+
.min(1)
|
|
114
|
+
.max(50)
|
|
115
|
+
.describe('List IDs to detach the contacts from (max 50)'),
|
|
116
|
+
},
|
|
117
|
+
}, async ({ contact_ids, list_ids }) => {
|
|
118
|
+
const response = await lettr.delete('/audience/contacts/lists/bulk', { contact_ids, list_ids });
|
|
119
|
+
const { detached, not_present, total_pairs } = response.data;
|
|
120
|
+
return {
|
|
121
|
+
content: [
|
|
122
|
+
{
|
|
123
|
+
type: 'text',
|
|
124
|
+
text: `Detached ${detached} of ${total_pairs} contact-list pair(s); ${not_present} were not attached.`,
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
}
|