@aiwerk/mcp-server-resend 0.1.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/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +154 -0
- package/dist/src/errors.d.ts +16 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +21 -0
- package/dist/src/lib/destructive-gate.d.ts +27 -0
- package/dist/src/lib/destructive-gate.d.ts.map +1 -0
- package/dist/src/lib/destructive-gate.js +72 -0
- package/dist/src/lib/idempotency.d.ts +2 -0
- package/dist/src/lib/idempotency.d.ts.map +1 -0
- package/dist/src/lib/idempotency.js +12 -0
- package/dist/src/lib/resend-client.d.ts +13 -0
- package/dist/src/lib/resend-client.d.ts.map +1 -0
- package/dist/src/lib/resend-client.js +128 -0
- package/dist/src/server.d.ts +12 -0
- package/dist/src/server.d.ts.map +1 -0
- package/dist/src/server.js +400 -0
- package/dist/src/tools/apikeys.d.ts +37 -0
- package/dist/src/tools/apikeys.d.ts.map +1 -0
- package/dist/src/tools/apikeys.js +72 -0
- package/dist/src/tools/broadcasts.d.ts +85 -0
- package/dist/src/tools/broadcasts.d.ts.map +1 -0
- package/dist/src/tools/broadcasts.js +118 -0
- package/dist/src/tools/contact-properties.d.ts +47 -0
- package/dist/src/tools/contact-properties.d.ts.map +1 -0
- package/dist/src/tools/contact-properties.js +80 -0
- package/dist/src/tools/contacts.d.ts +137 -0
- package/dist/src/tools/contacts.d.ts.map +1 -0
- package/dist/src/tools/contacts.js +139 -0
- package/dist/src/tools/domains.d.ts +95 -0
- package/dist/src/tools/domains.d.ts.map +1 -0
- package/dist/src/tools/domains.js +118 -0
- package/dist/src/tools/emails.d.ts +214 -0
- package/dist/src/tools/emails.d.ts.map +1 -0
- package/dist/src/tools/emails.js +142 -0
- package/dist/src/tools/logs.d.ts +18 -0
- package/dist/src/tools/logs.d.ts.map +1 -0
- package/dist/src/tools/logs.js +18 -0
- package/dist/src/tools/segments.d.ts +49 -0
- package/dist/src/tools/segments.d.ts.map +1 -0
- package/dist/src/tools/segments.js +79 -0
- package/dist/src/tools/templates.d.ts +113 -0
- package/dist/src/tools/templates.d.ts.map +1 -0
- package/dist/src/tools/templates.js +113 -0
- package/dist/src/tools/topics.d.ts +53 -0
- package/dist/src/tools/topics.d.ts.map +1 -0
- package/dist/src/tools/topics.js +81 -0
- package/dist/src/tools/webhooks.d.ts +49 -0
- package/dist/src/tools/webhooks.d.ts.map +1 -0
- package/dist/src/tools/webhooks.js +87 -0
- package/dist/src/version.d.ts +2 -0
- package/dist/src/version.d.ts.map +1 -0
- package/dist/src/version.js +2 -0
- package/package.json +42 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
### Added
|
|
5
|
+
- Initial v0.1.0: 8 email tools (Phase A skeleton; Phases B-E add remaining 54 tools)
|
|
6
|
+
- Auth: RESEND_API_KEY (Bearer token) via RESEND_API_KEY env var
|
|
7
|
+
- α destructive-confirmation gate on send operations (2-phase, 60s TTL)
|
|
8
|
+
- Idempotency-Key auto-derived on all email sends
|
|
9
|
+
- Mandatory User-Agent header (required by Resend API)
|
|
10
|
+
- 5 req/s client-side rate throttle
|
|
11
|
+
- Tests: unit tests for client, gate, idempotency
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AIWerk
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# @aiwerk/mcp-server-resend
|
|
2
|
+
|
|
3
|
+
Resend API MCP server — full API coverage: transactional email, batch sending, domains, contacts, segments, broadcasts, templates, topics, webhooks, API keys, and logs.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx -y @aiwerk/mcp-server-resend
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or add to your MCP client config:
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"mcpServers": {
|
|
16
|
+
"resend": {
|
|
17
|
+
"command": "npx",
|
|
18
|
+
"args": ["-y", "@aiwerk/mcp-server-resend"],
|
|
19
|
+
"env": {
|
|
20
|
+
"RESEND_API_KEY": "re_..."
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Environment variables
|
|
28
|
+
|
|
29
|
+
| Name | Required | Description |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| `RESEND_API_KEY` | ✅ | Resend API key (starts with `re_`). Get one at [resend.com/api-keys](https://resend.com/api-keys). |
|
|
32
|
+
| `RESEND_API_BASE_URL` | — | Override API base URL (default: `https://api.resend.com`). |
|
|
33
|
+
| `RESEND_API_TIMEOUT_MS` | — | Request timeout in milliseconds (default: `30000`). |
|
|
34
|
+
|
|
35
|
+
## Auth
|
|
36
|
+
|
|
37
|
+
1. Sign up at [resend.com](https://resend.com) (free tier: 3,000 emails/month, 100/day).
|
|
38
|
+
2. Open **API Keys** in the dashboard and create a key with Full access.
|
|
39
|
+
3. Copy the key (starts with `re_`) into `RESEND_API_KEY`.
|
|
40
|
+
4. To send from your own address, verify a domain under **Domains**. For testing, use `onboarding@resend.dev` (sender) → `delivered@resend.dev` / `bounced@resend.dev` / `complained@resend.dev` without domain setup.
|
|
41
|
+
|
|
42
|
+
## Tools (62)
|
|
43
|
+
|
|
44
|
+
All destructive and send operations use a 2-phase α-gate: the first call returns a `confirm_token`; supply it on the second call to execute. Tokens expire after 60 seconds. Bulk delete tools accept an array of IDs under one token.
|
|
45
|
+
|
|
46
|
+
### Emails (8)
|
|
47
|
+
| Tool | Description |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `resend_email_send` | Send a transactional email [α-gated] |
|
|
50
|
+
| `resend_email_send_batch` | Send up to 100 emails in one call [α-gated] |
|
|
51
|
+
| `resend_email_get` | Get email status and details by ID |
|
|
52
|
+
| `resend_email_list` | List sent emails (cursor pagination) |
|
|
53
|
+
| `resend_email_update` | Reschedule a scheduled email |
|
|
54
|
+
| `resend_email_cancel` | Cancel a scheduled email (safety action, not α-gated) |
|
|
55
|
+
| `resend_email_list_received` | List inbound emails (requires routing config) |
|
|
56
|
+
| `resend_email_get_received` | Get a specific inbound email |
|
|
57
|
+
|
|
58
|
+
### Domains (6)
|
|
59
|
+
| Tool | Description |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `resend_domain_create` | Add a new sending domain |
|
|
62
|
+
| `resend_domain_list` | List all domains |
|
|
63
|
+
| `resend_domain_get` | Get domain details and DNS status |
|
|
64
|
+
| `resend_domain_update` | Update tracking settings (open, click, TLS) |
|
|
65
|
+
| `resend_domain_verify` | Trigger DNS verification |
|
|
66
|
+
| `resend_domain_delete` | Delete domains [α-gated, bulk] |
|
|
67
|
+
|
|
68
|
+
### API Keys (3)
|
|
69
|
+
| Tool | Description |
|
|
70
|
+
|---|---|
|
|
71
|
+
| `resend_apikey_create` | Create an API key (token shown once — store immediately) |
|
|
72
|
+
| `resend_apikey_list` | List API keys |
|
|
73
|
+
| `resend_apikey_delete` | Revoke API keys [α-gated, bulk] |
|
|
74
|
+
|
|
75
|
+
### Contacts (10)
|
|
76
|
+
| Tool | Description |
|
|
77
|
+
|---|---|
|
|
78
|
+
| `resend_contact_create` | Create a contact |
|
|
79
|
+
| `resend_contact_list` | List contacts (optional segment_id filter) |
|
|
80
|
+
| `resend_contact_get` | Get a contact by ID or email |
|
|
81
|
+
| `resend_contact_update` | Update contact details |
|
|
82
|
+
| `resend_contact_delete` | Delete contacts [α-gated, bulk] |
|
|
83
|
+
| `resend_contact_list_segments` | List segments a contact belongs to |
|
|
84
|
+
| `resend_contact_add_segment` | Add a contact to a segment |
|
|
85
|
+
| `resend_contact_remove_segment` | Remove a contact from a segment |
|
|
86
|
+
| `resend_contact_get_topics` | Get topic subscription statuses |
|
|
87
|
+
| `resend_contact_update_topics` | Update topic subscriptions (opt_in/opt_out) |
|
|
88
|
+
|
|
89
|
+
### Contact Properties (5)
|
|
90
|
+
| Tool | Description |
|
|
91
|
+
|---|---|
|
|
92
|
+
| `resend_contactproperty_create` | Create a custom contact property |
|
|
93
|
+
| `resend_contactproperty_list` | List contact properties |
|
|
94
|
+
| `resend_contactproperty_get` | Get a property by ID |
|
|
95
|
+
| `resend_contactproperty_update` | Update fallback value |
|
|
96
|
+
| `resend_contactproperty_delete` | Delete properties [α-gated, bulk] |
|
|
97
|
+
|
|
98
|
+
### Segments (5)
|
|
99
|
+
| Tool | Description |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `resend_segment_create` | Create a segment |
|
|
102
|
+
| `resend_segment_list` | List segments |
|
|
103
|
+
| `resend_segment_get` | Get a segment by ID |
|
|
104
|
+
| `resend_segment_list_contacts` | List contacts in a segment |
|
|
105
|
+
| `resend_segment_delete` | Delete segments [α-gated, bulk] |
|
|
106
|
+
|
|
107
|
+
### Templates (7)
|
|
108
|
+
| Tool | Description |
|
|
109
|
+
|---|---|
|
|
110
|
+
| `resend_template_create` | Create an email template |
|
|
111
|
+
| `resend_template_list` | List templates |
|
|
112
|
+
| `resend_template_get` | Get a template by ID |
|
|
113
|
+
| `resend_template_update` | Update template content |
|
|
114
|
+
| `resend_template_delete` | Delete templates [α-gated, bulk] |
|
|
115
|
+
| `resend_template_duplicate` | Copy an existing template |
|
|
116
|
+
| `resend_template_publish` | Publish a draft template |
|
|
117
|
+
|
|
118
|
+
### Topics (5)
|
|
119
|
+
| Tool | Description |
|
|
120
|
+
|---|---|
|
|
121
|
+
| `resend_topic_create` | Create a topic (e.g. "Weekly Newsletter") |
|
|
122
|
+
| `resend_topic_list` | List topics |
|
|
123
|
+
| `resend_topic_get` | Get a topic by ID |
|
|
124
|
+
| `resend_topic_update` | Update name/description/visibility |
|
|
125
|
+
| `resend_topic_delete` | Delete topics [α-gated, bulk] |
|
|
126
|
+
|
|
127
|
+
### Broadcasts (6)
|
|
128
|
+
| Tool | Description |
|
|
129
|
+
|---|---|
|
|
130
|
+
| `resend_broadcast_create` | Create a broadcast draft (send:false always enforced) |
|
|
131
|
+
| `resend_broadcast_list` | List broadcasts |
|
|
132
|
+
| `resend_broadcast_get` | Get broadcast details |
|
|
133
|
+
| `resend_broadcast_update` | Update a draft broadcast |
|
|
134
|
+
| `resend_broadcast_delete` | Delete draft broadcasts [α-gated, bulk] |
|
|
135
|
+
| `resend_broadcast_send` | Send/schedule a broadcast [α-gated, idempotent] |
|
|
136
|
+
|
|
137
|
+
### Webhooks (5)
|
|
138
|
+
| Tool | Description |
|
|
139
|
+
|---|---|
|
|
140
|
+
| `resend_webhook_create` | Create a webhook endpoint |
|
|
141
|
+
| `resend_webhook_list` | List webhooks |
|
|
142
|
+
| `resend_webhook_get` | Get a webhook by ID |
|
|
143
|
+
| `resend_webhook_update` | Update endpoint, events, or status |
|
|
144
|
+
| `resend_webhook_delete` | Delete webhooks [α-gated, bulk] |
|
|
145
|
+
|
|
146
|
+
### Logs (2)
|
|
147
|
+
| Tool | Description |
|
|
148
|
+
|---|---|
|
|
149
|
+
| `resend_log_list` | List API request logs |
|
|
150
|
+
| `resend_log_get` | Get a log entry by ID |
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
|
|
154
|
+
MIT © [AIWerk](https://aiwerkmcp.com)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare class ResendConfigError extends Error {
|
|
2
|
+
constructor(m: string);
|
|
3
|
+
}
|
|
4
|
+
export declare class ResendApiError extends Error {
|
|
5
|
+
readonly status: number;
|
|
6
|
+
readonly statusText: string;
|
|
7
|
+
readonly body: unknown;
|
|
8
|
+
constructor(status: number, statusText: string, body: unknown, m: string);
|
|
9
|
+
}
|
|
10
|
+
export declare class ResendTimeoutError extends Error {
|
|
11
|
+
constructor(m: string);
|
|
12
|
+
}
|
|
13
|
+
export declare class ResendNetworkError extends Error {
|
|
14
|
+
constructor(m: string);
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/errors.ts"],"names":[],"mappings":"AAAA,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,CAAC,EAAE,MAAM;CACtB;AAED,qBAAa,cAAe,SAAQ,KAAK;aAErB,MAAM,EAAE,MAAM;aACd,UAAU,EAAE,MAAM;aAClB,IAAI,EAAE,OAAO;gBAFb,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,OAAO,EAC7B,CAAC,EAAE,MAAM;CAEZ;AAED,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,CAAC,EAAE,MAAM;CACtB;AAED,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,CAAC,EAAE,MAAM;CACtB"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export class ResendConfigError extends Error {
|
|
2
|
+
constructor(m) { super(m); this.name = 'ResendConfigError'; }
|
|
3
|
+
}
|
|
4
|
+
export class ResendApiError extends Error {
|
|
5
|
+
status;
|
|
6
|
+
statusText;
|
|
7
|
+
body;
|
|
8
|
+
constructor(status, statusText, body, m) {
|
|
9
|
+
super(m);
|
|
10
|
+
this.status = status;
|
|
11
|
+
this.statusText = statusText;
|
|
12
|
+
this.body = body;
|
|
13
|
+
this.name = 'ResendApiError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export class ResendTimeoutError extends Error {
|
|
17
|
+
constructor(m) { super(m); this.name = 'ResendTimeoutError'; }
|
|
18
|
+
}
|
|
19
|
+
export class ResendNetworkError extends Error {
|
|
20
|
+
constructor(m) { super(m); this.name = 'ResendNetworkError'; }
|
|
21
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface ConfirmationRequired {
|
|
2
|
+
requires_confirmation: true;
|
|
3
|
+
summary: string;
|
|
4
|
+
confirm_token: string;
|
|
5
|
+
expires_in_seconds: number;
|
|
6
|
+
item_count?: number;
|
|
7
|
+
items_preview?: string[];
|
|
8
|
+
}
|
|
9
|
+
export declare function requireConfirmation(opts: {
|
|
10
|
+
tool: string;
|
|
11
|
+
id?: string;
|
|
12
|
+
ids?: string[];
|
|
13
|
+
payload?: unknown;
|
|
14
|
+
summary: string;
|
|
15
|
+
}): ConfirmationRequired;
|
|
16
|
+
export declare function validateAndConsume(opts: {
|
|
17
|
+
confirm_token: string;
|
|
18
|
+
tool: string;
|
|
19
|
+
id?: string;
|
|
20
|
+
ids?: string[];
|
|
21
|
+
payload?: unknown;
|
|
22
|
+
}): {
|
|
23
|
+
valid: boolean;
|
|
24
|
+
reason?: string;
|
|
25
|
+
};
|
|
26
|
+
export declare function gateMapSize(): number;
|
|
27
|
+
//# sourceMappingURL=destructive-gate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"destructive-gate.d.ts","sourceRoot":"","sources":["../../../src/lib/destructive-gate.ts"],"names":[],"mappings":"AAuCA,MAAM,WAAW,oBAAoB;IACnC,qBAAqB,EAAE,IAAI,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB,GAAG,oBAAoB,CAuBvB;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IACvC,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAoBtC;AAED,wBAAgB,WAAW,IAAI,MAAM,CAGpC"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// α destructive-confirmation gate — 2-phase confirm pattern.
|
|
2
|
+
//
|
|
3
|
+
// Phase 1 (no confirm_token): caller calls requireConfirmation() → gets a token back.
|
|
4
|
+
// Phase 2 (confirm_token present): caller calls validateAndConsume() → token is validated
|
|
5
|
+
// and removed from the map (single-use). If valid, the tool executes the real API call.
|
|
6
|
+
//
|
|
7
|
+
// Bulk mode: ids[] is hashed together with the tool name and payload hash.
|
|
8
|
+
// ONE confirm_token covers N items.
|
|
9
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
10
|
+
const TTL_MS = 60_000; // 60 seconds
|
|
11
|
+
const gateMap = new Map();
|
|
12
|
+
function hashPayload(payload) {
|
|
13
|
+
return createHash('sha256').update(JSON.stringify(payload ?? null)).digest('hex');
|
|
14
|
+
}
|
|
15
|
+
function itemKey(tool, id, ids, payload) {
|
|
16
|
+
const idPart = ids ? `ids:${hashPayload(ids.slice().sort())}` : `id:${id ?? ''}`;
|
|
17
|
+
return createHash('sha256')
|
|
18
|
+
.update(`${tool}|${idPart}|${hashPayload(payload)}`)
|
|
19
|
+
.digest('hex');
|
|
20
|
+
}
|
|
21
|
+
function pruneExpired() {
|
|
22
|
+
const now = Date.now();
|
|
23
|
+
for (const [token, entry] of gateMap) {
|
|
24
|
+
if (entry.expires_at <= now)
|
|
25
|
+
gateMap.delete(token);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function requireConfirmation(opts) {
|
|
29
|
+
pruneExpired();
|
|
30
|
+
const token = randomBytes(32).toString('hex');
|
|
31
|
+
const key = itemKey(opts.tool, opts.id, opts.ids, opts.payload ?? null);
|
|
32
|
+
gateMap.set(token, {
|
|
33
|
+
tool: opts.tool,
|
|
34
|
+
key,
|
|
35
|
+
expires_at: Date.now() + TTL_MS,
|
|
36
|
+
});
|
|
37
|
+
const result = {
|
|
38
|
+
requires_confirmation: true,
|
|
39
|
+
summary: opts.summary,
|
|
40
|
+
confirm_token: token,
|
|
41
|
+
expires_in_seconds: TTL_MS / 1000,
|
|
42
|
+
};
|
|
43
|
+
if (opts.ids) {
|
|
44
|
+
result.item_count = opts.ids.length;
|
|
45
|
+
result.items_preview = opts.ids.slice(0, 3);
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
export function validateAndConsume(opts) {
|
|
50
|
+
pruneExpired();
|
|
51
|
+
const entry = gateMap.get(opts.confirm_token);
|
|
52
|
+
if (!entry) {
|
|
53
|
+
return { valid: false, reason: 'confirm_token not found or expired. Call the tool again without confirm_token to get a new one.' };
|
|
54
|
+
}
|
|
55
|
+
if (entry.expires_at <= Date.now()) {
|
|
56
|
+
gateMap.delete(opts.confirm_token);
|
|
57
|
+
return { valid: false, reason: 'confirm_token expired (60s TTL). Call the tool again without confirm_token to start a new confirmation.' };
|
|
58
|
+
}
|
|
59
|
+
if (entry.tool !== opts.tool) {
|
|
60
|
+
return { valid: false, reason: `confirm_token was issued for tool "${entry.tool}", not "${opts.tool}".` };
|
|
61
|
+
}
|
|
62
|
+
const expectedKey = itemKey(opts.tool, opts.id, opts.ids, opts.payload ?? null);
|
|
63
|
+
if (entry.key !== expectedKey) {
|
|
64
|
+
return { valid: false, reason: 'confirm_token does not match the current request (id/ids or payload changed). Call the tool again without confirm_token.' };
|
|
65
|
+
}
|
|
66
|
+
gateMap.delete(opts.confirm_token);
|
|
67
|
+
return { valid: true };
|
|
68
|
+
}
|
|
69
|
+
export function gateMapSize() {
|
|
70
|
+
pruneExpired();
|
|
71
|
+
return gateMap.size;
|
|
72
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idempotency.d.ts","sourceRoot":"","sources":["../../../src/lib/idempotency.ts"],"names":[],"mappings":"AAOA,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CAK3E"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
// Derive a stable Idempotency-Key from tool name + serialized payload.
|
|
3
|
+
// Resend supports 24-hour dedup on this key for email sends.
|
|
4
|
+
// Key is SHA-256(tool:JSON(payload)), first 64 hex chars = 32 bytes = 256-bit.
|
|
5
|
+
// JSON.stringify key order is insertion-order — callers must NOT sort or reorder
|
|
6
|
+
// payload fields between phase-1 and phase-2 calls or the key will differ.
|
|
7
|
+
export function deriveIdempotencyKey(tool, payload) {
|
|
8
|
+
return createHash('sha256')
|
|
9
|
+
.update(`${tool}:${JSON.stringify(payload ?? null)}`)
|
|
10
|
+
.digest('hex')
|
|
11
|
+
.slice(0, 64);
|
|
12
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare function getApiKey(): string;
|
|
2
|
+
export declare function resendFetch<T>(method: string, path: string, body?: unknown, opts?: {
|
|
3
|
+
idempotencyKey?: string;
|
|
4
|
+
query?: Record<string, string | number | boolean | undefined>;
|
|
5
|
+
}): Promise<T>;
|
|
6
|
+
export declare function resendGet<T>(path: string, query?: Record<string, string | number | boolean | undefined>): Promise<T>;
|
|
7
|
+
export declare function resendPost<T>(path: string, body?: unknown, opts?: {
|
|
8
|
+
idempotencyKey?: string;
|
|
9
|
+
}): Promise<T>;
|
|
10
|
+
export declare function resendPut<T>(path: string, body?: unknown): Promise<T>;
|
|
11
|
+
export declare function resendPatch<T>(path: string, body?: unknown): Promise<T>;
|
|
12
|
+
export declare function resendDelete<T>(path: string): Promise<T>;
|
|
13
|
+
//# sourceMappingURL=resend-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resend-client.d.ts","sourceRoot":"","sources":["../../../src/lib/resend-client.ts"],"names":[],"mappings":"AAUA,wBAAgB,SAAS,IAAI,MAAM,CAIlC;AAsDD,wBAAsB,WAAW,CAAC,CAAC,EACjC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,OAAO,EACd,IAAI,CAAC,EAAE;IACL,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,CAAC;CAC/D,GACA,OAAO,CAAC,CAAC,CAAC,CAuDZ;AAED,wBAAsB,SAAS,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAE1H;AAED,wBAAsB,UAAU,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE;IAAE,cAAc,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,CAAC,CAAC,CAEhH;AAED,wBAAsB,SAAS,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,CAE3E;AAED,wBAAsB,WAAW,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,CAE7E;AAED,wBAAsB,YAAY,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAE9D"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { VERSION } from '../version.js';
|
|
2
|
+
import { ResendConfigError, ResendApiError, ResendTimeoutError, ResendNetworkError, } from '../errors.js';
|
|
3
|
+
// --- Config (read at call time so tests can override process.env) ---
|
|
4
|
+
export function getApiKey() {
|
|
5
|
+
const key = process.env.RESEND_API_KEY;
|
|
6
|
+
if (!key)
|
|
7
|
+
throw new ResendConfigError('RESEND_API_KEY environment variable is required');
|
|
8
|
+
return key;
|
|
9
|
+
}
|
|
10
|
+
function getBaseUrl() {
|
|
11
|
+
return (process.env.RESEND_API_BASE_URL ?? 'https://api.resend.com').replace(/\/$/, '');
|
|
12
|
+
}
|
|
13
|
+
function getTimeoutMs() {
|
|
14
|
+
const raw = process.env.RESEND_API_TIMEOUT_MS;
|
|
15
|
+
if (!raw)
|
|
16
|
+
return 30_000;
|
|
17
|
+
const n = Number.parseInt(raw, 10);
|
|
18
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
19
|
+
throw new ResendConfigError(`RESEND_API_TIMEOUT_MS must be a positive integer (got "${raw}")`);
|
|
20
|
+
}
|
|
21
|
+
return n;
|
|
22
|
+
}
|
|
23
|
+
// --- 5 req/s client-side throttle ---
|
|
24
|
+
// Simple leaky-bucket: track timestamps of last N requests.
|
|
25
|
+
const RATE_LIMIT_RPS = 5;
|
|
26
|
+
const recentRequests = [];
|
|
27
|
+
async function throttle() {
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
const windowMs = 1000;
|
|
30
|
+
// Remove requests older than 1s
|
|
31
|
+
while (recentRequests.length > 0 && now - recentRequests[0] >= windowMs) {
|
|
32
|
+
recentRequests.shift();
|
|
33
|
+
}
|
|
34
|
+
if (recentRequests.length >= RATE_LIMIT_RPS) {
|
|
35
|
+
const oldest = recentRequests[0];
|
|
36
|
+
const wait = windowMs - (now - oldest) + 1;
|
|
37
|
+
await new Promise(r => setTimeout(r, wait));
|
|
38
|
+
// Recurse to re-check after sleep (another request may have filled the window)
|
|
39
|
+
return throttle();
|
|
40
|
+
}
|
|
41
|
+
recentRequests.push(Date.now());
|
|
42
|
+
}
|
|
43
|
+
// --- Error body formatting ---
|
|
44
|
+
function compactError(status, endpoint, body) {
|
|
45
|
+
let hint = '';
|
|
46
|
+
if (body && typeof body === 'object') {
|
|
47
|
+
const b = body;
|
|
48
|
+
hint = String(b.message ?? b.error ?? b.name ?? '');
|
|
49
|
+
}
|
|
50
|
+
else if (typeof body === 'string') {
|
|
51
|
+
hint = body;
|
|
52
|
+
}
|
|
53
|
+
return `Resend API ${status} on ${endpoint}${hint ? ` — ${hint}` : ''}`;
|
|
54
|
+
}
|
|
55
|
+
// --- Core fetch ---
|
|
56
|
+
export async function resendFetch(method, path, body, opts) {
|
|
57
|
+
await throttle();
|
|
58
|
+
const baseUrl = getBaseUrl();
|
|
59
|
+
let url = `${baseUrl}${path}`;
|
|
60
|
+
if (opts?.query) {
|
|
61
|
+
const params = new URLSearchParams();
|
|
62
|
+
for (const [k, v] of Object.entries(opts.query)) {
|
|
63
|
+
if (v !== undefined && v !== null)
|
|
64
|
+
params.set(k, String(v));
|
|
65
|
+
}
|
|
66
|
+
const qs = params.toString();
|
|
67
|
+
if (qs)
|
|
68
|
+
url += `?${qs}`;
|
|
69
|
+
}
|
|
70
|
+
const headers = new Headers();
|
|
71
|
+
// Never log the key — only set it in the Authorization header
|
|
72
|
+
headers.set('Authorization', `Bearer ${getApiKey()}`);
|
|
73
|
+
headers.set('Content-Type', 'application/json');
|
|
74
|
+
headers.set('Accept', 'application/json');
|
|
75
|
+
headers.set('User-Agent', `@aiwerk/mcp-server-resend/${VERSION}`);
|
|
76
|
+
if (opts?.idempotencyKey) {
|
|
77
|
+
headers.set('Idempotency-Key', opts.idempotencyKey);
|
|
78
|
+
}
|
|
79
|
+
const init = { method, headers };
|
|
80
|
+
if (body !== undefined && body !== null) {
|
|
81
|
+
init.body = JSON.stringify(body);
|
|
82
|
+
}
|
|
83
|
+
let response;
|
|
84
|
+
try {
|
|
85
|
+
response = await fetch(url, { ...init, signal: AbortSignal.timeout(getTimeoutMs()) });
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
89
|
+
if (e.name === 'TimeoutError' || e.name === 'AbortError') {
|
|
90
|
+
throw new ResendTimeoutError(`Resend request to ${path} timed out after ${getTimeoutMs()}ms`);
|
|
91
|
+
}
|
|
92
|
+
throw new ResendNetworkError(`Resend network error on ${path}: ${e.message}`);
|
|
93
|
+
}
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
let errorBody = null;
|
|
96
|
+
try {
|
|
97
|
+
errorBody = await response.json();
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
try {
|
|
101
|
+
errorBody = await response.text();
|
|
102
|
+
}
|
|
103
|
+
catch { /* */ }
|
|
104
|
+
}
|
|
105
|
+
throw new ResendApiError(response.status, response.statusText, errorBody, compactError(response.status, path, errorBody));
|
|
106
|
+
}
|
|
107
|
+
if (response.status === 204)
|
|
108
|
+
return undefined;
|
|
109
|
+
const ct = response.headers.get('content-type') ?? '';
|
|
110
|
+
if (!ct.includes('application/json'))
|
|
111
|
+
return undefined;
|
|
112
|
+
return await response.json();
|
|
113
|
+
}
|
|
114
|
+
export async function resendGet(path, query) {
|
|
115
|
+
return resendFetch('GET', path, undefined, { query });
|
|
116
|
+
}
|
|
117
|
+
export async function resendPost(path, body, opts) {
|
|
118
|
+
return resendFetch('POST', path, body, opts);
|
|
119
|
+
}
|
|
120
|
+
export async function resendPut(path, body) {
|
|
121
|
+
return resendFetch('PUT', path, body);
|
|
122
|
+
}
|
|
123
|
+
export async function resendPatch(path, body) {
|
|
124
|
+
return resendFetch('PATCH', path, body);
|
|
125
|
+
}
|
|
126
|
+
export async function resendDelete(path) {
|
|
127
|
+
return resendFetch('DELETE', path);
|
|
128
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
export declare function toolError(error: unknown): {
|
|
4
|
+
isError: boolean;
|
|
5
|
+
content: {
|
|
6
|
+
type: "text";
|
|
7
|
+
text: string;
|
|
8
|
+
}[];
|
|
9
|
+
};
|
|
10
|
+
export declare function createServer(): McpServer;
|
|
11
|
+
export declare function isCliEntry(moduleUrl?: string, argv1?: string | undefined): boolean;
|
|
12
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/server.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAkGpE,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO;;;;;;EAkBvC;AASD,wBAAgB,YAAY,cA2b3B;AAOD,wBAAgB,UAAU,CACxB,SAAS,GAAE,MAAwB,EACnC,KAAK,GAAE,MAAM,GAAG,SAA2B,GAC1C,OAAO,CAIT"}
|