lettr-mcp 1.0.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/LICENSE +7 -0
- package/README.md +187 -0
- package/dist/index.js +35 -0
- package/dist/lettr.js +50 -0
- package/dist/package.json +49 -0
- package/dist/scripts/check-dependency-versions.js +37 -0
- package/dist/scripts/pull-request-title-check.js +17 -0
- package/dist/tools/domains.js +134 -0
- package/dist/tools/emails.js +191 -0
- package/dist/tools/index.js +4 -0
- package/dist/tools/templates.js +265 -0
- package/dist/tools/webhooks.js +53 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2026 Plus Five Five, Inc
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# Lettr MCP Server
|
|
2
|
+
|
|
3
|
+
The official [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) server for [Lettr](https://lettr.com) — the email API for developers. Send transactional emails, manage templates with merge tags, configure domains, and monitor webhooks — directly from any MCP client like [Claude Desktop](https://claude.ai/download), [Cursor](https://cursor.com), or [Claude Code](https://docs.anthropic.com/en/docs/claude-code).
|
|
4
|
+
|
|
5
|
+
## Why Lettr?
|
|
6
|
+
|
|
7
|
+
[Lettr](https://lettr.com) is a modern email sending platform built for developers. It offers a clean REST API, a powerful drag-and-drop template editor, merge tag personalization, open and click tracking, and best-in-class deliverability. Whether you're sending password resets, order confirmations, or onboarding sequences, Lettr makes it simple and reliable.
|
|
8
|
+
|
|
9
|
+
- **[API Reference](https://docs.lettr.com/api-reference/introduction)** — Full REST API documentation
|
|
10
|
+
- **[Templates](https://docs.lettr.com/learn/templates/introduction)** — Visual email editor with merge tags
|
|
11
|
+
- **[Domains](https://docs.lettr.com/learn/domains/introduction)** — Domain verification and DNS setup
|
|
12
|
+
- **[Webhooks](https://docs.lettr.com/learn/webhooks/introduction)** — Real-time event notifications
|
|
13
|
+
- **[Quickstart Guides](https://docs.lettr.com/quickstart/nodejs/introduction)** — Node.js, PHP, Laravel, Python, Go, Rust, and more
|
|
14
|
+
|
|
15
|
+
## Features
|
|
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.
|
|
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.
|
|
21
|
+
|
|
22
|
+
## Setup
|
|
23
|
+
|
|
24
|
+
1. Create a free [Lettr account](https://app.lettr.com/register)
|
|
25
|
+
2. [Create an API key](https://docs.lettr.com/learn/api-keys/introduction) in your dashboard
|
|
26
|
+
3. [Verify your domain](https://docs.lettr.com/learn/domains/sending-domains) to send emails to any recipient
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
### Claude Code
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
claude mcp add lettr -e LETTR_API_KEY=lttr_xxxxxxxxx -- npx -y lettr-mcp
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Cursor
|
|
37
|
+
|
|
38
|
+
Open the command palette and choose "Cursor Settings" > "MCP" > "Add new global MCP server".
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"mcpServers": {
|
|
43
|
+
"lettr": {
|
|
44
|
+
"command": "npx",
|
|
45
|
+
"args": ["-y", "lettr-mcp"],
|
|
46
|
+
"env": {
|
|
47
|
+
"LETTR_API_KEY": "lttr_xxxxxxxxx"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Claude Desktop
|
|
55
|
+
|
|
56
|
+
Open Claude Desktop settings > "Developer" tab > "Edit Config".
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"mcpServers": {
|
|
61
|
+
"lettr": {
|
|
62
|
+
"command": "npx",
|
|
63
|
+
"args": ["-y", "lettr-mcp"],
|
|
64
|
+
"env": {
|
|
65
|
+
"LETTR_API_KEY": "lttr_xxxxxxxxx"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Options
|
|
73
|
+
|
|
74
|
+
You can pass additional arguments to configure the server:
|
|
75
|
+
|
|
76
|
+
- `--key`: Your Lettr API key (alternative to `LETTR_API_KEY` env var)
|
|
77
|
+
- `--sender`: Default sender email address from a [verified domain](https://docs.lettr.com/learn/domains/sending-domains)
|
|
78
|
+
- `--reply-to`: Default reply-to email address
|
|
79
|
+
|
|
80
|
+
Environment variables:
|
|
81
|
+
|
|
82
|
+
- `LETTR_API_KEY`: Your Lettr API key (required)
|
|
83
|
+
- `SENDER_EMAIL_ADDRESS`: Default sender email address from a verified domain (optional)
|
|
84
|
+
- `REPLY_TO_EMAIL_ADDRESS`: Default reply-to email address (optional)
|
|
85
|
+
|
|
86
|
+
> **Note:** If you don't provide a sender email address, the MCP server will ask for one each time you send an email.
|
|
87
|
+
|
|
88
|
+
## Available Tools
|
|
89
|
+
|
|
90
|
+
### Emails
|
|
91
|
+
|
|
92
|
+
| Tool | Description |
|
|
93
|
+
|------|-------------|
|
|
94
|
+
| `send-email` | Send a transactional email with HTML, plain text, templates, attachments, tracking, and personalization |
|
|
95
|
+
|
|
96
|
+
### Templates
|
|
97
|
+
|
|
98
|
+
| Tool | Description |
|
|
99
|
+
|------|-------------|
|
|
100
|
+
| `list-templates` | List email templates with pagination |
|
|
101
|
+
| `get-template` | Get full template details including HTML content |
|
|
102
|
+
| `create-template` | Create a new template with HTML or visual editor JSON |
|
|
103
|
+
| `update-template` | Update template name and/or content (creates new version) |
|
|
104
|
+
| `delete-template` | Permanently delete a template and all versions |
|
|
105
|
+
| `get-merge-tags` | Discover merge tag variables a template expects |
|
|
106
|
+
|
|
107
|
+
### Domains
|
|
108
|
+
|
|
109
|
+
| Tool | Description |
|
|
110
|
+
|------|-------------|
|
|
111
|
+
| `list-domains` | List all sending domains and their verification status |
|
|
112
|
+
| `create-domain` | Register a new sending domain |
|
|
113
|
+
| `get-domain` | Get domain details with DNS records |
|
|
114
|
+
| `delete-domain` | Remove a sending domain |
|
|
115
|
+
| `verify-domain` | Trigger DNS verification for a domain |
|
|
116
|
+
|
|
117
|
+
### Webhooks
|
|
118
|
+
|
|
119
|
+
| Tool | Description |
|
|
120
|
+
|------|-------------|
|
|
121
|
+
| `list-webhooks` | List all webhook configurations |
|
|
122
|
+
| `get-webhook` | Get webhook details and delivery status |
|
|
123
|
+
|
|
124
|
+
## Local Development
|
|
125
|
+
|
|
126
|
+
1. Clone and build:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
git clone https://github.com/nicholasgriffintn/lettr-mcp.git
|
|
130
|
+
cd lettr-mcp
|
|
131
|
+
pnpm install
|
|
132
|
+
pnpm run build
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
2. Use the local build in your MCP client:
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"mcpServers": {
|
|
140
|
+
"lettr": {
|
|
141
|
+
"command": "node",
|
|
142
|
+
"args": ["ABSOLUTE_PATH_TO_PROJECT/dist/index.js"],
|
|
143
|
+
"env": {
|
|
144
|
+
"LETTR_API_KEY": "lttr_xxxxxxxxx"
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Testing with MCP Inspector
|
|
152
|
+
|
|
153
|
+
> Make sure you've built the project first (see [Local Development](#local-development) above).
|
|
154
|
+
|
|
155
|
+
1. Set your API key:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
export LETTR_API_KEY=lttr_your_key_here
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
2. Start the inspector:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
pnpm inspector
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
3. In the browser (Inspector UI):
|
|
168
|
+
|
|
169
|
+
- Choose **stdio** (launch a process).
|
|
170
|
+
- **Command:** `node`
|
|
171
|
+
- **Args:** `dist/index.js`
|
|
172
|
+
- **Env:** `LETTR_API_KEY=lttr_your_key_here`
|
|
173
|
+
- Click **Connect**, then use "List tools" to verify the server is working.
|
|
174
|
+
|
|
175
|
+
## Resources
|
|
176
|
+
|
|
177
|
+
- [Lettr Website](https://lettr.com)
|
|
178
|
+
- [API Documentation](https://docs.lettr.com/api-reference/introduction)
|
|
179
|
+
- [MCP Setup Guide](https://docs.lettr.com/learn/mcp/setup)
|
|
180
|
+
- [MCP Tools Reference](https://docs.lettr.com/learn/mcp/tools-reference)
|
|
181
|
+
- [Template Language](https://docs.lettr.com/learn/templates/template-language)
|
|
182
|
+
- [DNS Setup Guides](https://docs.lettr.com/knowledge-base/dns-guides/cloudflare)
|
|
183
|
+
- [Knowledge Base](https://docs.lettr.com/knowledge-base/introduction)
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
MIT
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import minimist from 'minimist';
|
|
5
|
+
import { LettrClient } from './lettr.js';
|
|
6
|
+
import packageJson from './package.json' with { type: 'json' };
|
|
7
|
+
import { addDomainTools, addEmailTools, addTemplateTools, addWebhookTools, } from './tools/index.js';
|
|
8
|
+
const argv = minimist(process.argv.slice(2));
|
|
9
|
+
const apiKey = argv.key || process.env.LETTR_API_KEY;
|
|
10
|
+
const senderEmailAddress = argv.sender || process.env.SENDER_EMAIL_ADDRESS;
|
|
11
|
+
const replierEmailAddress = typeof argv['reply-to'] === 'string'
|
|
12
|
+
? argv['reply-to']
|
|
13
|
+
: process.env.REPLY_TO_EMAIL_ADDRESS || undefined;
|
|
14
|
+
if (!apiKey) {
|
|
15
|
+
console.error('No API key provided. Please set LETTR_API_KEY environment variable or use --key argument');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
const lettr = new LettrClient(apiKey);
|
|
19
|
+
const server = new McpServer({
|
|
20
|
+
name: 'lettr',
|
|
21
|
+
version: packageJson.version,
|
|
22
|
+
});
|
|
23
|
+
addEmailTools(server, lettr, { senderEmailAddress, replierEmailAddress });
|
|
24
|
+
addTemplateTools(server, lettr);
|
|
25
|
+
addDomainTools(server, lettr);
|
|
26
|
+
addWebhookTools(server, lettr);
|
|
27
|
+
async function main() {
|
|
28
|
+
const transport = new StdioServerTransport();
|
|
29
|
+
await server.connect(transport);
|
|
30
|
+
console.error('Lettr MCP Server running on stdio');
|
|
31
|
+
}
|
|
32
|
+
main().catch((error) => {
|
|
33
|
+
console.error('Fatal error in main():', error);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
});
|
package/dist/lettr.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const BASE_URL = 'https://app.lettr.com/api';
|
|
2
|
+
export class LettrClient {
|
|
3
|
+
apiKey;
|
|
4
|
+
constructor(apiKey) {
|
|
5
|
+
this.apiKey = apiKey;
|
|
6
|
+
}
|
|
7
|
+
async request(method, path, body, query) {
|
|
8
|
+
const url = new URL(`${BASE_URL}${path}`);
|
|
9
|
+
if (query) {
|
|
10
|
+
for (const [key, value] of Object.entries(query)) {
|
|
11
|
+
if (value !== undefined) {
|
|
12
|
+
url.searchParams.set(key, String(value));
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const headers = {
|
|
17
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
18
|
+
Accept: 'application/json',
|
|
19
|
+
};
|
|
20
|
+
const options = { method, headers };
|
|
21
|
+
if (body && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
|
|
22
|
+
headers['Content-Type'] = 'application/json';
|
|
23
|
+
options.body = JSON.stringify(body);
|
|
24
|
+
}
|
|
25
|
+
const response = await fetch(url.toString(), options);
|
|
26
|
+
const json = await response.json();
|
|
27
|
+
if (!response.ok) {
|
|
28
|
+
const err = json;
|
|
29
|
+
const detail = err.errors
|
|
30
|
+
? `\n${Object.entries(err.errors)
|
|
31
|
+
.map(([field, msgs]) => ` ${field}: ${msgs.join(', ')}`)
|
|
32
|
+
.join('\n')}`
|
|
33
|
+
: '';
|
|
34
|
+
throw new Error(`Lettr API error (${response.status}): ${err.message}${detail}`);
|
|
35
|
+
}
|
|
36
|
+
return json;
|
|
37
|
+
}
|
|
38
|
+
async get(path, query) {
|
|
39
|
+
return this.request('GET', path, undefined, query);
|
|
40
|
+
}
|
|
41
|
+
async post(path, body, query) {
|
|
42
|
+
return this.request('POST', path, body, query);
|
|
43
|
+
}
|
|
44
|
+
async put(path, body, query) {
|
|
45
|
+
return this.request('PUT', path, body, query);
|
|
46
|
+
}
|
|
47
|
+
async delete(path, query) {
|
|
48
|
+
return this.request('DELETE', path, undefined, query);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lettr-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for the Lettr email API — send transactional emails, manage templates, domains, and webhooks from any AI assistant",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"lettr",
|
|
7
|
+
"email",
|
|
8
|
+
"email-api",
|
|
9
|
+
"transactional-email",
|
|
10
|
+
"mcp",
|
|
11
|
+
"model-context-protocol",
|
|
12
|
+
"ai",
|
|
13
|
+
"claude",
|
|
14
|
+
"cursor",
|
|
15
|
+
"templates",
|
|
16
|
+
"email-templates"
|
|
17
|
+
],
|
|
18
|
+
"homepage": "https://lettr.com",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"type": "module",
|
|
21
|
+
"bin": {
|
|
22
|
+
"lettr-mcp": "dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc && node -e \"require('fs').chmodSync('dist/index.js', '755')\"",
|
|
29
|
+
"prepare": "npm run build",
|
|
30
|
+
"inspector": "npx @modelcontextprotocol/inspector@latest",
|
|
31
|
+
"lint": "biome check .",
|
|
32
|
+
"lint:fix": "biome check . --write"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@modelcontextprotocol/sdk": "1.26.0",
|
|
36
|
+
"minimist": "1.2.8",
|
|
37
|
+
"zod": "4.3.6"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=20"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@biomejs/biome": "2.4.2",
|
|
44
|
+
"@types/minimist": "1.2.5",
|
|
45
|
+
"@types/node": "25.0.3",
|
|
46
|
+
"typescript": "5.9.3"
|
|
47
|
+
},
|
|
48
|
+
"packageManager": "pnpm@10.30.0"
|
|
49
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
(async () => {
|
|
3
|
+
const pkg = JSON.parse(await fs.readFile('package.json', 'utf8'));
|
|
4
|
+
const errors = [];
|
|
5
|
+
function isPinned(version) {
|
|
6
|
+
if (version.startsWith('workspace:')) {
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
if (version.startsWith('npm:')) {
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
if (/^\d+\.\d+\.\d+(-\S+)?$/.test(version)) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
if (/^[a-z]+:[a-z]+@\d+$/.test(version)) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
for (const [dep, version] of Object.entries(pkg.dependencies || {})) {
|
|
21
|
+
if (!isPinned(version)) {
|
|
22
|
+
errors.push(`Dependency "${dep}" is not pinned: "${version}"`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
for (const [dep, version] of Object.entries(pkg.devDependencies || {})) {
|
|
26
|
+
if (!isPinned(version)) {
|
|
27
|
+
errors.push(`Dev dependency "${dep}" is not pinned: "${version}"`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (errors.length > 0) {
|
|
31
|
+
console.error(`\n${errors.join('\n')}\n`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
console.log('All dependencies are pinned.');
|
|
36
|
+
}
|
|
37
|
+
})();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
3
|
+
if (!eventPath) {
|
|
4
|
+
throw new Error('GITHUB_EVENT_PATH environment variable is not set. This script is only meant to run in during a Github workflow.');
|
|
5
|
+
}
|
|
6
|
+
const eventJson = JSON.parse(fs.readFileSync(eventPath, 'utf8'));
|
|
7
|
+
const { title } = eventJson.pull_request;
|
|
8
|
+
const validTypeRegex = /^(feat|fix|chore|refactor)(\([a-zA-Z0-9-]+\))?:\s[a-z].*$/;
|
|
9
|
+
if (!validTypeRegex.test(title)) {
|
|
10
|
+
console.error(`pull request title does not follow the required format.
|
|
11
|
+
example: "type: description of the change"
|
|
12
|
+
|
|
13
|
+
- type: "feat", "fix", "chore", or "refactor"
|
|
14
|
+
- first letter of the title after the 'type' needs to be lowercased`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
console.info('pull request title is valid');
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
function formatDnsRecords(records) {
|
|
3
|
+
if (!records || records.length === 0)
|
|
4
|
+
return 'No DNS records.';
|
|
5
|
+
return records
|
|
6
|
+
.map((r) => `${r.type}:\n Name: ${r.name}\n Value: ${r.value}\n Status: ${r.status}${r.priority !== undefined ? `\n Priority: ${r.priority}` : ''}`)
|
|
7
|
+
.join('\n\n');
|
|
8
|
+
}
|
|
9
|
+
export function addDomainTools(server, lettr) {
|
|
10
|
+
server.registerTool('list-domains', {
|
|
11
|
+
title: 'List Domains',
|
|
12
|
+
description: 'List all sending domains registered with your Lettr account. Returns domain names, statuses, and verification state.',
|
|
13
|
+
inputSchema: {},
|
|
14
|
+
}, async () => {
|
|
15
|
+
console.error('Debug - Listing domains');
|
|
16
|
+
const response = await lettr.get('/domains');
|
|
17
|
+
const domains = response.data.domains;
|
|
18
|
+
if (domains.length === 0) {
|
|
19
|
+
return {
|
|
20
|
+
content: [{ type: 'text', text: 'No domains found.' }],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
content: [
|
|
25
|
+
{
|
|
26
|
+
type: 'text',
|
|
27
|
+
text: `Found ${domains.length} domain${domains.length === 1 ? '' : 's'}:`,
|
|
28
|
+
},
|
|
29
|
+
...domains.map((d) => ({
|
|
30
|
+
type: 'text',
|
|
31
|
+
text: `Domain: ${d.domain}\nStatus: ${d.status_label}\nCan Send: ${d.can_send}\nCNAME: ${d.cname_status ?? 'not set'}\nDKIM: ${d.dkim_status ?? 'not set'}`,
|
|
32
|
+
})),
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
server.registerTool('create-domain', {
|
|
37
|
+
title: 'Create Domain',
|
|
38
|
+
description: 'Register a new sending domain with Lettr. The domain will be in a pending state until DNS records are verified and the domain is approved. You MUST display the DNS records to the user so they can set them up.',
|
|
39
|
+
inputSchema: {
|
|
40
|
+
domain: z
|
|
41
|
+
.string()
|
|
42
|
+
.nonempty()
|
|
43
|
+
.describe('The domain name to register (e.g., example.com)'),
|
|
44
|
+
},
|
|
45
|
+
}, async ({ domain }) => {
|
|
46
|
+
console.error(`Debug - Creating domain: ${domain}`);
|
|
47
|
+
const response = await lettr.post('/domains', { domain });
|
|
48
|
+
const created = response.data;
|
|
49
|
+
return {
|
|
50
|
+
content: [
|
|
51
|
+
{ type: 'text', text: 'Domain created successfully.' },
|
|
52
|
+
{
|
|
53
|
+
type: 'text',
|
|
54
|
+
text: `Domain: ${created.domain}\nStatus: ${created.status_label}`,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
type: 'text',
|
|
58
|
+
text: `DNS Records to configure:\n\n${formatDnsRecords(created.dns_records)}`,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
type: 'text',
|
|
62
|
+
text: 'IMPORTANT: Display the DNS records above to the user so they can configure them with their DNS provider. After configuration, use verify-domain to start verification.',
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
server.registerTool('get-domain', {
|
|
68
|
+
title: 'Get Domain',
|
|
69
|
+
description: 'Retrieve details of a sending domain including DNS records, verification status, and tracking domain configuration.',
|
|
70
|
+
inputSchema: {
|
|
71
|
+
domain: z
|
|
72
|
+
.string()
|
|
73
|
+
.nonempty()
|
|
74
|
+
.describe('The domain name (e.g., example.com)'),
|
|
75
|
+
},
|
|
76
|
+
}, async ({ domain }) => {
|
|
77
|
+
console.error(`Debug - Getting domain: ${domain}`);
|
|
78
|
+
const response = await lettr.get(`/domains/${encodeURIComponent(domain)}`);
|
|
79
|
+
const d = response.data;
|
|
80
|
+
return {
|
|
81
|
+
content: [
|
|
82
|
+
{
|
|
83
|
+
type: 'text',
|
|
84
|
+
text: `Domain: ${d.domain}\nStatus: ${d.status_label}\nCan Send: ${d.can_send}\nCNAME: ${d.cname_status ?? 'not set'}\nDKIM: ${d.dkim_status ?? 'not set'}${d.tracking_domain ? `\nTracking Domain: ${d.tracking_domain}` : ''}`,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
type: 'text',
|
|
88
|
+
text: `DNS Records:\n\n${formatDnsRecords(d.dns_records)}`,
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
server.registerTool('delete-domain', {
|
|
94
|
+
title: 'Delete Domain',
|
|
95
|
+
description: 'Delete a sending domain from Lettr. Before using this tool, you MUST double-check with the user that they want to delete this domain. Warn them that this action is irreversible and will stop all email sending for that domain.',
|
|
96
|
+
inputSchema: {
|
|
97
|
+
domain: z.string().nonempty().describe('The domain name to delete'),
|
|
98
|
+
},
|
|
99
|
+
}, async ({ domain }) => {
|
|
100
|
+
console.error(`Debug - Deleting domain: ${domain}`);
|
|
101
|
+
await lettr.delete(`/domains/${encodeURIComponent(domain)}`);
|
|
102
|
+
return {
|
|
103
|
+
content: [
|
|
104
|
+
{
|
|
105
|
+
type: 'text',
|
|
106
|
+
text: `Domain "${domain}" deleted successfully.`,
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
server.registerTool('verify-domain', {
|
|
112
|
+
title: 'Verify Domain',
|
|
113
|
+
description: 'Trigger DNS verification for a domain. This checks if the DKIM and CNAME records are correctly configured. The domain status will update once records are verified.',
|
|
114
|
+
inputSchema: {
|
|
115
|
+
domain: z.string().nonempty().describe('The domain name to verify'),
|
|
116
|
+
},
|
|
117
|
+
}, async ({ domain }) => {
|
|
118
|
+
console.error(`Debug - Verifying domain: ${domain}`);
|
|
119
|
+
const response = await lettr.post(`/domains/${encodeURIComponent(domain)}/verify`);
|
|
120
|
+
const result = response.data;
|
|
121
|
+
return {
|
|
122
|
+
content: [
|
|
123
|
+
{
|
|
124
|
+
type: 'text',
|
|
125
|
+
text: `Domain verification triggered.\nDomain: ${result.domain}\nStatus: ${result.status_label}\nCNAME: ${result.cname_status ?? 'not set'}\nDKIM: ${result.dkim_status ?? 'not set'}`,
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
type: 'text',
|
|
129
|
+
text: `DNS Records:\n\n${formatDnsRecords(result.dns_records)}`,
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
export function addEmailTools(server, lettr, { senderEmailAddress, replierEmailAddress, }) {
|
|
4
|
+
server.registerTool('send-email', {
|
|
5
|
+
title: 'Send Email',
|
|
6
|
+
description: `**Purpose:** Send a transactional email to one or more recipients. Supports HTML, plain text, templates with merge tags, attachments, tracking, and personalization.
|
|
7
|
+
|
|
8
|
+
**Returns:** Send confirmation with request ID and accepted/rejected counts.
|
|
9
|
+
|
|
10
|
+
**When to use:**
|
|
11
|
+
- User wants to "send an email" to specific people
|
|
12
|
+
- One-off messages: password reset, order confirmation, receipt, alert
|
|
13
|
+
- User says "email this to X", "notify them", "send a message to..."
|
|
14
|
+
- Sending with a template: use template_slug and substitution_data
|
|
15
|
+
|
|
16
|
+
**Key trigger phrases:** "Send an email", "Email this to", "Notify", "Send a message", "Reply to them"`,
|
|
17
|
+
inputSchema: {
|
|
18
|
+
to: z
|
|
19
|
+
.array(z.email())
|
|
20
|
+
.min(1)
|
|
21
|
+
.max(50)
|
|
22
|
+
.describe('Array of recipient email addresses (1-50 recipients)'),
|
|
23
|
+
subject: z.string().describe('Email subject line'),
|
|
24
|
+
text: z
|
|
25
|
+
.string()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe('Plain text email content. At least one of html, text, or template_slug is required.'),
|
|
28
|
+
html: z
|
|
29
|
+
.string()
|
|
30
|
+
.optional()
|
|
31
|
+
.describe('HTML email content. At least one of html, text, or template_slug is required.'),
|
|
32
|
+
template_slug: z
|
|
33
|
+
.string()
|
|
34
|
+
.optional()
|
|
35
|
+
.describe("Template slug to use for email content. When provided, the template's HTML will be used. Use get-merge-tags to discover required substitution_data keys."),
|
|
36
|
+
template_version: z
|
|
37
|
+
.number()
|
|
38
|
+
.optional()
|
|
39
|
+
.describe('Specific template version to use. If not provided, the active version is used.'),
|
|
40
|
+
project_id: z
|
|
41
|
+
.number()
|
|
42
|
+
.optional()
|
|
43
|
+
.describe("Project ID containing the template. If not provided, the team's default project is used."),
|
|
44
|
+
substitution_data: z
|
|
45
|
+
.record(z.string(), z.string())
|
|
46
|
+
.optional()
|
|
47
|
+
.describe('Variables for template substitution (e.g. { "first_name": "John", "company": "Acme" }). Use with template_slug or inline {{variables}} in html/text.'),
|
|
48
|
+
cc: z
|
|
49
|
+
.array(z.email())
|
|
50
|
+
.optional()
|
|
51
|
+
.describe('Optional array of CC email addresses. You MUST ask the user for this parameter. Under no circumstance provide it yourself'),
|
|
52
|
+
bcc: z
|
|
53
|
+
.array(z.email())
|
|
54
|
+
.optional()
|
|
55
|
+
.describe('Optional array of BCC email addresses. You MUST ask the user for this parameter. Under no circumstance provide it yourself'),
|
|
56
|
+
from_name: z
|
|
57
|
+
.string()
|
|
58
|
+
.optional()
|
|
59
|
+
.describe('Sender display name (e.g. "Acme Support")'),
|
|
60
|
+
reply_to_name: z.string().optional().describe('Reply-To display name'),
|
|
61
|
+
tag: z
|
|
62
|
+
.string()
|
|
63
|
+
.optional()
|
|
64
|
+
.describe('Tag for tracking and analytics (max 64 characters)'),
|
|
65
|
+
metadata: z
|
|
66
|
+
.record(z.string(), z.string())
|
|
67
|
+
.optional()
|
|
68
|
+
.describe('Custom metadata for tracking (e.g. { "user_id": "123", "campaign": "onboarding" })'),
|
|
69
|
+
options: z
|
|
70
|
+
.object({
|
|
71
|
+
click_tracking: z
|
|
72
|
+
.boolean()
|
|
73
|
+
.optional()
|
|
74
|
+
.describe('Enable click tracking for links'),
|
|
75
|
+
open_tracking: z
|
|
76
|
+
.boolean()
|
|
77
|
+
.optional()
|
|
78
|
+
.describe('Enable open tracking via pixel'),
|
|
79
|
+
transactional: z
|
|
80
|
+
.boolean()
|
|
81
|
+
.optional()
|
|
82
|
+
.describe('Mark as transactional (not marketing)'),
|
|
83
|
+
inline_css: z
|
|
84
|
+
.boolean()
|
|
85
|
+
.optional()
|
|
86
|
+
.describe('Inline CSS styles in HTML'),
|
|
87
|
+
perform_substitutions: z
|
|
88
|
+
.boolean()
|
|
89
|
+
.optional()
|
|
90
|
+
.describe('Perform variable substitutions in content'),
|
|
91
|
+
})
|
|
92
|
+
.optional()
|
|
93
|
+
.describe('Email delivery options'),
|
|
94
|
+
attachments: z
|
|
95
|
+
.array(z.object({
|
|
96
|
+
name: z
|
|
97
|
+
.string()
|
|
98
|
+
.describe('Filename with extension (e.g. "report.pdf")'),
|
|
99
|
+
type: z.string().describe('MIME type (e.g. "application/pdf")'),
|
|
100
|
+
data: z
|
|
101
|
+
.string()
|
|
102
|
+
.optional()
|
|
103
|
+
.describe('Base64-encoded file content'),
|
|
104
|
+
filePath: z
|
|
105
|
+
.string()
|
|
106
|
+
.optional()
|
|
107
|
+
.describe('Local file path to read and attach (will be base64 encoded automatically)'),
|
|
108
|
+
}))
|
|
109
|
+
.optional()
|
|
110
|
+
.describe('Array of file attachments. Each needs name, type, and either data (base64) or filePath.'),
|
|
111
|
+
...(!senderEmailAddress
|
|
112
|
+
? {
|
|
113
|
+
from: z
|
|
114
|
+
.email()
|
|
115
|
+
.nonempty()
|
|
116
|
+
.describe('Sender email address. You MUST ask the user for this parameter. Under no circumstance provide it yourself'),
|
|
117
|
+
}
|
|
118
|
+
: {}),
|
|
119
|
+
...(!replierEmailAddress
|
|
120
|
+
? {
|
|
121
|
+
reply_to: z
|
|
122
|
+
.email()
|
|
123
|
+
.optional()
|
|
124
|
+
.describe('Optional reply-to email address. You MUST ask the user for this parameter. Under no circumstance provide it yourself'),
|
|
125
|
+
}
|
|
126
|
+
: {}),
|
|
127
|
+
},
|
|
128
|
+
}, async ({ from, to, subject, text, html, template_slug, template_version, project_id, substitution_data, cc, bcc, reply_to, from_name, reply_to_name, tag, metadata, options, attachments, }) => {
|
|
129
|
+
const fromAddress = from ?? senderEmailAddress;
|
|
130
|
+
const replyToAddress = reply_to ?? replierEmailAddress;
|
|
131
|
+
if (typeof fromAddress !== 'string') {
|
|
132
|
+
throw new Error('from argument must be provided.');
|
|
133
|
+
}
|
|
134
|
+
console.error(`Debug - Sending email with from: ${fromAddress}`);
|
|
135
|
+
const body = {
|
|
136
|
+
from: fromAddress,
|
|
137
|
+
to,
|
|
138
|
+
subject,
|
|
139
|
+
};
|
|
140
|
+
if (from_name)
|
|
141
|
+
body.from_name = from_name;
|
|
142
|
+
if (text)
|
|
143
|
+
body.text = text;
|
|
144
|
+
if (html)
|
|
145
|
+
body.html = html;
|
|
146
|
+
if (template_slug)
|
|
147
|
+
body.template_slug = template_slug;
|
|
148
|
+
if (template_version)
|
|
149
|
+
body.template_version = template_version;
|
|
150
|
+
if (project_id)
|
|
151
|
+
body.project_id = project_id;
|
|
152
|
+
if (substitution_data)
|
|
153
|
+
body.substitution_data = substitution_data;
|
|
154
|
+
if (cc)
|
|
155
|
+
body.cc = cc;
|
|
156
|
+
if (bcc)
|
|
157
|
+
body.bcc = bcc;
|
|
158
|
+
if (replyToAddress)
|
|
159
|
+
body.reply_to = replyToAddress;
|
|
160
|
+
if (reply_to_name)
|
|
161
|
+
body.reply_to_name = reply_to_name;
|
|
162
|
+
if (tag)
|
|
163
|
+
body.tag = tag;
|
|
164
|
+
if (metadata)
|
|
165
|
+
body.metadata = metadata;
|
|
166
|
+
if (options)
|
|
167
|
+
body.options = options;
|
|
168
|
+
if (attachments && attachments.length > 0) {
|
|
169
|
+
body.attachments = await Promise.all(attachments.map(async (att) => {
|
|
170
|
+
let data = att.data;
|
|
171
|
+
if (!data && att.filePath) {
|
|
172
|
+
const fileBuffer = await fs.readFile(att.filePath);
|
|
173
|
+
data = fileBuffer.toString('base64');
|
|
174
|
+
}
|
|
175
|
+
if (!data) {
|
|
176
|
+
throw new Error(`Attachment "${att.name}" requires either data (base64) or filePath.`);
|
|
177
|
+
}
|
|
178
|
+
return { name: att.name, type: att.type, data };
|
|
179
|
+
}));
|
|
180
|
+
}
|
|
181
|
+
const response = await lettr.post('/emails', body);
|
|
182
|
+
return {
|
|
183
|
+
content: [
|
|
184
|
+
{
|
|
185
|
+
type: 'text',
|
|
186
|
+
text: `Email sent successfully! Request ID: ${response.data.request_id}, Accepted: ${response.data.accepted}, Rejected: ${response.data.rejected}`,
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
};
|
|
190
|
+
});
|
|
191
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export function addTemplateTools(server, lettr) {
|
|
3
|
+
server.registerTool('list-templates', {
|
|
4
|
+
title: 'List Templates',
|
|
5
|
+
description: `**Purpose:** List email templates with pagination. Returns template names, slugs, and project info.
|
|
6
|
+
|
|
7
|
+
**Returns:** Paginated list of templates with id, name, slug, project_id, folder_id, timestamps.
|
|
8
|
+
|
|
9
|
+
**When to use:**
|
|
10
|
+
- User asks "show my templates", "what templates do I have?"
|
|
11
|
+
- Before sending a template-based email, to find the template slug
|
|
12
|
+
- Use get-template for full details of a specific template`,
|
|
13
|
+
inputSchema: {
|
|
14
|
+
project_id: z
|
|
15
|
+
.number()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe("Project ID to filter templates. If not provided, uses the team's default project."),
|
|
18
|
+
per_page: z
|
|
19
|
+
.number()
|
|
20
|
+
.min(1)
|
|
21
|
+
.max(100)
|
|
22
|
+
.optional()
|
|
23
|
+
.describe('Number of results per page (1-100). Default: 25'),
|
|
24
|
+
page: z.number().min(1).optional().describe('Page number. Default: 1'),
|
|
25
|
+
},
|
|
26
|
+
}, async ({ project_id, per_page, page }) => {
|
|
27
|
+
console.error('Debug - Listing templates');
|
|
28
|
+
const query = {};
|
|
29
|
+
if (project_id)
|
|
30
|
+
query.project_id = project_id;
|
|
31
|
+
if (per_page)
|
|
32
|
+
query.per_page = per_page;
|
|
33
|
+
if (page)
|
|
34
|
+
query.page = page;
|
|
35
|
+
const response = await lettr.get('/templates', query);
|
|
36
|
+
const templates = response.data.templates;
|
|
37
|
+
const pagination = response.data.pagination;
|
|
38
|
+
if (templates.length === 0) {
|
|
39
|
+
return {
|
|
40
|
+
content: [{ type: 'text', text: 'No templates found.' }],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const templateList = templates
|
|
44
|
+
.map((t) => `- ${t.name} (slug: ${t.slug}) | Project: ${t.project_id} | Updated: ${t.updated_at}`)
|
|
45
|
+
.join('\n');
|
|
46
|
+
return {
|
|
47
|
+
content: [
|
|
48
|
+
{
|
|
49
|
+
type: 'text',
|
|
50
|
+
text: `Found ${pagination.total} template(s) (page ${pagination.current_page}/${pagination.last_page}):\n\n${templateList}`,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
server.registerTool('get-template', {
|
|
56
|
+
title: 'Get Template',
|
|
57
|
+
description: 'Retrieve full details of a template by its slug, including active version HTML/JSON content and version count.',
|
|
58
|
+
inputSchema: {
|
|
59
|
+
slug: z
|
|
60
|
+
.string()
|
|
61
|
+
.nonempty()
|
|
62
|
+
.describe('The template slug (URL-friendly identifier)'),
|
|
63
|
+
project_id: z
|
|
64
|
+
.number()
|
|
65
|
+
.optional()
|
|
66
|
+
.describe("Project ID to find the template in. If not provided, uses the team's default project."),
|
|
67
|
+
},
|
|
68
|
+
}, async ({ slug, project_id }) => {
|
|
69
|
+
console.error(`Debug - Getting template: ${slug}`);
|
|
70
|
+
const query = {};
|
|
71
|
+
if (project_id)
|
|
72
|
+
query.project_id = project_id;
|
|
73
|
+
const response = await lettr.get(`/templates/${encodeURIComponent(slug)}`, query);
|
|
74
|
+
const t = response.data;
|
|
75
|
+
let details = 'Template Details:\n';
|
|
76
|
+
details += `- Name: ${t.name}\n`;
|
|
77
|
+
details += `- Slug: ${t.slug}\n`;
|
|
78
|
+
details += `- Project ID: ${t.project_id}\n`;
|
|
79
|
+
details += `- Folder ID: ${t.folder_id}\n`;
|
|
80
|
+
details += `- Active Version: ${t.active_version ?? 'none'}\n`;
|
|
81
|
+
details += `- Total Versions: ${t.versions_count}\n`;
|
|
82
|
+
details += `- Created: ${t.created_at}\n`;
|
|
83
|
+
details += `- Updated: ${t.updated_at}\n`;
|
|
84
|
+
if (t.html) {
|
|
85
|
+
details += `\n--- HTML Content ---\n${t.html}\n`;
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
content: [{ type: 'text', text: details }],
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
server.registerTool('create-template', {
|
|
92
|
+
title: 'Create Template',
|
|
93
|
+
description: 'Create a new email template with HTML or Topol editor JSON content. Provide either html or json — they are mutually exclusive. Merge tags are automatically extracted from the content.',
|
|
94
|
+
inputSchema: {
|
|
95
|
+
name: z
|
|
96
|
+
.string()
|
|
97
|
+
.nonempty()
|
|
98
|
+
.describe('Name of the template (max 255 characters)'),
|
|
99
|
+
html: z
|
|
100
|
+
.string()
|
|
101
|
+
.optional()
|
|
102
|
+
.describe('HTML content for the template. Mutually exclusive with json. Use {{VARIABLE}} syntax for merge tags.'),
|
|
103
|
+
json: z
|
|
104
|
+
.string()
|
|
105
|
+
.optional()
|
|
106
|
+
.describe('JSON content for Topol visual editor templates. Mutually exclusive with html.'),
|
|
107
|
+
project_id: z
|
|
108
|
+
.number()
|
|
109
|
+
.optional()
|
|
110
|
+
.describe("Project ID to create the template in. If not provided, uses the team's default project."),
|
|
111
|
+
folder_id: z
|
|
112
|
+
.number()
|
|
113
|
+
.optional()
|
|
114
|
+
.describe('Folder ID within the project. If not provided, uses the first folder.'),
|
|
115
|
+
},
|
|
116
|
+
}, async ({ name, html, json, project_id, folder_id }) => {
|
|
117
|
+
console.error(`Debug - Creating template: ${name}`);
|
|
118
|
+
const body = { name };
|
|
119
|
+
if (html)
|
|
120
|
+
body.html = html;
|
|
121
|
+
if (json)
|
|
122
|
+
body.json = json;
|
|
123
|
+
if (project_id)
|
|
124
|
+
body.project_id = project_id;
|
|
125
|
+
if (folder_id)
|
|
126
|
+
body.folder_id = folder_id;
|
|
127
|
+
const response = await lettr.post('/templates', body);
|
|
128
|
+
const t = response.data;
|
|
129
|
+
const mergeTags = t.merge_tags.length > 0
|
|
130
|
+
? `\nMerge Tags: ${t.merge_tags.map((m) => `${m.key}${m.required ? ' (required)' : ''}`).join(', ')}`
|
|
131
|
+
: '';
|
|
132
|
+
return {
|
|
133
|
+
content: [
|
|
134
|
+
{
|
|
135
|
+
type: 'text',
|
|
136
|
+
text: `Template created successfully!\nName: ${t.name}\nSlug: ${t.slug}\nVersion: ${t.active_version}${mergeTags}`,
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
server.registerTool('update-template', {
|
|
142
|
+
title: 'Update Template',
|
|
143
|
+
description: "Update an existing template's name and/or content. Providing new html or json content creates a new version automatically. The html and json fields are mutually exclusive.",
|
|
144
|
+
inputSchema: {
|
|
145
|
+
slug: z.string().nonempty().describe('The template slug to update'),
|
|
146
|
+
name: z.string().optional().describe('New name for the template'),
|
|
147
|
+
html: z
|
|
148
|
+
.string()
|
|
149
|
+
.optional()
|
|
150
|
+
.describe('New HTML content. Creates a new active version. Mutually exclusive with json.'),
|
|
151
|
+
json: z
|
|
152
|
+
.string()
|
|
153
|
+
.optional()
|
|
154
|
+
.describe('New JSON content for visual editor. Creates a new active version. Mutually exclusive with html.'),
|
|
155
|
+
project_id: z
|
|
156
|
+
.number()
|
|
157
|
+
.optional()
|
|
158
|
+
.describe("Project ID to find the template in. If not provided, uses the team's default project."),
|
|
159
|
+
},
|
|
160
|
+
}, async ({ slug, name, html, json, project_id }) => {
|
|
161
|
+
console.error(`Debug - Updating template: ${slug}`);
|
|
162
|
+
const body = {};
|
|
163
|
+
if (name)
|
|
164
|
+
body.name = name;
|
|
165
|
+
if (html)
|
|
166
|
+
body.html = html;
|
|
167
|
+
if (json)
|
|
168
|
+
body.json = json;
|
|
169
|
+
if (project_id)
|
|
170
|
+
body.project_id = project_id;
|
|
171
|
+
const response = await lettr.put(`/templates/${encodeURIComponent(slug)}`, body);
|
|
172
|
+
const t = response.data;
|
|
173
|
+
const mergeTags = t.merge_tags.length > 0
|
|
174
|
+
? `\nMerge Tags: ${t.merge_tags.map((m) => `${m.key}${m.required ? ' (required)' : ''}`).join(', ')}`
|
|
175
|
+
: '';
|
|
176
|
+
return {
|
|
177
|
+
content: [
|
|
178
|
+
{
|
|
179
|
+
type: 'text',
|
|
180
|
+
text: `Template updated successfully!\nName: ${t.name}\nSlug: ${t.slug}\nActive Version: ${t.active_version}${mergeTags}`,
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
};
|
|
184
|
+
});
|
|
185
|
+
server.registerTool('delete-template', {
|
|
186
|
+
title: 'Delete Template',
|
|
187
|
+
description: 'Permanently delete an email template and all its versions. Before using this tool, you MUST double-check with the user that they want to delete this template. Warn them that this action is irreversible and any emails referencing this template slug will fail.',
|
|
188
|
+
inputSchema: {
|
|
189
|
+
slug: z.string().nonempty().describe('The template slug to delete'),
|
|
190
|
+
project_id: z
|
|
191
|
+
.number()
|
|
192
|
+
.optional()
|
|
193
|
+
.describe("Project ID to find the template in. If not provided, uses the team's default project."),
|
|
194
|
+
},
|
|
195
|
+
}, async ({ slug, project_id }) => {
|
|
196
|
+
console.error(`Debug - Deleting template: ${slug}`);
|
|
197
|
+
const query = {};
|
|
198
|
+
if (project_id)
|
|
199
|
+
query.project_id = project_id;
|
|
200
|
+
await lettr.delete(`/templates/${encodeURIComponent(slug)}`, query);
|
|
201
|
+
return {
|
|
202
|
+
content: [
|
|
203
|
+
{
|
|
204
|
+
type: 'text',
|
|
205
|
+
text: `Template "${slug}" deleted successfully.`,
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
};
|
|
209
|
+
});
|
|
210
|
+
server.registerTool('get-merge-tags', {
|
|
211
|
+
title: 'Get Template Merge Tags',
|
|
212
|
+
description: 'Retrieve the merge tags (variables) for a template. Use this to discover which substitution_data keys a template expects when sending an email. By default returns merge tags for the active version.',
|
|
213
|
+
inputSchema: {
|
|
214
|
+
slug: z.string().nonempty().describe('The template slug'),
|
|
215
|
+
project_id: z
|
|
216
|
+
.number()
|
|
217
|
+
.optional()
|
|
218
|
+
.describe("Project ID to find the template in. If not provided, uses the team's default project."),
|
|
219
|
+
version: z
|
|
220
|
+
.number()
|
|
221
|
+
.optional()
|
|
222
|
+
.describe('Template version number. If not provided, uses the active version.'),
|
|
223
|
+
},
|
|
224
|
+
}, async ({ slug, project_id, version }) => {
|
|
225
|
+
console.error(`Debug - Getting merge tags for template: ${slug}`);
|
|
226
|
+
const query = {};
|
|
227
|
+
if (project_id)
|
|
228
|
+
query.project_id = project_id;
|
|
229
|
+
if (version)
|
|
230
|
+
query.version = version;
|
|
231
|
+
const response = await lettr.get(`/templates/${encodeURIComponent(slug)}/merge-tags`, query);
|
|
232
|
+
const { template_slug, version: ver, merge_tags } = response.data;
|
|
233
|
+
if (merge_tags.length === 0) {
|
|
234
|
+
return {
|
|
235
|
+
content: [
|
|
236
|
+
{
|
|
237
|
+
type: 'text',
|
|
238
|
+
text: `Template "${template_slug}" (version ${ver}) has no merge tags.`,
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
const formatTag = (tag) => {
|
|
244
|
+
let line = `- ${tag.key}${tag.required ? ' (required)' : ' (optional)'}`;
|
|
245
|
+
if (tag.type)
|
|
246
|
+
line += ` [${tag.type}]`;
|
|
247
|
+
if (tag.children && tag.children.length > 0) {
|
|
248
|
+
line += ' (loop):';
|
|
249
|
+
for (const child of tag.children) {
|
|
250
|
+
line += `\n - ${child.key}${child.type ? ` [${child.type}]` : ''}`;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return line;
|
|
254
|
+
};
|
|
255
|
+
const tagList = merge_tags.map(formatTag).join('\n');
|
|
256
|
+
return {
|
|
257
|
+
content: [
|
|
258
|
+
{
|
|
259
|
+
type: 'text',
|
|
260
|
+
text: `Merge tags for "${template_slug}" (version ${ver}):\n\n${tagList}`,
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
};
|
|
264
|
+
});
|
|
265
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export function addWebhookTools(server, lettr) {
|
|
3
|
+
server.registerTool('list-webhooks', {
|
|
4
|
+
title: 'List Webhooks',
|
|
5
|
+
description: 'List all webhooks configured for your Lettr account. Returns webhook URLs, statuses, and subscribed event types.',
|
|
6
|
+
inputSchema: {},
|
|
7
|
+
}, async () => {
|
|
8
|
+
console.error('Debug - Listing webhooks');
|
|
9
|
+
const response = await lettr.get('/webhooks');
|
|
10
|
+
const webhooks = response.data.webhooks;
|
|
11
|
+
if (webhooks.length === 0) {
|
|
12
|
+
return {
|
|
13
|
+
content: [{ type: 'text', text: 'No webhooks found.' }],
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
content: [
|
|
18
|
+
{
|
|
19
|
+
type: 'text',
|
|
20
|
+
text: `Found ${webhooks.length} webhook${webhooks.length === 1 ? '' : 's'}:`,
|
|
21
|
+
},
|
|
22
|
+
...webhooks.map((w) => ({
|
|
23
|
+
type: 'text',
|
|
24
|
+
text: `URL: ${w.url}\nStatus: ${w.status}\nEvents: ${w.event_types.join(', ')}\nID: ${w.id}`,
|
|
25
|
+
})),
|
|
26
|
+
],
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
server.registerTool('get-webhook', {
|
|
30
|
+
title: 'Get Webhook',
|
|
31
|
+
description: 'Retrieve details of a specific webhook including its status, event types, and delivery information.',
|
|
32
|
+
inputSchema: {
|
|
33
|
+
id: z.number().describe('The webhook ID'),
|
|
34
|
+
},
|
|
35
|
+
}, async ({ id }) => {
|
|
36
|
+
console.error(`Debug - Getting webhook: ${id}`);
|
|
37
|
+
const response = await lettr.get(`/webhooks/${id}`);
|
|
38
|
+
const w = response.data;
|
|
39
|
+
let details = 'Webhook Details:\n';
|
|
40
|
+
details += `- ID: ${w.id}\n`;
|
|
41
|
+
details += `- URL: ${w.url}\n`;
|
|
42
|
+
details += `- Status: ${w.status}\n`;
|
|
43
|
+
details += `- Events: ${w.event_types.join(', ')}\n`;
|
|
44
|
+
details += `- Created: ${w.created_at}\n`;
|
|
45
|
+
details += `- Updated: ${w.updated_at}\n`;
|
|
46
|
+
if (w.last_delivery_at) {
|
|
47
|
+
details += `- Last Delivery: ${w.last_delivery_at} (${w.last_delivery_status})\n`;
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
content: [{ type: 'text', text: details }],
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lettr-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for the Lettr email API — send transactional emails, manage templates, domains, and webhooks from any AI assistant",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"lettr",
|
|
7
|
+
"email",
|
|
8
|
+
"email-api",
|
|
9
|
+
"transactional-email",
|
|
10
|
+
"mcp",
|
|
11
|
+
"model-context-protocol",
|
|
12
|
+
"ai",
|
|
13
|
+
"claude",
|
|
14
|
+
"cursor",
|
|
15
|
+
"templates",
|
|
16
|
+
"email-templates"
|
|
17
|
+
],
|
|
18
|
+
"homepage": "https://lettr.com",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"type": "module",
|
|
21
|
+
"bin": {
|
|
22
|
+
"lettr-mcp": "dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc && node -e \"require('fs').chmodSync('dist/index.js', '755')\"",
|
|
29
|
+
"prepare": "npm run build",
|
|
30
|
+
"inspector": "npx @modelcontextprotocol/inspector@latest",
|
|
31
|
+
"lint": "biome check .",
|
|
32
|
+
"lint:fix": "biome check . --write"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@modelcontextprotocol/sdk": "1.26.0",
|
|
36
|
+
"minimist": "1.2.8",
|
|
37
|
+
"zod": "4.3.6"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=20"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@biomejs/biome": "2.4.2",
|
|
44
|
+
"@types/minimist": "1.2.5",
|
|
45
|
+
"@types/node": "25.0.3",
|
|
46
|
+
"typescript": "5.9.3"
|
|
47
|
+
},
|
|
48
|
+
"packageManager": "pnpm@10.30.0"
|
|
49
|
+
}
|