@sendinel/mcp-server 1.0.17 → 1.0.19

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 CHANGED
@@ -12,7 +12,7 @@ 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 311 tools (gateway mode adds 3 meta-tools). These counts are checked against the tool registry in CI, so this table matches what ships.
15
+ Sendinel currently exposes 34 tool groups and 314 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
  |-------|-------|---------|
@@ -45,7 +45,7 @@ Sendinel currently exposes 34 tool groups and 311 tools (gateway mode adds 3 met
45
45
  | `sites` | 5 | Site, sender, and brand voice management |
46
46
  | `skills` | 2 | Operator skills catalog |
47
47
  | `sms` | 7 | SMS send, log, consent, inbound |
48
- | `social-posts` | 6 | Social post creation, adaptation, scheduling state, archive/delete |
48
+ | `social-posts` | 9 | Social post creation, adaptation, scheduling state, connection health, engagement reads, archive/delete |
49
49
  | `surveys` | 5 | Survey creation, listing, status, results |
50
50
  | `templates` | 7 | Email template CRUD, version restore, migration translation |
51
51
  | `warmup` | 4 | IP/domain warmup schedule control |
@@ -13,7 +13,27 @@ const PLATFORMS = [
13
13
  'instagram', 'linkedin', 'twitter', 'x', 'facebook', 'tiktok',
14
14
  'youtube', 'pinterest', 'threads', 'reddit', 'bluesky', 'google_business',
15
15
  ];
16
- const STATUSES = ['draft', 'scheduled', 'queued', 'publishing', 'published', 'failed', 'archived'];
16
+ const STATUSES = ['draft', 'scheduled', 'queued', 'publishing', 'published', 'partially_published', 'failed', 'archived'];
17
+ const ENGAGEMENT_STATUSES = ['open', 'replied', 'dismissed', 'failed', 'expired'];
18
+ const SOCIAL_CONNECTION_COLUMNS = [
19
+ 'platform', 'account_name', 'account_id', 'account_status', 'expires_at',
20
+ 'follower_count', 'follower_count_synced_at', 'updated_at', 'created_at',
21
+ ];
22
+ const SOCIAL_ENGAGEMENT_LIST_COLUMNS = [
23
+ 'id', 'social_post_id', 'platform', 'engagement_type', 'direction',
24
+ 'sender_name', 'sender_handle', 'body', 'status', 'received_at',
25
+ 'responded_at', 'created_at',
26
+ ];
27
+ // `metadata` is deliberately not selected: nothing in the dashboard writes it, so its contents are
28
+ // unknown, and the v1 social-inbox route keeps to this same plain column list. Re-add it only once
29
+ // something defines what it holds.
30
+ const SOCIAL_ENGAGEMENT_DETAIL_COLUMNS = [
31
+ ...SOCIAL_ENGAGEMENT_LIST_COLUMNS,
32
+ 'dismissed_at',
33
+ ];
34
+ function selectFields(row, columns) {
35
+ return Object.fromEntries(columns.map((column) => [column, row[column]]));
36
+ }
17
37
  function cleanText(value) {
18
38
  return typeof value === 'string' && value.trim() ? value.trim() : null;
19
39
  }
@@ -101,6 +121,103 @@ async function requireProjectCampaign(campaignId) {
101
121
  throw new Error('Email campaign not found for this project');
102
122
  }
103
123
  export function registerSocialPostTools(server) {
124
+ server.registerTool('list_social_connections', {
125
+ title: 'List Social Connections',
126
+ annotations: {
127
+ readOnlyHint: true,
128
+ destructiveHint: false,
129
+ idempotentHint: true,
130
+ openWorldHint: false,
131
+ },
132
+ description: 'List this organization\'s connected social accounts with platform, account identity, connection status, and when access expires. Use before scheduling to detect no connected accounts, expired access, or a reauth-required status.',
133
+ inputSchema: {},
134
+ }, async () => {
135
+ const projectId = getProjectId();
136
+ const { data: project, error: projectError } = await db()
137
+ .from('projects')
138
+ .select('org_id')
139
+ .eq('id', projectId)
140
+ .maybeSingle();
141
+ if (projectError)
142
+ return txt(`Error: ${projectError.message}`);
143
+ if (!project?.org_id)
144
+ return txt('Error: Project organization not found.');
145
+ const { data, error } = await db()
146
+ .from('social_platform_connections')
147
+ .select(SOCIAL_CONNECTION_COLUMNS.join(', '))
148
+ .eq('org_id', project.org_id)
149
+ .order('platform', { ascending: true });
150
+ if (error)
151
+ return txt(`Error: ${error.message}`);
152
+ const connections = (data ?? [])
153
+ .map((row) => selectFields(row, SOCIAL_CONNECTION_COLUMNS));
154
+ if (connections.length === 0) {
155
+ return j({ connections: [], message: 'No social accounts are connected.' });
156
+ }
157
+ return j({ connections });
158
+ });
159
+ server.registerTool('list_social_engagements', {
160
+ title: 'List Social Engagements',
161
+ annotations: {
162
+ readOnlyHint: true,
163
+ destructiveHint: false,
164
+ idempotentHint: true,
165
+ openWorldHint: false,
166
+ },
167
+ description: 'List inbound social inbox engagements such as comments, replies, direct messages, and mentions across connected platforms. Read-only: this tool never responds on a user\'s behalf.',
168
+ inputSchema: {
169
+ status: z.enum([...ENGAGEMENT_STATUSES, 'all']).optional().default('open'),
170
+ platform: z.string().optional(),
171
+ social_post_id: z.string().uuid().optional(),
172
+ limit: z.number().int().min(1).max(200).optional().default(50),
173
+ },
174
+ }, async ({ status, platform, social_post_id, limit }) => {
175
+ let query = db()
176
+ .from('social_engagements')
177
+ .select(SOCIAL_ENGAGEMENT_LIST_COLUMNS.join(', '))
178
+ .eq('project_id', getProjectId())
179
+ .eq('direction', 'inbound')
180
+ .order('received_at', { ascending: false })
181
+ .limit(limit ?? 50);
182
+ if (status && status !== 'all')
183
+ query = query.eq('status', status);
184
+ if (platform)
185
+ query = query.eq('platform', platform);
186
+ if (social_post_id)
187
+ query = query.eq('social_post_id', social_post_id);
188
+ const { data, error } = await query;
189
+ if (error)
190
+ return txt(`Error: ${error.message}`);
191
+ const engagements = (data ?? [])
192
+ .map((row) => selectFields(row, SOCIAL_ENGAGEMENT_LIST_COLUMNS));
193
+ if (engagements.length === 0) {
194
+ return j({ engagements: [], message: 'No social engagements matched the requested filters.' });
195
+ }
196
+ return j({ engagements });
197
+ });
198
+ server.registerTool('get_social_engagement', {
199
+ title: 'Get Social Engagement',
200
+ annotations: {
201
+ readOnlyHint: true,
202
+ destructiveHint: false,
203
+ idempotentHint: true,
204
+ openWorldHint: false,
205
+ },
206
+ description: 'Get one inbound social inbox engagement, including a comment, reply, direct message, or mention, for human review. Read-only: this tool does not respond.',
207
+ inputSchema: { engagement_id: z.string().uuid() },
208
+ }, async ({ engagement_id }) => {
209
+ const { data, error } = await db()
210
+ .from('social_engagements')
211
+ .select(SOCIAL_ENGAGEMENT_DETAIL_COLUMNS.join(', '))
212
+ .eq('project_id', getProjectId())
213
+ .eq('id', engagement_id)
214
+ .maybeSingle();
215
+ if (error)
216
+ return txt(`Error: ${error.message}`);
217
+ if (!data)
218
+ return j({ error: 'Social engagement not found', code: 'NOT_FOUND' });
219
+ return j(selectFields(data, SOCIAL_ENGAGEMENT_DETAIL_COLUMNS));
220
+ });
104
221
  server.registerTool('list_social_posts', {
105
222
  title: 'List Social Posts',
106
223
  annotations: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendinel/mcp-server",
3
- "version": "1.0.17",
3
+ "version": "1.0.19",
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",