@sendinel/mcp-server 1.0.24 → 1.0.25
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 +2 -2
- package/build/db.d.ts +2 -0
- package/build/db.js +1 -0
- package/build/server.js +1 -0
- package/build/tools/analytics.js +110 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,14 +12,14 @@ AI-controlled email operations control plane for campaigns, contacts, segments,
|
|
|
12
12
|
|
|
13
13
|
## Tool groups
|
|
14
14
|
|
|
15
|
-
Sendinel currently exposes 34 tool groups and
|
|
15
|
+
Sendinel currently exposes 34 tool groups and 315 tools (gateway mode adds 3 meta-tools). These counts are checked against the tool registry in CI, so this table matches what ships.
|
|
16
16
|
|
|
17
17
|
| Group | Count | Purpose |
|
|
18
18
|
|-------|-------|---------|
|
|
19
19
|
| `ab-testing` | 4 | A/B test creation, listing, significance checks, winner promotion |
|
|
20
20
|
| `advisor` | 6 | Campaign advisor: recommendations, benchmarks, subject/content/send-time guidance |
|
|
21
21
|
| `ai-work` | 9 | AI work items, recommended segments, context events, agent proposals |
|
|
22
|
-
| `analytics` |
|
|
22
|
+
| `analytics` | 18 | Stats, portfolio analytics, project switching, domain health, engagement, deliverability, revenue analytics |
|
|
23
23
|
| `approvals` | 2 | Poll and list risk-tiered action approvals |
|
|
24
24
|
| `automations` | 7 | Automation triggers, previews, and response-gated escalation ladders |
|
|
25
25
|
| `brand_kit` | 5 | Brand Core text import, media library upload/URL ingest, asset-context reads |
|
package/build/db.d.ts
CHANGED
|
@@ -37,6 +37,7 @@ export declare function runWithClient<T>(client: any, fn: () => Promise<T>, requ
|
|
|
37
37
|
channelVerified?: boolean;
|
|
38
38
|
actorType?: string;
|
|
39
39
|
actorId?: string | null;
|
|
40
|
+
oauthTokenId?: string | null;
|
|
40
41
|
workspaceId?: string | null;
|
|
41
42
|
v1Caller?: V1Caller;
|
|
42
43
|
}): Promise<T>;
|
|
@@ -52,6 +53,7 @@ export declare function requestContext(): {
|
|
|
52
53
|
channelVerified?: boolean;
|
|
53
54
|
actorType?: string;
|
|
54
55
|
actorId?: string | null;
|
|
56
|
+
oauthTokenId?: string | null;
|
|
55
57
|
workspaceId?: string | null;
|
|
56
58
|
v1Caller?: V1Caller;
|
|
57
59
|
} | undefined;
|
package/build/db.js
CHANGED
package/build/server.js
CHANGED
|
@@ -103,6 +103,7 @@ const WRITE_TOOLS = new Set([
|
|
|
103
103
|
'update_warmup_schedule', 'pause_warmup', 'resume_warmup',
|
|
104
104
|
'create_form', 'invite_team_member',
|
|
105
105
|
'remove_team_member', 'set_team_member_role', 'set_member_project_access',
|
|
106
|
+
'set_active_project',
|
|
106
107
|
'resend_invitation', 'revoke_invitation',
|
|
107
108
|
'create_landing_page', 'update_landing_page', 'delete_landing_page',
|
|
108
109
|
'start_provider_switch', 'get_provider_switch_status', 'rollback_provider_switch',
|
package/build/tools/analytics.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { fetchAllRows } from '@sendinel/domain';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import { db, adminDb } from '../db.js';
|
|
3
|
+
import { db, adminDb, requestContext } from '../db.js';
|
|
4
4
|
import { getProjectId } from '../project.js';
|
|
5
5
|
import { resolveProjectClient } from '../byod-client.js';
|
|
6
6
|
import { decrypt } from '../crypto.js';
|
|
7
|
+
import { logAudit } from '../audit.js';
|
|
7
8
|
const txt = (text) => ({ content: [{ type: 'text', text }] });
|
|
8
9
|
const j = (data) => txt(JSON.stringify(data, null, 2));
|
|
9
10
|
const percent = (rate) => `${(rate * 100).toFixed(1)}%`;
|
|
@@ -348,9 +349,63 @@ export function registerAnalyticsTools(server) {
|
|
|
348
349
|
description: 'List all projects accessible to this API key. Returns project ID, name, plan, and associated organization. Useful for orchestrators managing multiple products through a single Sendinel account.',
|
|
349
350
|
inputSchema: {},
|
|
350
351
|
}, async () => {
|
|
352
|
+
const projectId = getProjectId();
|
|
353
|
+
const ctx = requestContext();
|
|
354
|
+
if (ctx?.channel === 'mcp_oauth_session' && ctx.actorId) {
|
|
355
|
+
const [{ data: directAccess, error: directError }, { data: ownedOrgs, error: ownerError }] = await Promise.all([
|
|
356
|
+
adminDb()
|
|
357
|
+
.from('user_project_access')
|
|
358
|
+
.select('project_id')
|
|
359
|
+
.eq('user_id', ctx.actorId),
|
|
360
|
+
adminDb()
|
|
361
|
+
.from('org_members')
|
|
362
|
+
.select('org_id')
|
|
363
|
+
.eq('user_id', ctx.actorId)
|
|
364
|
+
.eq('role', 'owner'),
|
|
365
|
+
]);
|
|
366
|
+
if (directError)
|
|
367
|
+
return txt(`Error: ${directError.message}`);
|
|
368
|
+
if (ownerError)
|
|
369
|
+
return txt(`Error: ${ownerError.message}`);
|
|
370
|
+
const directProjectIds = Array.from(new Set((directAccess ?? []).map((row) => row.project_id).filter(Boolean)));
|
|
371
|
+
const ownedOrgIds = Array.from(new Set((ownedOrgs ?? []).map((row) => row.org_id).filter(Boolean)));
|
|
372
|
+
const [{ data: directProjects, error: directProjectsError }, { data: ownedProjects, error: ownedProjectsError }] = await Promise.all([
|
|
373
|
+
directProjectIds.length
|
|
374
|
+
? adminDb().from('projects').select('id, name, org_id').in('id', directProjectIds)
|
|
375
|
+
: Promise.resolve({ data: [], error: null }),
|
|
376
|
+
ownedOrgIds.length
|
|
377
|
+
? adminDb().from('projects').select('id, name, org_id').in('org_id', ownedOrgIds)
|
|
378
|
+
: Promise.resolve({ data: [], error: null }),
|
|
379
|
+
]);
|
|
380
|
+
if (directProjectsError)
|
|
381
|
+
return txt(`Error: ${directProjectsError.message}`);
|
|
382
|
+
if (ownedProjectsError)
|
|
383
|
+
return txt(`Error: ${ownedProjectsError.message}`);
|
|
384
|
+
const byId = new Map();
|
|
385
|
+
for (const project of [...(directProjects ?? []), ...(ownedProjects ?? [])]) {
|
|
386
|
+
byId.set(project.id, project);
|
|
387
|
+
}
|
|
388
|
+
const orgIds = Array.from(new Set(Array.from(byId.values()).map((project) => project.org_id).filter((id) => Boolean(id))));
|
|
389
|
+
const { data: organizations, error: organizationsError } = orgIds.length
|
|
390
|
+
? await adminDb().from('organizations').select('id, name').in('id', orgIds)
|
|
391
|
+
: { data: [], error: null };
|
|
392
|
+
if (organizationsError)
|
|
393
|
+
return txt(`Error: ${organizationsError.message}`);
|
|
394
|
+
const orgNames = new Map((organizations ?? []).map((org) => [org.id, org.name]));
|
|
395
|
+
return j({
|
|
396
|
+
projects: Array.from(byId.values())
|
|
397
|
+
.sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id))
|
|
398
|
+
.map((project) => ({
|
|
399
|
+
id: project.id,
|
|
400
|
+
name: project.name,
|
|
401
|
+
org_id: project.org_id,
|
|
402
|
+
org_name: project.org_id ? orgNames.get(project.org_id) ?? null : null,
|
|
403
|
+
active: project.id === projectId,
|
|
404
|
+
})),
|
|
405
|
+
});
|
|
406
|
+
}
|
|
351
407
|
// API keys are scoped to a single project, but an organization can have multiple projects.
|
|
352
408
|
// Look up the org for this project, then list all projects in that org.
|
|
353
|
-
const projectId = getProjectId();
|
|
354
409
|
const { data: project, error: projErr } = await adminDb()
|
|
355
410
|
.from('projects')
|
|
356
411
|
.select('id, name, org_id')
|
|
@@ -385,6 +440,59 @@ export function registerAnalyticsTools(server) {
|
|
|
385
440
|
})),
|
|
386
441
|
});
|
|
387
442
|
});
|
|
443
|
+
server.registerTool('set_active_project', {
|
|
444
|
+
title: 'Set Active Project',
|
|
445
|
+
annotations: {
|
|
446
|
+
readOnlyHint: false,
|
|
447
|
+
destructiveHint: false,
|
|
448
|
+
idempotentHint: true,
|
|
449
|
+
openWorldHint: false,
|
|
450
|
+
},
|
|
451
|
+
description: 'Switch this connection to act on a different project you have access to, without reconnecting. Only available on an OAuth-connected session (claude.ai / Claude Code / Cursor); API keys and Synchronex connections stay pinned to their original project.',
|
|
452
|
+
inputSchema: {
|
|
453
|
+
project_id: z.string().uuid().describe('Project UUID to switch to. Use list_projects to see what you can access.'),
|
|
454
|
+
},
|
|
455
|
+
}, async ({ project_id }) => {
|
|
456
|
+
const ctx = requestContext();
|
|
457
|
+
if (ctx?.channel !== 'mcp_oauth_session' || !ctx.oauthTokenId || !ctx.actorId) {
|
|
458
|
+
return j({
|
|
459
|
+
error: 'set_active_project is only available on an OAuth-connected session (claude.ai, Claude Code, Cursor). This connection is pinned to a single project.',
|
|
460
|
+
code: 'SWITCH_NOT_SUPPORTED',
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
const { data: hasAccess, error: accessError } = await adminDb().rpc('user_has_project_access', {
|
|
464
|
+
p_user_id: ctx.actorId,
|
|
465
|
+
p_project_id: project_id,
|
|
466
|
+
});
|
|
467
|
+
if (accessError)
|
|
468
|
+
return j({ error: accessError.message });
|
|
469
|
+
if (!hasAccess)
|
|
470
|
+
return j({ error: 'You do not have access to this project.', code: 'PROJECT_ACCESS_DENIED' });
|
|
471
|
+
const previousProjectId = getProjectId();
|
|
472
|
+
const { error: updateError } = await adminDb()
|
|
473
|
+
.from('mcp_oauth_tokens')
|
|
474
|
+
.update({ active_project_id: project_id })
|
|
475
|
+
.eq('id', ctx.oauthTokenId);
|
|
476
|
+
if (updateError)
|
|
477
|
+
return j({ error: updateError.message });
|
|
478
|
+
const { data: project } = await adminDb()
|
|
479
|
+
.from('projects')
|
|
480
|
+
.select('id, name, org_id')
|
|
481
|
+
.eq('id', project_id)
|
|
482
|
+
.maybeSingle();
|
|
483
|
+
void logAudit({
|
|
484
|
+
action: 'mcp.set_active_project',
|
|
485
|
+
resourceType: 'project',
|
|
486
|
+
resourceId: project_id,
|
|
487
|
+
details: { previous_project_id: previousProjectId },
|
|
488
|
+
channel: 'mcp_oauth_session',
|
|
489
|
+
}).catch(() => { });
|
|
490
|
+
return j({
|
|
491
|
+
ok: true,
|
|
492
|
+
active_project: project ? { id: project.id, name: project.name, org_id: project.org_id } : { id: project_id },
|
|
493
|
+
note: 'Applies starting with your next tool call on this connection.',
|
|
494
|
+
});
|
|
495
|
+
});
|
|
388
496
|
server.registerTool('get_sites', {
|
|
389
497
|
title: 'Get Sites',
|
|
390
498
|
annotations: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendinel/mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.25",
|
|
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",
|