@sendinel/mcp-server 1.0.188 → 1.0.190
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/build/lib/how-to-recipes.js +4 -1
- package/build/tools/ai-work.js +158 -58
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Hand-written workflows, checked against the registered schemas in CI (#4061). */
|
|
2
2
|
import { SEGMENT_OPERATORS_DESCRIPTION } from '@sendinel/domain';
|
|
3
|
+
import { ADMIN_TOOLS } from './admin-tools.js';
|
|
3
4
|
import { rankTools, scoreTool, tokenize, TOOL_ALIASES } from '../tools/tool-search.js';
|
|
4
5
|
const approval = { required: true, how: 'returns approve_url; re-call with approval_id' };
|
|
5
6
|
const from = (n, field) => `<from step ${n}: ${field}>`;
|
|
@@ -244,7 +245,9 @@ export function matchRecipes(goal, limit = 3, available = HOW_TO_RECIPES, tools
|
|
|
244
245
|
.filter(({ score }) => score > 0).sort((a, b) => b.score - a.score || a.order - b.order);
|
|
245
246
|
const selected = scored.slice(0, Math.max(1, Math.min(10, Math.trunc(limit))));
|
|
246
247
|
const returned = new Set(selected.map(({ entry }) => entry.name));
|
|
247
|
-
const
|
|
248
|
+
const allowAdminTools = selected.some(({ entry: { recipe } }) => recipe.requires.scopes.includes('admin'));
|
|
249
|
+
const candidateTools = allowAdminTools ? tools : tools.filter((tool) => !ADMIN_TOOLS.has(tool.name));
|
|
250
|
+
const candidates = [...entries.filter((e) => !returned.has(e.name)), ...candidateTools];
|
|
248
251
|
const suggestions = rankTools(candidates, goal, { aliases: { ...TOOL_ALIASES, ...aliases }, nearestLimit: 5 });
|
|
249
252
|
const also_try = [...new Set([...suggestions.matches, ...suggestions.nearest].map(({ entry }) => entry.name))].slice(0, 5);
|
|
250
253
|
if (!also_try.length && !selected.length)
|
package/build/tools/ai-work.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
+
import { AI_WORK_ITEM_STATUSES } from '@sendinel/domain';
|
|
3
4
|
import { db } from '../db.js';
|
|
4
5
|
import { getProjectId } from '../project.js';
|
|
5
|
-
import { dbToolError } from '../lib/db-error.js';
|
|
6
|
+
import { dbToolError, toolError } from '../lib/db-error.js';
|
|
6
7
|
const txt = (text) => ({ content: [{ type: 'text', text }] });
|
|
7
8
|
const j = (data) => txt(JSON.stringify(data, null, 2));
|
|
8
9
|
const workItemSelect = [
|
|
@@ -47,32 +48,67 @@ const proposalSelect = [
|
|
|
47
48
|
'generated_by',
|
|
48
49
|
'llm_model',
|
|
49
50
|
].join(', ');
|
|
50
|
-
const workStatusSchema = z.enum(['new', 'seen', 'accepted', 'dismissed', 'snoozed', 'resolved', 'failed']);
|
|
51
51
|
const activeStatuses = ['new', 'seen', 'snoozed'];
|
|
52
52
|
const contextEventTypes = ['strategic_segment_intent', 'business_priority', 'customer_journey_signal', 'campaign_opportunity', 'risk_signal'];
|
|
53
53
|
const contextEventInput = {
|
|
54
|
-
workspace_id: z.string().min(1).max(255),
|
|
54
|
+
workspace_id: z.string().trim().min(1).max(255),
|
|
55
55
|
event_type: z.enum(contextEventTypes),
|
|
56
|
-
title: z.string().min(1).max(255),
|
|
57
|
-
detail: z.string().min(1).max(4000),
|
|
56
|
+
title: z.string().trim().min(1).max(255),
|
|
57
|
+
detail: z.string().trim().min(1).max(4000),
|
|
58
58
|
evidence: z.record(z.string(), z.unknown()).optional(),
|
|
59
59
|
suggested_actions: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
60
|
-
idempotency_key: z.string().min(1).max(255),
|
|
60
|
+
idempotency_key: z.string().trim().min(1).max(255),
|
|
61
61
|
};
|
|
62
62
|
function parseStatuses(status) {
|
|
63
|
-
if (
|
|
64
|
-
return [...activeStatuses];
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
if (status === undefined)
|
|
64
|
+
return { statuses: [...activeStatuses] };
|
|
65
|
+
const rawParts = status.split(',').map((item) => item.trim());
|
|
66
|
+
if (rawParts.length === 0 || (rawParts.length === 1 && rawParts[0] === '')) {
|
|
67
|
+
return {
|
|
68
|
+
error: {
|
|
69
|
+
code: 'validation_failed',
|
|
70
|
+
error: `Invalid status "${status}". Allowed values: ${AI_WORK_ITEM_STATUSES.join(', ')}`,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const invalid = rawParts.filter((item) => !AI_WORK_ITEM_STATUSES.includes(item));
|
|
75
|
+
if (invalid.length > 0) {
|
|
76
|
+
return {
|
|
77
|
+
error: {
|
|
78
|
+
code: 'validation_failed',
|
|
79
|
+
error: `Invalid status "${invalid.join(', ')}". Allowed values: ${AI_WORK_ITEM_STATUSES.join(', ')}`,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return { statuses: [...new Set(rawParts)] };
|
|
68
84
|
}
|
|
69
85
|
async function updateWorkItemStatus(id, status, snoozedUntil) {
|
|
86
|
+
const { data: existing, error: existingErr } = await db()
|
|
87
|
+
.from('ai_work_items')
|
|
88
|
+
.select(workItemSelect)
|
|
89
|
+
.eq('project_id', getProjectId())
|
|
90
|
+
.eq('id', id)
|
|
91
|
+
.maybeSingle();
|
|
92
|
+
if (existingErr)
|
|
93
|
+
return toolError(dbToolError(existingErr));
|
|
94
|
+
if (!existing)
|
|
95
|
+
return toolError({ code: 'not_found', error: 'AI work item not found.' });
|
|
96
|
+
if (status === 'seen') {
|
|
97
|
+
// #4558: a read receipt must not undo a dismissal or other non-new status
|
|
98
|
+
if (existing.status !== 'new') {
|
|
99
|
+
return j({ work_item: existing });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
70
102
|
const updates = { status };
|
|
71
103
|
if (status === 'dismissed')
|
|
72
104
|
updates.dismissed_at = new Date().toISOString();
|
|
73
105
|
if (status === 'snoozed') {
|
|
74
106
|
if (!snoozedUntil)
|
|
75
|
-
return { error: 'snoozed_until is required.' };
|
|
107
|
+
return toolError({ code: 'validation_failed', error: 'snoozed_until is required.' });
|
|
108
|
+
const snoozedTime = new Date(snoozedUntil).getTime();
|
|
109
|
+
if (!Number.isFinite(snoozedTime) || snoozedTime <= Date.now()) {
|
|
110
|
+
return toolError({ code: 'validation_failed', error: 'snoozed_until must be in the future.' });
|
|
111
|
+
}
|
|
76
112
|
updates.snoozed_until = snoozedUntil;
|
|
77
113
|
}
|
|
78
114
|
else {
|
|
@@ -86,10 +122,10 @@ async function updateWorkItemStatus(id, status, snoozedUntil) {
|
|
|
86
122
|
.select(workItemSelect)
|
|
87
123
|
.maybeSingle();
|
|
88
124
|
if (error)
|
|
89
|
-
return
|
|
125
|
+
return toolError(dbToolError(error));
|
|
90
126
|
if (!data)
|
|
91
|
-
return { error: 'AI work item not found.' };
|
|
92
|
-
return { work_item: data };
|
|
127
|
+
return toolError({ code: 'not_found', error: 'AI work item not found.' });
|
|
128
|
+
return j({ work_item: data });
|
|
93
129
|
}
|
|
94
130
|
function stableUuid(input) {
|
|
95
131
|
const hex = createHash('sha256').update(input).digest('hex').slice(0, 32);
|
|
@@ -121,33 +157,87 @@ function segmentActionFromContext(eventType, suggestedActions, title, detail) {
|
|
|
121
157
|
rules: action.rules,
|
|
122
158
|
};
|
|
123
159
|
}
|
|
160
|
+
function isSameContextEventPayload(existing, input) {
|
|
161
|
+
if (existing.workspace_id !== input.workspace_id.trim())
|
|
162
|
+
return false;
|
|
163
|
+
if (existing.event_type !== input.event_type)
|
|
164
|
+
return false;
|
|
165
|
+
if (existing.title !== input.title.trim())
|
|
166
|
+
return false;
|
|
167
|
+
if (existing.detail !== input.detail.trim())
|
|
168
|
+
return false;
|
|
169
|
+
const existingEvidence = existing.evidence && typeof existing.evidence === 'object' ? existing.evidence : {};
|
|
170
|
+
const inputEvidence = input.evidence && typeof input.evidence === 'object' ? input.evidence : {};
|
|
171
|
+
if (JSON.stringify(existingEvidence) !== JSON.stringify(inputEvidence))
|
|
172
|
+
return false;
|
|
173
|
+
const existingActions = Array.isArray(existing.suggested_actions) ? existing.suggested_actions : [];
|
|
174
|
+
const inputActions = Array.isArray(input.suggested_actions) ? input.suggested_actions : [];
|
|
175
|
+
if (JSON.stringify(existingActions) !== JSON.stringify(inputActions))
|
|
176
|
+
return false;
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
async function buildContextReplay(projectId, event) {
|
|
180
|
+
let workItem = null;
|
|
181
|
+
if (event.work_item_id) {
|
|
182
|
+
const { data } = await db()
|
|
183
|
+
.from('ai_work_items')
|
|
184
|
+
.select(workItemSelect)
|
|
185
|
+
.eq('project_id', projectId)
|
|
186
|
+
.eq('id', event.work_item_id)
|
|
187
|
+
.maybeSingle();
|
|
188
|
+
if (data)
|
|
189
|
+
workItem = data;
|
|
190
|
+
}
|
|
191
|
+
let proposals = [];
|
|
192
|
+
if (Array.isArray(event.proposal_ids) && event.proposal_ids.length) {
|
|
193
|
+
const { data } = await db()
|
|
194
|
+
.from('agent_proposals')
|
|
195
|
+
.select(proposalSelect)
|
|
196
|
+
.eq('project_id', projectId)
|
|
197
|
+
.in('id', event.proposal_ids);
|
|
198
|
+
if (data)
|
|
199
|
+
proposals = data;
|
|
200
|
+
}
|
|
201
|
+
return j({ context_event: event, work_item: workItem, proposals, idempotent: true });
|
|
202
|
+
}
|
|
124
203
|
async function submitContextEvent(input) {
|
|
125
204
|
const projectId = getProjectId();
|
|
205
|
+
const normalizedKey = input.idempotency_key.trim();
|
|
126
206
|
const existing = await db()
|
|
127
207
|
.from('ai_context_events')
|
|
128
208
|
.select('*')
|
|
129
209
|
.eq('project_id', projectId)
|
|
130
210
|
.eq('source', 'synchronex')
|
|
131
|
-
.eq('idempotency_key',
|
|
211
|
+
.eq('idempotency_key', normalizedKey)
|
|
132
212
|
.maybeSingle();
|
|
133
213
|
if (existing.error)
|
|
134
|
-
return
|
|
135
|
-
if (existing.data)
|
|
136
|
-
|
|
137
|
-
|
|
214
|
+
return toolError(dbToolError(existing.error));
|
|
215
|
+
if (existing.data) {
|
|
216
|
+
if (!isSameContextEventPayload(existing.data, input)) {
|
|
217
|
+
return toolError({ code: 'conflict', error: 'idempotency_key was already used with different content' });
|
|
218
|
+
}
|
|
219
|
+
return buildContextReplay(projectId, existing.data);
|
|
220
|
+
}
|
|
221
|
+
let orgId;
|
|
222
|
+
try {
|
|
223
|
+
orgId = await getOrgIdForProject(projectId);
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
return toolError(dbToolError(err));
|
|
227
|
+
}
|
|
138
228
|
const inserted = await db()
|
|
139
229
|
.from('ai_context_events')
|
|
140
230
|
.insert({
|
|
141
231
|
org_id: orgId,
|
|
142
232
|
project_id: projectId,
|
|
143
|
-
workspace_id: input.workspace_id,
|
|
233
|
+
workspace_id: input.workspace_id.trim(),
|
|
144
234
|
source: 'synchronex',
|
|
145
235
|
event_type: input.event_type,
|
|
146
|
-
title: input.title,
|
|
147
|
-
detail: input.detail,
|
|
236
|
+
title: input.title.trim(),
|
|
237
|
+
detail: input.detail.trim(),
|
|
148
238
|
evidence: input.evidence ?? {},
|
|
149
239
|
suggested_actions: input.suggested_actions ?? [],
|
|
150
|
-
idempotency_key:
|
|
240
|
+
idempotency_key: normalizedKey,
|
|
151
241
|
})
|
|
152
242
|
.select('*')
|
|
153
243
|
.single();
|
|
@@ -157,17 +247,21 @@ async function submitContextEvent(input) {
|
|
|
157
247
|
.select('*')
|
|
158
248
|
.eq('project_id', projectId)
|
|
159
249
|
.eq('source', 'synchronex')
|
|
160
|
-
.eq('idempotency_key',
|
|
250
|
+
.eq('idempotency_key', normalizedKey)
|
|
161
251
|
.maybeSingle();
|
|
162
252
|
if (duplicate.error)
|
|
163
|
-
return
|
|
164
|
-
if (duplicate.data)
|
|
165
|
-
|
|
253
|
+
return toolError(dbToolError(duplicate.error));
|
|
254
|
+
if (duplicate.data) {
|
|
255
|
+
if (!isSameContextEventPayload(duplicate.data, input)) {
|
|
256
|
+
return toolError({ code: 'conflict', error: 'idempotency_key was already used with different content' });
|
|
257
|
+
}
|
|
258
|
+
return buildContextReplay(projectId, duplicate.data);
|
|
259
|
+
}
|
|
166
260
|
}
|
|
167
261
|
if (inserted.error)
|
|
168
|
-
return
|
|
262
|
+
return toolError(dbToolError(inserted.error));
|
|
169
263
|
const event = inserted.data;
|
|
170
|
-
const segmentAction = segmentActionFromContext(input.event_type, input.suggested_actions, input.title, input.detail);
|
|
264
|
+
const segmentAction = segmentActionFromContext(input.event_type, input.suggested_actions, input.title.trim(), input.detail.trim());
|
|
171
265
|
const proposals = [];
|
|
172
266
|
if (segmentAction) {
|
|
173
267
|
const targetId = stableUuid(`synchronex-context:${projectId}:${event.id}:segment`);
|
|
@@ -190,7 +284,7 @@ async function submitContextEvent(input) {
|
|
|
190
284
|
sample_contact_ids: [],
|
|
191
285
|
recommendation_kind: 'synchronex_context',
|
|
192
286
|
},
|
|
193
|
-
reasoning: input.detail,
|
|
287
|
+
reasoning: input.detail.trim(),
|
|
194
288
|
source_data_refs: [{ table: 'email.ai_context_events', ids: [event.id] }],
|
|
195
289
|
confidence: typeof input.evidence?.confidence === 'number' ? Math.max(0, Math.min(100, Math.trunc(input.evidence.confidence))) : 75,
|
|
196
290
|
status: 'pending',
|
|
@@ -201,7 +295,7 @@ async function submitContextEvent(input) {
|
|
|
201
295
|
.select(proposalSelect)
|
|
202
296
|
.single();
|
|
203
297
|
if (error)
|
|
204
|
-
return
|
|
298
|
+
return toolError(dbToolError(error));
|
|
205
299
|
proposals.push(proposal);
|
|
206
300
|
}
|
|
207
301
|
const proposalIds = proposals.map((proposal) => proposal.id);
|
|
@@ -215,21 +309,21 @@ async function submitContextEvent(input) {
|
|
|
215
309
|
severity: input.event_type === 'risk_signal' ? 'medium' : 'info',
|
|
216
310
|
confidence: typeof input.evidence?.confidence === 'number' ? Math.max(0, Math.min(100, Math.trunc(input.evidence.confidence))) : null,
|
|
217
311
|
source: 'synchronex',
|
|
218
|
-
title: input.title,
|
|
219
|
-
why_now: input.detail,
|
|
220
|
-
evidence: { ...(input.evidence ?? {}), context_event_id: event.id, synchronex_workspace_id: input.workspace_id, event_type: input.event_type },
|
|
312
|
+
title: input.title.trim(),
|
|
313
|
+
why_now: input.detail.trim(),
|
|
314
|
+
evidence: { ...(input.evidence ?? {}), context_event_id: event.id, synchronex_workspace_id: input.workspace_id.trim(), event_type: input.event_type },
|
|
221
315
|
recommended_actions: proposalIds.length
|
|
222
316
|
? proposalIds.map((proposalId) => ({ type: 'review_proposal', label: 'Review recommended segment', proposal_id: proposalId, risk: 'low' }))
|
|
223
317
|
: [{ type: 'review_context', label: 'Review Synchronex context', risk: 'low' }],
|
|
224
318
|
surfaces: proposalIds.length ? ['overview', 'segments'] : ['overview'],
|
|
225
319
|
proposal_ids: proposalIds,
|
|
226
|
-
dedupe_key: `synchronex:${input.workspace_id}:${
|
|
320
|
+
dedupe_key: `synchronex:${input.workspace_id.trim()}:${normalizedKey}`,
|
|
227
321
|
cooldown_until: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
228
322
|
})
|
|
229
323
|
.select(workItemSelect)
|
|
230
324
|
.single();
|
|
231
325
|
if (workError)
|
|
232
|
-
return
|
|
326
|
+
return toolError(dbToolError(workError));
|
|
233
327
|
const { data: updatedEvent, error: updateError } = await db()
|
|
234
328
|
.from('ai_context_events')
|
|
235
329
|
.update({ work_item_id: workItem.id, proposal_ids: proposalIds })
|
|
@@ -238,8 +332,8 @@ async function submitContextEvent(input) {
|
|
|
238
332
|
.select('*')
|
|
239
333
|
.single();
|
|
240
334
|
if (updateError)
|
|
241
|
-
return
|
|
242
|
-
return { context_event: updatedEvent, work_item: workItem, proposals, idempotent: false };
|
|
335
|
+
return toolError(dbToolError(updateError));
|
|
336
|
+
return j({ context_event: updatedEvent, work_item: workItem, proposals, idempotent: false });
|
|
243
337
|
}
|
|
244
338
|
export function registerAiWorkTools(server) {
|
|
245
339
|
server.registerTool('list_ai_work_items', {
|
|
@@ -257,18 +351,21 @@ export function registerAiWorkTools(server) {
|
|
|
257
351
|
limit: z.number().int().min(1).max(100).optional().default(25),
|
|
258
352
|
},
|
|
259
353
|
}, async ({ status, surface, limit }) => {
|
|
354
|
+
const parsed = parseStatuses(status);
|
|
355
|
+
if ('error' in parsed)
|
|
356
|
+
return toolError(parsed.error);
|
|
260
357
|
let query = db()
|
|
261
358
|
.from('ai_work_items')
|
|
262
359
|
.select(workItemSelect)
|
|
263
360
|
.eq('project_id', getProjectId())
|
|
264
|
-
.in('status',
|
|
361
|
+
.in('status', parsed.statuses)
|
|
265
362
|
.order('generated_at', { ascending: false })
|
|
266
363
|
.limit(limit ?? 25);
|
|
267
364
|
if (surface)
|
|
268
365
|
query = query.contains('surfaces', [surface]);
|
|
269
366
|
const { data, error } = await query;
|
|
270
367
|
if (error)
|
|
271
|
-
return
|
|
368
|
+
return toolError(dbToolError(error));
|
|
272
369
|
return j({ work_items: data ?? [] });
|
|
273
370
|
});
|
|
274
371
|
server.registerTool('get_ai_work_item', {
|
|
@@ -291,9 +388,9 @@ export function registerAiWorkTools(server) {
|
|
|
291
388
|
.eq('id', work_item_id)
|
|
292
389
|
.maybeSingle();
|
|
293
390
|
if (error)
|
|
294
|
-
return
|
|
391
|
+
return toolError(dbToolError(error));
|
|
295
392
|
if (!data)
|
|
296
|
-
return
|
|
393
|
+
return toolError({ code: 'not_found', error: 'AI work item not found.' });
|
|
297
394
|
return j({ work_item: data });
|
|
298
395
|
});
|
|
299
396
|
server.registerTool('list_recommended_segments', {
|
|
@@ -310,16 +407,19 @@ export function registerAiWorkTools(server) {
|
|
|
310
407
|
limit: z.number().int().min(1).max(100).optional().default(25),
|
|
311
408
|
},
|
|
312
409
|
}, async ({ status, limit }) => {
|
|
410
|
+
const parsed = parseStatuses(status);
|
|
411
|
+
if ('error' in parsed)
|
|
412
|
+
return toolError(parsed.error);
|
|
313
413
|
const { data: workItems, error } = await db()
|
|
314
414
|
.from('ai_work_items')
|
|
315
415
|
.select(workItemSelect)
|
|
316
416
|
.eq('project_id', getProjectId())
|
|
317
417
|
.eq('kind', 'segment_recommendation')
|
|
318
|
-
.in('status',
|
|
418
|
+
.in('status', parsed.statuses)
|
|
319
419
|
.order('generated_at', { ascending: false })
|
|
320
420
|
.limit(limit ?? 25);
|
|
321
421
|
if (error)
|
|
322
|
-
return
|
|
422
|
+
return toolError(dbToolError(error));
|
|
323
423
|
const proposalIds = [...new Set((workItems ?? []).flatMap((item) => Array.isArray(item.proposal_ids) ? item.proposal_ids : []))];
|
|
324
424
|
let proposals = [];
|
|
325
425
|
if (proposalIds.length) {
|
|
@@ -329,7 +429,7 @@ export function registerAiWorkTools(server) {
|
|
|
329
429
|
.eq('project_id', getProjectId())
|
|
330
430
|
.in('id', proposalIds);
|
|
331
431
|
if (proposalResult.error)
|
|
332
|
-
return
|
|
432
|
+
return toolError(dbToolError(proposalResult.error));
|
|
333
433
|
proposals = proposalResult.data ?? [];
|
|
334
434
|
}
|
|
335
435
|
return j({ work_items: workItems ?? [], proposals });
|
|
@@ -347,8 +447,7 @@ export function registerAiWorkTools(server) {
|
|
|
347
447
|
work_item_id: z.string().uuid(),
|
|
348
448
|
},
|
|
349
449
|
}, async ({ work_item_id }) => {
|
|
350
|
-
|
|
351
|
-
return 'error' in result ? txt(`Error: ${result.error}`) : j(result);
|
|
450
|
+
return updateWorkItemStatus(work_item_id, 'seen');
|
|
352
451
|
});
|
|
353
452
|
server.registerTool('snooze_ai_work_item', {
|
|
354
453
|
title: 'Snooze AI Work Item',
|
|
@@ -364,8 +463,11 @@ export function registerAiWorkTools(server) {
|
|
|
364
463
|
snoozed_until: z.string().datetime(),
|
|
365
464
|
},
|
|
366
465
|
}, async ({ work_item_id, snoozed_until }) => {
|
|
367
|
-
const
|
|
368
|
-
|
|
466
|
+
const snoozedTime = new Date(snoozed_until).getTime();
|
|
467
|
+
if (!Number.isFinite(snoozedTime) || snoozedTime <= Date.now()) {
|
|
468
|
+
return toolError({ code: 'validation_failed', error: 'snoozed_until must be in the future.' });
|
|
469
|
+
}
|
|
470
|
+
return updateWorkItemStatus(work_item_id, 'snoozed', snoozed_until);
|
|
369
471
|
});
|
|
370
472
|
server.registerTool('dismiss_ai_work_item', {
|
|
371
473
|
title: 'Dismiss AI Work Item',
|
|
@@ -380,8 +482,7 @@ export function registerAiWorkTools(server) {
|
|
|
380
482
|
work_item_id: z.string().uuid(),
|
|
381
483
|
},
|
|
382
484
|
}, async ({ work_item_id }) => {
|
|
383
|
-
|
|
384
|
-
return 'error' in result ? txt(`Error: ${result.error}`) : j(result);
|
|
485
|
+
return updateWorkItemStatus(work_item_id, 'dismissed');
|
|
385
486
|
});
|
|
386
487
|
server.registerTool('submit_ai_context_event', {
|
|
387
488
|
title: 'Submit AI Context Event',
|
|
@@ -394,8 +495,7 @@ export function registerAiWorkTools(server) {
|
|
|
394
495
|
description: 'Submit Synchronex strategic context into Sendinel AI work queue without directly mutating email state.',
|
|
395
496
|
inputSchema: contextEventInput,
|
|
396
497
|
}, async (input) => {
|
|
397
|
-
|
|
398
|
-
return 'error' in result ? txt(`Error: ${result.error}`) : j(result);
|
|
498
|
+
return submitContextEvent(input);
|
|
399
499
|
});
|
|
400
500
|
server.registerTool('list_agent_proposals', {
|
|
401
501
|
title: 'List Agent Proposals',
|
|
@@ -410,7 +510,7 @@ export function registerAiWorkTools(server) {
|
|
|
410
510
|
status: z.enum(['pending', 'applied', 'auto_applied', 'dismissed', 'disputed', 'stale', 'failed', 'expired']).optional(),
|
|
411
511
|
type: z.string().min(1).max(96).optional(),
|
|
412
512
|
target_table: z.string().min(1).max(96).optional(),
|
|
413
|
-
target_id: z.string().optional(),
|
|
513
|
+
target_id: z.string().uuid().optional(),
|
|
414
514
|
limit: z.number().int().min(1).max(100).optional().default(25),
|
|
415
515
|
},
|
|
416
516
|
}, async ({ status, type, target_table, target_id, limit }) => {
|
|
@@ -430,7 +530,7 @@ export function registerAiWorkTools(server) {
|
|
|
430
530
|
query = query.eq('target_id', target_id);
|
|
431
531
|
const { data, error } = await query;
|
|
432
532
|
if (error)
|
|
433
|
-
return
|
|
533
|
+
return toolError(dbToolError(error));
|
|
434
534
|
return j({ proposals: data ?? [] });
|
|
435
535
|
});
|
|
436
536
|
server.registerTool('get_agent_proposal', {
|
|
@@ -453,9 +553,9 @@ export function registerAiWorkTools(server) {
|
|
|
453
553
|
.eq('id', proposal_id)
|
|
454
554
|
.maybeSingle();
|
|
455
555
|
if (error)
|
|
456
|
-
return
|
|
556
|
+
return toolError(dbToolError(error));
|
|
457
557
|
if (!data)
|
|
458
|
-
return
|
|
558
|
+
return toolError({ code: 'not_found', error: 'Agent proposal not found.' });
|
|
459
559
|
return j({ proposal: data });
|
|
460
560
|
});
|
|
461
561
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendinel/mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.190",
|
|
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",
|