@sendinel/mcp-server 1.0.190 → 1.0.192
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 +3 -1
- package/build/lib/action-approvals.d.ts +2 -0
- package/build/lib/action-approvals.js +9 -2
- package/build/lib/approval-tool-decisions.d.ts +2 -0
- package/build/lib/approval-tool-decisions.js +2 -2
- package/build/lib/db-error.d.ts +26 -0
- package/build/lib/db-error.js +33 -0
- package/build/lib/delete-blockers.d.ts +7 -0
- package/build/lib/delete-blockers.js +31 -0
- package/build/tools/approvals.js +2 -2
- package/build/tools/campaigns.js +9 -35
- package/build/tools/contacts.js +7 -1
- package/build/tools/content.js +5 -2
- package/build/tools/data.js +4 -1
- package/build/tools/gdpr.js +5 -1
- package/build/tools/landing-pages.js +5 -2
- package/build/tools/lists.js +5 -2
- package/build/tools/segments.js +6 -2
- package/build/tools/signatures.js +5 -2
- package/build/tools/webhooks.js +5 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -59,7 +59,9 @@ Sendinel currently exposes 35 tool groups and 349 tools (gateway mode exposes 4
|
|
|
59
59
|
| `webhooks` | 5 | Webhook subscription management |
|
|
60
60
|
|
|
61
61
|
`cancel_approval({approval_id})` rejects an unexpired, unused pending/approved
|
|
62
|
-
request in the current project for any write-scoped credential.
|
|
62
|
+
request in the current project for any write-scoped credential. It returns
|
|
63
|
+
`status: "rejected"` (kept for compatibility) plus `outcome: "cancelled"` and
|
|
64
|
+
`cancelled: true`.
|
|
63
65
|
`approve_approval({approval_id})` requires the person's own live OAuth connection
|
|
64
66
|
(`connection_role: NULL`) and its own pending request in that project. Tester,
|
|
65
67
|
agent, API key, service-token and scoped-JWT credentials cannot approve. Sends
|
|
@@ -100,6 +100,8 @@ export declare function checkApproval(approvalId: string, options?: {
|
|
|
100
100
|
}): Promise<Omit<Awaited<ReturnType<typeof checkApprovalState>>, 'decision_channel'> & {
|
|
101
101
|
decision_channel?: string;
|
|
102
102
|
decision_evidence?: DecisionEvidence;
|
|
103
|
+
outcome?: 'cancelled';
|
|
104
|
+
cancelled?: true;
|
|
103
105
|
}>;
|
|
104
106
|
declare function checkApprovalState(approvalId: string, options?: {
|
|
105
107
|
consume?: boolean;
|
|
@@ -469,7 +469,7 @@ export async function checkApproval(approvalId, options = {}) {
|
|
|
469
469
|
.eq('action', 'approval.cancelled').limit(1);
|
|
470
470
|
const cancel = data?.[0];
|
|
471
471
|
if (!error && cancel?.actor_id && typeof cancel.details?.channel === 'string') {
|
|
472
|
-
return { ...result, decided_by: cancel.actor_id, decision_channel: cancel.details.channel };
|
|
472
|
+
return { ...result, decided_by: cancel.actor_id, decision_channel: cancel.details.channel, outcome: 'cancelled', cancelled: true };
|
|
473
473
|
}
|
|
474
474
|
}
|
|
475
475
|
catch { /* A failed audit read cannot grant authority or change the status. */ }
|
|
@@ -604,10 +604,17 @@ async function checkApprovalState(approvalId, options = {}) {
|
|
|
604
604
|
approved: true,
|
|
605
605
|
status: 'approved',
|
|
606
606
|
decided_by: claimed.decided_by,
|
|
607
|
-
...(options.action &&
|
|
607
|
+
...(options.action && RELEASABLE_APPROVAL_ACTIONS.has(options.action) ? { consumed_at: consumedAt } : {}),
|
|
608
608
|
...(sandboxDecision ? { decision_channel: 'sandbox' } : {}),
|
|
609
609
|
};
|
|
610
610
|
}
|
|
611
|
+
/** Actions whose approval claim can be released (markNotExecuted) after a refusal that changed nothing. */
|
|
612
|
+
const RELEASABLE_APPROVAL_ACTIONS = new Set([
|
|
613
|
+
'send_test_email', 'identify_subscriber', 'emit_contact_event', 'delete_campaign',
|
|
614
|
+
// Deletes refused by a foreign key (#4503): nothing was removed, so the same approval can be retried.
|
|
615
|
+
'delete_campaign_step', 'delete_content_block', 'delete_landing_page', 'delete_signature', 'delete_list',
|
|
616
|
+
'delete_segment', 'delete_subscriber_view', 'delete_custom_field', 'delete_webhook_subscription',
|
|
617
|
+
]);
|
|
611
618
|
const approvalPreflights = new Map();
|
|
612
619
|
/** Validate live targets before any approval is requested or consumed, including registry gates. */
|
|
613
620
|
export function registerApprovalPreflight(action, fn) {
|
|
@@ -4,6 +4,8 @@ export declare function cancelApproval(approvalId: string, transportClientName?:
|
|
|
4
4
|
} | {
|
|
5
5
|
approval_id: string;
|
|
6
6
|
status: 'rejected';
|
|
7
|
+
outcome: 'cancelled';
|
|
8
|
+
cancelled: true;
|
|
7
9
|
}>;
|
|
8
10
|
export declare function approveApproval(approvalId: string, transportClientName?: string): Promise<{
|
|
9
11
|
code: string;
|
|
@@ -53,12 +53,12 @@ export async function cancelApproval(approvalId, transportClientName) {
|
|
|
53
53
|
project_id: projectId, actor_type: actor.actorType ?? actor.authKind ?? 'api_key',
|
|
54
54
|
actor_id: actor.actorId ?? actor.keyId ?? actor.oauthTokenId ?? null,
|
|
55
55
|
action: 'approval.cancelled', resource_type: 'approval', resource_id: approvalId,
|
|
56
|
-
details: { status: 'rejected', channel, client_name: name, auth_kind: actor.authKind ?? null,
|
|
56
|
+
details: { status: 'rejected', outcome: 'cancelled', channel, client_name: name, auth_kind: actor.authKind ?? null,
|
|
57
57
|
oauth_token_id: actor.oauthTokenId ?? null, api_key_id: actor.keyId ?? null },
|
|
58
58
|
});
|
|
59
59
|
if (auditError)
|
|
60
60
|
return refuse('approval_audit_failed', 'Approval cancelled, but audit failed. Check its status before retrying.');
|
|
61
|
-
return { approval_id: approvalId, status: 'rejected' };
|
|
61
|
+
return { approval_id: approvalId, status: 'rejected', outcome: 'cancelled', cancelled: true };
|
|
62
62
|
}
|
|
63
63
|
catch {
|
|
64
64
|
return refuse('approval_failed', 'Cancellation failed. Check approval status before retrying.');
|
package/build/lib/db-error.d.ts
CHANGED
|
@@ -28,6 +28,32 @@ export declare function toolError(body: unknown): {
|
|
|
28
28
|
text: string;
|
|
29
29
|
}[];
|
|
30
30
|
};
|
|
31
|
+
/** A table known to block a delete. `count` is best-effort; it may throw or return null. */
|
|
32
|
+
export interface DeleteBlocker {
|
|
33
|
+
table: string;
|
|
34
|
+
singular: string;
|
|
35
|
+
plural: string;
|
|
36
|
+
count: () => Promise<number | null | undefined>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A DELETE refused by a foreign key (23503) is a conflict ("something still uses this"), not a
|
|
40
|
+
* missing record. Returns null for any other error so callers fall through to their normal path.
|
|
41
|
+
* The global 23503 -> not_found mapping stays correct for INSERTs and is deliberately untouched.
|
|
42
|
+
* Never copies the database message or constraint names; counts are best-effort and a failed
|
|
43
|
+
* count never changes the conflict outcome.
|
|
44
|
+
*/
|
|
45
|
+
export declare function deleteBlockedResult(error: unknown, subject: string, blockers?: DeleteBlocker[]): Promise<{
|
|
46
|
+
error: string;
|
|
47
|
+
code: 'conflict';
|
|
48
|
+
} | null>;
|
|
49
|
+
/** MCP-result form of deleteBlockedResult: an isError toolError, or null when not a 23503. */
|
|
50
|
+
export declare function deleteBlockedToolResult(error: unknown, subject: string, blockers?: DeleteBlocker[]): Promise<{
|
|
51
|
+
isError: true;
|
|
52
|
+
content: {
|
|
53
|
+
type: 'text';
|
|
54
|
+
text: string;
|
|
55
|
+
}[];
|
|
56
|
+
} | null>;
|
|
31
57
|
/** Formats Zod validation issues into plain-English sentences without raw JSON. */
|
|
32
58
|
export declare function formatZodError(error: z.ZodError | {
|
|
33
59
|
issues: Array<{
|
package/build/lib/db-error.js
CHANGED
|
@@ -47,6 +47,39 @@ export function dbToolError(error, message = 'Unable to complete database operat
|
|
|
47
47
|
export function toolError(body) {
|
|
48
48
|
return { isError: true, content: [{ type: 'text', text: JSON.stringify(body, null, 2) }] };
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* A DELETE refused by a foreign key (23503) is a conflict ("something still uses this"), not a
|
|
52
|
+
* missing record. Returns null for any other error so callers fall through to their normal path.
|
|
53
|
+
* The global 23503 -> not_found mapping stays correct for INSERTs and is deliberately untouched.
|
|
54
|
+
* Never copies the database message or constraint names; counts are best-effort and a failed
|
|
55
|
+
* count never changes the conflict outcome.
|
|
56
|
+
*/
|
|
57
|
+
export async function deleteBlockedResult(error, subject, blockers = []) {
|
|
58
|
+
const code = error && typeof error === 'object' && 'code' in error ? String(error.code) : '';
|
|
59
|
+
if (code !== '23503')
|
|
60
|
+
return null;
|
|
61
|
+
console.warn('[mcp] Delete blocked by linked records:', error);
|
|
62
|
+
const results = await Promise.allSettled(blockers.map(async (b) => b.count()));
|
|
63
|
+
const linked = [];
|
|
64
|
+
const unknown = [];
|
|
65
|
+
results.forEach((result, index) => {
|
|
66
|
+
const blocker = blockers[index];
|
|
67
|
+
const count = result.status === 'fulfilled' ? result.value : null;
|
|
68
|
+
if (typeof count !== 'number' || !Number.isInteger(count) || count < 0)
|
|
69
|
+
unknown.push(blocker.table);
|
|
70
|
+
else if (count > 0)
|
|
71
|
+
linked.push(`${count} ${count === 1 ? blocker.singular : blocker.plural} (${blocker.table})`);
|
|
72
|
+
});
|
|
73
|
+
const reason = linked.length
|
|
74
|
+
? `This ${subject} still has ${linked.join(', ')} linked to it, so it cannot be deleted.`
|
|
75
|
+
: `This ${subject} cannot be deleted because linked records still reference it.`;
|
|
76
|
+
return { error: unknown.length ? `${reason} Counts for ${unknown.join(', ')} are unavailable.` : reason, code: 'conflict' };
|
|
77
|
+
}
|
|
78
|
+
/** MCP-result form of deleteBlockedResult: an isError toolError, or null when not a 23503. */
|
|
79
|
+
export async function deleteBlockedToolResult(error, subject, blockers = []) {
|
|
80
|
+
const body = await deleteBlockedResult(error, subject, blockers);
|
|
81
|
+
return body ? toolError(body) : null;
|
|
82
|
+
}
|
|
50
83
|
/** Formats Zod validation issues into plain-English sentences without raw JSON. */
|
|
51
84
|
export function formatZodError(error) {
|
|
52
85
|
const messages = [];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DeleteBlocker } from './db-error.js';
|
|
2
|
+
/** Best-effort count of rows in email.<table> where <column> = id. Returns null when unknown. */
|
|
3
|
+
export declare function countBlocker(table: string, column: string, id: string, singular: string, plural: string): DeleteBlocker;
|
|
4
|
+
export declare const campaignStepBlockers: (stepId: string) => DeleteBlocker[];
|
|
5
|
+
export declare const segmentBlockers: (segmentId: string) => DeleteBlocker[];
|
|
6
|
+
export declare const subscriberBlockers: (contactId: string) => DeleteBlocker[];
|
|
7
|
+
export declare const campaignBlockers: (campaignId: string) => DeleteBlocker[];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { db } from '../db.js';
|
|
2
|
+
/** Best-effort count of rows in email.<table> where <column> = id. Returns null when unknown. */
|
|
3
|
+
export function countBlocker(table, column, id, singular, plural) {
|
|
4
|
+
return {
|
|
5
|
+
table,
|
|
6
|
+
singular,
|
|
7
|
+
plural,
|
|
8
|
+
count: async () => {
|
|
9
|
+
const { count, error } = await db().schema('email').from(table)
|
|
10
|
+
.select('id', { count: 'exact', head: true }).eq(column, id);
|
|
11
|
+
return error ? null : count;
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
// NO ACTION foreign keys found by the 2026-11-09 prod scan. History tables stay blockers.
|
|
16
|
+
export const campaignStepBlockers = (stepId) => [
|
|
17
|
+
countBlocker('email_log', 'step_id', stepId, 'email history record', 'email history records'),
|
|
18
|
+
];
|
|
19
|
+
export const segmentBlockers = (segmentId) => [
|
|
20
|
+
countBlocker('campaigns', 'segment_id', segmentId, 'campaign', 'campaigns'),
|
|
21
|
+
];
|
|
22
|
+
export const subscriberBlockers = (contactId) => [
|
|
23
|
+
countBlocker('email_log', 'contact_id', contactId, 'email history record', 'email history records'),
|
|
24
|
+
countBlocker('revenue_attribution', 'contact_id', contactId, 'revenue attribution record', 'revenue attribution records'),
|
|
25
|
+
];
|
|
26
|
+
export const campaignBlockers = (campaignId) => [
|
|
27
|
+
countBlocker('ai_drafts', 'campaign_id', campaignId, 'AI draft', 'AI drafts'),
|
|
28
|
+
countBlocker('email_log', 'campaign_id', campaignId, 'email history record', 'email history records'),
|
|
29
|
+
countBlocker('revenue_attribution', 'campaign_id', campaignId, 'revenue attribution record', 'revenue attribution records'),
|
|
30
|
+
countBlocker('campaign_goals', 'target_campaign_id', campaignId, 'campaign goal target', 'campaign goal targets'),
|
|
31
|
+
];
|
package/build/tools/approvals.js
CHANGED
|
@@ -23,7 +23,7 @@ export function registerApprovalTools(server, options) {
|
|
|
23
23
|
});
|
|
24
24
|
server.registerTool('cancel_approval', {
|
|
25
25
|
title: 'Cancel Approval',
|
|
26
|
-
description: 'Cancel an unexpired, unused pending or approved action approval in this project. Read-scoped OAuth connections can cancel only their own token’s requests; write/admin credentials can cancel any eligible request in this project.
|
|
26
|
+
description: 'Cancel an unexpired, unused pending or approved action approval in this project. Read-scoped OAuth connections can cancel only their own token’s requests; write/admin credentials can cancel any eligible request in this project. Marks it cancelled (stored and reported as status `rejected` for backward compatibility; `outcome: "cancelled"` and `cancelled: true` distinguish a withdrawal from a person’s rejection) and records who cancelled and the request channel in the audit; check_approval returns that cancellation attribution. Cannot approve or undo cancellation.',
|
|
27
27
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
28
28
|
inputSchema: { approval_id: z.string().uuid() },
|
|
29
29
|
}, async ({ approval_id }) => {
|
|
@@ -59,7 +59,7 @@ export function registerApprovalTools(server, options) {
|
|
|
59
59
|
idempotentHint: true,
|
|
60
60
|
openWorldHint: false,
|
|
61
61
|
},
|
|
62
|
-
description: 'Check the status of a pending action approval. Use this to poll after requesting an action that requires human approval. Returns approved/pending/rejected/expired
|
|
62
|
+
description: 'Check the status of a pending action approval. Use this to poll after requesting an action that requires human approval. Returns approved/pending/rejected/expired; a cancelled approval keeps status `rejected` and adds `outcome: "cancelled"` and `cancelled: true`.',
|
|
63
63
|
inputSchema: {
|
|
64
64
|
approval_id: z.string().uuid().describe('The approval ID returned by the original tool call'),
|
|
65
65
|
},
|
package/build/tools/campaigns.js
CHANGED
|
@@ -17,7 +17,8 @@ import { finishCampaignEdit, resumeCampaignEdit, TRIGGER_EDIT_SELECT } from '../
|
|
|
17
17
|
import { validTimezone } from '@sendinel/domain';
|
|
18
18
|
import { adminDb, db, requestContext } from '../db.js';
|
|
19
19
|
import { getProjectId } from '../project.js';
|
|
20
|
-
import { dbErrorResult, dbToolError, toolError } from '../lib/db-error.js';
|
|
20
|
+
import { dbErrorResult, dbToolError, toolError, deleteBlockedToolResult } from '../lib/db-error.js';
|
|
21
|
+
import { campaignBlockers, campaignStepBlockers } from '../lib/delete-blockers.js';
|
|
21
22
|
import { checkPlanLimit } from '../plan-limits.js';
|
|
22
23
|
import { buildVoicePrompt, buildEmailSystemPrompt, deriveTemplateVariables, validateTemplate } from '../lib/voice.js';
|
|
23
24
|
import { fireWebhookEvent } from '../lib/webhook-events.js';
|
|
@@ -38,36 +39,6 @@ import { callSendinelV1 } from '../lib/v1-client.js';
|
|
|
38
39
|
import { countUniqueEngagementByCampaign } from '../lib/campaign-engagement.js';
|
|
39
40
|
const txt = (text) => ({ content: [{ type: 'text', text }] });
|
|
40
41
|
const j = (data) => txt(JSON.stringify(data, null, 2));
|
|
41
|
-
/** Diagnose a rejected DELETE, without exposing Postgres diagnostics or guessing missing counts. */
|
|
42
|
-
async function campaignDeleteConflict(campaignId) {
|
|
43
|
-
const sources = [
|
|
44
|
-
{ table: 'ai_drafts', column: 'campaign_id', singular: 'AI draft', plural: 'AI drafts' },
|
|
45
|
-
{ table: 'email_log', column: 'campaign_id', singular: 'email history record', plural: 'email history records' },
|
|
46
|
-
{ table: 'revenue_attribution', column: 'campaign_id', singular: 'revenue attribution record', plural: 'revenue attribution records' },
|
|
47
|
-
{ table: 'campaign_goals', column: 'target_campaign_id', singular: 'campaign goal target', plural: 'campaign goal targets' },
|
|
48
|
-
];
|
|
49
|
-
// The caller has verified ownership of this campaign. Count all references to its UUID;
|
|
50
|
-
// campaign_goals has no project_id, and filtering by the owning campaign misses targets.
|
|
51
|
-
const results = await Promise.allSettled(sources.map(async (source) => {
|
|
52
|
-
const { count, error } = await db().schema('email').from(source.table)
|
|
53
|
-
.select('id', { count: 'exact', head: true }).eq(source.column, campaignId);
|
|
54
|
-
return !error && typeof count === 'number' && Number.isInteger(count) && count >= 0 ? count : null;
|
|
55
|
-
}));
|
|
56
|
-
const linked = [];
|
|
57
|
-
const unknown = [];
|
|
58
|
-
results.forEach((result, index) => {
|
|
59
|
-
const source = sources[index];
|
|
60
|
-
const count = result.status === 'fulfilled' ? result.value : null;
|
|
61
|
-
if (count === null)
|
|
62
|
-
unknown.push(source.table);
|
|
63
|
-
else if (count > 0)
|
|
64
|
-
linked.push(`${count} ${count === 1 ? source.singular : source.plural} (${source.table})`);
|
|
65
|
-
});
|
|
66
|
-
const reason = linked.length
|
|
67
|
-
? `This campaign still has ${linked.join(', ')} linked to it, so it cannot be deleted.`
|
|
68
|
-
: 'This campaign cannot be deleted because linked records still reference it.';
|
|
69
|
-
return unknown.length ? `${reason} Counts for ${unknown.join(', ')} are unavailable.` : reason;
|
|
70
|
-
}
|
|
71
42
|
const ACTIVATION_APPROVAL_DESCRIPTION = 'Activation-only: requires_approval gates campaign activation. It does not stop sends of an already-active campaign or sends triggered by other paths.';
|
|
72
43
|
const REENTRY_DESCRIPTION = 'Re-entry defaults to once ever. per_event_period uses a flat event-data period_key on event-triggered campaigns only. Changing a live campaign returns an approval request. Existing enrollments without a period cannot re-enter; sends wait for campaign-cycle approval.';
|
|
73
44
|
const reentryFields = {
|
|
@@ -3611,6 +3582,9 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3611
3582
|
.from('campaign_steps')
|
|
3612
3583
|
.delete()
|
|
3613
3584
|
.eq('id', step_id);
|
|
3585
|
+
const blocked = await deleteBlockedToolResult(error, 'step', campaignStepBlockers(step_id));
|
|
3586
|
+
if (blocked)
|
|
3587
|
+
return markNotExecuted(blocked);
|
|
3614
3588
|
if (error)
|
|
3615
3589
|
return j(dbToolError(error));
|
|
3616
3590
|
return j({ ok: true, deleted: step_id });
|
|
@@ -3646,10 +3620,10 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3646
3620
|
.delete()
|
|
3647
3621
|
.eq('id', campaign_id)
|
|
3648
3622
|
.eq('project_id', projectId);
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3623
|
+
// A rejected statement made no changes, so the same approval is safe to retry.
|
|
3624
|
+
const blocked = await deleteBlockedToolResult(error, 'campaign', campaignBlockers(campaign_id));
|
|
3625
|
+
if (blocked)
|
|
3626
|
+
return markNotExecuted(blocked);
|
|
3653
3627
|
if (error)
|
|
3654
3628
|
return j(dbToolError(error));
|
|
3655
3629
|
return j({ success: true, deleted: campaign.name });
|
package/build/tools/contacts.js
CHANGED
|
@@ -8,7 +8,7 @@ import { z } from 'zod';
|
|
|
8
8
|
import { callSendinelV1 } from '../lib/v1-client.js';
|
|
9
9
|
import { db, projectClientForWriteback } from '../db.js';
|
|
10
10
|
import { getProjectId } from '../project.js';
|
|
11
|
-
import { dbErrorResult, dbToolError, toolError } from '../lib/db-error.js';
|
|
11
|
+
import { dbErrorResult, dbToolError, toolError, deleteBlockedToolResult } from '../lib/db-error.js';
|
|
12
12
|
import { resolveSegmentContactIds } from '../lib/send-audience.js';
|
|
13
13
|
import { checkPlanLimit, contactLimitMessage } from '../plan-limits.js';
|
|
14
14
|
import { logConsent, logAudit } from '../audit.js';
|
|
@@ -1230,6 +1230,9 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
1230
1230
|
.eq('project_id', getProjectId())
|
|
1231
1231
|
.select('id')
|
|
1232
1232
|
.maybeSingle();
|
|
1233
|
+
const blocked = await deleteBlockedToolResult(error, 'subscriber view');
|
|
1234
|
+
if (blocked)
|
|
1235
|
+
return markNotExecuted(blocked);
|
|
1233
1236
|
if (error)
|
|
1234
1237
|
return txt(`Error: ${dbToolError(error).error}`);
|
|
1235
1238
|
if (!data)
|
|
@@ -2940,6 +2943,9 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
2940
2943
|
.eq('project_id', projectId)
|
|
2941
2944
|
.select('id, key')
|
|
2942
2945
|
.maybeSingle();
|
|
2946
|
+
const blocked = await deleteBlockedToolResult(error, 'custom field');
|
|
2947
|
+
if (blocked)
|
|
2948
|
+
return markNotExecuted(blocked);
|
|
2943
2949
|
if (error)
|
|
2944
2950
|
return txt(`Error: ${dbToolError(error).error}`);
|
|
2945
2951
|
if (!data)
|
package/build/tools/content.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { withCreatedItems } from '../lib/mcp-created-items.js';
|
|
2
|
-
import { withApproval } from '../lib/action-approvals.js';
|
|
2
|
+
import { markNotExecuted, withApproval } from '../lib/action-approvals.js';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { adminDb, db } from '../db.js';
|
|
5
5
|
import { getProjectId } from '../project.js';
|
|
6
6
|
import { refuseActiveTextFields, stripActiveEmailContent, validateUnsubscribeFooterTemplate } from '@sendinel/domain';
|
|
7
|
-
import { dbToolError, toolError } from '../lib/db-error.js';
|
|
7
|
+
import { dbToolError, toolError, deleteBlockedToolResult } from '../lib/db-error.js';
|
|
8
8
|
import { logAudit } from '../audit.js';
|
|
9
9
|
import { assertHttpUrl } from '../lib/url-validation.js';
|
|
10
10
|
const j = (data) => ({ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] });
|
|
@@ -263,6 +263,9 @@ export function registerContentTools(server) {
|
|
|
263
263
|
inputSchema: { approval_id: z.string().uuid().optional().describe('Get this by calling once without it, then have a human approve the returned approve_url. Re-call with only approval_id to run the action.'), block_id: z.string().uuid() },
|
|
264
264
|
}, withApproval('delete_content_block', async ({ block_id }) => {
|
|
265
265
|
const { error } = await db().from('content_blocks').delete().eq('project_id', getProjectId()).eq('id', block_id);
|
|
266
|
+
const blocked = await deleteBlockedToolResult(error, 'content block');
|
|
267
|
+
if (blocked)
|
|
268
|
+
return markNotExecuted(blocked);
|
|
266
269
|
if (error)
|
|
267
270
|
return j(dbToolError(error));
|
|
268
271
|
return j({ deleted: true });
|
package/build/tools/data.js
CHANGED
|
@@ -7,7 +7,7 @@ import { normalizeActiveCampaignAccountUrl } from '../lib/url-validation.js';
|
|
|
7
7
|
import { assertChannelCanExecute, checkApproval, currentChannelContext, requestApproval, withApproval } from '../lib/action-approvals.js';
|
|
8
8
|
import { getProjectId } from '../project.js';
|
|
9
9
|
import { logAudit } from '../audit.js';
|
|
10
|
-
import { dbToolError } from '../lib/db-error.js';
|
|
10
|
+
import { dbToolError, deleteBlockedToolResult } from '../lib/db-error.js';
|
|
11
11
|
const j = (data) => ({ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] });
|
|
12
12
|
function generateApiKey() {
|
|
13
13
|
const secret = `snk_${randomBytes(32).toString('hex')}`;
|
|
@@ -449,6 +449,9 @@ export function registerDataTools(server) {
|
|
|
449
449
|
.eq('project_id', context.projectId)
|
|
450
450
|
.select('id, platform')
|
|
451
451
|
.maybeSingle();
|
|
452
|
+
const blocked = await deleteBlockedToolResult(error, 'provider connection');
|
|
453
|
+
if (blocked)
|
|
454
|
+
return blocked;
|
|
452
455
|
if (error)
|
|
453
456
|
return j(dbToolError(error));
|
|
454
457
|
if (!data)
|
package/build/tools/gdpr.js
CHANGED
|
@@ -4,7 +4,8 @@ import { db } from '../db.js';
|
|
|
4
4
|
import { getProjectId } from '../project.js';
|
|
5
5
|
import { logAudit } from '../audit.js';
|
|
6
6
|
import { approvalSatisfied, assertChannelCanExecute, currentChannelContext, requestApproval, checkApproval } from '../lib/action-approvals.js';
|
|
7
|
-
import { dbToolError } from '../lib/db-error.js';
|
|
7
|
+
import { dbToolError, deleteBlockedToolResult } from '../lib/db-error.js';
|
|
8
|
+
import { subscriberBlockers } from '../lib/delete-blockers.js';
|
|
8
9
|
const txt = (text) => ({ content: [{ type: 'text', text }] });
|
|
9
10
|
const j = (data) => txt(JSON.stringify(data, null, 2));
|
|
10
11
|
export function registerGdprTools(server) {
|
|
@@ -52,6 +53,9 @@ export function registerGdprTools(server) {
|
|
|
52
53
|
p_reason: reason ?? 'gdpr_request',
|
|
53
54
|
p_deleted_by: 'mcp',
|
|
54
55
|
});
|
|
56
|
+
const blocked = await deleteBlockedToolResult(error, 'subscriber', subscriberBlockers(contact_id));
|
|
57
|
+
if (blocked)
|
|
58
|
+
return blocked;
|
|
55
59
|
if (error)
|
|
56
60
|
return txt(`Error: ${dbToolError(error).error}`);
|
|
57
61
|
logAudit({ action: 'contact.gdpr_deleted', resourceType: 'contact', resourceId: contact_id, details: { reason, result: data } });
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { withCreatedItems } from '../lib/mcp-created-items.js';
|
|
2
|
-
import { withApproval } from '../lib/action-approvals.js';
|
|
2
|
+
import { markNotExecuted, withApproval } from '../lib/action-approvals.js';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { db } from '../db.js';
|
|
5
5
|
import { getProjectId } from '../project.js';
|
|
6
6
|
import { logAudit } from '../audit.js';
|
|
7
7
|
import { stripActiveEmailContent } from '@sendinel/domain';
|
|
8
|
-
import { dbToolError, toolError } from '../lib/db-error.js';
|
|
8
|
+
import { dbToolError, toolError, deleteBlockedToolResult } from '../lib/db-error.js';
|
|
9
9
|
const j = (data) => ({ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] });
|
|
10
10
|
const SLUG_RE = /^[a-z0-9][a-z0-9\-]{0,62}$/;
|
|
11
11
|
const VALID_STATUSES = ['draft', 'published', 'archived'];
|
|
@@ -274,6 +274,9 @@ export function registerLandingPageTools(server) {
|
|
|
274
274
|
.delete()
|
|
275
275
|
.eq('id', page_id)
|
|
276
276
|
.eq('project_id', projectId);
|
|
277
|
+
const blocked = await deleteBlockedToolResult(error, 'landing page');
|
|
278
|
+
if (blocked)
|
|
279
|
+
return markNotExecuted(blocked);
|
|
277
280
|
if (error)
|
|
278
281
|
return j(dbToolError(error));
|
|
279
282
|
await logAudit({ action: 'landing_page.deleted', resourceType: 'landing_page', resourceId: page_id });
|
package/build/tools/lists.js
CHANGED
|
@@ -5,8 +5,8 @@ import { applySegmentRules, buildSegmentBaseQuery, evaluateSegmentRules, hydrate
|
|
|
5
5
|
import { db } from '../db.js';
|
|
6
6
|
import { getProjectId } from '../project.js';
|
|
7
7
|
import { logAudit } from '../audit.js';
|
|
8
|
-
import { assertChannelCanExecute, checkApproval, currentChannelContext, requestApproval, withApproval, } from '../lib/action-approvals.js';
|
|
9
|
-
import { dbToolError } from '../lib/db-error.js';
|
|
8
|
+
import { assertChannelCanExecute, checkApproval, currentChannelContext, requestApproval, markNotExecuted, withApproval, } from '../lib/action-approvals.js';
|
|
9
|
+
import { dbToolError, deleteBlockedToolResult } from '../lib/db-error.js';
|
|
10
10
|
const LIST_MEMBER_BATCH_SIZE = 500;
|
|
11
11
|
const txt = (text) => ({ content: [{ type: 'text', text }] });
|
|
12
12
|
const j = (data) => txt(JSON.stringify(data, null, 2));
|
|
@@ -196,6 +196,9 @@ export function registerListTools(server) {
|
|
|
196
196
|
.delete()
|
|
197
197
|
.eq('id', list_id)
|
|
198
198
|
.eq('project_id', projectId);
|
|
199
|
+
const blocked = await deleteBlockedToolResult(error, 'list');
|
|
200
|
+
if (blocked)
|
|
201
|
+
return markNotExecuted(blocked);
|
|
199
202
|
if (error)
|
|
200
203
|
return txt(`Error: ${dbToolError(error).error}`);
|
|
201
204
|
await logAudit({ action: 'list.deleted', resourceType: 'list', resourceId: list_id });
|
package/build/tools/segments.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { withCreatedItems } from '../lib/mcp-created-items.js';
|
|
2
|
-
import { withApproval } from '../lib/action-approvals.js';
|
|
2
|
+
import { markNotExecuted, withApproval } from '../lib/action-approvals.js';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import Anthropic from '@anthropic-ai/sdk';
|
|
5
5
|
import { db } from '../db.js';
|
|
6
6
|
import { getProjectId } from '../project.js';
|
|
7
|
-
import { dbErrorResult, dbToolError, toolError } from '../lib/db-error.js';
|
|
7
|
+
import { dbErrorResult, dbToolError, toolError, deleteBlockedToolResult } from '../lib/db-error.js';
|
|
8
|
+
import { segmentBlockers } from '../lib/delete-blockers.js';
|
|
8
9
|
import { getAnthropicModelId } from '../anthropic-model.js';
|
|
9
10
|
import { AI_MAX_TOKENS, outputConfig, anthropicCheckedText } from '@sendinel/domain';
|
|
10
11
|
import { SEGMENT_OPERATORS_DESCRIPTION, applySegmentRules, buildSegmentBaseQuery, evaluateSegmentRules, hydrateEmailSendsContacts, isSegmentRulesError, normalizeSegmentRulesToTree, prepareSegmentRulesForStorage, readAllRows, } from '@sendinel/domain';
|
|
@@ -219,6 +220,9 @@ export function registerSegmentTools(server) {
|
|
|
219
220
|
.delete()
|
|
220
221
|
.eq('id', segment_id)
|
|
221
222
|
.eq('project_id', getProjectId());
|
|
223
|
+
const blocked = await deleteBlockedToolResult(error, 'segment', segmentBlockers(segment_id));
|
|
224
|
+
if (blocked)
|
|
225
|
+
return markNotExecuted(blocked);
|
|
222
226
|
if (error)
|
|
223
227
|
return txt(`Error: ${dbToolError(error).error}`);
|
|
224
228
|
return j({ deleted: true, segment_id });
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { withCreatedItems } from '../lib/mcp-created-items.js';
|
|
2
|
-
import { withApproval } from '../lib/action-approvals.js';
|
|
2
|
+
import { markNotExecuted, withApproval } from '../lib/action-approvals.js';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { db } from '../db.js';
|
|
5
5
|
import { getProjectId } from '../project.js';
|
|
6
6
|
import { logAudit } from '../audit.js';
|
|
7
|
-
import { dbToolError } from '../lib/db-error.js';
|
|
7
|
+
import { dbToolError, deleteBlockedToolResult } from '../lib/db-error.js';
|
|
8
8
|
const txt = (text) => ({ content: [{ type: 'text', text }] });
|
|
9
9
|
const j = (data) => txt(JSON.stringify(data, null, 2));
|
|
10
10
|
const COLUMNS = 'id, site_id, name, html, is_default, created_at, updated_at';
|
|
@@ -158,6 +158,9 @@ export function registerSignatureTools(server) {
|
|
|
158
158
|
.delete()
|
|
159
159
|
.eq('id', id)
|
|
160
160
|
.eq('project_id', projectId);
|
|
161
|
+
const blocked = await deleteBlockedToolResult(error, 'signature');
|
|
162
|
+
if (blocked)
|
|
163
|
+
return markNotExecuted(blocked);
|
|
161
164
|
if (error)
|
|
162
165
|
return j(dbToolError(error));
|
|
163
166
|
await logAudit({
|
package/build/tools/webhooks.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { withCreatedItems } from '../lib/mcp-created-items.js';
|
|
2
|
-
import { withApproval } from '../lib/action-approvals.js';
|
|
2
|
+
import { markNotExecuted, withApproval } from '../lib/action-approvals.js';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { listSandboxWebhookEvents } from '@sendinel/domain';
|
|
5
5
|
import { db, adminDb } from '../db.js';
|
|
6
6
|
import { getProjectId } from '../project.js';
|
|
7
7
|
import { validateWebhookUrl, generateWebhookSecret } from '../lib/webhook-events.js';
|
|
8
|
-
import { dbToolError } from '../lib/db-error.js';
|
|
8
|
+
import { dbToolError, deleteBlockedToolResult } from '../lib/db-error.js';
|
|
9
9
|
const txt = (text) => ({ content: [{ type: 'text', text }] });
|
|
10
10
|
const j = (data) => txt(JSON.stringify(data, null, 2));
|
|
11
11
|
const VALID_EVENTS = [
|
|
@@ -169,6 +169,9 @@ export function registerWebhookTools(server) {
|
|
|
169
169
|
.delete()
|
|
170
170
|
.eq('id', subscription_id)
|
|
171
171
|
.eq('project_id', getProjectId());
|
|
172
|
+
const blocked = await deleteBlockedToolResult(error, 'webhook subscription');
|
|
173
|
+
if (blocked)
|
|
174
|
+
return markNotExecuted(blocked);
|
|
172
175
|
if (error)
|
|
173
176
|
return txt(`Error: ${dbToolError(error).error}`);
|
|
174
177
|
return txt(`Subscription ${subscription_id} deleted.`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendinel/mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.192",
|
|
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",
|