@sendinel/mcp-server 1.0.220 → 1.0.222

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.
@@ -0,0 +1,3 @@
1
+ export declare function extractStepLinks(bodyHtml: string): string[];
2
+ export declare function extractStepPreheader(bodyHtml: string): string | null;
3
+ export declare function summarizeStepForStatus(step: Record<string, unknown>): Record<string, unknown>;
@@ -0,0 +1,41 @@
1
+ const HREF_RE = /<a\b[^>]*?\shref\s*=\s*(?:"([^"]*)"|'([^']*)')/gi;
2
+ const PREHEADER_RE = /^\s*<span\b[^>]*\bstyle\s*=\s*(?:"[^"]*display:\s*none[^"]*"|'[^']*display:\s*none[^']*')[^>]*>([\s\S]*?)<\/span>/i;
3
+ function htmlUnescape(text) {
4
+ return text
5
+ .replace(/&amp;/g, '&')
6
+ .replace(/&lt;/g, '<')
7
+ .replace(/&gt;/g, '>')
8
+ .replace(/&quot;/g, '"')
9
+ .replace(/&#39;/g, "'");
10
+ }
11
+ export function extractStepLinks(bodyHtml) {
12
+ const links = [];
13
+ const seen = new Set();
14
+ HREF_RE.lastIndex = 0;
15
+ let match;
16
+ while ((match = HREF_RE.exec(bodyHtml)) !== null) {
17
+ const href = match[1] ?? match[2] ?? '';
18
+ if (!seen.has(href)) {
19
+ seen.add(href);
20
+ links.push(href);
21
+ }
22
+ }
23
+ return links;
24
+ }
25
+ export function extractStepPreheader(bodyHtml) {
26
+ const match = bodyHtml.match(PREHEADER_RE);
27
+ if (!match)
28
+ return null;
29
+ return htmlUnescape(match[1]);
30
+ }
31
+ export function summarizeStepForStatus(step) {
32
+ const { body_html, body_text, blocks, ...rest } = step;
33
+ const bodyHtml = typeof body_html === 'string' ? body_html : '';
34
+ return {
35
+ ...rest,
36
+ subject: step.subject,
37
+ preheader: bodyHtml ? extractStepPreheader(bodyHtml) : null,
38
+ links: extractStepLinks(bodyHtml),
39
+ body_length: bodyHtml.length,
40
+ };
41
+ }
@@ -36,6 +36,7 @@ import { checkCampaignMergeVariables, persistMergeVariableWarnings, summarizeMer
36
36
  import { recordBrandAssetUsage } from '../lib/brand-asset-usage.js';
37
37
  import { BLOCK_GENERATION_SCHEMA, blocksToHtml, blocksToText, normalizeComposerBlocks } from '../lib/composer-blocks.js';
38
38
  import { callSendinelV1 } from '../lib/v1-client.js';
39
+ import { summarizeStepForStatus } from '../lib/summarize-step-for-status.js';
39
40
  import { countUniqueEngagementByCampaign } from '../lib/campaign-engagement.js';
40
41
  const txt = (text) => ({ content: [{ type: 'text', text }] });
41
42
  const j = (data) => txt(JSON.stringify(data, null, 2));
@@ -4028,9 +4029,13 @@ Return ONLY valid JSON with this exact shape:
4028
4029
  idempotentHint: true,
4029
4030
  openWorldHint: false,
4030
4031
  },
4031
- description: 'Get full campaign details including all steps, enrollment counts and derived trigger_kind (event/tag/date_field/contact_event/manual), saved trigger_type, trigger_event_name, trigger_tag, trigger_config (including where filter), fires_on and trigger_diagnostics. Read what event a dunning campaign fires on; use set_campaign_trigger to change it. trigger_kind is the canonical answer to what fires the campaign; trigger_type is the stored schedule mode and stays "manual" for event and tag triggers. ' + ACTIVATION_APPROVAL_DESCRIPTION,
4032
- inputSchema: { campaign_id: z.string().uuid(), period: z.string().min(1).optional().describe("Return results for one enrollment period; totals remain all-time.") },
4033
- }, async ({ campaign_id, period }) => {
4032
+ description: 'Get full campaign details including all steps, enrollment counts and derived trigger_kind (event/tag/date_field/contact_event/manual), saved trigger_type, trigger_event_name, trigger_tag, trigger_config (including where filter), fires_on and trigger_diagnostics. Read what event a dunning campaign fires on; use set_campaign_trigger to change it. trigger_kind is the canonical answer to what fires the campaign; trigger_type is the stored schedule mode and stays "manual" for event and tag triggers. Optional include_bodies (default true): when false, each step returns subject, preheader, links and body_length instead of body_html, body_text, and blocks. ' + ACTIVATION_APPROVAL_DESCRIPTION,
4033
+ inputSchema: {
4034
+ campaign_id: z.string().uuid(),
4035
+ period: z.string().min(1).optional().describe("Return results for one enrollment period; totals remain all-time."),
4036
+ include_bodies: z.boolean().optional().default(true).describe('When false, each step returns subject, preheader, links and body_length instead of body_html/body_text/blocks. Default true.'),
4037
+ },
4038
+ }, async ({ campaign_id, period, include_bodies }) => {
4034
4039
  const projectId = getProjectId();
4035
4040
  const { data: campaign, error: campErr } = await db()
4036
4041
  .from('campaigns')
@@ -4100,6 +4105,7 @@ Return ONLY valid JSON with this exact shape:
4100
4105
  const queuedCount = queuedEmailCount ?? 0;
4101
4106
  const failedCount = failedSendCount ?? 0;
4102
4107
  const unresolvedCount = dlqUnresolvedCount ?? 0;
4108
+ const mappedSteps = (steps ?? []).map((step) => ({ ...step, ...stepMultiEventRulesView(step), ...stepAbsenceRuleView(step), total_sent: stepMetrics.sent.get(step.id) ?? 0 }));
4103
4109
  return j({
4104
4110
  ...campaignMetricsView(campaign),
4105
4111
  merge_variable_warnings: campaign.metadata?.merge_variable_warnings ?? [],
@@ -4109,7 +4115,7 @@ Return ONLY valid JSON with this exact shape:
4109
4115
  total_sent: periods?.totals?.sent ?? sentCount ?? 0,
4110
4116
  ...(periods?.totals ? { unique_opened: periods.totals.opened, unique_clicked: periods.totals.clicked } : {}),
4111
4117
  ...campaignPeriodFields(periods, period),
4112
- steps: (steps ?? []).map((step) => ({ ...step, ...stepMultiEventRulesView(step), ...stepAbsenceRuleView(step), total_sent: stepMetrics.sent.get(step.id) ?? 0 })),
4118
+ steps: include_bodies === false ? mappedSteps.map(summarizeStepForStatus) : mappedSteps,
4113
4119
  active_enrollments: enrollCount ?? 0,
4114
4120
  send_incomplete: queuedCount > 0 || failedCount > 0 || outstandingRows.length > 0 || unresolvedCount > 0,
4115
4121
  queued_email_count: queuedCount,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendinel/mcp-server",
3
- "version": "1.0.220",
3
+ "version": "1.0.222",
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",
@@ -27,24 +27,24 @@
27
27
  "@desler/models": "file:../models",
28
28
  "@sendinel/domain": "file:../domain",
29
29
  "@modelcontextprotocol/sdk": "^1.32.0",
30
- "@supabase/supabase-js": "^2.98.0",
30
+ "@supabase/supabase-js": "^2.119.0",
31
31
  "dotenv": "^16.4.7",
32
- "express": "^5.2.1",
33
- "fast-xml-parser": "^5.11.2",
34
- "file-type": "^22.1.1",
35
- "jose": "^6.2.12",
32
+ "express": "^5.3.0",
33
+ "fast-xml-parser": "^5.12.0",
34
+ "file-type": "^22.3.0",
35
+ "jose": "^6.2.16",
36
36
  "qrcode": "^1.5.4",
37
- "resend": "^6.32.0",
38
- "undici": "^8.12.2",
37
+ "resend": "^6.37.0",
38
+ "undici": "^8.12.4",
39
39
  "zod": "^4.4.3"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/express": "^5.0.3",
43
- "@types/node": "^26.6.4",
43
+ "@types/node": "^26.6.6",
44
44
  "@types/qrcode": "^1.5.6",
45
45
  "tsx": "^4.23.15",
46
46
  "typescript": "^7.0.2",
47
- "vitest": "^5.0.5"
47
+ "vitest": "^5.1.0"
48
48
  },
49
49
  "publishConfig": {
50
50
  "access": "public"