@sendinel/mcp-server 1.0.186 → 1.0.188
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.
|
@@ -119,6 +119,8 @@ type ToolResult = {
|
|
|
119
119
|
}>;
|
|
120
120
|
isError?: boolean;
|
|
121
121
|
};
|
|
122
|
+
/** Validate live targets before any approval is requested or consumed, including registry gates. */
|
|
123
|
+
export declare function registerApprovalPreflight(action: string, fn: (params: Record<string, unknown>) => Promise<ToolResult | null | undefined>): void;
|
|
122
124
|
export declare function approvalSatisfied(action: string): boolean;
|
|
123
125
|
/** Catch preview/draft branches that historically returned before their hand-written gate.
|
|
124
126
|
* Existing gated execution paths retain their frozen-payload approval flow. */
|
|
@@ -608,6 +608,11 @@ async function checkApprovalState(approvalId, options = {}) {
|
|
|
608
608
|
...(sandboxDecision ? { decision_channel: 'sandbox' } : {}),
|
|
609
609
|
};
|
|
610
610
|
}
|
|
611
|
+
const approvalPreflights = new Map();
|
|
612
|
+
/** Validate live targets before any approval is requested or consumed, including registry gates. */
|
|
613
|
+
export function registerApprovalPreflight(action, fn) {
|
|
614
|
+
approvalPreflights.set(action, fn);
|
|
615
|
+
}
|
|
611
616
|
function toolText(value) {
|
|
612
617
|
return { content: [{ type: 'text', text: value }] };
|
|
613
618
|
}
|
|
@@ -663,6 +668,11 @@ export function withApproval(action, handler, options = {}) {
|
|
|
663
668
|
const decision = await assertChannelCanExecute(action, params, channelContext);
|
|
664
669
|
if (!decision.allowed)
|
|
665
670
|
return toolText(decision.reason ?? 'This channel cannot request this action.');
|
|
671
|
+
// Use exactly the payload passed to requestApproval/checkApproval. Expected failures
|
|
672
|
+
// return toolError; unexpected exceptions reach the normal internal_error boundary.
|
|
673
|
+
const preflightResult = await approvalPreflights.get(action)?.(params);
|
|
674
|
+
if (preflightResult)
|
|
675
|
+
return preflightResult;
|
|
666
676
|
let decidedBy;
|
|
667
677
|
let decisionChannel;
|
|
668
678
|
let consumedAt;
|
|
@@ -65,6 +65,19 @@ export const HOW_TO_RECIPES = [
|
|
|
65
65
|
gotchas: [projectRule, 'An empty list means there is no campaign to report. Distinguish drafts and zero sends from failed reads. Raw total_opened/total_clicked are event counters, not unique people.'],
|
|
66
66
|
done_when: 'The chosen campaign and its unique engagement counts are reported, with unknown counts and incomplete sends explained.',
|
|
67
67
|
},
|
|
68
|
+
{
|
|
69
|
+
id: 'analytics-summary', title: 'Summarize project email analytics',
|
|
70
|
+
when_to_use: 'Review sender-level email performance and recent campaigns for one project.',
|
|
71
|
+
synonyms: ['analytics summary', 'pull an analytics summary', 'analytics for my project', 'stats summary', 'project stats', 'performance overview', 'how are my emails doing'],
|
|
72
|
+
requires: { scopes: ['read'], tester_ok: true },
|
|
73
|
+
steps: [
|
|
74
|
+
{ n: 1, tool: 'get_sites', args_example: {}, note: 'Choose the sender for the project whose analytics you want to review.' },
|
|
75
|
+
{ n: 2, tool: 'get_stats', args_example: { site_id: from(1, '0.id'), start_date: '2026-10-01' }, note: 'Replace start_date with the beginning of the requested reporting period; end_date defaults to today.' },
|
|
76
|
+
{ n: 3, tool: 'list_campaigns', args_example: { site_id: from(1, '0.id'), limit: 25 }, note: 'Use campaign metrics to add context to sender-wide performance; newest-created first, not newest-sent.' },
|
|
77
|
+
],
|
|
78
|
+
gotchas: [projectRule, 'get_stats is sender-wide for the selected site, not campaign-specific. Distinguish drafts from sent campaigns and call out the reporting period.'],
|
|
79
|
+
done_when: 'Summarize the selected sender’s metrics and reporting period, with recent campaign context and unknowns called out.',
|
|
80
|
+
},
|
|
68
81
|
{
|
|
69
82
|
id: 'add-or-import-contacts', title: 'Add a contact or import contacts',
|
|
70
83
|
when_to_use: 'Subscribe a single contact to a sender, or bulk import an opted-in list.',
|
|
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { checkAgentMailboxLimit, validateMailboxDisplayName } from '@sendinel/domain';
|
|
3
3
|
import { db, requestContext } from '../db.js';
|
|
4
4
|
import { getProjectId } from '../project.js';
|
|
5
|
-
import { withApproval, approvalExcerpt } from '../lib/action-approvals.js';
|
|
5
|
+
import { withApproval, approvalExcerpt, registerApprovalPreflight } from '../lib/action-approvals.js';
|
|
6
6
|
import { toolError, formatZodError } from '../lib/db-error.js';
|
|
7
7
|
import { checked, mailboxDb, mailboxAudit, messageOutput, resolveMailbox, sendAgentReply, visibleMessage, visibleMessages, MailboxError } from '../lib/agent-mailbox.js';
|
|
8
8
|
const j = (value) => ({ content: [{ type: 'text', text: JSON.stringify(value) }] });
|
|
@@ -51,12 +51,39 @@ const draftReplySchema = z.object({
|
|
|
51
51
|
const createMailboxSchema = z.object({
|
|
52
52
|
local_part: z.string().toLowerCase().regex(/^[a-z0-9][a-z0-9._+-]{0,63}$/),
|
|
53
53
|
domain: z.string().toLowerCase().regex(/^agents\.[a-z0-9.-]+\.[a-z]{2,}$/),
|
|
54
|
-
display_name: z.string().max(200).
|
|
54
|
+
display_name: z.string().max(200).refine(value => validateMailboxDisplayName(value).valid, { message: 'Display name must be plain text without HTML, control characters, or bidirectional formatting characters.' }).optional(),
|
|
55
55
|
mcp_connection_id: z.string().uuid(),
|
|
56
56
|
site_id: z.string().uuid().optional(),
|
|
57
57
|
daily_send_cap: z.number().int().positive().optional().default(50),
|
|
58
58
|
approval_id,
|
|
59
59
|
});
|
|
60
|
+
async function createMailboxPreflight(params) {
|
|
61
|
+
const actorId = requestContext()?.actorId;
|
|
62
|
+
if (!actorId)
|
|
63
|
+
return toolError({ code: 'forbidden', error: 'An authenticated admin login is required to create a mailbox' });
|
|
64
|
+
const projectId = getProjectId();
|
|
65
|
+
const { data: tokenRow, error: tokenErr } = await mailboxDb().from('mcp_oauth_tokens')
|
|
66
|
+
.select('id, expires_at, revoked_at, user_id, project_id')
|
|
67
|
+
.eq('id', params.mcp_connection_id).eq('project_id', projectId).eq('user_id', actorId)
|
|
68
|
+
.is('revoked_at', null).maybeSingle();
|
|
69
|
+
if (tokenErr)
|
|
70
|
+
throw tokenErr;
|
|
71
|
+
const tokenExpired = !tokenRow || (tokenRow.expires_at != null &&
|
|
72
|
+
(!Number.isFinite(Date.parse(tokenRow.expires_at)) || Date.parse(tokenRow.expires_at) <= Date.now()));
|
|
73
|
+
if (tokenExpired) {
|
|
74
|
+
return toolError({ code: 'not_found', error: 'No active OAuth connection with that id for this login in this project.' });
|
|
75
|
+
}
|
|
76
|
+
if (params.site_id) {
|
|
77
|
+
const { data: siteRow, error: siteErr } = await mailboxDb().from('sites').select('id')
|
|
78
|
+
.eq('id', params.site_id).eq('project_id', projectId).maybeSingle();
|
|
79
|
+
if (siteErr)
|
|
80
|
+
throw siteErr;
|
|
81
|
+
if (!siteRow)
|
|
82
|
+
return toolError({ code: 'not_found', error: 'Site not found in this project.' });
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
registerApprovalPreflight('create_agent_mailbox', createMailboxPreflight);
|
|
60
87
|
export function registerAgentMailboxTools(server) {
|
|
61
88
|
server.registerTool('list_agent_inbox', {
|
|
62
89
|
title: 'List Agent Inbox', description: 'Check your bound agent mailbox inbox. Unread first, then newest; filter by thread or status. Returns metadata and short text previews.' + outside,
|
|
@@ -173,9 +200,7 @@ export function registerAgentMailboxTools(server) {
|
|
|
173
200
|
server.registerTool('create_agent_mailbox', {
|
|
174
201
|
title: 'Create Agent Mailbox', description: 'Admin: create an agent mailbox inbox on an agents subdomain and bind it to an active OAuth connection owned by this login in this project. Atomically restricts that connection to the agent role. Optional site_id is its sending anchor; no site means replies fail. Extra scope grants are a later slice.' + gate,
|
|
175
202
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
176
|
-
inputSchema:
|
|
177
|
-
domain: z.string().toLowerCase().regex(/^agents\.[a-z0-9.-]+\.[a-z]{2,}$/), display_name: z.string().max(200).regex(/^[^\r\n\u0000]*$/).optional(),
|
|
178
|
-
mcp_connection_id: z.string().uuid(), site_id: z.string().uuid().optional(), daily_send_cap: z.number().int().positive().optional().default(50), approval_id },
|
|
203
|
+
inputSchema: createMailboxSchema.shape,
|
|
179
204
|
}, wrapMailboxTool(async (args, extra) => {
|
|
180
205
|
if (!args?.approval_id) {
|
|
181
206
|
const missing = [];
|
|
@@ -207,27 +232,9 @@ export function registerAgentMailboxTools(server) {
|
|
|
207
232
|
: `This project's plan allows ${limit.limit} agent mailbox${limit.limit === 1 ? '' : 'es'} and it already has ${limit.current}.`,
|
|
208
233
|
}), isError: true };
|
|
209
234
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
.eq('id', args.mcp_connection_id)
|
|
214
|
-
.eq('project_id', projectId)
|
|
215
|
-
.eq('user_id', actorId)
|
|
216
|
-
.is('revoked_at', null);
|
|
217
|
-
const { data: tokenRow, error: tokenErr } = await tokenQuery.maybeSingle();
|
|
218
|
-
const tokenExpired = !tokenRow || (tokenRow.expires_at && Date.parse(tokenRow.expires_at) <= Date.now());
|
|
219
|
-
if (tokenErr || tokenExpired) {
|
|
220
|
-
return toolError({ code: 'not_found', error: 'No active OAuth connection with that id for this login in this project.' });
|
|
221
|
-
}
|
|
222
|
-
if (args.site_id) {
|
|
223
|
-
const { data: siteRow, error: siteErr } = await mailboxDb().from('sites').select('id')
|
|
224
|
-
.eq('id', args.site_id)
|
|
225
|
-
.eq('project_id', projectId)
|
|
226
|
-
.maybeSingle();
|
|
227
|
-
if (siteErr || !siteRow) {
|
|
228
|
-
return toolError({ code: 'not_found', error: 'Site not found in this project.' });
|
|
229
|
-
}
|
|
230
|
-
}
|
|
235
|
+
const preflight = await createMailboxPreflight(args);
|
|
236
|
+
if (preflight)
|
|
237
|
+
return preflight;
|
|
231
238
|
const boundArgs = { ...args, login_actor_id: actorId };
|
|
232
239
|
return withApproval('create_agent_mailbox', async (_approved) => {
|
|
233
240
|
const mailbox = checked(await mailboxDb().rpc('provision_agent_mailbox', { p_project_id: getProjectId(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendinel/mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.188",
|
|
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",
|