0nmcp 3.2.1 → 4.0.0

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/crm/billing.js ADDED
@@ -0,0 +1,173 @@
1
+ // ============================================================
2
+ // 0nMCP — CRM External Billing Tools
3
+ // ============================================================
4
+ // Reports payment status to the CRM billing webhook.
5
+ // Required for marketplace apps using external billing (Stripe).
6
+ //
7
+ // Flow:
8
+ // 1. User purchases add-on via Stripe
9
+ // 2. Stripe webhook fires checkout.session.completed
10
+ // 3. We POST to CRM billing webhook: "location X paid"
11
+ // 4. CRM activates the marketplace app for that location
12
+ // 5. If recurring payment fails: POST status: FAILED → CRM deactivates
13
+ //
14
+ // Endpoint: POST https://services.leadconnectorhq.com/oauth/billing/webhook
15
+ // Headers: x-ghl-client-key, x-ghl-client-secret
16
+ // ============================================================
17
+
18
+ const CRM_BILLING_URL = "https://services.leadconnectorhq.com/oauth/billing/webhook";
19
+
20
+ const DEFAULT_CLIENT_ID = process.env.CRM_MARKETPLACE_CLIENT_ID || "69c762225a31e1cd2f28dd4c-mnu5pazi";
21
+ const DEFAULT_CLIENT_SECRET = process.env.CRM_MARKETPLACE_CLIENT_SECRET || "92cb8fc3-2b23-40bf-adce-6b6afe9b8445";
22
+
23
+ /**
24
+ * Report billing status to CRM for a location or company.
25
+ *
26
+ * @param {Object} params
27
+ * @param {string} params.locationId - CRM location ID
28
+ * @param {string} [params.companyId] - CRM company ID (for agency-level billing)
29
+ * @param {string} params.authType - "location" or "company"
30
+ * @param {number} params.amount - Payment amount in dollars
31
+ * @param {string} params.status - "COMPLETED" or "FAILED"
32
+ * @param {string} params.paymentType - "one_time" or "recurring"
33
+ * @param {string} [params.subscriptionId] - External subscription ID
34
+ * @param {string} [params.paymentId] - External payment ID
35
+ * @param {string} [params.clientId] - Override client ID
36
+ * @param {string} [params.clientSecret] - Override client secret
37
+ */
38
+ export async function reportBilling({
39
+ locationId,
40
+ companyId,
41
+ authType = "location",
42
+ amount,
43
+ status,
44
+ paymentType = "one_time",
45
+ subscriptionId,
46
+ paymentId,
47
+ clientId,
48
+ clientSecret,
49
+ }) {
50
+ const cid = clientId || DEFAULT_CLIENT_ID;
51
+ const csec = clientSecret || DEFAULT_CLIENT_SECRET;
52
+
53
+ const body = {
54
+ clientId: cid.split("-")[0], // App ID portion only
55
+ authType,
56
+ locationId: locationId || undefined,
57
+ companyId: companyId || undefined,
58
+ subscriptionId: subscriptionId || `sub_${Date.now()}`,
59
+ paymentId: paymentId || `pay_${Date.now()}`,
60
+ amount,
61
+ status,
62
+ paymentType,
63
+ };
64
+
65
+ const res = await fetch(CRM_BILLING_URL, {
66
+ method: "POST",
67
+ headers: {
68
+ "Content-Type": "application/json",
69
+ "x-ghl-client-key": cid,
70
+ "x-ghl-client-secret": csec,
71
+ },
72
+ body: JSON.stringify(body),
73
+ });
74
+
75
+ const data = await res.json().catch(() => ({ status: res.status }));
76
+
77
+ if (!res.ok) {
78
+ console.error("[billing] CRM billing webhook failed:", res.status, data);
79
+ } else {
80
+ console.error(`[billing] Reported ${status} for ${authType} ${locationId || companyId}: $${amount}`);
81
+ }
82
+
83
+ return { ok: res.ok, status: res.status, data };
84
+ }
85
+
86
+ /**
87
+ * Register billing MCP tools.
88
+ */
89
+ export function registerBillingTools(server, z) {
90
+ server.tool(
91
+ "crm_report_billing",
92
+ "Report a payment to the CRM billing webhook. Activates or deactivates a marketplace app for a location based on payment status. Required for external billing (SaaS subscriptions through Stripe).",
93
+ {
94
+ location_id: z.string().optional().describe("CRM location ID (for location-level billing)"),
95
+ company_id: z.string().optional().describe("CRM company ID (for agency-level billing)"),
96
+ auth_type: z.enum(["location", "company"]).describe("Billing scope: location or company"),
97
+ amount: z.number().describe("Payment amount in dollars"),
98
+ status: z.enum(["COMPLETED", "FAILED"]).describe("Payment status"),
99
+ payment_type: z.enum(["one_time", "recurring"]).optional().describe("Payment type (default: one_time)"),
100
+ subscription_id: z.string().optional().describe("External subscription ID (e.g. Stripe sub_xxx)"),
101
+ payment_id: z.string().optional().describe("External payment ID (e.g. Stripe pi_xxx)"),
102
+ },
103
+ async ({ location_id, company_id, auth_type, amount, status, payment_type, subscription_id, payment_id }) => {
104
+ const result = await reportBilling({
105
+ locationId: location_id,
106
+ companyId: company_id,
107
+ authType: auth_type,
108
+ amount,
109
+ status,
110
+ paymentType: payment_type || "one_time",
111
+ subscriptionId: subscription_id,
112
+ paymentId: payment_id,
113
+ });
114
+
115
+ return {
116
+ content: [{
117
+ type: "text",
118
+ text: JSON.stringify({
119
+ reported: true,
120
+ crm_status: result.ok ? "accepted" : "rejected",
121
+ http_status: result.status,
122
+ location_id,
123
+ company_id,
124
+ amount,
125
+ status,
126
+ payment_type: payment_type || "one_time",
127
+ }, null, 2),
128
+ }],
129
+ };
130
+ }
131
+ );
132
+
133
+ server.tool(
134
+ "crm_create_payment_config",
135
+ "Configure payment processing (Stripe) for a CRM location. This wires Stripe into the location so it can accept payments natively.",
136
+ {
137
+ location_id: z.string().describe("CRM location ID"),
138
+ live_api_key: z.string().describe("Stripe live secret key"),
139
+ live_publishable_key: z.string().describe("Stripe live publishable key"),
140
+ test_api_key: z.string().optional().describe("Stripe test secret key"),
141
+ test_publishable_key: z.string().optional().describe("Stripe test publishable key"),
142
+ },
143
+ async ({ location_id, live_api_key, live_publishable_key, test_api_key, test_publishable_key }) => {
144
+ try {
145
+ const { default: getSDK } = await import("./sdk.js");
146
+ const sdk = await getSDK();
147
+
148
+ const config = {
149
+ live: { apiKey: live_api_key, publishableKey: live_publishable_key },
150
+ };
151
+ if (test_api_key && test_publishable_key) {
152
+ config.test = { apiKey: test_api_key, publishableKey: test_publishable_key };
153
+ }
154
+
155
+ const response = await sdk.payments.createConfig({ locationId: location_id }, config);
156
+
157
+ return {
158
+ content: [{
159
+ type: "text",
160
+ text: JSON.stringify({ status: "configured", location_id, response }, null, 2),
161
+ }],
162
+ };
163
+ } catch (err) {
164
+ return {
165
+ content: [{
166
+ type: "text",
167
+ text: JSON.stringify({ error: err.message, status: "failed" }, null, 2),
168
+ }],
169
+ };
170
+ }
171
+ }
172
+ );
173
+ }
@@ -114,6 +114,13 @@ const conversationTools = [
114
114
  replyToMessageId: { type: "string", description: "Message ID to reply to", required: false, in: "body" },
115
115
  templateId: { type: "string", description: "Template ID for pre-built messages", required: false, in: "body" },
116
116
  scheduledTimestamp: { type: "number", description: "Unix timestamp (ms) to schedule the message", required: false, in: "body" },
117
+ appointmentId: { type: "string", description: "Link message to an appointment", required: false, in: "body" },
118
+ threadId: { type: "string", description: "Email thread ID for threading", required: false, in: "body" },
119
+ emailReplyMode: { type: "string", description: "Reply mode: reply, reply_all", required: false, in: "body" },
120
+ fromNumber: { type: "string", description: "From phone number (E.164)", required: false, in: "body" },
121
+ toNumber: { type: "string", description: "To phone number (E.164)", required: false, in: "body" },
122
+ mentions: { type: "array_string", description: "Array of user IDs to mention", required: false, in: "body" },
123
+ userId: { type: "string", description: "Send as this user", required: false, in: "body" },
117
124
  },
118
125
  body: [
119
126
  "type",
@@ -132,6 +139,13 @@ const conversationTools = [
132
139
  "replyToMessageId",
133
140
  "templateId",
134
141
  "scheduledTimestamp",
142
+ "appointmentId",
143
+ "threadId",
144
+ "emailReplyMode",
145
+ "fromNumber",
146
+ "toNumber",
147
+ "mentions",
148
+ "userId",
135
149
  ],
136
150
  },
137
151
 
@@ -0,0 +1,161 @@
1
+ // ============================================================
2
+ // 0nMCP — AI Course Generator
3
+ // ============================================================
4
+ // User describes what they want → AI generates course structure
5
+ // → SDK imports it into their CRM location.
6
+ //
7
+ // Flow:
8
+ // 1. User describes course topic + goals
9
+ // 2. AI generates full course JSON (title, categories, lessons)
10
+ // 3. SDK calls courses.importCourses() → live in CRM
11
+ // 4. User sees course in their CRM memberships area
12
+ // ============================================================
13
+
14
+ import getSDK from "./sdk.js";
15
+
16
+ /**
17
+ * Generate course content structure from a description.
18
+ * Uses the AI to create categories, lessons, and materials.
19
+ *
20
+ * @param {string} topic - What the course is about
21
+ * @param {string} audience - Who it's for
22
+ * @param {number} lessonCount - Approximate number of lessons (default 8)
23
+ * @param {string} [instructorName] - Instructor name
24
+ * @returns {Object} - Course structure ready for importCourses()
25
+ */
26
+ export function generateCourseStructure(topic, audience, lessonCount = 8, instructorName = "AI Instructor") {
27
+ // This returns the structure template — the AI fills in the content
28
+ // when called through the MCP tool
29
+ return {
30
+ title: topic,
31
+ description: `A comprehensive course on ${topic} designed for ${audience}.`,
32
+ imageUrl: "",
33
+ categories: [],
34
+ instructorDetails: {
35
+ name: instructorName,
36
+ description: `Expert instructor covering ${topic}.`,
37
+ },
38
+ };
39
+ }
40
+
41
+ /**
42
+ * Import a fully-structured course into a CRM location.
43
+ *
44
+ * @param {string} locationId - CRM location ID
45
+ * @param {string} userId - CRM user ID
46
+ * @param {Object} courseData - Full course structure
47
+ */
48
+ export async function importCourse(locationId, userId, courseData) {
49
+ const sdk = await getSDK();
50
+
51
+ const response = await sdk.courses.importCourses({
52
+ locationId,
53
+ userId,
54
+ products: [courseData],
55
+ });
56
+
57
+ return response;
58
+ }
59
+
60
+ /**
61
+ * Register course generator MCP tools.
62
+ */
63
+ export function registerCourseTools(server, z) {
64
+ server.tool(
65
+ "crm_generate_course",
66
+ "Generate a full AI course and import it into a CRM location. Provide the topic, audience, and desired structure. The AI builds categories with lessons and imports everything.",
67
+ {
68
+ location_id: z.string().describe("CRM location ID where the course will be created"),
69
+ user_id: z.string().describe("CRM user ID (course owner)"),
70
+ title: z.string().describe("Course title"),
71
+ description: z.string().describe("Course description"),
72
+ instructor_name: z.string().optional().describe("Instructor name (default: AI Instructor)"),
73
+ categories: z.array(z.object({
74
+ title: z.string().describe("Category/module title"),
75
+ lessons: z.array(z.object({
76
+ title: z.string().describe("Lesson title"),
77
+ description: z.string().describe("Lesson content/description (can be full HTML)"),
78
+ content_type: z.string().optional().describe("Content type: video, text, audio (default: text)"),
79
+ })).describe("Lessons in this category"),
80
+ })).describe("Course categories with lessons"),
81
+ },
82
+ async ({ location_id, user_id, title, description, instructor_name, categories }) => {
83
+ try {
84
+ const courseData = {
85
+ title,
86
+ description,
87
+ imageUrl: "",
88
+ categories: categories.map((cat) => ({
89
+ title: cat.title,
90
+ visibility: "published",
91
+ thumbnailUrl: "",
92
+ posts: cat.lessons.map((lesson) => ({
93
+ title: lesson.title,
94
+ visibility: "published",
95
+ thumbnailUrl: "",
96
+ contentType: lesson.content_type || "text",
97
+ description: lesson.description,
98
+ bucketVideoUrl: "",
99
+ postMaterials: [],
100
+ })),
101
+ subCategories: [],
102
+ })),
103
+ instructorDetails: {
104
+ name: instructor_name || "AI Instructor",
105
+ description: `Course instructor for ${title}`,
106
+ },
107
+ };
108
+
109
+ const result = await importCourse(location_id, user_id, courseData);
110
+
111
+ return {
112
+ content: [{
113
+ type: "text",
114
+ text: JSON.stringify({
115
+ status: "imported",
116
+ title,
117
+ categories: categories.length,
118
+ total_lessons: categories.reduce((sum, c) => sum + c.lessons.length, 0),
119
+ location_id,
120
+ result,
121
+ }, null, 2),
122
+ }],
123
+ };
124
+ } catch (err) {
125
+ return {
126
+ content: [{
127
+ type: "text",
128
+ text: JSON.stringify({ error: err.message, status: "failed" }, null, 2),
129
+ }],
130
+ };
131
+ }
132
+ }
133
+ );
134
+
135
+ server.tool(
136
+ "crm_list_courses",
137
+ "List all courses/products in a CRM location.",
138
+ {
139
+ location_id: z.string().describe("CRM location ID"),
140
+ },
141
+ async ({ location_id }) => {
142
+ try {
143
+ const sdk = await getSDK();
144
+ const response = await sdk.courses.getProducts({ locationId: location_id });
145
+ return {
146
+ content: [{
147
+ type: "text",
148
+ text: JSON.stringify(response, null, 2),
149
+ }],
150
+ };
151
+ } catch (err) {
152
+ return {
153
+ content: [{
154
+ type: "text",
155
+ text: JSON.stringify({ error: err.message }, null, 2),
156
+ }],
157
+ };
158
+ }
159
+ }
160
+ );
161
+ }
@@ -0,0 +1,250 @@
1
+ // ============================================================
2
+ // 0nMCP — CRM Email Campaign Execution Tools
3
+ // ============================================================
4
+ // Full email campaign platform:
5
+ // - Fetch & display CRM email templates
6
+ // - Create new templates
7
+ // - Fetch campaigns with stats
8
+ // - IP warm-up scheduling
9
+ // - Template selection for campaign execution
10
+ //
11
+ // SDK methods: emails.fetchCampaigns, emails.fetchTemplate,
12
+ // emails.createTemplate, emails.updateTemplate, emails.deleteTemplate
13
+ // campaigns.getCampaigns, conversations.sendANewMessage
14
+ // ============================================================
15
+
16
+ import getSDK from "./sdk.js";
17
+ import { crmHeaders, CRM_API_BASE } from "./helpers.js";
18
+
19
+ /**
20
+ * Register email campaign MCP tools.
21
+ */
22
+ export function registerEmailCampaignTools(server, z) {
23
+
24
+ // ── List email templates ───────────────────────────────────
25
+ server.tool(
26
+ "crm_list_email_templates",
27
+ "List all email templates for a location. Returns template names, preview URLs, and IDs for selection in campaign builders.",
28
+ {
29
+ location_id: z.string().describe("CRM location ID"),
30
+ search: z.string().optional().describe("Search by template name"),
31
+ limit: z.number().optional().describe("Max results (default 20)"),
32
+ offset: z.number().optional().describe("Pagination offset"),
33
+ },
34
+ async ({ location_id, search, limit, offset }) => {
35
+ try {
36
+ const sdk = await getSDK();
37
+ const result = await sdk.emails.fetchTemplate({
38
+ locationId: location_id,
39
+ limit: String(limit || 20),
40
+ offset: String(offset || 0),
41
+ search: search || undefined,
42
+ templatesOnly: "true",
43
+ });
44
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
45
+ } catch (err) {
46
+ return { content: [{ type: "text", text: JSON.stringify({ error: err.message }) }] };
47
+ }
48
+ }
49
+ );
50
+
51
+ // ── Create email template ──────────────────────────────────
52
+ server.tool(
53
+ "crm_create_email_template",
54
+ "Create a new email template. Returns a redirect URL to the CRM email builder where the template can be visually edited.",
55
+ {
56
+ location_id: z.string().describe("CRM location ID"),
57
+ name: z.string().describe("Template name"),
58
+ type: z.string().optional().describe("Template type (html, folder). Default: html"),
59
+ import_url: z.string().optional().describe("URL to import template from"),
60
+ },
61
+ async ({ location_id, name, type, import_url }) => {
62
+ try {
63
+ const sdk = await getSDK();
64
+ const result = await sdk.emails.createTemplate({
65
+ locationId: location_id,
66
+ name,
67
+ type: type || "html",
68
+ importProvider: import_url ? "url" : "blank",
69
+ importURL: import_url || undefined,
70
+ });
71
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
72
+ } catch (err) {
73
+ return { content: [{ type: "text", text: JSON.stringify({ error: err.message }) }] };
74
+ }
75
+ }
76
+ );
77
+
78
+ // ── List campaigns with stats ──────────────────────────────
79
+ server.tool(
80
+ "crm_list_email_campaigns",
81
+ "List email campaigns for a location with optional stats. Shows campaign name, status, open/click rates.",
82
+ {
83
+ location_id: z.string().describe("CRM location ID"),
84
+ status: z.string().optional().describe("Filter by status (draft, scheduled, sent, archived)"),
85
+ show_stats: z.boolean().optional().describe("Include open/click stats (default true)"),
86
+ limit: z.number().optional().describe("Max results"),
87
+ offset: z.number().optional().describe("Pagination offset"),
88
+ },
89
+ async ({ location_id, status, show_stats, limit, offset }) => {
90
+ try {
91
+ const sdk = await getSDK();
92
+ const result = await sdk.emails.fetchCampaigns({
93
+ locationId: location_id,
94
+ status: status || undefined,
95
+ showStats: show_stats !== false,
96
+ limit: limit || 20,
97
+ offset: offset || 0,
98
+ campaignsOnly: true,
99
+ });
100
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
101
+ } catch (err) {
102
+ return { content: [{ type: "text", text: JSON.stringify({ error: err.message }) }] };
103
+ }
104
+ }
105
+ );
106
+
107
+ // ── Send email to contact ──────────────────────────────────
108
+ server.tool(
109
+ "crm_send_email",
110
+ "Send an email to a contact. Supports HTML body, templates, attachments, CC/BCC, threading, and scheduling.",
111
+ {
112
+ access_token: z.string().optional().describe("CRM access token (auto-resolved if location_id provided)"),
113
+ location_id: z.string().optional().describe("CRM location ID"),
114
+ contact_id: z.string().describe("Contact ID to send to"),
115
+ subject: z.string().describe("Email subject line"),
116
+ html: z.string().optional().describe("HTML email body"),
117
+ message: z.string().optional().describe("Plain text message body"),
118
+ from_email: z.string().optional().describe("From email address"),
119
+ to_email: z.string().optional().describe("To email (overrides contact default)"),
120
+ cc: z.array(z.string()).optional().describe("CC email addresses"),
121
+ bcc: z.array(z.string()).optional().describe("BCC email addresses"),
122
+ template_id: z.string().optional().describe("CRM email template ID"),
123
+ attachments: z.array(z.string()).optional().describe("Attachment URLs"),
124
+ thread_id: z.string().optional().describe("Email thread ID for threading"),
125
+ reply_mode: z.string().optional().describe("Reply mode: reply, reply_all"),
126
+ scheduled_timestamp: z.number().optional().describe("Unix timestamp to schedule send"),
127
+ },
128
+ async (args) => {
129
+ try {
130
+ // Use direct API since conversations.sendANewMessage covers this
131
+ const token = args.access_token;
132
+ if (!token) {
133
+ return { content: [{ type: "text", text: JSON.stringify({ error: "access_token required for sending" }) }] };
134
+ }
135
+
136
+ const body = {
137
+ type: "Email",
138
+ contactId: args.contact_id,
139
+ subject: args.subject,
140
+ html: args.html || undefined,
141
+ message: args.message || undefined,
142
+ emailFrom: args.from_email || undefined,
143
+ emailTo: args.to_email || undefined,
144
+ emailCc: args.cc || undefined,
145
+ emailBcc: args.bcc || undefined,
146
+ templateId: args.template_id || undefined,
147
+ attachments: args.attachments || undefined,
148
+ threadId: args.thread_id || undefined,
149
+ emailReplyMode: args.reply_mode || undefined,
150
+ scheduledTimestamp: args.scheduled_timestamp || undefined,
151
+ };
152
+
153
+ const headers = crmHeaders(token);
154
+ const res = await fetch(`${CRM_API_BASE}/conversations/messages`, {
155
+ method: "POST",
156
+ headers,
157
+ body: JSON.stringify(body),
158
+ });
159
+ const data = await res.json();
160
+
161
+ return { content: [{ type: "text", text: JSON.stringify({ sent: res.ok, status: res.status, data }, null, 2) }] };
162
+ } catch (err) {
163
+ return { content: [{ type: "text", text: JSON.stringify({ error: err.message }) }] };
164
+ }
165
+ }
166
+ );
167
+
168
+ // ── IP warm-up schedule generator ──────────────────────────
169
+ server.tool(
170
+ "crm_generate_warmup_schedule",
171
+ "Generate an IP warm-up schedule for a new email sending domain. Returns a day-by-day plan with volume targets, engagement monitoring, and escalation thresholds.",
172
+ {
173
+ daily_target: z.number().describe("Target daily send volume after warm-up"),
174
+ domain: z.string().describe("Sending domain being warmed up"),
175
+ warmup_days: z.number().optional().describe("Number of warm-up days (default 30)"),
176
+ },
177
+ async ({ daily_target, domain, warmup_days }) => {
178
+ const days = warmup_days || 30;
179
+ const schedule = [];
180
+
181
+ for (let day = 1; day <= days; day++) {
182
+ const pct = Math.min(1, Math.pow(day / days, 1.5));
183
+ const volume = Math.max(10, Math.round(daily_target * pct));
184
+ const phase = day <= 7 ? "cautious" : day <= 14 ? "building" : day <= 21 ? "accelerating" : "full";
185
+
186
+ schedule.push({
187
+ day,
188
+ volume,
189
+ phase,
190
+ engagement_target: phase === "cautious" ? ">30% open rate" : phase === "building" ? ">25% open rate" : ">20% open rate",
191
+ action: phase === "cautious"
192
+ ? "Send to most engaged contacts only (opened in last 30 days)"
193
+ : phase === "building"
194
+ ? "Expand to contacts who opened in last 90 days"
195
+ : phase === "accelerating"
196
+ ? "Include contacts who opened in last 180 days"
197
+ : "Full list — monitor bounce rate (<2%) and spam rate (<0.1%)",
198
+ pause_if: "Bounce rate >5% OR spam complaints >0.3% OR open rate drops below 15%",
199
+ });
200
+ }
201
+
202
+ return {
203
+ content: [{
204
+ type: "text",
205
+ text: JSON.stringify({
206
+ domain,
207
+ total_days: days,
208
+ target_daily_volume: daily_target,
209
+ schedule,
210
+ rules: [
211
+ "Never send to unengaged contacts in first 14 days",
212
+ "Monitor postmaster tools daily (Google, Yahoo, Microsoft)",
213
+ "If any ISP blocks, pause 48 hours and reduce volume 50%",
214
+ "Authenticate SPF, DKIM, DMARC before starting",
215
+ "Use consistent from address and branding throughout",
216
+ ],
217
+ }, null, 2),
218
+ }],
219
+ };
220
+ }
221
+ );
222
+
223
+ // ── Get template iframe URL ────────────────────────────────
224
+ server.tool(
225
+ "crm_get_template_preview_url",
226
+ "Get the preview URL for a CRM email template. This URL can be loaded in an iframe to show the template visually.",
227
+ {
228
+ location_id: z.string().describe("CRM location ID"),
229
+ template_id: z.string().describe("Template ID"),
230
+ },
231
+ async ({ location_id, template_id }) => {
232
+ // CRM template preview URLs follow this pattern
233
+ const previewUrl = `https://app.leadconnectorhq.com/v2/location/${location_id}/marketing/emails/editor/${template_id}`;
234
+ const thumbnailUrl = `https://services.leadconnectorhq.com/emails/builder/${template_id}/thumbnail?locationId=${location_id}`;
235
+
236
+ return {
237
+ content: [{
238
+ type: "text",
239
+ text: JSON.stringify({
240
+ template_id,
241
+ location_id,
242
+ preview_url: previewUrl,
243
+ thumbnail_url: thumbnailUrl,
244
+ embed_note: "Use thumbnail_url in an img tag for preview. preview_url requires CRM auth to load in iframe.",
245
+ }, null, 2),
246
+ }],
247
+ };
248
+ }
249
+ );
250
+ }
package/crm/helpers.js CHANGED
@@ -35,12 +35,21 @@ export function crmHeaders(accessToken) {
35
35
  * Every tool automatically receives `access_token` (required) and
36
36
  * `location_id` (optional) parameters.
37
37
  */
38
+ let _resolveToken = null;
39
+ export function setTokenResolver(fn) { _resolveToken = fn; }
40
+
38
41
  export function registerTools(server, z, definitions, proxy) {
39
42
  for (const def of definitions) {
40
43
  const schema = buildSchema(z, def);
41
44
 
42
45
  server.tool(def.name, def.description, schema, async (args) => {
43
46
  try {
47
+ if (!args.access_token && args.location_id && _resolveToken) {
48
+ args.access_token = await _resolveToken(args.location_id);
49
+ }
50
+ if (!args.access_token) {
51
+ return { content: [{ type: "text", text: JSON.stringify({ error: "No access_token and no OAuth token for this location. Use crm_oauth_connect or pass access_token." }) }] };
52
+ }
44
53
  const headers = crmHeaders(args.access_token);
45
54
  let url = CRM_API_BASE + def.path;
46
55