@sendinel/mcp-server 1.0.131 → 1.0.133

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.
@@ -87,6 +87,7 @@ export declare function checkApproval(approvalId: string, options?: {
87
87
  status: string;
88
88
  decided_by?: string;
89
89
  decision_channel?: 'sandbox';
90
+ consumed_at?: string;
90
91
  }>;
91
92
  type ToolResult = {
92
93
  content: Array<{
@@ -438,9 +438,10 @@ export async function checkApproval(approvalId, options = {}) {
438
438
  }
439
439
  if (expiry <= Date.now())
440
440
  return { approved: false, status: 'expired' };
441
+ const consumedAt = new Date().toISOString();
441
442
  const claimQuery = approvalStore()
442
443
  .from('action_approvals')
443
- .update({ status: 'used', used_at: new Date().toISOString() })
444
+ .update({ status: 'used', used_at: consumedAt })
444
445
  .eq('id', approvalId)
445
446
  .eq('project_id', getProjectId())
446
447
  .eq('status', 'approved')
@@ -461,6 +462,7 @@ export async function checkApproval(approvalId, options = {}) {
461
462
  approved: true,
462
463
  status: 'approved',
463
464
  decided_by: claimed.decided_by,
465
+ ...(options.action === 'send_test_email' ? { consumed_at: consumedAt } : {}),
464
466
  ...(sandboxDecision ? { decision_channel: 'sandbox' } : {}),
465
467
  };
466
468
  }
@@ -484,6 +486,7 @@ export function withApproval(action, handler, options = {}) {
484
486
  return toolText(decision.reason ?? 'This channel cannot request this action.');
485
487
  let decidedBy;
486
488
  let decisionChannel;
489
+ let consumedAt;
487
490
  if (options.registeredRequestOnly || decision.tier !== 'autonomous') {
488
491
  if (!approval_id) {
489
492
  const display = options.display ? await options.display(params) : undefined;
@@ -491,6 +494,7 @@ export function withApproval(action, handler, options = {}) {
491
494
  }
492
495
  const check = await checkApproval(approval_id, { consume: true, action, params });
493
496
  decidedBy = check.decided_by;
497
+ consumedAt = check.consumed_at;
494
498
  decisionChannel = check.decision_channel;
495
499
  if (!check.approved) {
496
500
  return toolText(`Approval ${approval_id} is ${check.status}. ${check.status === 'pending' ? 'Waiting for human approval via dashboard.' : 'Request a new approval.'}`);
@@ -499,6 +503,34 @@ export function withApproval(action, handler, options = {}) {
499
503
  if (options.registeredRequestOnly) {
500
504
  return registeredApproval.run({ id: approval_id, action, projectId: getProjectId(), phase: 'approved', decidedBy, decisionChannel }, () => handler(args, extra));
501
505
  }
502
- return handler(params, extra);
506
+ const result = await handler(params, extra);
507
+ // Keep the atomic claim across the outbound hop. Only a definitive pre-acceptance
508
+ // failure may release it; transport errors and unknown outcomes can already have sent.
509
+ if (action === 'send_test_email' && approval_id && consumedAt && testSendNotAccepted(result)) {
510
+ const { data: restored, error } = await approvalStore().from('action_approvals')
511
+ .update({ status: 'approved', used_at: null })
512
+ .eq('id', approval_id).eq('project_id', getProjectId()).eq('action', 'send_test_email')
513
+ .eq('status', 'used').eq('used_at', consumedAt)
514
+ .select('id').maybeSingle();
515
+ if (error || !restored) {
516
+ console.error('Test-send approval could not be restored after a pre-acceptance failure.');
517
+ }
518
+ }
519
+ return result;
503
520
  };
504
521
  }
522
+ function testSendNotAccepted(result) {
523
+ const [block] = result.content;
524
+ if (result.content.length !== 1 || block.type !== 'text')
525
+ return false;
526
+ try {
527
+ const outcome = JSON.parse(block.text);
528
+ const code = outcome?.error?.error?.code;
529
+ return (outcome.status === 500 && ['provider_unconfigured', 'provider_refused'].includes(code))
530
+ || (outcome.status === 422 && ['sandbox_refused', 'send_allowlist_refused'].includes(code))
531
+ || (outcome.status === 503 && code === 'send_allowlist_unavailable');
532
+ }
533
+ catch {
534
+ return false;
535
+ }
536
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendinel/mcp-server",
3
- "version": "1.0.131",
3
+ "version": "1.0.133",
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",