@sendinel/mcp-server 1.0.37 → 1.0.38

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.
@@ -703,14 +703,15 @@ export function registerDeliveryOpsTools(server) {
703
703
  idempotentHint: true,
704
704
  openWorldHint: false,
705
705
  },
706
- description: 'Export contacts, email log, campaign, or suppression data as JSON.',
706
+ description: 'Export contacts, email log, campaign, or suppression data as JSON, newest first. Returns up to `limit` rows per call plus `total` (all matching rows). When `truncated` is true, call again with `offset` set to `next_offset` until it is false to get every row.',
707
707
  inputSchema: {
708
708
  resource: z.enum(['contacts', 'email_log', 'campaigns', 'suppressions']),
709
709
  site_id: z.string().uuid().optional(),
710
710
  since: z.string().datetime().optional(),
711
711
  limit: z.number().int().min(1).max(5000).default(1000).optional(),
712
+ offset: z.number().int().min(0).default(0).optional().describe('Rows to skip. Use the next_offset from the previous call to continue.'),
712
713
  },
713
- }, async ({ resource, site_id, since, limit = 1000 }) => {
714
+ }, async ({ resource, site_id, since, limit = 1000, offset = 0 }) => {
714
715
  const projectId = getProjectId();
715
716
  const tableMap = {
716
717
  contacts: 'subscribers',
@@ -718,16 +719,40 @@ export function registerDeliveryOpsTools(server) {
718
719
  campaigns: 'campaigns',
719
720
  suppressions: 'suppressions',
720
721
  };
721
- let query = db().from(tableMap[resource]).select('*').eq('project_id', projectId).limit(limit);
722
- if (site_id && (resource === 'email_log' || resource === 'campaigns'))
723
- query = query.eq('site_id', site_id);
724
- if (since)
725
- query = query.gte('created_at', since);
726
- query = query.order('created_at', { ascending: false });
727
- const { data, error } = await query;
728
- if (error)
729
- return j({ error: error.message });
730
- return j({ resource, count: data?.length ?? 0, data: data ?? [], truncated: (data?.length ?? 0) === limit });
722
+ // #3715: one request returns at most 1,000 rows however large `limit` is, with no error.
723
+ // Read `limit` rows in pages, and report the real total so the caller can tell when it has
724
+ // everything. id breaks ties so pages do not overlap.
725
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
726
+ const filtered = (query) => {
727
+ let q = query.eq('project_id', projectId);
728
+ if (site_id && (resource === 'email_log' || resource === 'campaigns'))
729
+ q = q.eq('site_id', site_id);
730
+ if (since)
731
+ q = q.gte('created_at', since);
732
+ return q;
733
+ };
734
+ const PAGE = 1000;
735
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
736
+ const rows = [];
737
+ while (rows.length < limit) {
738
+ const size = Math.min(PAGE, limit - rows.length);
739
+ const from = offset + rows.length;
740
+ const { data, error } = await filtered(db().from(tableMap[resource]).select('*'))
741
+ .order('created_at', { ascending: false })
742
+ .order('id', { ascending: true })
743
+ .range(from, from + size - 1);
744
+ if (error)
745
+ return j({ error: error.message });
746
+ rows.push(...(data ?? []));
747
+ if ((data?.length ?? 0) < size)
748
+ break;
749
+ }
750
+ const { count, error: countError } = await filtered(db().from(tableMap[resource]).select('id', { count: 'exact', head: true }));
751
+ // An unknown total is reported as null, never as a number.
752
+ const total = countError || typeof count !== 'number' ? null : count;
753
+ const nextOffset = offset + rows.length;
754
+ const truncated = total === null ? rows.length === limit : nextOffset < total;
755
+ return j({ resource, count: rows.length, total, offset, next_offset: truncated ? nextOffset : null, truncated, data: rows });
731
756
  });
732
757
  server.registerTool('list_domains', {
733
758
  title: 'List Domains',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendinel/mcp-server",
3
- "version": "1.0.37",
3
+ "version": "1.0.38",
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",