notifycx-mcp 0.0.0-stage → 1.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/README.md +42 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +305 -0
- package/package.json +46 -4
package/README.md
CHANGED
|
@@ -1,3 +1,43 @@
|
|
|
1
|
-
#
|
|
1
|
+
# notifycx-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Official [Model Context Protocol](https://modelcontextprotocol.io) server for [Notify](https://notify.cx).
|
|
4
|
+
|
|
5
|
+
Use this in Cursor, Claude Code, or other MCP hosts when a coding agent should call Notify tools directly. For app/CI code, prefer the [`notifycx`](https://www.npmjs.com/package/notifycx) SDK or plain `fetch`.
|
|
6
|
+
|
|
7
|
+
## Install (Cursor)
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"mcpServers": {
|
|
12
|
+
"notify": {
|
|
13
|
+
"command": "npx",
|
|
14
|
+
"args": ["-y", "notifycx-mcp"],
|
|
15
|
+
"env": {
|
|
16
|
+
"NOTIFY_API_KEY": "nk_..."
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Get your API key from [Credentials](https://notify.cx/dashboard/credentials). Docs: [notify.cx/mcp](https://notify.cx/mcp).
|
|
24
|
+
|
|
25
|
+
## Tools
|
|
26
|
+
|
|
27
|
+
| Tool | When to use |
|
|
28
|
+
|------|-------------|
|
|
29
|
+
| `send_email` | Production transactional send (verified `from`) |
|
|
30
|
+
| `send_test_email` | Non-delivering API rehearsal |
|
|
31
|
+
| `add_domain` | Add domain + get DNS records (SPF/DKIM) |
|
|
32
|
+
| `check_domain` | Poll verification after DNS is configured |
|
|
33
|
+
| `list_domains` / `get_domain` | Domain / SPF / DKIM status |
|
|
34
|
+
| `list_webhooks` / `create_webhook` / `update_webhook` / `delete_webhook` / `get_webhook` / `test_webhook` | Webhook setup (Pro+) |
|
|
35
|
+
| `list_email_logs` | Recent delivery logs |
|
|
36
|
+
| `get_usage` | Quota / account_verified |
|
|
37
|
+
| `list_api_keys` | Key metadata (prefix only — create keys in the dashboard) |
|
|
38
|
+
|
|
39
|
+
Typical setup path: `add_domain` → add DNS → `check_domain` → `send_email`. Optional: `create_webhook` after a verified domain.
|
|
40
|
+
|
|
41
|
+
## Pricing
|
|
42
|
+
|
|
43
|
+
Free: 1,000 emails/mo. Pro: **$10 / 10,000**. Scale: $50 / 100,000.
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import Notify, { NotifyError } from "notifycx";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
function requireApiKey() {
|
|
9
|
+
const apiKey = process.env.NOTIFY_API_KEY;
|
|
10
|
+
if (!apiKey) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
"Missing NOTIFY_API_KEY. Set it in your MCP server env (dashboard \u2192 Credentials)."
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
return apiKey;
|
|
16
|
+
}
|
|
17
|
+
function client() {
|
|
18
|
+
return new Notify({
|
|
19
|
+
apiKey: requireApiKey(),
|
|
20
|
+
baseUrl: process.env.NOTIFY_BASE_URL,
|
|
21
|
+
client: "mcp"
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function formatError(err) {
|
|
25
|
+
if (err instanceof NotifyError) {
|
|
26
|
+
return JSON.stringify(
|
|
27
|
+
{
|
|
28
|
+
error: err.message,
|
|
29
|
+
status: err.status,
|
|
30
|
+
body: err.body,
|
|
31
|
+
hint: err.status === 401 ? "Check NOTIFY_API_KEY in MCP config" : err.message.toLowerCase().includes("domain") || err.message.toLowerCase().includes("sandbox") ? "Verify a domain (SPF/DKIM) or use send_test_email until production is unlocked" : void 0
|
|
32
|
+
},
|
|
33
|
+
null,
|
|
34
|
+
2
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
if (err instanceof Error) return err.message;
|
|
38
|
+
return String(err);
|
|
39
|
+
}
|
|
40
|
+
function ok(result) {
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function fail(err) {
|
|
46
|
+
return {
|
|
47
|
+
content: [{ type: "text", text: formatError(err) }],
|
|
48
|
+
isError: true
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
var eventTypeEnum = z.enum([
|
|
52
|
+
"Bounce",
|
|
53
|
+
"Click",
|
|
54
|
+
"Complaint",
|
|
55
|
+
"Delivery",
|
|
56
|
+
"DeliveryDelay",
|
|
57
|
+
"Open",
|
|
58
|
+
"Send"
|
|
59
|
+
]);
|
|
60
|
+
var sendShape = {
|
|
61
|
+
to: z.string().email().describe("Recipient email address"),
|
|
62
|
+
from: z.string().min(1).describe(
|
|
63
|
+
"Sender address or Name <email@verified-domain.com>. Production custom from requires a verified domain."
|
|
64
|
+
),
|
|
65
|
+
subject: z.string().min(1).describe("Email subject line"),
|
|
66
|
+
html: z.string().min(1).optional().describe("HTML (or plain text) body. Prefer html; message/text also work."),
|
|
67
|
+
message: z.string().min(1).optional().describe("Alias for html/body content"),
|
|
68
|
+
text: z.string().min(1).optional().describe("Alias for html/body content")
|
|
69
|
+
};
|
|
70
|
+
var webhookWriteShape = {
|
|
71
|
+
webhookUrl: z.string().url().describe("HTTPS URL to receive webhook events"),
|
|
72
|
+
subscribedEvents: z.array(eventTypeEnum).min(1).describe("Event types to subscribe to"),
|
|
73
|
+
domainId: z.string().uuid().describe("Verified sending domain id from list_domains"),
|
|
74
|
+
matchMode: z.enum(["domain", "hosts", "froms"]).optional().describe("Optional match scope (default domain)"),
|
|
75
|
+
matchHosts: z.array(z.string()).max(20).optional().describe("When matchMode is hosts"),
|
|
76
|
+
matchFroms: z.array(z.string()).max(20).optional().describe("When matchMode is froms")
|
|
77
|
+
};
|
|
78
|
+
var server = new McpServer({
|
|
79
|
+
name: "notifycx-mcp",
|
|
80
|
+
version: "1.1.0"
|
|
81
|
+
});
|
|
82
|
+
server.tool(
|
|
83
|
+
"send_email",
|
|
84
|
+
"Send transactional email via Notify (password reset, receipt, magic link, invoice, etc.) with a verified from domain. Use when the user wants production delivery. Prefer html for the body. For non-delivering API rehearsal before domain verification, use send_test_email instead.",
|
|
85
|
+
sendShape,
|
|
86
|
+
async (args) => {
|
|
87
|
+
try {
|
|
88
|
+
return ok(await client().send(args));
|
|
89
|
+
} catch (err) {
|
|
90
|
+
return fail(err);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
server.tool(
|
|
95
|
+
"send_test_email",
|
|
96
|
+
"API rehearsal for Notify: same request shape as production send but does not deliver and does not count toward quota. Use before domain verification or when testing the HTTP contract. For real inbox delivery, use send_email after verifying SPF/DKIM.",
|
|
97
|
+
sendShape,
|
|
98
|
+
async (args) => {
|
|
99
|
+
try {
|
|
100
|
+
return ok(await client().sendTest(args));
|
|
101
|
+
} catch (err) {
|
|
102
|
+
return fail(err);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
server.tool(
|
|
107
|
+
"list_domains",
|
|
108
|
+
"List Notify sending domains and verification status (SPF/DKIM). Use when wiring a production from address or debugging domain auth. Includes DNS tokens when present.",
|
|
109
|
+
{},
|
|
110
|
+
async () => {
|
|
111
|
+
try {
|
|
112
|
+
return ok(await client().listDomains());
|
|
113
|
+
} catch (err) {
|
|
114
|
+
return fail(err);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
server.tool(
|
|
119
|
+
"get_domain",
|
|
120
|
+
"Get one Notify sending domain by hostname, including DNS records/tokens for pending verification.",
|
|
121
|
+
{
|
|
122
|
+
domain: z.string().min(1).describe("Domain hostname, e.g. example.com")
|
|
123
|
+
},
|
|
124
|
+
async (args) => {
|
|
125
|
+
try {
|
|
126
|
+
return ok(await client().getDomain(args.domain));
|
|
127
|
+
} catch (err) {
|
|
128
|
+
return fail(err);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
server.tool(
|
|
133
|
+
"add_domain",
|
|
134
|
+
"Add a sending domain and start SPF/DKIM verification. Returns DNS records the user must add. After DNS is configured, call check_domain. Then use send_email with a from address on that domain.",
|
|
135
|
+
{
|
|
136
|
+
domain: z.string().min(1).describe("Domain hostname to verify, e.g. example.com"),
|
|
137
|
+
dnsProvider: z.enum([
|
|
138
|
+
"vercel",
|
|
139
|
+
"cloudflare",
|
|
140
|
+
"squarespace",
|
|
141
|
+
"namecheap",
|
|
142
|
+
"godaddy",
|
|
143
|
+
"route53",
|
|
144
|
+
"other"
|
|
145
|
+
]).optional().describe("Optional DNS provider hint for dashboard UX")
|
|
146
|
+
},
|
|
147
|
+
async (args) => {
|
|
148
|
+
try {
|
|
149
|
+
return ok(await client().addDomain(args));
|
|
150
|
+
} catch (err) {
|
|
151
|
+
return fail(err);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
);
|
|
155
|
+
server.tool(
|
|
156
|
+
"check_domain",
|
|
157
|
+
"Poll SES for domain SPF/DKIM verification status after DNS records were added. Call after add_domain. When both succeed, production send_email with that from domain is unlocked.",
|
|
158
|
+
{
|
|
159
|
+
domain: z.string().min(1).describe("Domain hostname to check")
|
|
160
|
+
},
|
|
161
|
+
async (args) => {
|
|
162
|
+
try {
|
|
163
|
+
return ok(await client().checkDomain(args.domain));
|
|
164
|
+
} catch (err) {
|
|
165
|
+
return fail(err);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
);
|
|
169
|
+
server.tool(
|
|
170
|
+
"list_email_logs",
|
|
171
|
+
"List recent Notify email delivery logs. Use after a send to check delivery, bounce, open, or click status.",
|
|
172
|
+
{
|
|
173
|
+
page: z.number().int().positive().optional().describe("Page number (default 1)"),
|
|
174
|
+
limit: z.number().int().min(1).max(100).optional().describe("Page size (default 20, max 100)"),
|
|
175
|
+
eventType: eventTypeEnum.optional().describe("Filter logs by analytics event type")
|
|
176
|
+
},
|
|
177
|
+
async (args) => {
|
|
178
|
+
try {
|
|
179
|
+
return ok(await client().listLogs(args));
|
|
180
|
+
} catch (err) {
|
|
181
|
+
return fail(err);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
);
|
|
185
|
+
server.tool(
|
|
186
|
+
"get_usage",
|
|
187
|
+
"Get Notify plan usage and quota (hourly/monthly sends, account_verified). Use when debugging limits or confirming production unlock.",
|
|
188
|
+
{},
|
|
189
|
+
async () => {
|
|
190
|
+
try {
|
|
191
|
+
return ok(await client().getUsage());
|
|
192
|
+
} catch (err) {
|
|
193
|
+
return fail(err);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
);
|
|
197
|
+
server.tool(
|
|
198
|
+
"list_api_keys",
|
|
199
|
+
"List API key metadata for the account (id, name, prefix only \u2014 never the full secret). Create or rotate keys in the Notify dashboard Credentials page.",
|
|
200
|
+
{},
|
|
201
|
+
async () => {
|
|
202
|
+
try {
|
|
203
|
+
return ok(await client().listApiKeys());
|
|
204
|
+
} catch (err) {
|
|
205
|
+
return fail(err);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
);
|
|
209
|
+
server.tool(
|
|
210
|
+
"list_webhooks",
|
|
211
|
+
"List Notify webhook subscriptions (URL, events, domain scope).",
|
|
212
|
+
{},
|
|
213
|
+
async () => {
|
|
214
|
+
try {
|
|
215
|
+
return ok(await client().listWebhooks());
|
|
216
|
+
} catch (err) {
|
|
217
|
+
return fail(err);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
);
|
|
221
|
+
server.tool(
|
|
222
|
+
"get_webhook",
|
|
223
|
+
"Get one Notify webhook by id.",
|
|
224
|
+
{
|
|
225
|
+
id: z.string().uuid().describe("Webhook subscription id")
|
|
226
|
+
},
|
|
227
|
+
async (args) => {
|
|
228
|
+
try {
|
|
229
|
+
return ok(await client().getWebhook(args.id));
|
|
230
|
+
} catch (err) {
|
|
231
|
+
return fail(err);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
);
|
|
235
|
+
server.tool(
|
|
236
|
+
"create_webhook",
|
|
237
|
+
"Create a Notify webhook for delivery events on a verified domain. Requires Pro or Scale (free plan has 0 webhooks). Use list_domains to get domainId.",
|
|
238
|
+
webhookWriteShape,
|
|
239
|
+
async (args) => {
|
|
240
|
+
try {
|
|
241
|
+
return ok(await client().createWebhook(args));
|
|
242
|
+
} catch (err) {
|
|
243
|
+
return fail(err);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
);
|
|
247
|
+
server.tool(
|
|
248
|
+
"update_webhook",
|
|
249
|
+
"Update an existing Notify webhook (URL, events, domain scope).",
|
|
250
|
+
{
|
|
251
|
+
id: z.string().uuid().describe("Webhook subscription id"),
|
|
252
|
+
webhookUrl: webhookWriteShape.webhookUrl.optional(),
|
|
253
|
+
subscribedEvents: webhookWriteShape.subscribedEvents.optional(),
|
|
254
|
+
domainId: webhookWriteShape.domainId.optional(),
|
|
255
|
+
matchMode: webhookWriteShape.matchMode,
|
|
256
|
+
matchHosts: webhookWriteShape.matchHosts,
|
|
257
|
+
matchFroms: webhookWriteShape.matchFroms
|
|
258
|
+
},
|
|
259
|
+
async (args) => {
|
|
260
|
+
try {
|
|
261
|
+
const { id, ...input } = args;
|
|
262
|
+
return ok(await client().updateWebhook(id, input));
|
|
263
|
+
} catch (err) {
|
|
264
|
+
return fail(err);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
);
|
|
268
|
+
server.tool(
|
|
269
|
+
"delete_webhook",
|
|
270
|
+
"Delete a Notify webhook subscription by id.",
|
|
271
|
+
{
|
|
272
|
+
id: z.string().uuid().describe("Webhook subscription id")
|
|
273
|
+
},
|
|
274
|
+
async (args) => {
|
|
275
|
+
try {
|
|
276
|
+
return ok(await client().deleteWebhook(args.id));
|
|
277
|
+
} catch (err) {
|
|
278
|
+
return fail(err);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
);
|
|
282
|
+
server.tool(
|
|
283
|
+
"test_webhook",
|
|
284
|
+
"Send a test Notify webhook payload to a URL (does not require an existing subscription id).",
|
|
285
|
+
{
|
|
286
|
+
webhookUrl: z.string().url().describe("HTTPS URL to receive the test payload"),
|
|
287
|
+
eventType: eventTypeEnum.describe("Event type for the test payload"),
|
|
288
|
+
domainId: z.string().uuid().optional().describe("Optional verified domain id for scoped test payloads")
|
|
289
|
+
},
|
|
290
|
+
async (args) => {
|
|
291
|
+
try {
|
|
292
|
+
return ok(await client().testWebhook(args));
|
|
293
|
+
} catch (err) {
|
|
294
|
+
return fail(err);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
);
|
|
298
|
+
async function main() {
|
|
299
|
+
const transport = new StdioServerTransport();
|
|
300
|
+
await server.connect(transport);
|
|
301
|
+
}
|
|
302
|
+
main().catch((err) => {
|
|
303
|
+
console.error(err);
|
|
304
|
+
process.exit(1);
|
|
305
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "notifycx-mcp",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Official MCP server for Notify — transactional email tools for coding agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"notifycx-mcp": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsup",
|
|
16
|
+
"start": "node dist/index.js",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"notify",
|
|
21
|
+
"mcp",
|
|
22
|
+
"model-context-protocol",
|
|
23
|
+
"transactional-email",
|
|
24
|
+
"cursor",
|
|
25
|
+
"claude"
|
|
26
|
+
],
|
|
27
|
+
"homepage": "https://notify.cx/mcp",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/sunil-sandhu/notifycx.git",
|
|
31
|
+
"directory": "mcp"
|
|
32
|
+
},
|
|
33
|
+
"author": "notify.cx",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
40
|
+
"notifycx": "^1.1.0",
|
|
41
|
+
"zod": "^3.24.2"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "^22.8.1",
|
|
45
|
+
"tsup": "^8.0.1",
|
|
46
|
+
"typescript": "^5.6.3"
|
|
47
|
+
}
|
|
48
|
+
}
|