@sendinel/mcp-server 1.0.10 → 1.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -22
- package/build/anthropic-model.d.ts +30 -0
- package/build/anthropic-model.js +26 -1
- package/build/db.d.ts +17 -0
- package/build/db.js +15 -1
- package/build/index.js +6 -0
- package/build/lib/action-approvals.js +28 -0
- package/build/lib/brand-asset-persist.d.ts +1 -0
- package/build/lib/brand-asset-persist.js +20 -1
- package/build/lib/composer-blocks.d.ts +25 -25
- package/build/lib/composer-blocks.js +25 -5
- package/build/lib/connector-table-discovery.d.ts +12 -0
- package/build/lib/connector-table-discovery.js +117 -0
- package/build/lib/custom-properties-diff.d.ts +16 -0
- package/build/lib/custom-properties-diff.js +22 -0
- package/build/lib/schedule.d.ts +7 -2
- package/build/lib/schedule.js +8 -0
- package/build/lib/v1-client.d.ts +3 -0
- package/build/lib/v1-client.js +45 -0
- package/build/lib/voice.d.ts +7 -30
- package/build/lib/voice.js +6 -138
- package/build/prompts.d.ts +6 -0
- package/build/prompts.js +18 -0
- package/build/scoped-token.d.ts +1 -1
- package/build/server.d.ts +1 -0
- package/build/server.js +65 -6
- package/build/tools/analytics.js +97 -0
- package/build/tools/brand-kit.js +29 -30
- package/build/tools/campaigns.js +326 -52
- package/build/tools/cloud-import.d.ts +2 -0
- package/build/tools/cloud-import.js +76 -0
- package/build/tools/compound.d.ts +11 -0
- package/build/tools/compound.js +37 -7
- package/build/tools/connector.d.ts +2 -0
- package/build/tools/connector.js +280 -0
- package/build/tools/contacts.js +744 -16
- package/build/tools/content.js +228 -0
- package/build/tools/data.js +441 -0
- package/build/tools/delivery-ops.js +508 -1
- package/build/tools/forms.js +7 -6
- package/build/tools/gateway.d.ts +6 -0
- package/build/tools/gateway.js +92 -0
- package/build/tools/gdpr.js +185 -0
- package/build/tools/landing-pages.d.ts +2 -0
- package/build/tools/landing-pages.js +202 -0
- package/build/tools/lists.d.ts +2 -0
- package/build/tools/lists.js +428 -0
- package/build/tools/newsletters.js +1 -8
- package/build/tools/org.js +549 -33
- package/build/tools/segments.js +35 -184
- package/build/tools/signatures.d.ts +2 -0
- package/build/tools/signatures.js +168 -0
- package/build/tools/sites.js +3 -1
- package/build/tools/skills.d.ts +2 -0
- package/build/tools/skills.js +29 -0
- package/build/tools/templates.js +133 -7
- package/package.json +13 -12
package/build/server.js
CHANGED
|
@@ -4,7 +4,9 @@ import { registerCampaignTools } from './tools/campaigns.js';
|
|
|
4
4
|
import { registerContactTools } from './tools/contacts.js';
|
|
5
5
|
import { registerDraftTools } from './tools/drafts.js';
|
|
6
6
|
import { registerGdprTools } from './tools/gdpr.js';
|
|
7
|
+
import { registerLandingPageTools } from './tools/landing-pages.js';
|
|
7
8
|
import { registerSegmentTools } from './tools/segments.js';
|
|
9
|
+
import { registerListTools } from './tools/lists.js';
|
|
8
10
|
import { registerSiteTools } from './tools/sites.js';
|
|
9
11
|
import { registerAbTestingTools } from './tools/ab-testing.js';
|
|
10
12
|
import { registerAdvisorTools } from './tools/advisor.js';
|
|
@@ -24,17 +26,24 @@ import { registerSocialPostTools } from './tools/social-posts.js';
|
|
|
24
26
|
import { registerDataProposalTools } from './tools/data-proposals.js';
|
|
25
27
|
import { registerSurveyTools } from './tools/surveys.js';
|
|
26
28
|
import { registerShortLinkTools } from './tools/short-links.js';
|
|
29
|
+
import { registerSignatureTools } from './tools/signatures.js';
|
|
27
30
|
import { registerAiWorkTools } from './tools/ai-work.js';
|
|
28
31
|
import { registerBrandKitTools } from './tools/brand-kit.js';
|
|
29
32
|
import { registerNewsletterTools } from './tools/newsletters.js';
|
|
33
|
+
import { registerGatewayTools } from './tools/gateway.js';
|
|
34
|
+
import { registerSkillTools } from './tools/skills.js';
|
|
35
|
+
import { registerConnectorTools } from './tools/connector.js';
|
|
36
|
+
import { registerCloudImportTools } from './tools/cloud-import.js';
|
|
30
37
|
const GROUP_REGISTRY = {
|
|
31
38
|
analytics: registerAnalyticsTools,
|
|
32
39
|
campaigns: registerCampaignTools,
|
|
33
40
|
contacts: registerContactTools,
|
|
34
41
|
segments: registerSegmentTools,
|
|
42
|
+
lists: registerListTools,
|
|
35
43
|
drafts: registerDraftTools,
|
|
36
44
|
sites: registerSiteTools,
|
|
37
45
|
gdpr: registerGdprTools,
|
|
46
|
+
'landing-pages': registerLandingPageTools,
|
|
38
47
|
'ab-testing': registerAbTestingTools,
|
|
39
48
|
advisor: registerAdvisorTools,
|
|
40
49
|
webhooks: registerWebhookTools,
|
|
@@ -53,36 +62,46 @@ const GROUP_REGISTRY = {
|
|
|
53
62
|
'data-proposals': registerDataProposalTools,
|
|
54
63
|
surveys: registerSurveyTools,
|
|
55
64
|
'short-links': registerShortLinkTools,
|
|
65
|
+
signatures: registerSignatureTools,
|
|
56
66
|
'ai-work': registerAiWorkTools,
|
|
57
67
|
brand_kit: registerBrandKitTools,
|
|
58
68
|
newsletters: registerNewsletterTools,
|
|
69
|
+
skills: registerSkillTools,
|
|
70
|
+
connector: registerConnectorTools,
|
|
71
|
+
'cloud-import': registerCloudImportTools,
|
|
59
72
|
};
|
|
60
73
|
export const ALL_TOOL_GROUPS = Object.keys(GROUP_REGISTRY);
|
|
61
74
|
// ── Scope enforcement ─────────────────────────────────────────────────────
|
|
62
75
|
// Maps tool names to required scope. Mirrors dashboard's WRITE_TOOLS/ADMIN_TOOLS.
|
|
63
76
|
const WRITE_TOOLS = new Set([
|
|
64
|
-
'add_subscriber', 'import_subscribers', 'update_subscriber',
|
|
77
|
+
'add_subscriber', 'import_subscribers', 'update_subscriber', 'bulk_update_subscribers',
|
|
78
|
+
'create_subscriber_view', 'update_subscriber_view', 'delete_subscriber_view',
|
|
65
79
|
'enrich_subscriber',
|
|
66
80
|
'create_campaign', 'update_campaign', 'delete_campaign', 'add_campaign_step', 'update_campaign_status',
|
|
67
81
|
'review_campaign_portfolio',
|
|
68
82
|
'list_campaign_steps', 'update_campaign_step', 'delete_campaign_step',
|
|
69
|
-
'enroll_contact', 'enroll_segment', 'broadcast_to_segment', 'get_enrollment_status', 'clone_campaign', 'generate_email',
|
|
83
|
+
'enroll_contact', 'enroll_segment', 'broadcast_to_segment', 'get_enrollment_status', 'clone_campaign', 'clone_sequence_template', 'generate_email',
|
|
70
84
|
'create_social_campaign', 'schedule_social_post',
|
|
71
85
|
'create_social_post', 'update_social_post', 'delete_social_post',
|
|
72
86
|
'create_draft', 'approve_draft', 'reject_draft',
|
|
73
87
|
'create_site', 'create_sender', 'update_site',
|
|
74
|
-
'create_template', 'update_template', 'delete_template', 'translate_template',
|
|
88
|
+
'create_template', 'update_template', 'delete_template', 'restore_template_version', 'delete_provider_credential', 'translate_template',
|
|
75
89
|
'delete_subscriber_data', 'identify_inactive', 'set_subscriber_tags',
|
|
76
90
|
'update_hygiene_config',
|
|
91
|
+
'create_custom_field', 'update_custom_field', 'delete_custom_field',
|
|
77
92
|
'create_segment', 'update_segment', 'delete_segment', 'create_segment_nl',
|
|
93
|
+
'create_list', 'update_list', 'delete_list', 'add_list_members', 'remove_list_members', 'add_list_members_by_filter',
|
|
78
94
|
'launch_campaign', 'merge_subscribers', 'create_campaign_with_content',
|
|
79
95
|
'send_test_email', 'send_transactional', 'subscribe_webhook', 'update_webhook_subscription',
|
|
80
96
|
'create_ab_test', 'promote_ab_winner', 'optimize_subject_lines',
|
|
81
97
|
'unsubscribe_subscriber', 'add_suppression', 'remove_suppression',
|
|
82
98
|
'trigger_automation', 'enroll_automation', 'create_escalation_ladder', 'enroll_in_escalation_ladder',
|
|
83
|
-
'update_scoring_rules', 'register_domain',
|
|
99
|
+
'update_scoring_rules', 'register_domain', 'publish_dmarc_record',
|
|
84
100
|
'update_warmup_schedule', 'pause_warmup', 'resume_warmup',
|
|
85
101
|
'create_form', 'invite_team_member',
|
|
102
|
+
'remove_team_member', 'set_team_member_role', 'set_member_project_access',
|
|
103
|
+
'resend_invitation', 'revoke_invitation',
|
|
104
|
+
'create_landing_page', 'update_landing_page', 'delete_landing_page',
|
|
86
105
|
'start_provider_switch', 'get_provider_switch_status', 'rollback_provider_switch',
|
|
87
106
|
'create_api_key', 'revoke_api_key', 'mark_notifications_read',
|
|
88
107
|
'migrate_campaigns_from', 'start_platform_migration',
|
|
@@ -90,30 +109,48 @@ const WRITE_TOOLS = new Set([
|
|
|
90
109
|
'create_asset_link',
|
|
91
110
|
'set_white_label_settings', 'add_white_label_domain',
|
|
92
111
|
'create_blog_post',
|
|
112
|
+
'generate_blog_post_image',
|
|
93
113
|
'run_list_hygiene', 'run_portfolio_analysis',
|
|
94
114
|
'abuse_monitor_override',
|
|
95
115
|
'send_sms', 'send_sms_campaign', 'set_contact_sms_consent',
|
|
96
116
|
'setup_campaign_from_brief', 'create_template_from_brief', 'diagnose_delivery_issue', 'onboard_new_site',
|
|
97
117
|
'connect_platform', 'disconnect_platform', 'configure_platform_trigger',
|
|
118
|
+
'create_custom_webhook', 'rotate_custom_webhook_secret',
|
|
98
119
|
'update_brand_voice', 'promote_draft_to_campaign',
|
|
99
120
|
'propose_campaign_from_data', 'approve_data_proposal',
|
|
100
121
|
'create_survey', 'update_survey', 'create_short_link', 'generate_qr_code',
|
|
122
|
+
'create_signature', 'update_signature',
|
|
101
123
|
'mark_ai_work_item_seen', 'snooze_ai_work_item', 'dismiss_ai_work_item',
|
|
102
124
|
'submit_ai_context_event',
|
|
103
|
-
'import_brand_kit_text', 'ingest_brand_assets_by_url', 'upload_brand_asset',
|
|
125
|
+
'import_brand_kit_text', 'rehost_brand_asset', 'ingest_brand_assets_by_url', 'upload_brand_asset',
|
|
104
126
|
'create_newsletter', 'update_newsletter', 'create_newsletter_issue', 'schedule_newsletter_issue',
|
|
105
127
|
'send_newsletter_issue', 'subscribe_to_newsletter', 'unsubscribe_from_newsletter',
|
|
128
|
+
'retry_send_queue_rows', 'cancel_send_queue_rows',
|
|
129
|
+
'replay_enrollment_dlq_rows',
|
|
130
|
+
'commit_staged_import', 'discard_staged_import', 'add_synced_folder', 'remove_synced_folder',
|
|
131
|
+
'retry_send_queue_rows', 'cancel_send_queue_rows', 'add_blackout_date', 'remove_blackout_date',
|
|
106
132
|
]);
|
|
107
133
|
const ADMIN_TOOLS = new Set([
|
|
108
|
-
|
|
134
|
+
// Connector mutations write schema into a customer-owned database and
|
|
135
|
+
// rewrite how their data maps in (admin, not write: Kevin, 2026-09-11).
|
|
136
|
+
// NOTE: no apostrophes in comments inside these Sets -- tool-scopes.test.ts
|
|
137
|
+
// parses the block by matching single-quoted strings.
|
|
138
|
+
'install_connector_schema', 'install_connector_view', 'set_connector_mapping', 'resync_connector',
|
|
139
|
+
'delete_subscriber_data', 'list_deletion_log', 'list_ccpa_requests', 'action_ccpa_request',
|
|
140
|
+
'delete_subscriber_data', 'list_deletion_log', 'list_audit_log', 'get_audit_stats',
|
|
109
141
|
'unsubscribe_subscriber', 'add_suppression', 'remove_suppression',
|
|
110
142
|
'create_site', 'update_site',
|
|
111
143
|
'delete_template', 'register_domain', 'onboard_new_site',
|
|
112
144
|
'create_api_key', 'revoke_api_key', 'invite_team_member',
|
|
113
145
|
'connect_platform', 'disconnect_platform', 'configure_platform_trigger',
|
|
146
|
+
'create_custom_webhook', 'rotate_custom_webhook_secret',
|
|
147
|
+
'remove_team_member', 'set_team_member_role', 'set_member_project_access',
|
|
148
|
+
'list_invitations', 'resend_invitation', 'revoke_invitation',
|
|
149
|
+
'connect_platform', 'disconnect_platform', 'configure_platform_trigger', 'delete_provider_credential',
|
|
114
150
|
'abuse_monitor_override',
|
|
115
151
|
'tenant_export_full',
|
|
116
152
|
'identify_inactive', 'delete_segment',
|
|
153
|
+
'delete_signature',
|
|
117
154
|
'merge_subscribers', 'delete_webhook_subscription',
|
|
118
155
|
'set_white_label_settings', 'add_white_label_domain',
|
|
119
156
|
]);
|
|
@@ -129,12 +166,27 @@ function scopeAllows(keyScopes, required) {
|
|
|
129
166
|
const requiredLevel = SCOPE_HIERARCHY[required] ?? 99;
|
|
130
167
|
return keyScopes.some((s) => (SCOPE_HIERARCHY[s] ?? 0) >= requiredLevel);
|
|
131
168
|
}
|
|
169
|
+
// Names of the meta-tools gateway mode registers in place of the real catalog
|
|
170
|
+
// (see tools/gateway.ts). Grouped as 'gateway' rather than any real tool
|
|
171
|
+
// group -- a gateway-mode client's toolGroups list holds the groups it can
|
|
172
|
+
// reach *through* invoke_tool, not a group these three tools belong to.
|
|
173
|
+
export const GATEWAY_TOOL_NAMES = ['search_tools', 'describe_tool', 'invoke_tool'];
|
|
132
174
|
/**
|
|
133
175
|
* Collects tool-name → group metadata from the same group registry used by
|
|
134
176
|
* registerAllTools(). Scope filtering and definition scanning are applied
|
|
135
177
|
* through the same proxy path as real MCP registration.
|
|
136
178
|
*/
|
|
137
179
|
export function getRegisteredToolGroups(options) {
|
|
180
|
+
const rawScopes = options?.scopes ?? [];
|
|
181
|
+
if (rawScopes.includes('gateway')) {
|
|
182
|
+
// Mirrors registerAllTools()'s own gateway branch: a client with only
|
|
183
|
+
// 'gateway' and no real permission scope (read/write/admin) alongside it
|
|
184
|
+
// gets nothing registered, so it must get nothing labeled here either.
|
|
185
|
+
const permissionScopes = rawScopes.filter((scope) => scope !== 'gateway');
|
|
186
|
+
if (!scopeAllows(permissionScopes, 'read'))
|
|
187
|
+
return {};
|
|
188
|
+
return Object.fromEntries(GATEWAY_TOOL_NAMES.map((name) => [name, 'gateway']));
|
|
189
|
+
}
|
|
138
190
|
const groups = options?.toolGroups === undefined
|
|
139
191
|
? ALL_TOOL_GROUPS
|
|
140
192
|
: options.toolGroups;
|
|
@@ -245,6 +297,13 @@ function createScopeProxy(server, scopes, toolGroup) {
|
|
|
245
297
|
});
|
|
246
298
|
}
|
|
247
299
|
export function registerAllTools(server, options) {
|
|
300
|
+
const rawScopes = options?.scopes ?? [];
|
|
301
|
+
if (rawScopes.includes('gateway')) {
|
|
302
|
+
const permissionScopes = rawScopes.filter((scope) => scope !== 'gateway');
|
|
303
|
+
const proxied = createScopeProxy(server, permissionScopes, 'gateway');
|
|
304
|
+
registerGatewayTools(proxied, { scopes: permissionScopes, toolGroups: options?.toolGroups });
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
248
307
|
const groups = options?.toolGroups === undefined
|
|
249
308
|
? ALL_TOOL_GROUPS
|
|
250
309
|
: options.toolGroups;
|
package/build/tools/analytics.js
CHANGED
|
@@ -9,6 +9,7 @@ const percent = (rate) => `${(rate * 100).toFixed(1)}%`;
|
|
|
9
9
|
const pctNumber = (rate) => Math.round(rate * 1000) / 10;
|
|
10
10
|
const roundRate = (rate) => Math.round(rate * 1000) / 1000;
|
|
11
11
|
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
12
|
+
const stripeLifecycleStatuses = ['active', 'trialing', 'past_due', 'canceled', 'unpaid', 'incomplete', 'incomplete_expired', 'paused'];
|
|
12
13
|
const socialPlatforms = ['instagram', 'linkedin', 'twitter', 'facebook', 'tiktok', 'youtube', 'pinterest', 'uploadpost', 'other'];
|
|
13
14
|
const socialPublishers = ['uploadpost', 'buffer'];
|
|
14
15
|
const UPLOADPOST_API_BASE = process.env.UPLOADPOST_API_BASE || 'https://api.upload-post.com/api';
|
|
@@ -142,6 +143,61 @@ function addDeviationFlag(flags, metric, recent, baseline) {
|
|
|
142
143
|
flags.push(`${metric} ${deviation > 0 ? 'up' : 'down'} ${(Math.abs(deviation) * 100).toFixed(1)}% vs 30d baseline`);
|
|
143
144
|
}
|
|
144
145
|
}
|
|
146
|
+
function monthlyNormalizedCents(amountCents, interval, intervalCount) {
|
|
147
|
+
const count = intervalCount && intervalCount > 0 ? intervalCount : 1;
|
|
148
|
+
switch (interval) {
|
|
149
|
+
case 'month':
|
|
150
|
+
return amountCents / count;
|
|
151
|
+
case 'year':
|
|
152
|
+
return amountCents / (12 * count);
|
|
153
|
+
case 'week':
|
|
154
|
+
return (amountCents * 52) / (12 * count);
|
|
155
|
+
case 'day':
|
|
156
|
+
return (amountCents * 365) / (12 * count);
|
|
157
|
+
default:
|
|
158
|
+
return amountCents;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function buildRevenueAnalytics(params) {
|
|
162
|
+
const now = new Date().toISOString();
|
|
163
|
+
const lifecycle = Object.fromEntries(stripeLifecycleStatuses.map(status => [status, 0]));
|
|
164
|
+
for (const subscription of params.subscriptions) {
|
|
165
|
+
if (subscription.status && lifecycle[subscription.status] !== undefined) {
|
|
166
|
+
lifecycle[subscription.status] += 1;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const mrr = params.subscriptions
|
|
170
|
+
.filter(subscription => subscription.status === 'active' || subscription.status === 'trialing')
|
|
171
|
+
.reduce((sum, subscription) => {
|
|
172
|
+
const amount = subscription.amount_cents ?? 0;
|
|
173
|
+
return sum + monthlyNormalizedCents(amount, subscription.billing_interval, subscription.billing_interval_count);
|
|
174
|
+
}, 0);
|
|
175
|
+
const activeAtPeriodStart = params.subscriptions.filter(subscription => (subscription.stripe_created_at
|
|
176
|
+
&& subscription.stripe_created_at < params.start
|
|
177
|
+
&& (!subscription.canceled_at || subscription.canceled_at >= params.start))).length;
|
|
178
|
+
const canceledInPeriod = params.subscriptions.filter(subscription => (subscription.canceled_at
|
|
179
|
+
&& subscription.canceled_at >= params.start
|
|
180
|
+
&& subscription.canceled_at <= now)).length;
|
|
181
|
+
const newSubscriptionsInPeriod = params.subscriptions.filter(subscription => (subscription.stripe_created_at && subscription.stripe_created_at >= params.start)).length;
|
|
182
|
+
const revenueCollectedCents = params.invoices
|
|
183
|
+
.filter(invoice => invoice.paid === true && invoice.stripe_created_at && invoice.stripe_created_at >= params.start)
|
|
184
|
+
.reduce((sum, invoice) => sum + (invoice.amount_paid_cents ?? 0), 0);
|
|
185
|
+
const failedPaymentsInPeriod = params.invoices.filter(invoice => (invoice.paid === false && invoice.stripe_created_at && invoice.stripe_created_at >= params.start)).length;
|
|
186
|
+
return {
|
|
187
|
+
period_days: params.days,
|
|
188
|
+
period: { days: params.days, start: params.start },
|
|
189
|
+
connection_id: params.connectionId,
|
|
190
|
+
mrr_cents: Math.round(mrr),
|
|
191
|
+
lifecycle,
|
|
192
|
+
active_at_period_start: activeAtPeriodStart,
|
|
193
|
+
canceled_in_period: canceledInPeriod,
|
|
194
|
+
churn_rate: activeAtPeriodStart > 0 ? roundRate(canceledInPeriod / activeAtPeriodStart) : 0,
|
|
195
|
+
new_subscriptions_in_period: newSubscriptionsInPeriod,
|
|
196
|
+
revenue_collected_cents: revenueCollectedCents,
|
|
197
|
+
failed_payments_in_period: failedPaymentsInPeriod,
|
|
198
|
+
failed_payments_note: 'Best-effort count of unpaid invoices in the period; stripe_invoices does not store a dedicated payment_failed flag.',
|
|
199
|
+
};
|
|
200
|
+
}
|
|
145
201
|
function distinctEventCount(events, eventType, logIds) {
|
|
146
202
|
return new Set(events
|
|
147
203
|
.filter(event => event.event_type === eventType && event.email_log_id && logIds.has(event.email_log_id))
|
|
@@ -1003,6 +1059,47 @@ export function registerAnalyticsTools(server) {
|
|
|
1003
1059
|
},
|
|
1004
1060
|
});
|
|
1005
1061
|
});
|
|
1062
|
+
server.registerTool('get_revenue_analytics', {
|
|
1063
|
+
title: 'Get Revenue Analytics',
|
|
1064
|
+
annotations: {
|
|
1065
|
+
readOnlyHint: true,
|
|
1066
|
+
destructiveHint: false,
|
|
1067
|
+
idempotentHint: true,
|
|
1068
|
+
openWorldHint: false,
|
|
1069
|
+
},
|
|
1070
|
+
description: 'Get Stripe connector revenue, churn, MRR, subscription lifecycle, and failed-payment analytics for the current project.',
|
|
1071
|
+
inputSchema: {
|
|
1072
|
+
days: z.number().int().min(1).max(365).optional().default(30),
|
|
1073
|
+
connection_id: z.string().uuid().optional(),
|
|
1074
|
+
},
|
|
1075
|
+
}, async ({ days = 30, connection_id }) => {
|
|
1076
|
+
const projectId = getProjectId();
|
|
1077
|
+
const start = new Date(Date.now() - days * 86_400_000).toISOString();
|
|
1078
|
+
let subscriptionQuery = db()
|
|
1079
|
+
.from('stripe_subscriptions')
|
|
1080
|
+
.select('status, amount_cents, billing_interval, billing_interval_count, stripe_created_at, canceled_at')
|
|
1081
|
+
.eq('project_id', projectId);
|
|
1082
|
+
let invoiceQuery = db()
|
|
1083
|
+
.from('stripe_invoices')
|
|
1084
|
+
.select('paid, status, amount_paid_cents, stripe_created_at')
|
|
1085
|
+
.eq('project_id', projectId);
|
|
1086
|
+
if (connection_id) {
|
|
1087
|
+
subscriptionQuery = subscriptionQuery.eq('connection_id', connection_id);
|
|
1088
|
+
invoiceQuery = invoiceQuery.eq('connection_id', connection_id);
|
|
1089
|
+
}
|
|
1090
|
+
const [subscriptionRes, invoiceRes] = await Promise.all([subscriptionQuery, invoiceQuery]);
|
|
1091
|
+
if (subscriptionRes.error)
|
|
1092
|
+
return j({ error: subscriptionRes.error.message });
|
|
1093
|
+
if (invoiceRes.error)
|
|
1094
|
+
return j({ error: invoiceRes.error.message });
|
|
1095
|
+
return j(buildRevenueAnalytics({
|
|
1096
|
+
days,
|
|
1097
|
+
start,
|
|
1098
|
+
subscriptions: (subscriptionRes.data ?? []),
|
|
1099
|
+
invoices: (invoiceRes.data ?? []),
|
|
1100
|
+
connectionId: connection_id ?? null,
|
|
1101
|
+
}));
|
|
1102
|
+
});
|
|
1006
1103
|
server.registerTool('abuse_monitor_status', {
|
|
1007
1104
|
title: 'Abuse Monitor Status',
|
|
1008
1105
|
annotations: {
|
package/build/tools/brand-kit.js
CHANGED
|
@@ -2,38 +2,15 @@ import { z } from 'zod';
|
|
|
2
2
|
import { adminDb } from '../db.js';
|
|
3
3
|
import { getProjectId } from '../project.js';
|
|
4
4
|
import { downloadPublicAsset } from '../lib/ssrf-pinned-fetch.js';
|
|
5
|
+
import { callSendinelV1 } from '../lib/v1-client.js';
|
|
5
6
|
import { BRAND_ASSET_KINDS, isValidKind, persistBrandAsset, resolveKind, resolveMime, } from '../lib/brand-asset-persist.js';
|
|
6
7
|
const j = (data) => ({ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] });
|
|
7
|
-
function brandKitApiBase() {
|
|
8
|
-
return (process.env.NEXT_PUBLIC_APP_URL ?? process.env.SENDINEL_APP_URL ?? process.env.APP_URL ?? 'https://sendinel.ai').replace(/\/$/, '');
|
|
9
|
-
}
|
|
10
8
|
// import_brand_kit_text parses free-form Brand Core/Brand Brain/Content Library
|
|
11
9
|
// markdown into voice fields, colors, templates, and social posts — a different,
|
|
12
|
-
// more complex capability than the media-library asset tools below.
|
|
13
|
-
//
|
|
14
|
-
// here; see #2356 for the media-library-scoped fix this file otherwise contains.
|
|
10
|
+
// more complex capability than the media-library asset tools below. Hosted MCP
|
|
11
|
+
// calls it in-process; stdio keeps the authenticated v1 HTTP path.
|
|
15
12
|
async function callBrandKitApi(path, body) {
|
|
16
|
-
|
|
17
|
-
if (!apiKey)
|
|
18
|
-
return { ok: false, error: 'SENDINEL_API_KEY is required to drive this brand_kit tool from MCP' };
|
|
19
|
-
const res = await fetch(`${brandKitApiBase()}${path}`, {
|
|
20
|
-
method: 'POST',
|
|
21
|
-
headers: {
|
|
22
|
-
Authorization: `Bearer ${apiKey}`,
|
|
23
|
-
'X-Sendinel-Project-Id': getProjectId(),
|
|
24
|
-
'Content-Type': 'application/json',
|
|
25
|
-
},
|
|
26
|
-
body: JSON.stringify(body),
|
|
27
|
-
});
|
|
28
|
-
const text = await res.text();
|
|
29
|
-
let parsed = text;
|
|
30
|
-
try {
|
|
31
|
-
parsed = JSON.parse(text);
|
|
32
|
-
}
|
|
33
|
-
catch { }
|
|
34
|
-
if (!res.ok)
|
|
35
|
-
return { ok: false, error: parsed || `HTTP ${res.status}` };
|
|
36
|
-
return { ok: true, data: parsed };
|
|
13
|
+
return callSendinelV1(path, { body });
|
|
37
14
|
}
|
|
38
15
|
async function getProjectOrgId(projectId) {
|
|
39
16
|
const { data, error } = await adminDb()
|
|
@@ -66,6 +43,24 @@ export function registerBrandKitTools(server) {
|
|
|
66
43
|
const result = await callBrandKitApi('/api/v1/brand-kit/import-text', { text });
|
|
67
44
|
return result.ok ? j(result.data) : j({ error: result.error });
|
|
68
45
|
});
|
|
46
|
+
server.registerTool('rehost_brand_asset', {
|
|
47
|
+
title: 'Rehost Drive-Linked Brand Asset',
|
|
48
|
+
annotations: {
|
|
49
|
+
readOnlyHint: false,
|
|
50
|
+
destructiveHint: false,
|
|
51
|
+
idempotentHint: false,
|
|
52
|
+
openWorldHint: false,
|
|
53
|
+
},
|
|
54
|
+
description: 'Convert a brand asset into a real hosted copy that can be used in email or social content. Drive-linked assets have a null url in list_brand_assets and the v1 brand-kit API; call this first to get a usable copy before referencing the asset in generated content.',
|
|
55
|
+
inputSchema: {
|
|
56
|
+
asset_id: z.string().uuid(),
|
|
57
|
+
purpose: z.enum(['email', 'social']),
|
|
58
|
+
platforms: z.array(z.string()).optional().describe('Required context when purpose is "social" and TikTok is a target platform (JPEG/WebP only).'),
|
|
59
|
+
},
|
|
60
|
+
}, async ({ asset_id, purpose, platforms }) => {
|
|
61
|
+
const result = await callBrandKitApi(`/api/v1/brand-kit/assets/${asset_id}/rehost`, { purpose, platforms });
|
|
62
|
+
return result.ok ? j(result.data) : j({ error: result.error });
|
|
63
|
+
});
|
|
69
64
|
server.registerTool('list_brand_assets', {
|
|
70
65
|
title: 'List Brand Assets',
|
|
71
66
|
annotations: {
|
|
@@ -88,7 +83,7 @@ export function registerBrandKitTools(server) {
|
|
|
88
83
|
const { data, error } = await sb
|
|
89
84
|
.schema('email')
|
|
90
85
|
.from('brand_media_assets')
|
|
91
|
-
.select('id, kind, filename, storage_path, mime_type, size_bytes, collection, tags, last_referenced_at, last_referenced_source, last_referenced_resource_id, reference_count, created_at, storage_backend')
|
|
86
|
+
.select('id, kind, filename, storage_path, description, backstory, mime_type, size_bytes, collection, tags, last_referenced_at, last_referenced_source, last_referenced_resource_id, reference_count, created_at, storage_backend')
|
|
92
87
|
.eq('project_id', projectId)
|
|
93
88
|
.eq('review_status', 'confirmed')
|
|
94
89
|
.order('created_at', { ascending: false });
|
|
@@ -110,7 +105,7 @@ export function registerBrandKitTools(server) {
|
|
|
110
105
|
}
|
|
111
106
|
const totalMatching = rows.length;
|
|
112
107
|
const page = rows.slice(0, cappedLimit);
|
|
113
|
-
const paths = page.filter((r) => r.storage_backend !== 'r2').map((r) => r.storage_path);
|
|
108
|
+
const paths = page.filter((r) => r.storage_backend !== 'r2' && r.storage_backend !== 'drive_link').map((r) => r.storage_path);
|
|
114
109
|
const signedByPath = new Map();
|
|
115
110
|
if (paths.length > 0) {
|
|
116
111
|
const { data: signed } = await sb.storage.from('brand-assets').createSignedUrls(paths, 3600, { download: true });
|
|
@@ -124,6 +119,8 @@ export function registerBrandKitTools(server) {
|
|
|
124
119
|
id: r.id,
|
|
125
120
|
kind: r.kind,
|
|
126
121
|
filename: r.filename,
|
|
122
|
+
description: r.description ?? null,
|
|
123
|
+
backstory: r.backstory ?? null,
|
|
127
124
|
collection: r.collection,
|
|
128
125
|
tags: r.tags ?? [],
|
|
129
126
|
mime_type: r.mime_type,
|
|
@@ -159,8 +156,9 @@ export function registerBrandKitTools(server) {
|
|
|
159
156
|
collection: z.string().max(120).optional(),
|
|
160
157
|
tags: z.array(z.string()).max(50).optional(),
|
|
161
158
|
brand_kit_id: z.string().uuid().optional(),
|
|
159
|
+
backstory: z.string().max(2000).optional().describe('Character backstory/profile text for avatar assets. Ignored for non-avatar assets.'),
|
|
162
160
|
},
|
|
163
|
-
}, async ({ filename, content_base64, kind, collection, tags, brand_kit_id }) => {
|
|
161
|
+
}, async ({ filename, content_base64, kind, collection, tags, brand_kit_id, backstory }) => {
|
|
164
162
|
const projectId = getProjectId();
|
|
165
163
|
let bytes;
|
|
166
164
|
try {
|
|
@@ -193,6 +191,7 @@ export function registerBrandKitTools(server) {
|
|
|
193
191
|
collection: collection ?? null,
|
|
194
192
|
tags: tags ?? [],
|
|
195
193
|
brandKitId: brand_kit_id ?? null,
|
|
194
|
+
backstory: resolvedKind === 'avatar' ? backstory?.trim() || null : null,
|
|
196
195
|
});
|
|
197
196
|
return j({
|
|
198
197
|
ok: true,
|