@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.
- package/build/lib/action-approvals.js +3 -1
- package/build/lib/db-error.d.ts +39 -6
- package/build/lib/db-error.js +221 -11
- package/build/lib/tags.d.ts +10 -0
- package/build/lib/tags.js +29 -0
- package/build/server.js +5 -3
- package/build/tools/ab-testing.js +4 -3
- package/build/tools/advisor.js +2 -2
- package/build/tools/ai-work.js +14 -13
- package/build/tools/analytics.js +26 -26
- package/build/tools/approvals.js +3 -1
- package/build/tools/automations.js +12 -11
- package/build/tools/brand-kit.js +4 -3
- package/build/tools/campaign-sets.js +11 -10
- package/build/tools/campaigns.js +73 -74
- package/build/tools/compound.js +7 -6
- package/build/tools/connector.js +4 -3
- package/build/tools/contacts.js +116 -86
- package/build/tools/content.js +16 -16
- package/build/tools/convert-imported.js +8 -7
- package/build/tools/data-proposals.js +8 -7
- package/build/tools/data.js +39 -38
- package/build/tools/delivery-ops.js +32 -32
- package/build/tools/drafts.js +10 -9
- package/build/tools/forms.js +5 -4
- package/build/tools/gdpr.js +10 -9
- package/build/tools/landing-pages.js +3 -3
- package/build/tools/lists.js +21 -20
- package/build/tools/newsletters.js +26 -25
- package/build/tools/org.js +26 -23
- package/build/tools/segments.js +4 -5
- package/build/tools/short-links.js +7 -6
- package/build/tools/signatures.js +10 -9
- package/build/tools/sites.js +9 -8
- package/build/tools/sms.js +14 -13
- package/build/tools/social-posts.js +20 -19
- package/build/tools/social-replies.js +7 -6
- package/build/tools/surveys.js +10 -9
- package/build/tools/templates.js +14 -13
- package/build/tools/warmup.js +5 -4
- package/build/tools/webhooks.js +5 -4
- package/package.json +1 -1
- package/build/lib/db-errors.d.ts +0 -12
- 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
|
|
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' };
|
package/build/lib/db-error.d.ts
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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:
|
|
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 {};
|
package/build/lib/db-error.js
CHANGED
|
@@ -1,17 +1,227 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
return { 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
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Normalize input only: preserve first-seen spelling for exact-match consumers. */
|
|
2
|
+
export declare function normalizeTagInput(input: string[]): {
|
|
3
|
+
tags: string[];
|
|
4
|
+
dropped_empty: number;
|
|
5
|
+
case_duplicates: {
|
|
6
|
+
kept: string;
|
|
7
|
+
dropped: string;
|
|
8
|
+
}[];
|
|
9
|
+
};
|
|
10
|
+
export declare function tagInputWarnings(result: ReturnType<typeof normalizeTagInput>): string[];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Normalize input only: preserve first-seen spelling for exact-match consumers. */
|
|
2
|
+
export function normalizeTagInput(input) {
|
|
3
|
+
const tags = [];
|
|
4
|
+
const seen = new Map();
|
|
5
|
+
let dropped_empty = 0;
|
|
6
|
+
const case_duplicates = [];
|
|
7
|
+
for (const value of input) {
|
|
8
|
+
const tag = value.trim();
|
|
9
|
+
if (!tag) {
|
|
10
|
+
dropped_empty++;
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
const key = tag.toLowerCase();
|
|
14
|
+
const kept = seen.get(key);
|
|
15
|
+
if (kept !== undefined) {
|
|
16
|
+
case_duplicates.push({ kept, dropped: tag });
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
seen.set(key, tag);
|
|
20
|
+
tags.push(tag);
|
|
21
|
+
}
|
|
22
|
+
return { tags, dropped_empty, case_duplicates };
|
|
23
|
+
}
|
|
24
|
+
export function tagInputWarnings(result) {
|
|
25
|
+
return [
|
|
26
|
+
...(result.dropped_empty ? [`Dropped ${result.dropped_empty} empty tag(s).`] : []),
|
|
27
|
+
...result.case_duplicates.map(({ kept, dropped }) => `Tag "${dropped}" duplicates "${kept}" (case-insensitive); kept "${kept}".`),
|
|
28
|
+
];
|
|
29
|
+
}
|
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 {
|
|
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.
|
|
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.
|
|
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.
|
|
160
|
+
return txt(`Error: ${dbToolError(error).error}`);
|
|
160
161
|
return j(data);
|
|
161
162
|
});
|
|
162
163
|
server.registerTool('promote_ab_winner', {
|
package/build/tools/advisor.js
CHANGED
|
@@ -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(
|
|
53
|
+
return j(dbToolError(error));
|
|
54
54
|
const recommendations = [];
|
|
55
55
|
let skipped = 0;
|
|
56
56
|
for (const campaign of campaigns ?? []) {
|
package/build/tools/ai-work.js
CHANGED
|
@@ -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
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
|
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 {
|
|
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 {
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 });
|