@sendinel/mcp-server 1.0.77 → 1.0.79

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.
@@ -1,3 +1,4 @@
1
+ import { assertSendAccepted, SendRefusedError } from "@sendinel/domain";
1
2
  import { randomUUID } from 'node:crypto';
2
3
  import { appendPhysicalAddressFooter, appendUnsubscribeFooter, buildListUnsubscribeHeaders, createUnsubscribeUrl, messageIds, normalizeSubject } from '@sendinel/domain';
3
4
  import { db, requestContext } from '../db.js';
@@ -141,6 +142,7 @@ export async function sendAgentReply(mailbox, input, approvalId) {
141
142
  replyTo: `${mailbox.local_part}@${mailbox.domain}`,
142
143
  headers: { 'Message-ID': message.message_id_header, ...(parentId ? { 'In-Reply-To': parentId, References: references.join(' ') } : {}), ...message.unsubscribeHeaders },
143
144
  });
145
+ assertSendAccepted(result);
144
146
  sent.push(message.id);
145
147
  checked(await mailboxDb().from('email_log').update({ status: 'sent', sent_at: new Date().toISOString(), provider_message_id: result.messageId })
146
148
  .eq('id', message.log_id).eq('project_id', getProjectId()));
@@ -148,7 +150,10 @@ export async function sendAgentReply(mailbox, input, approvalId) {
148
150
  .eq('id', message.id).eq('mailbox_id', mailbox.id).eq('project_id', getProjectId()));
149
151
  await mailboxAudit('reply_sent', mailbox, { approval_id: approvalId, thread_id: threadId, message_id: message.id });
150
152
  }
151
- catch {
153
+ catch (error) {
154
+ if (error instanceof SendRefusedError) {
155
+ checked(await mailboxDb().from('email_log').update({ status: 'failed', error_message: error.message }).eq('id', message.log_id).eq('project_id', getProjectId()));
156
+ }
152
157
  // Preserve reservation and queued log on ambiguous failures; never retry automatically or claim no send.
153
158
  await mailboxAudit('reply_needs_human', mailbox, { approval_id: approvalId, thread_id: threadId, message_id: message.id });
154
159
  return { sent: false, sent_message_ids: sent, thread_id: threadId,
@@ -1,15 +1,19 @@
1
+ import { type SendRefusal } from "@sendinel/domain";
1
2
  export declare const EMAIL_PROVIDER_NAMES: readonly ["resend", "sendgrid", "ses", "postmark", "mailgun", "elastic-email"];
2
3
  export type EmailProviderName = (typeof EMAIL_PROVIDER_NAMES)[number];
3
4
  type SendArgs = {
4
5
  from: string;
5
- to: string;
6
+ to: string | string[];
7
+ cc?: string[];
8
+ bcc?: string[];
6
9
  subject: string;
7
10
  html?: string;
8
11
  text?: string;
9
12
  replyTo?: string;
10
13
  headers?: Record<string, string>;
11
14
  };
12
- type SendResult = {
15
+ type SendResult = SendRefusal | {
16
+ refused?: false;
13
17
  provider: EmailProviderName;
14
18
  messageId: string;
15
19
  };
@@ -1,5 +1,6 @@
1
+ import { checkProjectSendAllowlist } from "@sendinel/domain";
1
2
  import { Resend } from "resend";
2
- import { db } from "../db.js";
3
+ import { adminDb, db } from "../db.js";
3
4
  import { decrypt } from "../crypto.js";
4
5
  export const EMAIL_PROVIDER_NAMES = [
5
6
  "resend",
@@ -125,6 +126,10 @@ async function sendViaResend(apiKey, args) {
125
126
  payload.html = args.html;
126
127
  if (args.text)
127
128
  payload.text = args.text;
129
+ if (args.cc?.length)
130
+ payload.cc = args.cc;
131
+ if (args.bcc?.length)
132
+ payload.bcc = args.bcc;
128
133
  if (args.replyTo)
129
134
  payload.reply_to = args.replyTo;
130
135
  if (args.headers)
@@ -143,7 +148,7 @@ async function sendViaElasticEmail(apiKey, args) {
143
148
  "X-ElasticEmail-ApiKey": apiKey,
144
149
  },
145
150
  body: JSON.stringify({
146
- Recipients: { To: [args.to] },
151
+ Recipients: { To: Array.isArray(args.to) ? args.to : [args.to], ...(args.cc?.length ? { CC: args.cc } : {}), ...(args.bcc?.length ? { BCC: args.bcc } : {}) },
147
152
  Content: {
148
153
  From: from.email,
149
154
  FromName: from.name,
@@ -171,7 +176,7 @@ async function sendViaSendGrid(apiKey, args) {
171
176
  "Content-Type": "application/json",
172
177
  },
173
178
  body: JSON.stringify({
174
- personalizations: [{ to: [{ email: args.to }] }],
179
+ personalizations: [{ to: (Array.isArray(args.to) ? args.to : [args.to]).map((email) => ({ email })), ...(args.cc?.length ? { cc: args.cc.map((email) => ({ email })) } : {}), ...(args.bcc?.length ? { bcc: args.bcc.map((email) => ({ email })) } : {}) }],
175
180
  from: { email: from.email, name: from.name },
176
181
  subject: args.subject,
177
182
  content: [
@@ -196,7 +201,9 @@ async function sendViaPostmark(apiKey, args) {
196
201
  },
197
202
  body: JSON.stringify({
198
203
  From: args.from,
199
- To: args.to,
204
+ To: Array.isArray(args.to) ? args.to.join(",") : args.to,
205
+ ...(args.cc?.length ? { Cc: args.cc.join(",") } : {}),
206
+ ...(args.bcc?.length ? { Bcc: args.bcc.join(",") } : {}),
200
207
  Subject: args.subject,
201
208
  HtmlBody: args.html,
202
209
  TextBody: args.text,
@@ -212,7 +219,11 @@ async function sendViaPostmark(apiKey, args) {
212
219
  async function sendViaMailgun(apiKey, args) {
213
220
  const form = new FormData();
214
221
  form.set("from", args.from);
215
- form.set("to", args.to);
222
+ form.set("to", Array.isArray(args.to) ? args.to.join(",") : args.to);
223
+ if (args.cc?.length)
224
+ form.set("cc", args.cc.join(","));
225
+ if (args.bcc?.length)
226
+ form.set("bcc", args.bcc.join(","));
216
227
  form.set("subject", args.subject);
217
228
  if (args.html)
218
229
  form.set("html", args.html);
@@ -237,6 +248,9 @@ async function sendViaMailgun(apiKey, args) {
237
248
  return id || `mg_${Date.now()}`;
238
249
  }
239
250
  export async function sendEmailForSite(projectId, siteId, args) {
251
+ const refusal = await checkProjectSendAllowlist(adminDb(), projectId, args, "mcp");
252
+ if (refusal)
253
+ return refusal;
240
254
  const config = await resolveProviderConfig(projectId, siteId);
241
255
  switch (config.name) {
242
256
  case "resend":
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import Anthropic from '@anthropic-ai/sdk';
3
- import { BLOCKING_CONTENT_ISSUE_KINDS, computeSendContentKey, findContentIssues, sendingHostOf, NAME_FALLBACKS, ONE_SEND_MATCH_STATUSES, ONE_SEND_SENT_STATUSES, ONE_SEND_WINDOW_DAYS, normalizeTriggerConfig, renderTemplateString as renderTemplateVars, resolveBrandTokens, readAllRows, } from '@sendinel/domain';
3
+ import { assertSendAccepted, SendRefusedError, BLOCKING_CONTENT_ISSUE_KINDS, computeSendContentKey, findContentIssues, sendingHostOf, NAME_FALLBACKS, ONE_SEND_MATCH_STATUSES, ONE_SEND_SENT_STATUSES, ONE_SEND_WINDOW_DAYS, normalizeTriggerConfig, renderTemplateString as renderTemplateVars, resolveBrandTokens, readAllRows, } from '@sendinel/domain';
4
4
  // Subpath import, not the barrel above -- reply-actions.ts uses node:crypto,
5
5
  // and MCP-server-side that's fine (this never runs in an Edge Function), but
6
6
  // keeping it off the shared barrel avoids re-creating the Edge-Function
@@ -2450,8 +2450,12 @@ Do not include body_html in your response; Sendinel renders body_html from block
2450
2450
  let sendResult;
2451
2451
  try {
2452
2452
  sendResult = await sendEmailForSite(projectId, site_id, payload);
2453
+ assertSendAccepted(sendResult);
2453
2454
  }
2454
2455
  catch (error) {
2456
+ if (error instanceof SendRefusedError) {
2457
+ await db().from('email_log').insert({ project_id: projectId, site_id, to_email: to, from_email: site.from_email, subject: resolvedSubject, category: 'transactional', status: 'failed', error_message: error.message });
2458
+ }
2455
2459
  return txt(`Error: Send failed — ${error instanceof Error ? error.message : 'unknown provider error'}`);
2456
2460
  }
2457
2461
  // Log to email_log
@@ -2692,6 +2696,7 @@ Do not include body_html in your response; Sendinel renders body_html from block
2692
2696
  let sendResult;
2693
2697
  try {
2694
2698
  sendResult = await sendEmailForSite(projectId, site_id, payload);
2699
+ assertSendAccepted(sendResult);
2695
2700
  }
2696
2701
  catch (error) {
2697
2702
  await db().from('email_log').update({ status: 'failed' }).eq('id', logRow.id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendinel/mcp-server",
3
- "version": "1.0.77",
3
+ "version": "1.0.79",
4
4
  "description": "AI-controlled email operations control plane for campaigns, contacts, segments, analytics, and multi-provider sending via MCP.",
5
5
  "mcpName": "ai.sendinel/mcp-server",
6
6
  "type": "module",
@@ -44,7 +44,7 @@
44
44
  "@types/qrcode": "^1.5.6",
45
45
  "tsx": "^4.23.15",
46
46
  "typescript": "^7.0.2",
47
- "vitest": "^4.1.10"
47
+ "vitest": "^5.0.5"
48
48
  },
49
49
  "publishConfig": {
50
50
  "access": "public"