notifycx-mcp 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,6 +6,8 @@ Use this in Cursor, Claude Code, or other MCP hosts when a coding agent should c
6
6
 
7
7
  ## Install (Cursor)
8
8
 
9
+ **Already have a key:**
10
+
9
11
  ```json
10
12
  {
11
13
  "mcpServers": {
@@ -20,16 +22,27 @@ Use this in Cursor, Claude Code, or other MCP hosts when a coding agent should c
20
22
  }
21
23
  ```
22
24
 
23
- Get your API key from [Credentials](https://notify.cx/dashboard/credentials). Docs: [notify.cx/mcp](https://notify.cx/mcp).
25
+ **No key yet:** omit `NOTIFY_API_KEY`, call `bootstrap_start` → user pastes email OTP → `bootstrap_confirm` → put the returned `api_key` into MCP env. See [agent bootstrap docs](https://notify.cx/docs/agent-bootstrap-no-dashboard).
26
+
27
+ Never paste API key secrets into chat logs.
24
28
 
25
29
  ## Tools
26
30
 
27
31
  | Tool | When to use |
28
32
  |------|-------------|
33
+ | `bootstrap_start` / `bootstrap_confirm` | Sign up or recover without the dashboard (no API key required) |
29
34
  | `send_email` | Production transactional send (verified `from`) |
30
35
  | `send_test_email` | Non-delivering API rehearsal |
31
- | `list_domains` | Domain / SPF / DKIM status |
36
+ | `add_domain` | Add domain + structured `dnsRecords` (type/name/value) for the user to publish at their host |
37
+ | `check_domain` | Poll verification after DNS is configured |
38
+ | `list_domains` / `get_domain` | Domain status + `dnsRecords` |
39
+ | `list_webhooks` / `create_webhook` / `update_webhook` / `delete_webhook` / `get_webhook` / `test_webhook` | Webhook setup (Pro+) |
32
40
  | `list_email_logs` | Recent delivery logs |
41
+ | `get_usage` | Quota / account_verified |
42
+ | `list_api_keys` / `create_api_key` / `regenerate_api_key` / `delete_api_key` | Manage keys |
43
+ | `create_billing_checkout` | Stripe Checkout URL for Pro/Scale |
44
+
45
+ Typical setup path: bootstrap → `add_domain` → user publishes `dnsRecords` → `check_domain` → `send_email`.
33
46
 
34
47
  ## Pricing
35
48
 
package/dist/index.d.ts CHANGED
@@ -1,2 +1 @@
1
-
2
- export { }
1
+ #!/usr/bin/env node
package/dist/index.js CHANGED
@@ -1,15 +1,22 @@
1
1
  #!/usr/bin/env node
2
+ #!/usr/bin/env node
2
3
 
3
4
  // src/index.ts
4
5
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
7
  import Notify, { NotifyError } from "notifycx";
7
8
  import { z } from "zod";
9
+ function baseUrl() {
10
+ return process.env.NOTIFY_BASE_URL;
11
+ }
12
+ function optionalApiKey() {
13
+ return process.env.NOTIFY_API_KEY;
14
+ }
8
15
  function requireApiKey() {
9
- const apiKey = process.env.NOTIFY_API_KEY;
16
+ const apiKey = optionalApiKey();
10
17
  if (!apiKey) {
11
18
  throw new Error(
12
- "Missing NOTIFY_API_KEY. Set it in your MCP server env (dashboard \u2192 Credentials)."
19
+ "Missing NOTIFY_API_KEY. Run bootstrap_start + bootstrap_confirm first, then set NOTIFY_API_KEY in MCP env to the returned api_key (never paste secrets into chat)."
13
20
  );
14
21
  }
15
22
  return apiKey;
@@ -17,7 +24,8 @@ function requireApiKey() {
17
24
  function client() {
18
25
  return new Notify({
19
26
  apiKey: requireApiKey(),
20
- baseUrl: process.env.NOTIFY_BASE_URL
27
+ baseUrl: baseUrl(),
28
+ client: "mcp"
21
29
  });
22
30
  }
23
31
  function formatError(err) {
@@ -36,6 +44,26 @@ function formatError(err) {
36
44
  if (err instanceof Error) return err.message;
37
45
  return String(err);
38
46
  }
47
+ function ok(result) {
48
+ return {
49
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
50
+ };
51
+ }
52
+ function fail(err) {
53
+ return {
54
+ content: [{ type: "text", text: formatError(err) }],
55
+ isError: true
56
+ };
57
+ }
58
+ var eventTypeEnum = z.enum([
59
+ "Bounce",
60
+ "Click",
61
+ "Complaint",
62
+ "Delivery",
63
+ "DeliveryDelay",
64
+ "Open",
65
+ "Send"
66
+ ]);
39
67
  var sendShape = {
40
68
  to: z.string().email().describe("Recipient email address"),
41
69
  from: z.string().min(1).describe(
@@ -46,25 +74,75 @@ var sendShape = {
46
74
  message: z.string().min(1).optional().describe("Alias for html/body content"),
47
75
  text: z.string().min(1).optional().describe("Alias for html/body content")
48
76
  };
77
+ var webhookWriteShape = {
78
+ webhookUrl: z.string().url().describe("HTTPS URL to receive webhook events"),
79
+ subscribedEvents: z.array(eventTypeEnum).min(1).describe("Event types to subscribe to"),
80
+ domainId: z.string().uuid().describe("Verified sending domain id from list_domains"),
81
+ matchMode: z.enum(["domain", "hosts", "froms"]).optional().describe("Optional match scope (default domain)"),
82
+ matchHosts: z.array(z.string()).max(20).optional().describe("When matchMode is hosts"),
83
+ matchFroms: z.array(z.string()).max(20).optional().describe("When matchMode is froms")
84
+ };
49
85
  var server = new McpServer({
50
86
  name: "notifycx-mcp",
51
- version: "1.0.0"
87
+ version: "1.2.0"
52
88
  });
89
+ server.tool(
90
+ "bootstrap_start",
91
+ "Start Notify agent signup or key recovery without the dashboard. Sends a 6-digit email OTP. No NOTIFY_API_KEY required. Ask the user to paste the code, then call bootstrap_confirm. Requires acceptTerms: true (https://notify.cx/terms).",
92
+ {
93
+ email: z.string().email().describe("User email for the Notify account"),
94
+ intent: z.enum(["signup", "recover"]).optional().describe("signup (default) or recover existing account"),
95
+ acceptTerms: z.boolean().describe("Must be true \u2014 user accepts Terms and AUP")
96
+ },
97
+ async (args) => {
98
+ try {
99
+ return ok(
100
+ await Notify.bootstrapStart({
101
+ email: args.email,
102
+ intent: args.intent,
103
+ acceptTerms: args.acceptTerms,
104
+ baseUrl: baseUrl()
105
+ })
106
+ );
107
+ } catch (err) {
108
+ return fail(err);
109
+ }
110
+ }
111
+ );
112
+ server.tool(
113
+ "bootstrap_confirm",
114
+ "Confirm the email OTP from bootstrap_start. Returns api_key once \u2014 instruct the user to put it in MCP env NOTIFY_API_KEY (do not leave the secret in chat). Then use add_domain / send_email. No NOTIFY_API_KEY required for this call.",
115
+ {
116
+ email: z.string().email(),
117
+ code: z.string().regex(/^\d{6}$/).describe("6-digit code from email"),
118
+ intent: z.enum(["signup", "recover"]).optional(),
119
+ clientHint: z.string().max(64).optional().describe("e.g. cursor, chatgpt")
120
+ },
121
+ async (args) => {
122
+ try {
123
+ return ok(
124
+ await Notify.bootstrapConfirm({
125
+ email: args.email,
126
+ code: args.code,
127
+ intent: args.intent,
128
+ clientHint: args.clientHint,
129
+ baseUrl: baseUrl()
130
+ })
131
+ );
132
+ } catch (err) {
133
+ return fail(err);
134
+ }
135
+ }
136
+ );
53
137
  server.tool(
54
138
  "send_email",
55
139
  "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.",
56
140
  sendShape,
57
141
  async (args) => {
58
142
  try {
59
- const result = await client().send(args);
60
- return {
61
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
62
- };
143
+ return ok(await client().send(args));
63
144
  } catch (err) {
64
- return {
65
- content: [{ type: "text", text: formatError(err) }],
66
- isError: true
67
- };
145
+ return fail(err);
68
146
  }
69
147
  }
70
148
  );
@@ -74,33 +152,72 @@ server.tool(
74
152
  sendShape,
75
153
  async (args) => {
76
154
  try {
77
- const result = await client().sendTest(args);
78
- return {
79
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
80
- };
155
+ return ok(await client().sendTest(args));
81
156
  } catch (err) {
82
- return {
83
- content: [{ type: "text", text: formatError(err) }],
84
- isError: true
85
- };
157
+ return fail(err);
86
158
  }
87
159
  }
88
160
  );
89
161
  server.tool(
90
162
  "list_domains",
91
- "List Notify sending domains and verification status (SPF/DKIM). Use when wiring a production from address or debugging domain auth.",
163
+ "List Notify sending domains and verification status (SPF/DKIM). Includes structured dnsRecords (type/name/value) for the user to publish at their DNS host.",
92
164
  {},
93
165
  async () => {
94
166
  try {
95
- const result = await client().listDomains();
96
- return {
97
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
98
- };
167
+ return ok(await client().listDomains());
99
168
  } catch (err) {
100
- return {
101
- content: [{ type: "text", text: formatError(err) }],
102
- isError: true
103
- };
169
+ return fail(err);
170
+ }
171
+ }
172
+ );
173
+ server.tool(
174
+ "get_domain",
175
+ "Get one Notify sending domain by hostname, including dnsRecords (type/name/value) and raw DNS tokens for pending verification. Show dnsRecords to the user so they can add them at their hosting provider.",
176
+ {
177
+ domain: z.string().min(1).describe("Domain hostname, e.g. example.com")
178
+ },
179
+ async (args) => {
180
+ try {
181
+ return ok(await client().getDomain(args.domain));
182
+ } catch (err) {
183
+ return fail(err);
184
+ }
185
+ }
186
+ );
187
+ server.tool(
188
+ "add_domain",
189
+ "Add a sending domain and start SPF/DKIM verification. Returns dnsRecords (type/name/value) the user must add at their DNS host \u2014 Notify does not publish DNS. After DNS is configured, call check_domain. Then use send_email with a from address on that domain.",
190
+ {
191
+ domain: z.string().min(1).describe("Domain hostname to verify, e.g. example.com"),
192
+ dnsProvider: z.enum([
193
+ "vercel",
194
+ "cloudflare",
195
+ "squarespace",
196
+ "namecheap",
197
+ "godaddy",
198
+ "route53",
199
+ "other"
200
+ ]).optional().describe("Optional DNS provider hint for host/name formatting")
201
+ },
202
+ async (args) => {
203
+ try {
204
+ return ok(await client().addDomain(args));
205
+ } catch (err) {
206
+ return fail(err);
207
+ }
208
+ }
209
+ );
210
+ server.tool(
211
+ "check_domain",
212
+ "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 and the plan email limit lifts.",
213
+ {
214
+ domain: z.string().min(1).describe("Domain hostname to check")
215
+ },
216
+ async (args) => {
217
+ try {
218
+ return ok(await client().checkDomain(args.domain));
219
+ } catch (err) {
220
+ return fail(err);
104
221
  }
105
222
  }
106
223
  );
@@ -110,27 +227,184 @@ server.tool(
110
227
  {
111
228
  page: z.number().int().positive().optional().describe("Page number (default 1)"),
112
229
  limit: z.number().int().min(1).max(100).optional().describe("Page size (default 20, max 100)"),
113
- eventType: z.enum([
114
- "Bounce",
115
- "Click",
116
- "Complaint",
117
- "Delivery",
118
- "DeliveryDelay",
119
- "Open",
120
- "Send"
121
- ]).optional().describe("Filter logs by analytics event type")
230
+ eventType: eventTypeEnum.optional().describe("Filter logs by analytics event type")
231
+ },
232
+ async (args) => {
233
+ try {
234
+ return ok(await client().listLogs(args));
235
+ } catch (err) {
236
+ return fail(err);
237
+ }
238
+ }
239
+ );
240
+ server.tool(
241
+ "get_usage",
242
+ "Get Notify plan usage and quota (hourly/monthly sends, account_verified). Use when debugging limits or confirming production unlock.",
243
+ {},
244
+ async () => {
245
+ try {
246
+ return ok(await client().getUsage());
247
+ } catch (err) {
248
+ return fail(err);
249
+ }
250
+ }
251
+ );
252
+ server.tool(
253
+ "list_api_keys",
254
+ "List API key metadata for the account (id, name, prefix only \u2014 never the full secret).",
255
+ {},
256
+ async () => {
257
+ try {
258
+ return ok(await client().listApiKeys());
259
+ } catch (err) {
260
+ return fail(err);
261
+ }
262
+ }
263
+ );
264
+ server.tool(
265
+ "create_api_key",
266
+ "Create an API key (full_access required). Free plan allows one key. Prefer sending_access + domainId for app keys on Pro+.",
267
+ {
268
+ name: z.string().min(1),
269
+ permission: z.enum(["full_access", "sending_access"]).optional(),
270
+ domainId: z.string().uuid().optional()
271
+ },
272
+ async (args) => {
273
+ try {
274
+ return ok(await client().createApiKey(args));
275
+ } catch (err) {
276
+ return fail(err);
277
+ }
278
+ }
279
+ );
280
+ server.tool(
281
+ "regenerate_api_key",
282
+ "Rotate an API key secret by keyId. Returns the new api_key once \u2014 update NOTIFY_API_KEY / app secrets.",
283
+ {
284
+ keyId: z.string().uuid()
285
+ },
286
+ async (args) => {
287
+ try {
288
+ return ok(await client().regenerateApiKey(args.keyId));
289
+ } catch (err) {
290
+ return fail(err);
291
+ }
292
+ }
293
+ );
294
+ server.tool(
295
+ "delete_api_key",
296
+ "Deactivate an API key by keyId. Cannot delete the last full_access key.",
297
+ {
298
+ keyId: z.string().uuid()
299
+ },
300
+ async (args) => {
301
+ try {
302
+ return ok(await client().deleteApiKey(args.keyId));
303
+ } catch (err) {
304
+ return fail(err);
305
+ }
306
+ }
307
+ );
308
+ server.tool(
309
+ "create_billing_checkout",
310
+ "Get a Stripe Checkout URL for Pro or Scale (full_access key). User opens the URL in a browser to pay \u2014 not the Notify dashboard.",
311
+ {
312
+ plan: z.enum(["pro", "scale"]).optional()
313
+ },
314
+ async (args) => {
315
+ try {
316
+ return ok(await client().createBillingCheckout({ plan: args.plan }));
317
+ } catch (err) {
318
+ return fail(err);
319
+ }
320
+ }
321
+ );
322
+ server.tool(
323
+ "list_webhooks",
324
+ "List Notify webhook subscriptions (URL, events, domain scope).",
325
+ {},
326
+ async () => {
327
+ try {
328
+ return ok(await client().listWebhooks());
329
+ } catch (err) {
330
+ return fail(err);
331
+ }
332
+ }
333
+ );
334
+ server.tool(
335
+ "get_webhook",
336
+ "Get one Notify webhook by id.",
337
+ {
338
+ id: z.string().uuid().describe("Webhook subscription id")
339
+ },
340
+ async (args) => {
341
+ try {
342
+ return ok(await client().getWebhook(args.id));
343
+ } catch (err) {
344
+ return fail(err);
345
+ }
346
+ }
347
+ );
348
+ server.tool(
349
+ "create_webhook",
350
+ "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.",
351
+ webhookWriteShape,
352
+ async (args) => {
353
+ try {
354
+ return ok(await client().createWebhook(args));
355
+ } catch (err) {
356
+ return fail(err);
357
+ }
358
+ }
359
+ );
360
+ server.tool(
361
+ "update_webhook",
362
+ "Update an existing Notify webhook (URL, events, domain scope).",
363
+ {
364
+ id: z.string().uuid().describe("Webhook subscription id"),
365
+ webhookUrl: webhookWriteShape.webhookUrl.optional(),
366
+ subscribedEvents: webhookWriteShape.subscribedEvents.optional(),
367
+ domainId: webhookWriteShape.domainId.optional(),
368
+ matchMode: webhookWriteShape.matchMode,
369
+ matchHosts: webhookWriteShape.matchHosts,
370
+ matchFroms: webhookWriteShape.matchFroms
371
+ },
372
+ async (args) => {
373
+ try {
374
+ const { id, ...input } = args;
375
+ return ok(await client().updateWebhook(id, input));
376
+ } catch (err) {
377
+ return fail(err);
378
+ }
379
+ }
380
+ );
381
+ server.tool(
382
+ "delete_webhook",
383
+ "Delete a Notify webhook subscription by id.",
384
+ {
385
+ id: z.string().uuid().describe("Webhook subscription id")
386
+ },
387
+ async (args) => {
388
+ try {
389
+ return ok(await client().deleteWebhook(args.id));
390
+ } catch (err) {
391
+ return fail(err);
392
+ }
393
+ }
394
+ );
395
+ server.tool(
396
+ "test_webhook",
397
+ "Send a test Notify webhook payload to a URL (does not require an existing subscription id).",
398
+ {
399
+ webhookUrl: z.string().url().describe("HTTPS URL to receive the test payload"),
400
+ eventType: eventTypeEnum.describe("Event type for the test payload"),
401
+ domainId: z.string().uuid().optional().describe("Optional verified domain id for scoped test payloads")
122
402
  },
123
403
  async (args) => {
124
404
  try {
125
- const result = await client().listLogs(args);
126
- return {
127
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
128
- };
405
+ return ok(await client().testWebhook(args));
129
406
  } catch (err) {
130
- return {
131
- content: [{ type: "text", text: formatError(err) }],
132
- isError: true
133
- };
407
+ return fail(err);
134
408
  }
135
409
  }
136
410
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "notifycx-mcp",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Official MCP server for Notify — transactional email tools for coding agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,7 +27,7 @@
27
27
  "homepage": "https://notify.cx/mcp",
28
28
  "repository": {
29
29
  "type": "git",
30
- "url": "https://github.com/sunil-sandhu/notifycx.git",
30
+ "url": "https://github.com/notifycx/notifycx.git",
31
31
  "directory": "mcp"
32
32
  },
33
33
  "author": "notify.cx",
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "@modelcontextprotocol/sdk": "^1.12.1",
40
- "notifycx": "^1.0.0",
40
+ "notifycx": "^1.2.0",
41
41
  "zod": "^3.24.2"
42
42
  },
43
43
  "devDependencies": {