@sendinel/mcp-server 1.0.145 → 1.0.147

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.
Files changed (44) hide show
  1. package/build/lib/action-approvals.js +3 -1
  2. package/build/lib/db-error.d.ts +39 -6
  3. package/build/lib/db-error.js +221 -11
  4. package/build/lib/tags.d.ts +10 -0
  5. package/build/lib/tags.js +29 -0
  6. package/build/server.js +5 -3
  7. package/build/tools/ab-testing.js +4 -3
  8. package/build/tools/advisor.js +2 -2
  9. package/build/tools/ai-work.js +14 -13
  10. package/build/tools/analytics.js +26 -26
  11. package/build/tools/approvals.js +3 -1
  12. package/build/tools/automations.js +12 -11
  13. package/build/tools/brand-kit.js +4 -3
  14. package/build/tools/campaign-sets.js +11 -10
  15. package/build/tools/campaigns.js +73 -74
  16. package/build/tools/compound.js +7 -6
  17. package/build/tools/connector.js +4 -3
  18. package/build/tools/contacts.js +116 -86
  19. package/build/tools/content.js +16 -16
  20. package/build/tools/convert-imported.js +8 -7
  21. package/build/tools/data-proposals.js +8 -7
  22. package/build/tools/data.js +39 -38
  23. package/build/tools/delivery-ops.js +32 -32
  24. package/build/tools/drafts.js +10 -9
  25. package/build/tools/forms.js +5 -4
  26. package/build/tools/gdpr.js +10 -9
  27. package/build/tools/landing-pages.js +3 -3
  28. package/build/tools/lists.js +21 -20
  29. package/build/tools/newsletters.js +26 -25
  30. package/build/tools/org.js +26 -23
  31. package/build/tools/segments.js +4 -5
  32. package/build/tools/short-links.js +7 -6
  33. package/build/tools/signatures.js +10 -9
  34. package/build/tools/sites.js +9 -8
  35. package/build/tools/sms.js +14 -13
  36. package/build/tools/social-posts.js +20 -19
  37. package/build/tools/social-replies.js +7 -6
  38. package/build/tools/surveys.js +10 -9
  39. package/build/tools/templates.js +14 -13
  40. package/build/tools/warmup.js +5 -4
  41. package/build/tools/webhooks.js +5 -4
  42. package/package.json +1 -1
  43. package/build/lib/db-errors.d.ts +0 -12
  44. package/build/lib/db-errors.js +0 -20
@@ -133,7 +133,7 @@ export function registerContentTools(server) {
133
133
  if (error?.code === '23505')
134
134
  return j({ error: 'slug already exists', code: 'conflict' });
135
135
  if (error)
136
- return j({ error: error.message, code: error.code });
136
+ return j({ ...dbToolError(error), code: error.code });
137
137
  const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? process.env.SENDINEL_APP_URL ?? 'https://sendinel.ai';
138
138
  return j({ asset_link: { ...data, url: `${appUrl.replace(/\/$/, '')}/a/${data.slug}` } });
139
139
  });
@@ -160,7 +160,7 @@ export function registerContentTools(server) {
160
160
  query = query.eq('status', status);
161
161
  const { data, error } = await query;
162
162
  if (error)
163
- return j({ error: error.message });
163
+ return j(dbToolError(error));
164
164
  const appUrl = (process.env.NEXT_PUBLIC_APP_URL ?? process.env.SENDINEL_APP_URL ?? 'https://sendinel.ai').replace(/\/$/, '');
165
165
  return j({ asset_links: (data ?? []).map((row) => ({ ...row, url: `${appUrl}/a/${row.slug}` })) });
166
166
  });
@@ -192,7 +192,7 @@ export function registerContentTools(server) {
192
192
  .order('viewed_at', { ascending: false })
193
193
  .limit(limit);
194
194
  if (error)
195
- return j({ error: error.message });
195
+ return j(dbToolError(error));
196
196
  return j({ views: data ?? [] });
197
197
  });
198
198
  server.registerTool('list_content_blocks', {
@@ -211,7 +211,7 @@ export function registerContentTools(server) {
211
211
  query = query.eq('category', category);
212
212
  const { data, error } = await query;
213
213
  if (error)
214
- return j({ error: error.message });
214
+ return j(dbToolError(error));
215
215
  return j({ blocks: data ?? [] });
216
216
  });
217
217
  server.registerTool('create_content_block', {
@@ -258,7 +258,7 @@ export function registerContentTools(server) {
258
258
  }, withApproval('delete_content_block', async ({ block_id }) => {
259
259
  const { error } = await db().from('content_blocks').delete().eq('project_id', getProjectId()).eq('id', block_id);
260
260
  if (error)
261
- return j({ error: error.message });
261
+ return j(dbToolError(error));
262
262
  return j({ deleted: true });
263
263
  }));
264
264
  server.registerTool('list_content_sources', {
@@ -277,7 +277,7 @@ export function registerContentTools(server) {
277
277
  .eq('project_id', getProjectId())
278
278
  .order('created_at', { ascending: false });
279
279
  if (error)
280
- return j({ error: error.message });
280
+ return j(dbToolError(error));
281
281
  return j({ sources: (data ?? []).map((row) => ({ id: row.id, name: row.name, type: row.source_type, url: row.feed_url, active: row.active, last_fetched: row.last_polled_at, draft_count: row.items_generated ?? 0 })) });
282
282
  });
283
283
  server.registerTool('trigger_content_pipeline', {
@@ -296,7 +296,7 @@ export function registerContentTools(server) {
296
296
  query = query.eq('id', source_id);
297
297
  const { error } = await query;
298
298
  if (error)
299
- return j({ error: error.message });
299
+ return j(dbToolError(error));
300
300
  return j({ drafts_created: 0, queued_for_pipeline: true });
301
301
  });
302
302
  // Blogs (#2033): the primary agent door for ingesting a blog written by the
@@ -350,7 +350,7 @@ export function registerContentTools(server) {
350
350
  .eq('project_id', getProjectId())
351
351
  .eq('is_active', true);
352
352
  if (destErr)
353
- return j({ error: destErr.message });
353
+ return j(dbToolError(destErr));
354
354
  const target = publish_to.trim().toLowerCase();
355
355
  const match = (dests ?? []).find((d) => d.id === publish_to || String(d.name ?? '').toLowerCase() === target);
356
356
  if (!match) {
@@ -382,7 +382,7 @@ export function registerContentTools(server) {
382
382
  .select('id, title, slug, status, source, scheduled_at, created_at')
383
383
  .single();
384
384
  if (error)
385
- return j({ error: error.message, code: error.code });
385
+ return j({ ...dbToolError(error), code: error.code });
386
386
  return j({ blog_post: data, ...(auto_publish ? { note: scheduled_at ? 'Scheduled — the publish worker will send it at scheduled_at.' : 'Queued for immediate publish — the worker publishes it within a few minutes.' } : {}) });
387
387
  }));
388
388
  server.registerTool('list_blog_posts', {
@@ -410,7 +410,7 @@ export function registerContentTools(server) {
410
410
  query = query.neq('status', 'archived');
411
411
  const { data, error } = await query;
412
412
  if (error)
413
- return j({ error: error.message });
413
+ return j(dbToolError(error));
414
414
  return j({ blog_posts: data ?? [] });
415
415
  });
416
416
  server.registerTool('get_blog_post', {
@@ -432,7 +432,7 @@ export function registerContentTools(server) {
432
432
  .eq('id', blog_post_id)
433
433
  .maybeSingle();
434
434
  if (error)
435
- return j({ error: error.message });
435
+ return j(dbToolError(error));
436
436
  if (!data)
437
437
  return j({ error: 'Blog post not found', code: 'NOT_FOUND' });
438
438
  return j({ blog_post: data });
@@ -460,7 +460,7 @@ export function registerContentTools(server) {
460
460
  .eq('id', blog_post_id)
461
461
  .maybeSingle();
462
462
  if (error)
463
- return j({ error: error.message });
463
+ return j(dbToolError(error));
464
464
  if (!post)
465
465
  return j({ error: 'Blog post not found', code: 'NOT_FOUND' });
466
466
  const tags = Array.isArray(post.tags) ? post.tags.map((tag) => String(tag).toLowerCase()) : [];
@@ -499,7 +499,7 @@ export function registerContentTools(server) {
499
499
  .eq('id', blog_post_id)
500
500
  .maybeSingle();
501
501
  if (error)
502
- return j({ error: error.message });
502
+ return j(dbToolError(error));
503
503
  if (!post)
504
504
  return j({ error: 'Blog post not found', code: 'NOT_FOUND' });
505
505
  const scene = BLOG_IMAGE_SCENES.find((s) => s.id === scene_id);
@@ -542,7 +542,7 @@ export function registerContentTools(server) {
542
542
  .select('id, title, cover_image_url, updated_at')
543
543
  .maybeSingle();
544
544
  if (updateErr)
545
- return j({ error: updateErr.message, code: updateErr.code });
545
+ return j({ ...dbToolError(updateErr), code: updateErr.code });
546
546
  await adminDb().from('audit_log').insert({
547
547
  project_id: projectId,
548
548
  org_id: post.org_id ?? null,
@@ -556,7 +556,7 @@ export function registerContentTools(server) {
556
556
  return j({ scene_id, format: chosenFormat, asset: generated.asset, prompt: generated.prompt, blog_post: updated });
557
557
  }
558
558
  catch (err) {
559
- return j({ error: err instanceof Error ? err.message : 'Image generation failed', code: 'IMAGE_GENERATION_FAILED' });
559
+ return j({ error: err instanceof Error ? dbToolError(err).error : 'Image generation failed', code: 'IMAGE_GENERATION_FAILED' });
560
560
  }
561
561
  });
562
562
  server.registerTool('list_blog_destinations', {
@@ -576,7 +576,7 @@ export function registerContentTools(server) {
576
576
  .eq('is_active', true)
577
577
  .order('created_at', { ascending: false });
578
578
  if (error)
579
- return j({ error: error.message });
579
+ return j(dbToolError(error));
580
580
  return j({ destinations: data ?? [] });
581
581
  });
582
582
  }
@@ -7,6 +7,7 @@ import { validateTemplate } from '../lib/voice.js';
7
7
  import { logAudit } from '../audit.js';
8
8
  import { eventKeysFromHistory, contactPropertyKeys } from '../lib/content-sources.js';
9
9
  import { loadSet, previewOf, writeCampaignDrafts } from './campaign-sets.js';
10
+ import { dbToolError } from '../lib/db-error.js';
10
11
  /**
11
12
  * convert_imported_campaigns (#3689): turns campaigns imported from another platform (Customer.io
12
13
  * first) into new Sendinel drafts that can send. Call it without create to preview: converted
@@ -44,7 +45,7 @@ function reviewView(plan) {
44
45
  original_subject: email.original_subject,
45
46
  subject: email.subject,
46
47
  text: email.preview_text,
47
- check_these: email.flags.map((flag) => ({ kind: flag.kind, message: flag.message, ...(flag.excerpt ? { where: flag.excerpt } : {}) })),
48
+ check_these: email.flags.map((flag) => ({ kind: flag.kind, message: dbToolError(flag).error, ...(flag.excerpt ? { where: flag.excerpt } : {}) })),
48
49
  })),
49
50
  ...(plan.blocking.length ? { must_fix: plan.blocking } : {}),
50
51
  ...(plan.notes.length ? { notes: plan.notes } : {}),
@@ -70,7 +71,7 @@ export function registerConvertImportedTools(server) {
70
71
  .eq('project_id', projectId)
71
72
  .in('id', ids);
72
73
  if (sourceErr)
73
- return j({ error: sourceErr.message, code: 'CAMPAIGN_LOOKUP_FAILED' });
74
+ return j({ ...dbToolError(sourceErr), code: 'CAMPAIGN_LOOKUP_FAILED' });
74
75
  const byId = new Map((sources ?? []).map((c) => [c.id, c]));
75
76
  const missing = ids.filter((id) => !byId.has(id));
76
77
  if (missing.length)
@@ -83,7 +84,7 @@ export function registerConvertImportedTools(server) {
83
84
  .select('campaign_id, step_order, subject, body_html, body_text, delay_days, delay_hours')
84
85
  .in('campaign_id', ids);
85
86
  if (stepErr)
86
- return j({ error: stepErr.message, code: 'STEP_LOOKUP_FAILED' });
87
+ return j({ ...dbToolError(stepErr), code: 'STEP_LOOKUP_FAILED' });
87
88
  const steps = (stepRows ?? []);
88
89
  // Event data seen in history, per trigger. A failed lookup is reported, never treated as "none".
89
90
  const historyNotes = [];
@@ -110,7 +111,7 @@ export function registerConvertImportedTools(server) {
110
111
  .eq('project_id', projectId)
111
112
  .in('metadata->>converted_from', ids);
112
113
  if (existingErr)
113
- return j({ error: existingErr.message, code: 'CAMPAIGN_LOOKUP_FAILED' });
114
+ return j({ ...dbToolError(existingErr), code: 'CAMPAIGN_LOOKUP_FAILED' });
114
115
  const earlier = (existing ?? []);
115
116
  plans.forEach((plan) => {
116
117
  const twin = earlier.find((e) => e.metadata?.converted_from === plan.source_campaign_id && e.name === plan.name);
@@ -138,7 +139,7 @@ export function registerConvertImportedTools(server) {
138
139
  // Sender: the given one, the imports' own, or the only one. Never guessed between several.
139
140
  const { data: sites, error: sitesErr } = await db().from('sites').select('id, name, from_email, unsub_link_enabled').eq('project_id', projectId);
140
141
  if (sitesErr)
141
- return j({ error: sitesErr.message, code: 'SITE_LOOKUP_FAILED' });
142
+ return j({ ...dbToolError(sitesErr), code: 'SITE_LOOKUP_FAILED' });
142
143
  const importSites = [...new Set(ids.map((id) => byId.get(id).site_id).filter(Boolean))];
143
144
  const pick = site_id ?? (importSites.length === 1 ? importSites[0] : sites?.length === 1 ? sites[0].id : null);
144
145
  const site = (sites ?? []).find((s) => s.id === pick);
@@ -170,7 +171,7 @@ export function registerConvertImportedTools(server) {
170
171
  metadata: {
171
172
  converted_from: plan.source_campaign_id,
172
173
  converted_from_name: plan.source_name,
173
- review_flags: plan.emails.flatMap((email) => email.flags.map((flag) => ({ email: email.email, kind: flag.kind, message: flag.message, ...(flag.excerpt ? { excerpt: flag.excerpt } : {}) }))),
174
+ review_flags: plan.emails.flatMap((email) => email.flags.map((flag) => ({ email: email.email, kind: flag.kind, message: dbToolError(flag).error, ...(flag.excerpt ? { excerpt: flag.excerpt } : {}) }))),
174
175
  },
175
176
  emails: plan.emails.map((email) => ({
176
177
  subject: email.subject,
@@ -200,7 +201,7 @@ export function registerConvertImportedTools(server) {
200
201
  preview = previewOf(loaded.campaigns, loaded.steps);
201
202
  }
202
203
  catch (error) {
203
- return j({ campaign_set_id: setId, campaign_ids: createdIds, status: 'draft', warning: `Created, but the preview could not be read back: ${error instanceof Error ? error.message : String(error)}` });
204
+ return j({ campaign_set_id: setId, campaign_ids: createdIds, status: 'draft', warning: `Created, but the preview could not be read back: ${error instanceof Error ? dbToolError(error).error : String(error)}` });
204
205
  }
205
206
  return j({
206
207
  campaign_set_id: setId,
@@ -3,6 +3,7 @@ import { z } from 'zod';
3
3
  import { db } from '../db.js';
4
4
  import { getProjectId } from '../project.js';
5
5
  import { getAnthropicModelId } from '../anthropic-model.js';
6
+ import { dbToolError } 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
  async function engagementSnapshot(projectId) {
@@ -47,7 +48,7 @@ export function registerDataProposalTools(server) {
47
48
  .eq('id', projectId)
48
49
  .maybeSingle();
49
50
  if (projectError)
50
- return txt(`Error: ${projectError.message}`);
51
+ return txt(`Error: ${dbToolError(projectError).error}`);
51
52
  if (!project)
52
53
  return txt('Project not found.');
53
54
  const snapshot = await engagementSnapshot(projectId);
@@ -76,7 +77,7 @@ export function registerDataProposalTools(server) {
76
77
  .select('*')
77
78
  .single();
78
79
  if (error)
79
- return txt(`Error: ${error.message}`);
80
+ return txt(`Error: ${dbToolError(error).error}`);
80
81
  return j({ proposal: data });
81
82
  });
82
83
  server.registerTool('list_data_proposals', {
@@ -103,7 +104,7 @@ export function registerDataProposalTools(server) {
103
104
  query = query.eq('status', status);
104
105
  const { data, error } = await query;
105
106
  if (error)
106
- return txt(`Error: ${error.message}`);
107
+ return txt(`Error: ${dbToolError(error).error}`);
107
108
  return j({ proposals: data ?? [] });
108
109
  });
109
110
  server.registerTool('approve_data_proposal', {
@@ -127,7 +128,7 @@ export function registerDataProposalTools(server) {
127
128
  .eq('project_id', projectId)
128
129
  .maybeSingle();
129
130
  if (proposalError)
130
- return txt(`Error: ${proposalError.message}`);
131
+ return txt(`Error: ${dbToolError(proposalError).error}`);
131
132
  if (!proposal)
132
133
  return txt('Proposal not found.');
133
134
  if (!['pending', 'approved'].includes(proposal.status))
@@ -141,7 +142,7 @@ export function registerDataProposalTools(server) {
141
142
  .limit(1)
142
143
  .maybeSingle();
143
144
  if (siteError)
144
- return txt(`Error: ${siteError.message}`);
145
+ return txt(`Error: ${dbToolError(siteError).error}`);
145
146
  if (!site)
146
147
  return txt('No active site available for draft conversion.');
147
148
  const { data: draft, error: draftError } = await db()
@@ -158,7 +159,7 @@ export function registerDataProposalTools(server) {
158
159
  .select('id')
159
160
  .single();
160
161
  if (draftError)
161
- return txt(`Error: ${draftError.message}`);
162
+ return txt(`Error: ${dbToolError(draftError).error}`);
162
163
  const { data: updated, error: updateError } = await db()
163
164
  .from('data_proposals')
164
165
  .update({
@@ -171,7 +172,7 @@ export function registerDataProposalTools(server) {
171
172
  .select('*')
172
173
  .single();
173
174
  if (updateError)
174
- return txt(`Error: ${updateError.message}`);
175
+ return txt(`Error: ${dbToolError(updateError).error}`);
175
176
  return j({ proposal: updated, draft_id: draft.id });
176
177
  });
177
178
  }
@@ -7,6 +7,7 @@ import { normalizeActiveCampaignAccountUrl } from '../lib/url-validation.js';
7
7
  import { assertChannelCanExecute, checkApproval, currentChannelContext, requestApproval, withApproval } from '../lib/action-approvals.js';
8
8
  import { getProjectId } from '../project.js';
9
9
  import { logAudit } from '../audit.js';
10
+ import { dbToolError } from '../lib/db-error.js';
10
11
  const j = (data) => ({ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] });
11
12
  function generateApiKey() {
12
13
  const secret = `snk_${randomBytes(32).toString('hex')}`;
@@ -263,14 +264,14 @@ export function registerDataTools(server) {
263
264
  query = query.eq('is_active', true);
264
265
  const { data, error } = await query;
265
266
  if (error)
266
- return j({ error: error.message });
267
+ return j(dbToolError(error));
267
268
  return j({
268
269
  platforms: CONNECTED_PLATFORMS.filter((platform) => platform !== 'kit'),
269
270
  connections: (data ?? []).map(sanitizeConnection),
270
271
  });
271
272
  }
272
273
  catch (error) {
273
- return j({ error: error instanceof Error ? error.message : 'Failed to list connected platforms' });
274
+ return j({ error: error instanceof Error ? dbToolError(error).error : 'Failed to list connected platforms' });
274
275
  }
275
276
  });
276
277
  server.registerTool('list_provider_credentials', {
@@ -309,9 +310,9 @@ export function registerDataTools(server) {
309
310
  .limit(50);
310
311
  const [platformResult, importResult] = await Promise.all([platformQuery, importQuery]);
311
312
  if (platformResult.error)
312
- return j({ error: platformResult.error.message });
313
+ return j(dbToolError(platformResult.error));
313
314
  if (importResult.error)
314
- return j({ error: importResult.error.message });
315
+ return j(dbToolError(importResult.error));
315
316
  const mapRow = (row, source) => {
316
317
  let credential;
317
318
  try {
@@ -351,7 +352,7 @@ export function registerDataTools(server) {
351
352
  return j({ credentials: [...platformCredentials, ...importCredentials] });
352
353
  }
353
354
  catch (error) {
354
- return j({ error: error instanceof Error ? error.message : 'Failed to list provider credentials' });
355
+ return j({ error: error instanceof Error ? dbToolError(error).error : 'Failed to list provider credentials' });
355
356
  }
356
357
  });
357
358
  server.registerTool('delete_provider_credential', {
@@ -389,7 +390,7 @@ export function registerDataTools(server) {
389
390
  .eq('project_id', context.projectId)
390
391
  .maybeSingle();
391
392
  if (credentialErr)
392
- return j({ error: credentialErr.message });
393
+ return j(dbToolError(credentialErr));
393
394
  if (!credential)
394
395
  return j({ error: 'Credential not found' });
395
396
  const provider = parsed.source === 'platform' ? credential.platform : credential.provider;
@@ -449,7 +450,7 @@ export function registerDataTools(server) {
449
450
  .select('id, platform')
450
451
  .maybeSingle();
451
452
  if (error)
452
- return j({ error: error.message });
453
+ return j(dbToolError(error));
453
454
  if (!data)
454
455
  return j({ error: 'Credential not found' });
455
456
  await logAudit({
@@ -471,7 +472,7 @@ export function registerDataTools(server) {
471
472
  .select('id, provider')
472
473
  .maybeSingle();
473
474
  if (error)
474
- return j({ error: error.message });
475
+ return j(dbToolError(error));
475
476
  if (!data)
476
477
  return j({ error: 'Credential not found' });
477
478
  await logAudit({
@@ -483,7 +484,7 @@ export function registerDataTools(server) {
483
484
  return j({ deleted: true, id: credential_id, source: 'import', provider: data.provider });
484
485
  }
485
486
  catch (error) {
486
- return j({ error: error instanceof Error ? error.message : 'Failed to delete provider credential' });
487
+ return j({ error: error instanceof Error ? dbToolError(error).error : 'Failed to delete provider credential' });
487
488
  }
488
489
  });
489
490
  server.registerTool('connect_platform', {
@@ -535,7 +536,7 @@ export function registerDataTools(server) {
535
536
  .select('id, org_id, project_id, platform, display_name, config, status, is_active, last_synced_at, last_error, created_at, updated_at')
536
537
  .single();
537
538
  if (error)
538
- return j({ error: error.message });
539
+ return j(dbToolError(error));
539
540
  await logAudit({
540
541
  action: 'platform_connection.connected',
541
542
  resourceType: 'platform_connection',
@@ -549,7 +550,7 @@ export function registerDataTools(server) {
549
550
  });
550
551
  }
551
552
  catch (error) {
552
- return j({ error: error instanceof Error ? error.message : 'Failed to connect platform' });
553
+ return j({ error: error instanceof Error ? dbToolError(error).error : 'Failed to connect platform' });
553
554
  }
554
555
  });
555
556
  server.registerTool('disconnect_platform', {
@@ -585,7 +586,7 @@ export function registerDataTools(server) {
585
586
  query = query.eq('platform', normalizePlatform(platform));
586
587
  const { data, error } = await query.select('id, platform');
587
588
  if (error)
588
- return j({ error: error.message });
589
+ return j(dbToolError(error));
589
590
  await logAudit({
590
591
  action: 'platform_connection.disconnected',
591
592
  resourceType: 'platform_connection',
@@ -595,7 +596,7 @@ export function registerDataTools(server) {
595
596
  return j({ disconnected: data?.length ?? 0, connections: data ?? [] });
596
597
  }
597
598
  catch (error) {
598
- return j({ error: error instanceof Error ? error.message : 'Failed to disconnect platform' });
599
+ return j({ error: error instanceof Error ? dbToolError(error).error : 'Failed to disconnect platform' });
599
600
  }
600
601
  }));
601
602
  server.registerTool('list_platform_events', {
@@ -628,11 +629,11 @@ export function registerDataTools(server) {
628
629
  query = query.eq('platform', normalizePlatform(platform));
629
630
  const { data, error } = await query;
630
631
  if (error)
631
- return j({ error: error.message });
632
+ return j(dbToolError(error));
632
633
  return j({ events: data ?? [] });
633
634
  }
634
635
  catch (error) {
635
- return j({ error: error instanceof Error ? error.message : 'Failed to list platform events' });
636
+ return j({ error: error instanceof Error ? dbToolError(error).error : 'Failed to list platform events' });
636
637
  }
637
638
  });
638
639
  server.registerTool('configure_platform_trigger', {
@@ -673,7 +674,7 @@ export function registerDataTools(server) {
673
674
  .eq('is_active', true)
674
675
  .maybeSingle();
675
676
  if (connectionError)
676
- return j({ error: connectionError.message });
677
+ return j(dbToolError(connectionError));
677
678
  if (!connection)
678
679
  return j({ error: 'active platform connection not found' });
679
680
  const currentConfig = connection.config ?? connection.meta ?? {};
@@ -692,7 +693,7 @@ export function registerDataTools(server) {
692
693
  .select('id, platform, display_name, config, status, is_active, last_synced_at, updated_at')
693
694
  .single();
694
695
  if (error)
695
- return j({ error: error.message });
696
+ return j(dbToolError(error));
696
697
  await logAudit({
697
698
  action: 'platform_trigger.configured',
698
699
  resourceType: 'platform_connection',
@@ -702,7 +703,7 @@ export function registerDataTools(server) {
702
703
  return j({ connection: sanitizeConnection(data), trigger });
703
704
  }
704
705
  catch (error) {
705
- return j({ error: error instanceof Error ? error.message : 'Failed to configure platform trigger' });
706
+ return j({ error: error instanceof Error ? dbToolError(error).error : 'Failed to configure platform trigger' });
706
707
  }
707
708
  });
708
709
  server.registerTool('list_custom_webhooks', {
@@ -734,7 +735,7 @@ export function registerDataTools(server) {
734
735
  query = query.eq('is_active', true);
735
736
  const { data, error } = await query;
736
737
  if (error)
737
- return j({ error: error.message });
738
+ return j(dbToolError(error));
738
739
  return j({
739
740
  connections: (data ?? []).map((connection) => ({
740
741
  id: connection.id,
@@ -749,7 +750,7 @@ export function registerDataTools(server) {
749
750
  });
750
751
  }
751
752
  catch (error) {
752
- return j({ error: error instanceof Error ? error.message : 'Failed to list custom webhooks' });
753
+ return j({ error: error instanceof Error ? dbToolError(error).error : 'Failed to list custom webhooks' });
753
754
  }
754
755
  });
755
756
  server.registerTool('create_custom_webhook', {
@@ -798,7 +799,7 @@ export function registerDataTools(server) {
798
799
  .select('id')
799
800
  .single();
800
801
  if (error || !data)
801
- return j({ error: error?.message ?? 'Failed to create custom webhook' });
802
+ return j({ error: (error ? dbToolError(error).error : undefined) ?? 'Failed to create custom webhook' });
802
803
  const endpointUrl = customWebhookEndpointUrl(data.id);
803
804
  await client
804
805
  .schema('email')
@@ -818,7 +819,7 @@ export function registerDataTools(server) {
818
819
  });
819
820
  }
820
821
  catch (error) {
821
- return j({ error: error instanceof Error ? error.message : 'Failed to create custom webhook' });
822
+ return j({ error: error instanceof Error ? dbToolError(error).error : 'Failed to create custom webhook' });
822
823
  }
823
824
  });
824
825
  server.registerTool('rotate_custom_webhook_secret', {
@@ -849,7 +850,7 @@ export function registerDataTools(server) {
849
850
  .eq('is_active', true)
850
851
  .maybeSingle();
851
852
  if (lookupError)
852
- return j({ error: lookupError.message });
853
+ return j(dbToolError(lookupError));
853
854
  if (!connection)
854
855
  return j({ error: 'Custom webhook not found' });
855
856
  if (!confirmed) {
@@ -903,7 +904,7 @@ export function registerDataTools(server) {
903
904
  .select('id')
904
905
  .maybeSingle();
905
906
  if (error)
906
- return j({ error: error.message });
907
+ return j(dbToolError(error));
907
908
  if (!updated)
908
909
  return j({ error: 'Custom webhook is no longer active' });
909
910
  await logAudit({
@@ -919,7 +920,7 @@ export function registerDataTools(server) {
919
920
  });
920
921
  }
921
922
  catch (error) {
922
- return j({ error: error instanceof Error ? error.message : 'Failed to rotate custom webhook secret' });
923
+ return j({ error: error instanceof Error ? dbToolError(error).error : 'Failed to rotate custom webhook secret' });
923
924
  }
924
925
  });
925
926
  server.registerTool('list_import_history', {
@@ -939,7 +940,7 @@ export function registerDataTools(server) {
939
940
  .order('created_at', { ascending: false })
940
941
  .limit(limit);
941
942
  if (error)
942
- return j({ error: error.message });
943
+ return j(dbToolError(error));
943
944
  return j({
944
945
  imports: (data ?? []).map((row) => ({
945
946
  ...row,
@@ -966,7 +967,7 @@ export function registerDataTools(server) {
966
967
  .is('revoked_at', null)
967
968
  .order('created_at', { ascending: false });
968
969
  if (error)
969
- return j({ error: error.message });
970
+ return j(dbToolError(error));
970
971
  return j({ keys: data ?? [] });
971
972
  });
972
973
  server.registerTool('create_api_key', {
@@ -997,7 +998,7 @@ export function registerDataTools(server) {
997
998
  .select('id, name, slug')
998
999
  .single();
999
1000
  if (error)
1000
- return j({ error: error.message });
1001
+ return j(dbToolError(error));
1001
1002
  return j({ key_id: data.id, name: data.name, slug: data.slug, secret });
1002
1003
  }));
1003
1004
  server.registerTool('revoke_api_key', {
@@ -1013,7 +1014,7 @@ export function registerDataTools(server) {
1013
1014
  }, withApproval('revoke_api_key', async ({ key_id }) => {
1014
1015
  const { error } = await db().from('api_keys').update({ revoked_at: new Date().toISOString() }).eq('project_id', getProjectId()).eq('id', key_id);
1015
1016
  if (error)
1016
- return j({ error: error.message });
1017
+ return j(dbToolError(error));
1017
1018
  return j({ revoked: true });
1018
1019
  }));
1019
1020
  server.registerTool('migrate_campaigns_from', {
@@ -1042,7 +1043,7 @@ export function registerDataTools(server) {
1042
1043
  .eq('id', projectId)
1043
1044
  .maybeSingle();
1044
1045
  if (projectError)
1045
- return j({ error: projectError.message });
1046
+ return j(dbToolError(projectError));
1046
1047
  if (!project?.org_id)
1047
1048
  return j({ error: 'project not found' });
1048
1049
  let normalizedAccountUrl = account_url;
@@ -1051,7 +1052,7 @@ export function registerDataTools(server) {
1051
1052
  normalizedAccountUrl = normalizeActiveCampaignAccountUrl(account_url ?? '');
1052
1053
  }
1053
1054
  catch (error) {
1054
- return j({ error: error instanceof Error ? error.message : 'Invalid ActiveCampaign account URL' });
1055
+ return j({ error: error instanceof Error ? dbToolError(error).error : 'Invalid ActiveCampaign account URL' });
1055
1056
  }
1056
1057
  }
1057
1058
  const credentials = { api_key: oauth_token, account_url: normalizedAccountUrl };
@@ -1073,7 +1074,7 @@ export function registerDataTools(server) {
1073
1074
  .select('id')
1074
1075
  .single();
1075
1076
  if (error)
1076
- return j({ error: error.message });
1077
+ return j(dbToolError(error));
1077
1078
  return j({
1078
1079
  job_id: data.id,
1079
1080
  status: 'pending',
@@ -1116,7 +1117,7 @@ export function registerDataTools(server) {
1116
1117
  .eq('is_active', true)
1117
1118
  .maybeSingle();
1118
1119
  if (connectionError)
1119
- return j({ error: connectionError.message });
1120
+ return j(dbToolError(connectionError));
1120
1121
  if (!connection)
1121
1122
  return j({ error: 'platform connection not found' });
1122
1123
  if (!MIGRATION_PROVIDERS.includes(connection.platform))
@@ -1142,7 +1143,7 @@ export function registerDataTools(server) {
1142
1143
  .select('id')
1143
1144
  .single();
1144
1145
  if (error)
1145
- return j({ error: error.message });
1146
+ return j(dbToolError(error));
1146
1147
  await client.schema('email').from('platform_connections').update({ last_synced_at: new Date().toISOString() }).eq('id', connection_id);
1147
1148
  return j({ job_id: data.id, status: 'pending', connection_id, provider: connection.platform });
1148
1149
  });
@@ -1165,7 +1166,7 @@ export function registerDataTools(server) {
1165
1166
  query = query.eq('read', false);
1166
1167
  const { data, error } = await query;
1167
1168
  if (error)
1168
- return j({ error: error.message });
1169
+ return j(dbToolError(error));
1169
1170
  return j({ notifications: (data ?? []).map((row) => ({ ...row, read: row.read === true, read_at: null })) });
1170
1171
  });
1171
1172
  server.registerTool('mark_notifications_read', {
@@ -1184,7 +1185,7 @@ export function registerDataTools(server) {
1184
1185
  query = query.in('id', ids);
1185
1186
  const { data, error } = await query.select('id');
1186
1187
  if (error)
1187
- return j({ error: error.message });
1188
+ return j(dbToolError(error));
1188
1189
  return j({ updated: data?.length ?? 0 });
1189
1190
  });
1190
1191
  server.registerTool('tenant_export_full', {
@@ -1278,10 +1279,10 @@ export function registerDataTools(server) {
1278
1279
  upload = await client.storage.from(bucket).upload(path, createZip(files), { contentType: 'application/zip', upsert: true });
1279
1280
  }
1280
1281
  if (upload.error)
1281
- return j({ error: upload.error.message });
1282
+ return j(dbToolError(upload.error));
1282
1283
  const { data, error } = await client.storage.from(bucket).createSignedUrl(path, 24 * 3600);
1283
1284
  if (error)
1284
- return j({ error: error.message });
1285
+ return j(dbToolError(error));
1285
1286
  return j({
1286
1287
  download_url: data.signedUrl,
1287
1288
  expires_at: new Date(Date.now() + 24 * 3600 * 1000).toISOString(),