@sendinel/mcp-server 1.0.146 → 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 (42) 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/server.js +5 -3
  5. package/build/tools/ab-testing.js +4 -3
  6. package/build/tools/advisor.js +2 -2
  7. package/build/tools/ai-work.js +14 -13
  8. package/build/tools/analytics.js +26 -26
  9. package/build/tools/approvals.js +3 -1
  10. package/build/tools/automations.js +12 -11
  11. package/build/tools/brand-kit.js +4 -3
  12. package/build/tools/campaign-sets.js +11 -10
  13. package/build/tools/campaigns.js +73 -74
  14. package/build/tools/compound.js +7 -6
  15. package/build/tools/connector.js +4 -3
  16. package/build/tools/contacts.js +44 -45
  17. package/build/tools/content.js +16 -16
  18. package/build/tools/convert-imported.js +8 -7
  19. package/build/tools/data-proposals.js +8 -7
  20. package/build/tools/data.js +39 -38
  21. package/build/tools/delivery-ops.js +32 -32
  22. package/build/tools/drafts.js +10 -9
  23. package/build/tools/forms.js +5 -4
  24. package/build/tools/gdpr.js +10 -9
  25. package/build/tools/landing-pages.js +3 -3
  26. package/build/tools/lists.js +21 -20
  27. package/build/tools/newsletters.js +26 -25
  28. package/build/tools/org.js +26 -23
  29. package/build/tools/segments.js +4 -5
  30. package/build/tools/short-links.js +7 -6
  31. package/build/tools/signatures.js +10 -9
  32. package/build/tools/sites.js +9 -8
  33. package/build/tools/sms.js +14 -13
  34. package/build/tools/social-posts.js +20 -19
  35. package/build/tools/social-replies.js +7 -6
  36. package/build/tools/surveys.js +10 -9
  37. package/build/tools/templates.js +14 -13
  38. package/build/tools/warmup.js +5 -4
  39. package/build/tools/webhooks.js +5 -4
  40. package/package.json +1 -1
  41. package/build/lib/db-errors.d.ts +0 -12
  42. package/build/lib/db-errors.js +0 -20
@@ -380,7 +380,9 @@ export async function checkApproval(approvalId, options = {}) {
380
380
  .eq('id', approvalId)
381
381
  .eq('project_id', getProjectId())
382
382
  .maybeSingle();
383
- if (error || !data)
383
+ if (error)
384
+ throw error;
385
+ if (!data)
384
386
  return { approved: false, status: 'not_found' };
385
387
  if (options.action && data.action !== options.action) {
386
388
  return { approved: false, status: 'action_mismatch' };
@@ -1,10 +1,23 @@
1
- /** Public tool errors must not expose SQL, constraint names or database internals. */
2
- export declare function dbToolError(error: {
3
- code?: string;
4
- message?: string;
5
- }, message?: string): {
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ /** Stable, caller-safe codes shared by MCP tools and hosted usage tracking. */
3
+ export declare const TOOL_ERROR_CODES: readonly ['conflict', 'not_found', 'validation_failed', 'forbidden', 'plan_limit', 'internal_error'];
4
+ export type ToolErrorCode = typeof TOOL_ERROR_CODES[number];
5
+ /** Preserve the original compact helper's public wording and serialization. */
6
+ export declare function mapDbError(error: unknown): {
6
7
  error: string;
7
- code: string;
8
+ code: ToolErrorCode;
9
+ };
10
+ export declare function dbErrorResult(error: unknown): {
11
+ isError: boolean;
12
+ content: {
13
+ type: 'text';
14
+ text: string;
15
+ }[];
16
+ };
17
+ /** Preserve the original pretty-printed helper's public wording. Never copy diagnostics. */
18
+ export declare function dbToolError(error: unknown, message?: string): {
19
+ error: string;
20
+ code: "conflict" | "forbidden" | "internal_error" | "not_found" | "plan_limit" | "validation_failed";
8
21
  };
9
22
  /** Mark coded failures as MCP errors so the hosted api_usage classifier sees them. */
10
23
  export declare function toolError(body: unknown): {
@@ -14,3 +27,23 @@ export declare function toolError(body: unknown): {
14
27
  text: string;
15
28
  }[];
16
29
  };
30
+ /** Machine-code classification for api_usage only. Never rewrite response codes.
31
+ * Keys are case-insensitive; every supported legacy spelling is listed explicitly.
32
+ * Existing usage-only policy classes remain supported alongside the six tool classes. */
33
+ export declare const API_USAGE_ERROR_CODE_MAP: Readonly<Record<string, string>>;
34
+ type ToolResult = {
35
+ content: Array<{
36
+ type: string;
37
+ text?: string;
38
+ [key: string]: unknown;
39
+ }>;
40
+ isError?: boolean;
41
+ [key: string]: unknown;
42
+ };
43
+ /** Add a stable class to refusals without dropping existing fields or success data. */
44
+ export declare function codedToolResult<T extends ToolResult>(result: T): T;
45
+ /** SDK, HTTP and gateway registration share the same exception/refusal boundary. */
46
+ export declare function withToolErrors(handler: (...args: any[]) => any): (...args: any[]) => Promise<any>;
47
+ /** Include SDK validation, which runs before registered tool handlers. */
48
+ export declare function installToolErrorBoundary(server: McpServer): void;
49
+ export {};
@@ -1,17 +1,227 @@
1
- /** Public tool errors must not expose SQL, constraint names or database internals. */
1
+ import { CallToolRequestSchema, ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
2
+ /** Stable, caller-safe codes shared by MCP tools and hosted usage tracking. */
3
+ export const TOOL_ERROR_CODES = ['conflict', 'not_found', 'validation_failed', 'forbidden', 'plan_limit', 'internal_error'];
4
+ function databaseCode(error) {
5
+ const code = error && typeof error === 'object' && 'code' in error ? String(error.code) : '';
6
+ if (code === '23505')
7
+ return 'conflict';
8
+ if (code === '23503' || code === 'PGRST116')
9
+ return 'not_found';
10
+ if (code === '42501')
11
+ return 'forbidden';
12
+ if (code.startsWith('23') || code.startsWith('22'))
13
+ return 'validation_failed';
14
+ return 'internal_error';
15
+ }
16
+ /** Preserve the original compact helper's public wording and serialization. */
17
+ export function mapDbError(error) {
18
+ const code = databaseCode(error);
19
+ const messages = {
20
+ conflict: 'A record with these values already exists.',
21
+ not_found: 'The requested or referenced record was not found.',
22
+ validation_failed: 'The supplied values are not valid. Check the input and try again.',
23
+ forbidden: 'Operation forbidden.',
24
+ plan_limit: 'Plan limit reached.',
25
+ internal_error: 'The operation could not be completed. Please try again later.',
26
+ };
27
+ return { error: messages[code], code };
28
+ }
29
+ export function dbErrorResult(error) {
30
+ console.warn('[mcp] Database operation failed:', error);
31
+ return { isError: true, content: [{ type: 'text', text: JSON.stringify(mapDbError(error)) }] };
32
+ }
33
+ /** Preserve the original pretty-printed helper's public wording. Never copy diagnostics. */
2
34
  export function dbToolError(error, message = 'Unable to complete database operation') {
3
- if (error.code === '23505')
4
- return { error: 'A record with these values already exists', code: 'conflict' };
5
- if (error.code === '23503')
6
- return { error: 'Referenced record not found', code: 'not_found' };
7
- if (error.code === '42501')
8
- return { error: 'Operation forbidden', code: 'forbidden' };
9
- if (error.code?.startsWith('22') || error.code === '23514' || error.code === '23502') {
10
- return { error: 'Invalid record values', code: 'validation_failed' };
11
- }
12
- return { error: message, code: 'internal_error' };
35
+ const code = databaseCode(error);
36
+ const messages = {
37
+ conflict: 'A record with these values already exists',
38
+ not_found: 'Referenced record not found',
39
+ forbidden: 'Operation forbidden',
40
+ validation_failed: 'Invalid record values',
41
+ plan_limit: 'Plan limit reached',
42
+ internal_error: message,
43
+ };
44
+ return { error: messages[code], code };
13
45
  }
14
46
  /** Mark coded failures as MCP errors so the hosted api_usage classifier sees them. */
15
47
  export function toolError(body) {
16
48
  return { isError: true, content: [{ type: 'text', text: JSON.stringify(body, null, 2) }] };
17
49
  }
50
+ /** Machine-code classification for api_usage only. Never rewrite response codes.
51
+ * Keys are case-insensitive; every supported legacy spelling is listed explicitly.
52
+ * Existing usage-only policy classes remain supported alongside the six tool classes. */
53
+ export const API_USAGE_ERROR_CODE_MAP = {
54
+ 'approval_call_takes_only_approval_id': 'validation_failed',
55
+ 'audience_not_understood': 'validation_failed',
56
+ 'audience_required': 'validation_failed',
57
+ 'bad_id': 'validation_failed',
58
+ 'bad_id_format': 'validation_failed',
59
+ 'bad_request': 'validation_failed',
60
+ 'campaigns_required': 'validation_failed',
61
+ 'content_required': 'validation_failed',
62
+ 'empty_audience': 'validation_failed',
63
+ 'empty_body': 'validation_failed',
64
+ 'empty_cohort': 'validation_failed',
65
+ 'empty_segment': 'validation_failed',
66
+ 'invalid_campaign_type': 'validation_failed',
67
+ 'invalid_condition_value': 'validation_failed',
68
+ 'invalid_date': 'validation_failed',
69
+ 'invalid_field': 'validation_failed',
70
+ 'invalid_id': 'validation_failed',
71
+ 'invalid_input': 'validation_failed',
72
+ 'invalid_json': 'validation_failed',
73
+ 'invalid_param': 'validation_failed',
74
+ 'invalid_project': 'validation_failed',
75
+ 'invalid_recipient': 'validation_failed',
76
+ 'invalid_segment_rules': 'validation_failed',
77
+ 'invalid_send_at': 'validation_failed',
78
+ 'invalid_set': 'validation_failed',
79
+ 'invalid_slug': 'validation_failed',
80
+ 'invalid_status': 'validation_failed',
81
+ 'invalid_target': 'validation_failed',
82
+ 'invalid_url': 'validation_failed',
83
+ 'missing_field': 'validation_failed',
84
+ 'no_body': 'validation_failed',
85
+ 'no_emails': 'validation_failed',
86
+ 'not_activatable': 'validation_failed',
87
+ 'not_imported': 'validation_failed',
88
+ 'site_mismatch': 'validation_failed',
89
+ 'step_wait_not_set': 'validation_failed',
90
+ 'subject_required': 'validation_failed',
91
+ 'switch_not_supported': 'validation_failed',
92
+ 'unprocessable_entity': 'validation_failed',
93
+ 'validation': 'validation_failed',
94
+ 'validation_failed': 'validation_failed',
95
+ '23505': 'conflict',
96
+ 'conflict': 'conflict',
97
+ 'content_changed_since_approval': 'conflict',
98
+ 'duplicate': 'conflict',
99
+ 'duplicate_email': 'conflict',
100
+ 'duplicate_name': 'conflict',
101
+ 'proposal_conflict': 'conflict',
102
+ 'site_changed_or_not_found': 'conflict',
103
+ 'slug_unavailable': 'conflict',
104
+ '23503': 'not_found',
105
+ 'campaign_not_found': 'not_found',
106
+ 'list_not_found': 'not_found',
107
+ 'not_found': 'not_found',
108
+ 'segment_not_found': 'not_found',
109
+ 'site_not_found': 'not_found',
110
+ 'approval_forbidden': 'forbidden',
111
+ 'chat_confirm_not_enabled': 'forbidden',
112
+ 'forbidden': 'forbidden',
113
+ 'implicit_access': 'forbidden',
114
+ 'insufficient_scope': 'forbidden',
115
+ 'last_owner': 'forbidden',
116
+ 'no_api_key': 'forbidden',
117
+ 'no_connection': 'forbidden',
118
+ 'no_org': 'forbidden',
119
+ 'no_synchronex_bundle': 'forbidden',
120
+ 'org_mismatch': 'forbidden',
121
+ 'project_access_denied': 'forbidden',
122
+ 'project_scope_mismatch': 'forbidden',
123
+ 'unauthorized': 'forbidden',
124
+ 'agent_mailbox_limit_reached': 'plan_limit',
125
+ 'ai_draft_limit_reached': 'plan_limit',
126
+ 'contact_limit_reached': 'plan_limit',
127
+ 'limit_reached': 'plan_limit',
128
+ 'plan_limit': 'plan_limit',
129
+ 'plan_required': 'plan_limit',
130
+ 'quota_exceeded': 'plan_limit',
131
+ 'activation_failed': 'internal_error',
132
+ 'ai_not_configured': 'internal_error',
133
+ 'ai_unavailable': 'internal_error',
134
+ 'approval_lookup_failed': 'internal_error',
135
+ 'audience_lookup_failed': 'internal_error',
136
+ 'campaign_create_failed': 'internal_error',
137
+ 'campaign_lookup_failed': 'internal_error',
138
+ 'campaign_visibility_query_failed': 'internal_error',
139
+ 'conversion_not_ready': 'internal_error',
140
+ 'draft_failed': 'internal_error',
141
+ 'enrollment_failed': 'internal_error',
142
+ 'event_intake_failed': 'internal_error',
143
+ 'generation_failed': 'internal_error',
144
+ 'image_generation_failed': 'internal_error',
145
+ 'internal_error': 'internal_error',
146
+ 'internal_server_error': 'internal_error',
147
+ 'one_send_lookup_failed': 'internal_error',
148
+ 'segment_lookup_failed': 'internal_error',
149
+ 'set_lookup_failed': 'internal_error',
150
+ 'site_lookup_failed': 'internal_error',
151
+ 'step_create_failed': 'internal_error',
152
+ 'step_lookup_failed': 'internal_error',
153
+ 'survey_link_not_ready': 'internal_error',
154
+ 'sandbox_blocked': 'sandbox_blocked',
155
+ 'send_allowlist_refused': 'send_allowlist_refused',
156
+ 'rate_limited': 'rate_limited',
157
+ };
158
+ /** Add a stable class to refusals without dropping existing fields or success data. */
159
+ export function codedToolResult(result) {
160
+ let failed = result.isError === true;
161
+ const content = result.content.map(block => {
162
+ if (block.type !== 'text' || typeof block.text !== 'string')
163
+ return block;
164
+ let body;
165
+ let textCode;
166
+ try {
167
+ body = JSON.parse(block.text);
168
+ }
169
+ catch {
170
+ // Legacy textual refusal paths. Ordinary text successes remain byte-for-byte.
171
+ if (!failed && !/^(?:Error:|Forbidden:|Permission denied:|Unknown skill:|Provide |Nothing to update|No fields to update|(?:Contact|Project|Source) (?:or.* )?not found)/i.test(block.text))
172
+ return block;
173
+ body = { error: block.text };
174
+ if (/^(?:MCP error -32602: )?(?:Input validation error: )?Invalid arguments for tool /.test(block.text))
175
+ textCode = 'validation_failed';
176
+ }
177
+ if (!body || typeof body !== 'object' || Array.isArray(body)) {
178
+ if (!failed)
179
+ return block;
180
+ body = { error: body };
181
+ }
182
+ if (!failed && !body.error && body.ok !== false && body.success !== false && body.valid !== false
183
+ && !(Array.isArray(body.errors) && body.errors.length) && !(body.status >= 400))
184
+ return block;
185
+ failed = true;
186
+ // An existing code is a frozen client surface, even when it is unfamiliar.
187
+ if ('code' in body)
188
+ return block;
189
+ const nestedCode = body.error && typeof body.error === 'object' ? body.error.code : undefined;
190
+ const nestedClass = typeof nestedCode === 'string' ? nestedCode.toLowerCase() : undefined;
191
+ const code = textCode ?? (TOOL_ERROR_CODES.includes(nestedClass) ? nestedClass
192
+ : (body.status === 404 ? 'not_found'
193
+ : body.status === 409 ? 'conflict' : body.status === 422 ? 'validation_failed'
194
+ : body.status === 401 || body.status === 403 ? 'forbidden' : 'internal_error'));
195
+ return { ...block, text: JSON.stringify({ ...body, code }, null, block.text.includes('\n') ? 2 : undefined) };
196
+ });
197
+ if (failed && !content.some(block => block.type === 'text')) {
198
+ content.push({ type: 'text', text: JSON.stringify(mapDbError(undefined)) });
199
+ }
200
+ return failed ? { ...result, isError: true, content } : result;
201
+ }
202
+ /** SDK, HTTP and gateway registration share the same exception/refusal boundary. */
203
+ export function withToolErrors(handler) {
204
+ return async (...args) => {
205
+ try {
206
+ return codedToolResult(await handler(...args));
207
+ }
208
+ catch (error) {
209
+ if (error instanceof McpError && error.code === ErrorCode.UrlElicitationRequired)
210
+ throw error;
211
+ return dbErrorResult(error);
212
+ }
213
+ };
214
+ }
215
+ const installedBoundaries = new WeakSet();
216
+ /** Include SDK validation, which runs before registered tool handlers. */
217
+ export function installToolErrorBoundary(server) {
218
+ if (!server.server || installedBoundaries.has(server.server))
219
+ return;
220
+ installedBoundaries.add(server.server);
221
+ const register = server.server.setRequestHandler.bind(server.server);
222
+ // Intercept the SDK's public registration method before its first registerTool.
223
+ // Other MCP methods and protocol exceptions (including URL elicitation) pass through.
224
+ server.server.setRequestHandler = ((schema, handler) => register(schema, schema === CallToolRequestSchema
225
+ ? async (...args) => codedToolResult(await handler(...args))
226
+ : handler));
227
+ }
package/build/server.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { withToolErrors, toolError, installToolErrorBoundary } from './lib/db-error.js';
1
2
  import { installApprovalCapabilityProbe } from './lib/approval-capabilities.js';
2
3
  import { installApprovalApp } from './lib/approval-app.js';
3
4
  import { runWithApprovalElicitation } from './lib/approval-elicitation.js';
@@ -343,13 +344,13 @@ function createScopeProxy(server, scopes, toolGroup, allowedTools) {
343
344
  const guardedHandler = (...args) => {
344
345
  if (!allowed()) {
345
346
  const text = agent ? agentConnectionRefusal(name) : tester ? testerConnectionRefusal(name) : `Forbidden: this action requires "${required}" scope.`;
346
- return { content: [{ type: 'text', text }] };
347
+ return toolError({ error: text, code: 'forbidden' });
347
348
  }
348
349
  return runWithToolContext(toolGroup, name, () => runWithApprovalElicitation(target, args[1], () => handler(...args)), scopes);
349
350
  };
350
- return target.registerTool(name, config, guardedHandler);
351
+ return target.registerTool(name, config, withToolErrors(guardedHandler));
351
352
  }
352
- return target.registerTool(name, config, (...args) => runWithToolContext(toolGroup, name, () => runWithApprovalElicitation(target, args[1], () => handler(...args))));
353
+ return target.registerTool(name, config, withToolErrors((...args) => runWithToolContext(toolGroup, name, () => runWithApprovalElicitation(target, args[1], () => handler(...args)))));
353
354
  };
354
355
  }
355
356
  return Reflect.get(target, prop, receiver);
@@ -369,6 +370,7 @@ export const SENDINEL_SERVER_INSTRUCTIONS = [
369
370
  'On the hosted connection, any tool call (and invoke_tool) also accepts an optional project_id to act on one project for that call only; it is checked each time, and approval-gated tools require it when the connection was switched to another project. In gateway mode, find tools with search_tools (plain words work, for example "send email" or "last campaign"), check arguments with describe_tool, then call invoke_tool.',
370
371
  ].join(' ');
371
372
  export function registerAllTools(server, options) {
373
+ installToolErrorBoundary(server);
372
374
  if (server.server)
373
375
  installApprovalCapabilityProbe(server.server);
374
376
  // Registry callers pass scopes only: the hosted decision bridge is never enabled from a catalog.
@@ -2,6 +2,7 @@ import { withApproval } from '../lib/action-approvals.js';
2
2
  import { z } from 'zod';
3
3
  import { db } from '../db.js';
4
4
  import { getProjectId } from '../project.js';
5
+ import { dbToolError } from '../lib/db-error.js';
5
6
  const txt = (text) => ({ content: [{ type: 'text', text }] });
6
7
  const j = (data) => txt(JSON.stringify(data, null, 2));
7
8
  export function registerAbTestingTools(server) {
@@ -79,7 +80,7 @@ export function registerAbTestingTools(server) {
79
80
  .select()
80
81
  .single();
81
82
  if (createErr)
82
- return txt(`Error: ${createErr.message}`);
83
+ return txt(`Error: ${dbToolError(createErr).error}`);
83
84
  return j({
84
85
  ...test,
85
86
  variant_steps: steps,
@@ -113,7 +114,7 @@ export function registerAbTestingTools(server) {
113
114
  q = q.eq('status', status);
114
115
  const { data, error } = await q;
115
116
  if (error)
116
- return txt(`Error: ${error.message}`);
117
+ return txt(`Error: ${dbToolError(error).error}`);
117
118
  return j(data);
118
119
  });
119
120
  server.registerTool('check_ab_significance', {
@@ -156,7 +157,7 @@ export function registerAbTestingTools(server) {
156
157
  .schema('email')
157
158
  .rpc('check_ab_significance', { p_test_id: test_id });
158
159
  if (error)
159
- return txt(`Error: ${error.message}`);
160
+ return txt(`Error: ${dbToolError(error).error}`);
160
161
  return j(data);
161
162
  });
162
163
  server.registerTool('promote_ab_winner', {
@@ -3,7 +3,7 @@ import { z } from 'zod';
3
3
  import Anthropic from '@anthropic-ai/sdk';
4
4
  import { db } from '../db.js';
5
5
  import { getProjectId } from '../project.js';
6
- import { toolError } from '../lib/db-error.js';
6
+ import { toolError, dbToolError } from '../lib/db-error.js';
7
7
  import { buildVoicePrompt } from '../lib/voice.js';
8
8
  import { getAnthropicModelId } from '../anthropic-model.js';
9
9
  import { dkimRemediationText } from '../lib/email-provider.js';
@@ -50,7 +50,7 @@ export function registerAdvisorTools(server) {
50
50
  const since = new Date(Date.now() - 30 * 86400000).toISOString();
51
51
  const { data: campaigns, error } = await db().from('campaigns').select('id, name, type').eq('project_id', projectId).eq('status', 'active').limit(200);
52
52
  if (error)
53
- return j({ error: error.message });
53
+ return j(dbToolError(error));
54
54
  const recommendations = [];
55
55
  let skipped = 0;
56
56
  for (const campaign of campaigns ?? []) {
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
2
2
  import { z } from 'zod';
3
3
  import { db } from '../db.js';
4
4
  import { getProjectId } from '../project.js';
5
+ import { dbToolError } from '../lib/db-error.js';
5
6
  const txt = (text) => ({ content: [{ type: 'text', text }] });
6
7
  const j = (data) => txt(JSON.stringify(data, null, 2));
7
8
  const workItemSelect = [
@@ -85,7 +86,7 @@ async function updateWorkItemStatus(id, status, snoozedUntil) {
85
86
  .select(workItemSelect)
86
87
  .maybeSingle();
87
88
  if (error)
88
- return { error: error.message };
89
+ return { ...dbToolError(error) };
89
90
  if (!data)
90
91
  return { error: 'AI work item not found.' };
91
92
  return { work_item: data };
@@ -130,7 +131,7 @@ async function submitContextEvent(input) {
130
131
  .eq('idempotency_key', input.idempotency_key)
131
132
  .maybeSingle();
132
133
  if (existing.error)
133
- return { error: existing.error.message };
134
+ return { ...dbToolError(existing.error) };
134
135
  if (existing.data)
135
136
  return { context_event: existing.data, work_item: null, proposals: [], idempotent: true };
136
137
  const orgId = await getOrgIdForProject(projectId);
@@ -159,12 +160,12 @@ async function submitContextEvent(input) {
159
160
  .eq('idempotency_key', input.idempotency_key)
160
161
  .maybeSingle();
161
162
  if (duplicate.error)
162
- return { error: duplicate.error.message };
163
+ return { ...dbToolError(duplicate.error) };
163
164
  if (duplicate.data)
164
165
  return { context_event: duplicate.data, work_item: null, proposals: [], idempotent: true };
165
166
  }
166
167
  if (inserted.error)
167
- return { error: inserted.error.message };
168
+ return { ...dbToolError(inserted.error) };
168
169
  const event = inserted.data;
169
170
  const segmentAction = segmentActionFromContext(input.event_type, input.suggested_actions, input.title, input.detail);
170
171
  const proposals = [];
@@ -200,7 +201,7 @@ async function submitContextEvent(input) {
200
201
  .select(proposalSelect)
201
202
  .single();
202
203
  if (error)
203
- return { error: error.message };
204
+ return { ...dbToolError(error) };
204
205
  proposals.push(proposal);
205
206
  }
206
207
  const proposalIds = proposals.map((proposal) => proposal.id);
@@ -228,7 +229,7 @@ async function submitContextEvent(input) {
228
229
  .select(workItemSelect)
229
230
  .single();
230
231
  if (workError)
231
- return { error: workError.message };
232
+ return { ...dbToolError(workError) };
232
233
  const { data: updatedEvent, error: updateError } = await db()
233
234
  .from('ai_context_events')
234
235
  .update({ work_item_id: workItem.id, proposal_ids: proposalIds })
@@ -237,7 +238,7 @@ async function submitContextEvent(input) {
237
238
  .select('*')
238
239
  .single();
239
240
  if (updateError)
240
- return { error: updateError.message };
241
+ return { ...dbToolError(updateError) };
241
242
  return { context_event: updatedEvent, work_item: workItem, proposals, idempotent: false };
242
243
  }
243
244
  export function registerAiWorkTools(server) {
@@ -267,7 +268,7 @@ export function registerAiWorkTools(server) {
267
268
  query = query.contains('surfaces', [surface]);
268
269
  const { data, error } = await query;
269
270
  if (error)
270
- return txt(`Error: ${error.message}`);
271
+ return txt(`Error: ${dbToolError(error).error}`);
271
272
  return j({ work_items: data ?? [] });
272
273
  });
273
274
  server.registerTool('get_ai_work_item', {
@@ -290,7 +291,7 @@ export function registerAiWorkTools(server) {
290
291
  .eq('id', work_item_id)
291
292
  .maybeSingle();
292
293
  if (error)
293
- return txt(`Error: ${error.message}`);
294
+ return txt(`Error: ${dbToolError(error).error}`);
294
295
  if (!data)
295
296
  return txt('AI work item not found.');
296
297
  return j({ work_item: data });
@@ -318,7 +319,7 @@ export function registerAiWorkTools(server) {
318
319
  .order('generated_at', { ascending: false })
319
320
  .limit(limit ?? 25);
320
321
  if (error)
321
- return txt(`Error: ${error.message}`);
322
+ return txt(`Error: ${dbToolError(error).error}`);
322
323
  const proposalIds = [...new Set((workItems ?? []).flatMap((item) => Array.isArray(item.proposal_ids) ? item.proposal_ids : []))];
323
324
  let proposals = [];
324
325
  if (proposalIds.length) {
@@ -328,7 +329,7 @@ export function registerAiWorkTools(server) {
328
329
  .eq('project_id', getProjectId())
329
330
  .in('id', proposalIds);
330
331
  if (proposalResult.error)
331
- return txt(`Error: ${proposalResult.error.message}`);
332
+ return txt(`Error: ${dbToolError(proposalResult.error).error}`);
332
333
  proposals = proposalResult.data ?? [];
333
334
  }
334
335
  return j({ work_items: workItems ?? [], proposals });
@@ -429,7 +430,7 @@ export function registerAiWorkTools(server) {
429
430
  query = query.eq('target_id', target_id);
430
431
  const { data, error } = await query;
431
432
  if (error)
432
- return txt(`Error: ${error.message}`);
433
+ return txt(`Error: ${dbToolError(error).error}`);
433
434
  return j({ proposals: data ?? [] });
434
435
  });
435
436
  server.registerTool('get_agent_proposal', {
@@ -452,7 +453,7 @@ export function registerAiWorkTools(server) {
452
453
  .eq('id', proposal_id)
453
454
  .maybeSingle();
454
455
  if (error)
455
- return txt(`Error: ${error.message}`);
456
+ return txt(`Error: ${dbToolError(error).error}`);
456
457
  if (!data)
457
458
  return txt('Agent proposal not found.');
458
459
  return j({ proposal: data });