@sendinel/mcp-server 1.0.117 → 1.0.118
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.
|
@@ -117,7 +117,7 @@ export function getApprovalTier(action, params) {
|
|
|
117
117
|
}
|
|
118
118
|
// send_broadcast exists for agents and always needs a person's approval, even in autonomous
|
|
119
119
|
// mode: it must never approve itself (#3669, AI-worker boundary in CLAUDE.md).
|
|
120
|
-
if (action === 'draft_agent_reply' || action === 'create_agent_mailbox' || action === 'send_broadcast' || action === 'activate_campaigns')
|
|
120
|
+
if (action === 'draft_agent_reply' || action === 'create_agent_mailbox' || action === 'send_broadcast' || action === 'activate_campaigns' || action === 'invite_team_member')
|
|
121
121
|
return 'gated';
|
|
122
122
|
// Autonomous mode: user explicitly opted out of approval gates for gated actions
|
|
123
123
|
// (request-scoped under HTTP — env is only trustworthy for single-project stdio)
|
|
@@ -289,6 +289,10 @@ export function buildApprovalSummary(action, params, display = {}) {
|
|
|
289
289
|
add_list_members: (p) => `Add ${p.contact_ids?.length ?? 0} contact(s) to list ${p.list_id}`,
|
|
290
290
|
remove_list_members: (p) => `Remove ${p.contact_ids?.length ?? 0} contact(s) from list ${p.list_id}`,
|
|
291
291
|
add_list_members_by_filter: (p) => `Add contacts matching filter rules to list ${p.list_id}`,
|
|
292
|
+
invite_team_member: (p) => {
|
|
293
|
+
const invite = p.__approval_snapshot?.invitation;
|
|
294
|
+
return `Send invitation to ${p.email} as ${p.role}; project_ids: ${JSON.stringify(p.project_ids ?? null)} (null = org-wide, [] = no projects); organization: ${invite?.org_name ?? 'unavailable'} (${invite?.org_id ?? 'unavailable'}); subject: "${invite?.subject ?? 'unavailable'}"`;
|
|
295
|
+
},
|
|
292
296
|
remove_team_member: (p) => `Remove team member ${p.user_id} from the organization`,
|
|
293
297
|
set_team_member_role: (p) => `Change team member ${p.user_id}'s role to ${p.role}`,
|
|
294
298
|
delete_provider_credential: (p) => `Delete saved ${p.provider ?? 'provider'} credential ${p.credential_id} (${p.source})`,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isTesterRequest } from './tester-policy.js';
|
|
2
2
|
import { captureTesterRequestSnapshot } from './tester-request-snapshot.js';
|
|
3
3
|
import { readAllRows, chunkIds, scopeSegmentRulesToSite } from '@sendinel/domain';
|
|
4
|
+
import { callSendinelV1 } from './v1-client.js';
|
|
4
5
|
import { db } from '../db.js';
|
|
5
6
|
import { resolveSendAudience, resolveSegmentContactIds } from './send-audience.js';
|
|
6
7
|
const TARGET_ACTIONS = new Set(['send_broadcast', 'broadcast_to_segment', 'launch_campaign', 'activate_campaigns',
|
|
@@ -8,6 +9,14 @@ const TARGET_ACTIONS = new Set(['send_broadcast', 'broadcast_to_segment', 'launc
|
|
|
8
9
|
'send_transactional', 'send_awaiting_reply', 'send_test_email', 'send_newsletter_issue', 'schedule_newsletter_issue']);
|
|
9
10
|
/** Server-owned display facts, saved inside the hash-bound params. Never supplied by a tool. */
|
|
10
11
|
export async function captureApprovalSnapshot(action, params, projectId) {
|
|
12
|
+
if (action === 'invite_team_member') {
|
|
13
|
+
const result = await callSendinelV1('/api/v1/org/invitations', { method: 'POST', body: { ...params, preview_only: true } });
|
|
14
|
+
const preview = result.ok ? result.data.preview : null;
|
|
15
|
+
if (!preview || typeof preview.org_id !== 'string' || typeof preview.org_name !== 'string' || typeof preview.subject !== 'string') {
|
|
16
|
+
throw new Error('Invitation approval preview unavailable; no email was sent.');
|
|
17
|
+
}
|
|
18
|
+
return { version: 1, invitation: preview, recipient_count: 1 };
|
|
19
|
+
}
|
|
11
20
|
const setup = isTesterRequest(action) ? await captureTesterRequestSnapshot(action, params, projectId) : null;
|
|
12
21
|
const snapshot = { version: 1, campaigns: [], steps: [], sites: [], recipient_count: null };
|
|
13
22
|
if (!TARGET_ACTIONS.has(action))
|
package/build/tools/org.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { readPlanUsage, readAllRows } from "@sendinel/domain";
|
|
2
|
-
import { randomBytes } from 'crypto';
|
|
3
2
|
import { z } from 'zod';
|
|
4
3
|
import { logAudit } from '../audit.js';
|
|
5
4
|
import { adminDb, db } from '../db.js';
|
|
@@ -174,21 +173,20 @@ export function registerOrgTools(server) {
|
|
|
174
173
|
idempotentHint: false,
|
|
175
174
|
openWorldHint: false,
|
|
176
175
|
},
|
|
177
|
-
description: '
|
|
178
|
-
inputSchema: {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
return j({ invitation_id: data.id, expires_at: data.expires_at });
|
|
176
|
+
description: 'Invite a team member by email to the current project organization using the system invitation template. Reuses a matching pending invitation. Always requires human approval; request once, then repeat identical email, role and project_ids with approval_id. Owner invitations are unsupported by the API.',
|
|
177
|
+
inputSchema: {
|
|
178
|
+
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 the same arguments plus this approval_id to run the action.'),
|
|
179
|
+
email: z.string().email(),
|
|
180
|
+
role: z.enum(['owner', 'admin', 'member', 'tester']).default('member'),
|
|
181
|
+
project_ids: z.array(z.string().uuid()).optional().describe('Same-org project IDs. Omit for org-wide access; [] grants no projects. Must match an existing pending invitation.'),
|
|
182
|
+
},
|
|
183
|
+
}, withApproval('invite_team_member', async (args) => {
|
|
184
|
+
const result = await callSendinelV1('/api/v1/org/invitations', { method: 'POST', body: args });
|
|
185
|
+
if (!result.ok)
|
|
186
|
+
return j({ error: result.error, status: result.status });
|
|
187
|
+
const data = result.data;
|
|
188
|
+
// Preserve the original top-level response fields; add the API's send outcome.
|
|
189
|
+
return j({ ...data, invitation_id: data.invitation.id, expires_at: data.invitation.expires_at });
|
|
192
190
|
}));
|
|
193
191
|
server.registerTool('remove_team_member', {
|
|
194
192
|
title: 'Remove Team Member',
|
|
@@ -443,7 +441,7 @@ export function registerOrgTools(server) {
|
|
|
443
441
|
idempotentHint: false,
|
|
444
442
|
openWorldHint: false,
|
|
445
443
|
},
|
|
446
|
-
description: "Extend a pending invitation's expiry and return its invite URL.
|
|
444
|
+
description: "Extend a pending invitation's expiry and return its invite URL. Link-only: sends no email. Use invite_team_member with the same email, role and project_ids to request an approval-gated template email.",
|
|
447
445
|
inputSchema: {
|
|
448
446
|
invitation_id: z.string().uuid(),
|
|
449
447
|
org_id: z.string().uuid().optional(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendinel/mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.118",
|
|
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",
|