lettr-mcp 1.0.0 → 1.2.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 CHANGED
@@ -14,10 +14,13 @@ The official [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) se
14
14
 
15
15
  ## Features
16
16
 
17
- - **Send Emails** — Send transactional emails with HTML, plain text, CC/BCC, attachments, tracking options, metadata, and tags. Supports [template-based sending](https://docs.lettr.com/learn/templates/introduction) with merge tag substitution.
18
- - **Templates** — List, create, get, update, and delete email templates. Retrieve [merge tags](https://docs.lettr.com/learn/templates/template-language) to discover which variables a template expects before sending.
17
+ - **Send Emails** — Send transactional emails with HTML, plain text, CC/BCC, attachments, tracking options, metadata, and tags. Supports [template-based sending](https://docs.lettr.com/learn/templates/introduction) with merge tag substitution, scheduled delivery, and inspecting sent messages and events.
18
+ - **Templates** — List, create, get, update, and delete email templates. Retrieve rendered HTML and [merge tags](https://docs.lettr.com/learn/templates/template-language) to discover which variables a template expects before sending.
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
- - **Webhooks** — List and inspect [webhook configurations](https://docs.lettr.com/learn/webhooks/introduction) for real-time email event notifications.
20
+ - **Webhooks** — List, create, get, update, and delete [webhook configurations](https://docs.lettr.com/learn/webhooks/introduction) for real-time email event notifications.
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.
23
+ - **System** — Health check and API key validation for client setup and diagnostics.
21
24
 
22
25
  ## Setup
23
26
 
@@ -92,6 +95,12 @@ Environment variables:
92
95
  | Tool | Description |
93
96
  |------|-------------|
94
97
  | `send-email` | Send a transactional email with HTML, plain text, templates, attachments, tracking, and personalization |
98
+ | `list-emails` | List recently sent emails (cursor-paginated, with recipient and date filters) |
99
+ | `list-email-events` | List email events (delivery, bounce, click, open, …) with filters by type, recipient, transmission, and date range |
100
+ | `get-email-detail` | Retrieve the full delivery timeline for a single transmission by request ID |
101
+ | `schedule-email` | Schedule a transactional email for future delivery (5+ minutes ahead, within 3 days) |
102
+ | `get-scheduled-email` | Get the state and events of a scheduled transmission |
103
+ | `cancel-scheduled-email` | Cancel a scheduled transmission before it is sent |
95
104
 
96
105
  ### Templates
97
106
 
@@ -103,6 +112,7 @@ Environment variables:
103
112
  | `update-template` | Update template name and/or content (creates new version) |
104
113
  | `delete-template` | Permanently delete a template and all versions |
105
114
  | `get-merge-tags` | Discover merge tag variables a template expects |
115
+ | `get-template-html` | Retrieve a template's rendered HTML, subject, and merge tags by project ID and slug |
106
116
 
107
117
  ### Domains
108
118
 
@@ -120,6 +130,60 @@ Environment variables:
120
130
  |------|-------------|
121
131
  | `list-webhooks` | List all webhook configurations |
122
132
  | `get-webhook` | Get webhook details and delivery status |
133
+ | `create-webhook` | Create a new webhook subscription with auth and event-type selection |
134
+ | `update-webhook` | Update an existing webhook (name, URL, auth, events, active flag) |
135
+ | `delete-webhook` | Delete a webhook subscription |
136
+
137
+ ### Projects
138
+
139
+ | Tool | Description |
140
+ |------|-------------|
141
+ | `list-projects` | List projects owned by the team — useful for discovering project IDs |
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
+
181
+ ### System
182
+
183
+ | Tool | Description |
184
+ |------|-------------|
185
+ | `health-check` | Check the Lettr API health status |
186
+ | `auth-check` | Validate the configured API key and return the team ID |
123
187
 
124
188
  ## Local Development
125
189
 
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, addTemplateTools, addWebhookTools, } from './tools/index.js';
7
+ import { addAudienceContactTools, addAudienceListTools, addAudienceMembershipTools, addAudiencePropertyTools, addAudienceSegmentTools, addAudienceTopicTools, 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;
@@ -24,6 +24,14 @@ addEmailTools(server, lettr, { senderEmailAddress, replierEmailAddress });
24
24
  addTemplateTools(server, lettr);
25
25
  addDomainTools(server, lettr);
26
26
  addWebhookTools(server, lettr);
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
+ addSystemTools(server, lettr);
27
35
  async function main() {
28
36
  const transport = new StdioServerTransport();
29
37
  await server.connect(transport);
package/dist/lettr.js CHANGED
@@ -18,20 +18,25 @@ export class LettrClient {
18
18
  Accept: 'application/json',
19
19
  };
20
20
  const options = { method, headers };
21
- if (body && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
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
  }
25
29
  const response = await fetch(url.toString(), options);
26
- const json = await response.json();
30
+ const hasBody = response.status !== 204 && response.headers.get('content-length') !== '0';
31
+ const json = hasBody ? await response.json() : undefined;
27
32
  if (!response.ok) {
28
- const err = json;
33
+ const err = (json ?? {});
29
34
  const detail = err.errors
30
35
  ? `\n${Object.entries(err.errors)
31
36
  .map(([field, msgs]) => ` ${field}: ${msgs.join(', ')}`)
32
37
  .join('\n')}`
33
38
  : '';
34
- throw new Error(`Lettr API error (${response.status}): ${err.message}${detail}`);
39
+ throw new Error(`Lettr API error (${response.status}): ${err.message ?? response.statusText}${detail}`);
35
40
  }
36
41
  return json;
37
42
  }
@@ -44,7 +49,10 @@ export class LettrClient {
44
49
  async put(path, body, query) {
45
50
  return this.request('PUT', path, body, query);
46
51
  }
47
- async delete(path, query) {
48
- return this.request('DELETE', path, undefined, query);
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);
49
57
  }
50
58
  }
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lettr-mcp",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "MCP server for the Lettr email API — send transactional emails, manage templates, domains, and webhooks from any AI assistant",
5
5
  "keywords": [
6
6
  "lettr",
@@ -16,6 +16,13 @@
16
16
  "email-templates"
17
17
  ],
18
18
  "homepage": "https://lettr.com",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/lettr-com/lettr-mcp.git"
22
+ },
23
+ "bugs": {
24
+ "url": "https://github.com/lettr-com/lettr-mcp/issues"
25
+ },
19
26
  "license": "MIT",
20
27
  "type": "module",
21
28
  "bin": {
@@ -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
+ }