@sendinel/mcp-server 1.0.46 → 1.0.48

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
@@ -12,7 +12,7 @@ AI-controlled email operations control plane for campaigns, contacts, segments,
12
12
 
13
13
  ## Tool groups
14
14
 
15
- Sendinel currently exposes 34 tool groups and 320 tools (gateway mode adds 3 meta-tools). These counts are checked against the tool registry in CI, so this table matches what ships.
15
+ Sendinel currently exposes 34 tool groups and 321 tools (gateway mode adds 3 meta-tools). These counts are checked against the tool registry in CI, so this table matches what ships.
16
16
 
17
17
  | Group | Count | Purpose |
18
18
  |-------|-------|---------|
@@ -38,7 +38,7 @@ Sendinel currently exposes 34 tool groups and 320 tools (gateway mode adds 3 met
38
38
  | `landing-pages` | 5 | Landing page CRUD |
39
39
  | `lists` | 7 | Static contact lists and membership management |
40
40
  | `newsletters` | 12 | Newsletter publications, issues, subscribers, scheduling, sending, stats |
41
- | `org` | 21 | Team members, plan usage/limits, audit log, model registry, white-label, support tickets, provider switching |
41
+ | `org` | 22 | Team members, plan usage/limits, audit log, model registry, white-label, support tickets, provider switching |
42
42
  | `segments` | 6 | Audience builder: create, edit, natural-language generation, preview |
43
43
  | `short-links` | 3 | Short links, click stats, QR codes |
44
44
  | `signatures` | 4 | Sending signature blocks |
@@ -625,15 +625,33 @@ export function registerContactTools(server) {
625
625
  }, async () => {
626
626
  const projectId = getProjectId();
627
627
  const cutoff = daysAgo(90);
628
- // Deliberately NOT paged in #3755: the update below REPLACES each contact's tags with ['cold']
629
- // instead of adding it. Paging would widen that data loss to every inactive contact. Fix the
630
- // overwrite first (#3834), then page this read.
631
- const { data: contacts } = await db().from('subscribers').select('id, email, tags, last_engaged_at').eq('project_id', projectId).lt('last_engaged_at', cutoff).limit(1000);
632
- const ids = (contacts ?? []).map((contact) => contact.id);
633
- if (ids.length) {
634
- await db().from('subscribers').update({ tags: ['cold'] }).eq('project_id', projectId).in('id', ids);
635
- }
636
- return j({ tagged: ids.length, suppressed: 0, skipped: 0 });
628
+ // Overwrite of existing tags is fixed (#3834). Writes are per-row, so paging this
629
+ // read past 1000 is now safe as a follow-up.
630
+ const { data: contacts, error: readError } = await db().from('subscribers').select('id, email, first_name, last_name, custom_properties, tags, last_engaged_at').eq('project_id', projectId).lt('last_engaged_at', cutoff).limit(1000);
631
+ if (readError)
632
+ return j({ error: readError.message });
633
+ let tagged = 0;
634
+ let skipped = 0;
635
+ let failed = 0;
636
+ let firstError;
637
+ for (const contact of contacts ?? []) {
638
+ const current = contact.tags ?? [];
639
+ if (current.includes('cold')) {
640
+ skipped++;
641
+ continue;
642
+ }
643
+ const merged = [...current, 'cold'];
644
+ const { error } = await db().from('subscribers').update({ tags: merged }).eq('id', contact.id).eq('project_id', projectId);
645
+ if (error) {
646
+ failed++;
647
+ if (!firstError)
648
+ firstError = error.message;
649
+ continue;
650
+ }
651
+ tagged++;
652
+ await writeBackMcpContact(projectId, contact, { tags: merged });
653
+ }
654
+ return j({ tagged, suppressed: 0, skipped, ...(failed ? { failed, error: firstError } : {}) });
637
655
  });
638
656
  server.registerTool('list_hygiene_audit', {
639
657
  title: 'List Hygiene Audit',
@@ -857,6 +857,31 @@ export function registerOrgTools(server) {
857
857
  });
858
858
  return result.ok ? j(result.data) : j({ error: result.error });
859
859
  });
860
+ server.registerTool('list_my_support_tickets', {
861
+ title: 'List My Support Tickets',
862
+ annotations: {
863
+ readOnlyHint: true,
864
+ destructiveHint: false,
865
+ idempotentHint: true,
866
+ openWorldHint: false,
867
+ },
868
+ description: 'Check the status of support tickets filed by the caller\'s organization, including plain-language status, assigned owner, next step, and related issue numbers. Optional ticket and status filters.',
869
+ inputSchema: {
870
+ ticket: z.string().optional().describe('Filter by ticket number or reference, e.g. "SND-1015" or "1015"'),
871
+ status: z.string().optional().describe('Filter by status, e.g. "open", "in progress", "resolved", "closed"'),
872
+ },
873
+ }, async ({ ticket, status }) => {
874
+ const query = {};
875
+ if (ticket)
876
+ query.ticket = ticket;
877
+ if (status)
878
+ query.status = status;
879
+ const result = await callSendinelV1('/api/v1/support/tickets', {
880
+ method: 'GET',
881
+ query,
882
+ });
883
+ return result.ok ? j(result.data) : j({ error: result.error });
884
+ });
860
885
  server.registerTool('start_provider_switch', {
861
886
  title: 'Start Provider Switch',
862
887
  annotations: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendinel/mcp-server",
3
- "version": "1.0.46",
3
+ "version": "1.0.48",
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",