lettr-mcp 1.6.0 → 1.7.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
@@ -98,9 +98,10 @@ Environment variables:
98
98
  | `list-emails` | List recently sent emails (cursor-paginated, with recipient and date filters) |
99
99
  | `list-email-events` | List email events (delivery, bounce, click, open, …) with filters by type, recipient, transmission, and date range |
100
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 |
101
+ | `schedule-email` | Schedule a transactional email for future delivery (5+ minutes ahead, within 30 days) |
102
+ | `list-scheduled-emails` | List emails waiting to be sent, with a state filter |
103
+ | `get-scheduled-email` | Get the state and events of a scheduled email |
104
+ | `cancel-scheduled-email` | Cancel a scheduled email before it is sent |
104
105
 
105
106
  ### Templates
106
107
 
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lettr-mcp",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "MCP server for the Lettr email API \u2014 send transactional emails, manage templates, domains, and webhooks from any AI assistant",
5
5
  "keywords": [
6
6
  "lettr",
@@ -19,6 +19,24 @@ const EMAIL_EVENT_TYPES = [
19
19
  'list_unsubscribe',
20
20
  'link_unsubscribe',
21
21
  ];
22
+ function formatScheduledEmail(d) {
23
+ return [
24
+ `Scheduled email: ${d.request_id}`,
25
+ `State: ${d.state}`,
26
+ d.scheduled_at ? `Scheduled for: ${d.scheduled_at}` : null,
27
+ `From: ${d.from_name ? `${d.from_name} <${d.from}>` : d.from}`,
28
+ d.subject ? `Subject: ${d.subject}` : null,
29
+ `Recipients (${d.num_recipients}): ${d.recipients.join(', ')}`,
30
+ d.tag ? `Tag: ${d.tag}` : null,
31
+ // Only meaningful once sent — this is the id webhook events carry.
32
+ d.transmission_id
33
+ ? `Transmission: ${d.transmission_id}`
34
+ : 'Transmission: not sent yet',
35
+ d.failure_reason ? `Failure reason: ${d.failure_reason}` : null,
36
+ ]
37
+ .filter((x) => x !== null)
38
+ .join('\n');
39
+ }
22
40
  const sendEmailShape = (senderEmailAddress, replierEmailAddress) => ({
23
41
  to: z
24
42
  .array(z.email().max(255))
@@ -429,14 +447,14 @@ export function addEmailTools(server, lettr, defaults) {
429
447
  });
430
448
  server.registerTool('schedule-email', {
431
449
  title: 'Schedule Email',
432
- description: `Schedule an email for future delivery. Accepts the same fields as send-email plus a required scheduled_at (ISO 8601, UTC) that is at least 5 minutes in the future and at most 3 days out.
450
+ description: `Schedule an email for future delivery. Accepts the same fields as send-email plus a required scheduled_at (ISO 8601, UTC) that is at least 5 minutes in the future and at most 30 days out.
433
451
 
434
- **Returns:** Same response as send-email (request_id + accepted/rejected counts).`,
452
+ **Returns:** The scheduled email, including the \`sch_\` request ID that get-scheduled-email and cancel-scheduled-email take.`,
435
453
  inputSchema: {
436
454
  ...sendEmailShape(senderEmailAddress, replierEmailAddress),
437
455
  scheduled_at: z
438
456
  .string()
439
- .describe('ISO 8601 UTC datetime (e.g. 2024-01-16T10:00:00Z). Must be 5+ minutes in the future and within 3 days.'),
457
+ .describe('ISO 8601 UTC datetime (e.g. 2024-01-16T10:00:00Z). Must be 5+ minutes in the future and within 30 days.'),
440
458
  },
441
459
  }, async (input) => {
442
460
  const { scheduled_at, ...sendInput } = input;
@@ -444,64 +462,80 @@ export function addEmailTools(server, lettr, defaults) {
444
462
  body.scheduled_at = scheduled_at;
445
463
  const response = await lettr.post('/emails/scheduled', body);
446
464
  return {
447
- content: [
448
- {
449
- type: 'text',
450
- text: `Email scheduled for ${scheduled_at}. Request ID: ${response.data.request_id}, Accepted: ${response.data.accepted}, Rejected: ${response.data.rejected}`,
451
- },
452
- ],
465
+ content: [{ type: 'text', text: formatScheduledEmail(response.data) }],
453
466
  };
454
467
  });
455
468
  server.registerTool('get-scheduled-email', {
456
469
  title: 'Get Scheduled Email',
457
- description: 'Retrieve details of a scheduled (but not yet sent) email, including its state, scheduled_at timestamp, recipients and any events collected so far.',
470
+ description: 'Retrieve details of a scheduled email, including its state, scheduled_at timestamp, recipients and any events collected so far. Works for every state, not only pending ones — a cancelled or sent email is still readable.',
458
471
  inputSchema: {
459
- transmission_id: z
472
+ request_id: z
460
473
  .string()
461
474
  .nonempty()
462
- .describe('Transmission ID returned by schedule-email'),
475
+ .describe('The `sch_` request ID returned by schedule-email'),
463
476
  },
464
- }, async ({ transmission_id }) => {
465
- const response = await lettr.get(`/emails/scheduled/${encodeURIComponent(transmission_id)}`);
477
+ }, async ({ request_id }) => {
478
+ const response = await lettr.get(`/emails/scheduled/${encodeURIComponent(request_id)}`);
466
479
  const d = response.data;
467
480
  const eventLines = d.events.length === 0
468
481
  ? '(no events yet)'
469
482
  : d.events.map(formatEvent).join('\n');
470
483
  return {
471
484
  content: [
472
- {
473
- type: 'text',
474
- text: [
475
- `Transmission: ${d.transmission_id}`,
476
- `State: ${d.state}`,
477
- d.scheduled_at ? `Scheduled for: ${d.scheduled_at}` : null,
478
- `From: ${d.from_name ? `${d.from_name} <${d.from}>` : d.from}`,
479
- `Subject: ${d.subject}`,
480
- `Recipients (${d.num_recipients}): ${d.recipients.join(', ')}`,
481
- ]
482
- .filter((x) => x !== null)
483
- .join('\n'),
484
- },
485
+ { type: 'text', text: formatScheduledEmail(d) },
485
486
  { type: 'text', text: `Events:\n${eventLines}` },
486
487
  ],
487
488
  };
488
489
  });
489
490
  server.registerTool('cancel-scheduled-email', {
490
491
  title: 'Cancel Scheduled Email',
491
- description: 'Cancel a scheduled email before it is sent. Before using this tool, you MUST confirm with the user that they really want to cancel this transmission — this action cannot be undone.',
492
+ description: 'Cancel a scheduled email before it is sent. Before using this tool, you MUST confirm with the user that they really want to cancel this email — this action cannot be undone. Only an email still in the `scheduled` state can be cancelled.',
492
493
  inputSchema: {
493
- transmission_id: z
494
+ request_id: z
494
495
  .string()
495
496
  .nonempty()
496
- .describe('Transmission ID to cancel'),
497
+ .describe('The `sch_` request ID to cancel'),
497
498
  },
498
- }, async ({ transmission_id }) => {
499
- await lettr.delete(`/emails/scheduled/${encodeURIComponent(transmission_id)}`);
499
+ }, async ({ request_id }) => {
500
+ // Cancelling answers with the cancelled email, so report its real state
501
+ // rather than asserting success.
502
+ const response = await lettr.delete(`/emails/scheduled/${encodeURIComponent(request_id)}`);
503
+ return {
504
+ content: [{ type: 'text', text: formatScheduledEmail(response.data) }],
505
+ };
506
+ });
507
+ server.registerTool('list-scheduled-emails', {
508
+ title: 'List Scheduled Emails',
509
+ description: 'List emails waiting to be sent, soonest delivery time first. Use this to find the `sch_` request ID of an email the user describes but cannot name, before getting or cancelling it.',
510
+ inputSchema: {
511
+ status: z
512
+ .enum(['scheduled', 'sending', 'sent', 'cancelled', 'failed'])
513
+ .optional()
514
+ .describe('Only return emails in this state. Omit to see every state.'),
515
+ per_page: z
516
+ .number()
517
+ .int()
518
+ .min(1)
519
+ .max(100)
520
+ .optional()
521
+ .describe('Results per page (1-100, default 25)'),
522
+ page: z.number().int().min(1).optional().describe('Page number'),
523
+ },
524
+ }, async (input) => {
525
+ const response = await lettr.get('/emails/scheduled', input);
526
+ const { scheduled_emails, pagination } = response.data;
527
+ if (scheduled_emails.length === 0) {
528
+ return {
529
+ content: [{ type: 'text', text: 'No scheduled emails found.' }],
530
+ };
531
+ }
500
532
  return {
501
533
  content: [
502
534
  {
503
535
  type: 'text',
504
- text: `Scheduled transmission "${transmission_id}" cancelled.`,
536
+ text: `Scheduled emails (total ${pagination.total}, page ${pagination.current_page} of ${pagination.last_page}, page size ${pagination.per_page}):\n\n${scheduled_emails
537
+ .map(formatScheduledEmail)
538
+ .join('\n\n')}`,
505
539
  },
506
540
  ],
507
541
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lettr-mcp",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "MCP server for the Lettr email API \u2014 send transactional emails, manage templates, domains, and webhooks from any AI assistant",
5
5
  "keywords": [
6
6
  "lettr",