@eugeneboondock/hubspot-mcp-server 0.2.15

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.
@@ -0,0 +1,2120 @@
1
+ import { Client } from '@hubspot/api-client';
2
+ import dotenv from 'dotenv';
3
+
4
+ dotenv.config();
5
+
6
+ // Convert any datetime objects to ISO strings
7
+ export function convertDatetimeFields(obj: any): any {
8
+ if (obj === null || obj === undefined) {
9
+ return obj;
10
+ }
11
+
12
+ if (typeof obj === 'object') {
13
+ if (obj instanceof Date) {
14
+ return obj.toISOString();
15
+ }
16
+
17
+ if (Array.isArray(obj)) {
18
+ return obj.map(item => convertDatetimeFields(item));
19
+ }
20
+
21
+ const result: Record<string, any> = {};
22
+ for (const [key, value] of Object.entries(obj)) {
23
+ result[key] = convertDatetimeFields(value);
24
+ }
25
+ return result;
26
+ }
27
+
28
+ return obj;
29
+ }
30
+
31
+ function filterRecordsByUpdatedAfter(
32
+ records: any[] | undefined,
33
+ updatedAfter?: string,
34
+ extractor?: (record: any) => string | number | null,
35
+ debugLabel: string = 'records'
36
+ ): any[] {
37
+ if (!Array.isArray(records) || !updatedAfter) {
38
+ return records || [];
39
+ }
40
+
41
+ const cutoff = Date.parse(updatedAfter);
42
+ if (Number.isNaN(cutoff)) {
43
+ return records;
44
+ }
45
+
46
+ let missingTimestampCount = 0;
47
+ let invalidTimestampCount = 0;
48
+
49
+ const filtered = records.filter((record) => {
50
+ const rawValue = extractor
51
+ ? extractor(record)
52
+ : record?.updatedAt ??
53
+ record?.updated_at ??
54
+ record?.updated ??
55
+ record?.lastUpdated ??
56
+ record?.lastUpdatedAt ??
57
+ record?.properties?.hs_lastmodifieddate ??
58
+ record?.properties?.lastmodifieddate ??
59
+ record?.metadata?.lastUpdated ??
60
+ record?.metadata?.updatedAt ??
61
+ record?.stats?.lastUpdated ??
62
+ null;
63
+
64
+ if (!rawValue) {
65
+ missingTimestampCount++;
66
+ return true;
67
+ }
68
+
69
+ const timestamp =
70
+ typeof rawValue === 'number' ? rawValue : Date.parse(rawValue as string);
71
+
72
+ if (Number.isNaN(timestamp)) {
73
+ invalidTimestampCount++;
74
+ return true;
75
+ }
76
+
77
+ return timestamp > cutoff;
78
+ });
79
+
80
+ const removed = records.length - filtered.length;
81
+ console.log(
82
+ `🔁 Incremental filter for ${debugLabel}: kept ${filtered.length}/${records.length} records updated after ${new Date(
83
+ cutoff
84
+ ).toISOString()}`
85
+ );
86
+
87
+ if (removed === 0) {
88
+ console.log(
89
+ `â„šī¸ No ${debugLabel} records newer than ${new Date(cutoff).toISOString()}`
90
+ );
91
+ }
92
+
93
+ if (missingTimestampCount > 0) {
94
+ console.warn(
95
+ `[Incremental] ${debugLabel}: ${missingTimestampCount} record(s) missing updated timestamp (included by default).`
96
+ );
97
+ }
98
+
99
+ if (invalidTimestampCount > 0) {
100
+ console.warn(
101
+ `[Incremental] ${debugLabel}: ${invalidTimestampCount} record(s) had invalid timestamp values (included by default).`
102
+ );
103
+ }
104
+
105
+ return filtered;
106
+ }
107
+
108
+ export class HubSpotScopeError extends Error {
109
+ public scope: string;
110
+ public code: string;
111
+
112
+ constructor(scopeName: string, cause?: any) {
113
+ super(`HubSpot scope "${scopeName}" is not granted for this account.`);
114
+ this.name = 'HubSpotScopeError';
115
+ this.scope = scopeName;
116
+ this.code = 'HUBSPOT_SCOPE_MISSING';
117
+ if (cause) {
118
+ (this as any).cause = cause;
119
+ }
120
+ }
121
+ }
122
+
123
+ export class HubSpotClient {
124
+ private client: Client;
125
+ private accessToken: string;
126
+ private lastRequestTime: number = 0;
127
+ private requestCount: number = 0;
128
+ private rateLimitWindow: number = 1000; // 1 second window
129
+
130
+ constructor(accessToken?: string) {
131
+ const token = accessToken ||
132
+ process.env.HUBSPOT_ACCESS_TOKEN ||
133
+ process.env.HUBSPOT_DEVELOPER_API_KEY ||
134
+ process.env.HUBSPOT_PERSONAL_ACCESS_KEY;
135
+
136
+ if (!token) {
137
+ throw new Error('HubSpot access token is required. Set HUBSPOT_ACCESS_TOKEN, HUBSPOT_DEVELOPER_API_KEY, or HUBSPOT_PERSONAL_ACCESS_KEY');
138
+ }
139
+
140
+ this.accessToken = token;
141
+ this.client = new Client({ accessToken: token });
142
+ }
143
+
144
+ /**
145
+ * Update the access token and reinitialize the client
146
+ */
147
+ updateAccessToken(newToken: string): void {
148
+ this.accessToken = newToken;
149
+ this.client = new Client({ accessToken: newToken });
150
+ console.log('🔄 HubSpot client: Access token updated');
151
+ }
152
+
153
+ /**
154
+ * Wrapper for API calls with automatic token refresh on 401 errors
155
+ */
156
+ private async callWithRetry<T>(apiCall: () => Promise<T>, retryOnAuth: boolean = true): Promise<T> {
157
+ try {
158
+ return await apiCall();
159
+ } catch (error: any) {
160
+ // Check if it's a 401 authentication error
161
+ if (error?.code === 401 &&
162
+ error?.body?.category === 'EXPIRED_AUTHENTICATION' &&
163
+ retryOnAuth) {
164
+
165
+ console.log('🔄 HubSpot client: Detected 401 error, attempting token refresh...');
166
+
167
+ // Try to get a fresh token from environment (in case it was updated)
168
+ const refreshedToken = process.env.HUBSPOT_ACCESS_TOKEN ||
169
+ process.env.HUBSPOT_DEVELOPER_API_KEY ||
170
+ process.env.HUBSPOT_PERSONAL_ACCESS_KEY;
171
+
172
+ if (refreshedToken && refreshedToken !== this.accessToken) {
173
+ console.log('🔄 HubSpot client: Found updated token, retrying...');
174
+ this.updateAccessToken(refreshedToken);
175
+
176
+ // Retry the API call once with the new token
177
+ return await apiCall();
178
+ } else {
179
+ console.log('❌ HubSpot client: No updated token available for refresh');
180
+ throw error;
181
+ }
182
+ }
183
+
184
+ throw error;
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Rate limiting to prevent 429 errors
190
+ */
191
+ private async rateLimitDelay(): Promise<void> {
192
+ const now = Date.now();
193
+ const timeSinceLastRequest = now - this.lastRequestTime;
194
+
195
+ // Reset counter if window has passed
196
+ if (timeSinceLastRequest > this.rateLimitWindow) {
197
+ this.requestCount = 0;
198
+ }
199
+
200
+ // If we've made too many requests, wait
201
+ if (this.requestCount >= 5) {
202
+ const waitTime = this.rateLimitWindow - timeSinceLastRequest;
203
+ if (waitTime > 0) {
204
+ await new Promise(resolve => setTimeout(resolve, waitTime));
205
+ }
206
+ this.requestCount = 0;
207
+ }
208
+
209
+ this.requestCount++;
210
+ this.lastRequestTime = Date.now();
211
+ }
212
+
213
+ /**
214
+ * Helper method to paginate through all results using HubSpot's after cursor
215
+ */
216
+ private async paginateSearch(
217
+ searchFn: (searchRequest: any) => Promise<any>,
218
+ baseRequest: any,
219
+ limit: number = 100
220
+ ): Promise<any[]> {
221
+ // Ensure limit doesn't exceed HubSpot's maximum
222
+ const safeLimit = Math.min(Math.max(limit, 1), 200);
223
+
224
+ const allResults: any[] = [];
225
+ let after: string | undefined = undefined;
226
+ let hasMore = true;
227
+
228
+ while (hasMore) {
229
+ await this.rateLimitDelay();
230
+
231
+ const searchRequest = {
232
+ ...baseRequest,
233
+ limit: safeLimit,
234
+ ...(after ? { after } : {})
235
+ };
236
+
237
+ const response = await searchFn(searchRequest);
238
+
239
+ if (response.results && response.results.length > 0) {
240
+ allResults.push(...response.results);
241
+ console.log(`📄 Fetched ${response.results.length} records (total: ${allResults.length})`);
242
+ }
243
+
244
+ // Check for pagination cursor
245
+ if (response.paging?.next?.after) {
246
+ after = response.paging.next.after;
247
+ } else {
248
+ hasMore = false;
249
+ }
250
+ }
251
+
252
+ return allResults;
253
+ }
254
+
255
+ private normalizeDateInput(value?: string | number | Date): string | null {
256
+ if (!value) return null;
257
+ const dateValue = value instanceof Date ? value : new Date(value);
258
+ if (Number.isNaN(dateValue.getTime())) {
259
+ return null;
260
+ }
261
+ return dateValue.toISOString();
262
+ }
263
+
264
+ private applyUpdatedAfterFilter(
265
+ baseRequest: any,
266
+ updatedAfter?: string,
267
+ propertyName: string = 'hs_lastmodifieddate'
268
+ ) {
269
+ if (!baseRequest || !updatedAfter) {
270
+ return baseRequest;
271
+ }
272
+
273
+ const normalized = this.normalizeDateInput(updatedAfter);
274
+ if (!normalized) {
275
+ return baseRequest;
276
+ }
277
+
278
+ const filter = {
279
+ propertyName,
280
+ operator: 'GT',
281
+ value: normalized
282
+ };
283
+
284
+ if (Array.isArray(baseRequest.filterGroups) && baseRequest.filterGroups.length > 0) {
285
+ baseRequest.filterGroups = baseRequest.filterGroups.map((group: any) => ({
286
+ ...group,
287
+ filters: Array.isArray(group.filters) ? [...group.filters, filter] : [filter]
288
+ }));
289
+ } else {
290
+ baseRequest.filterGroups = [{ filters: [filter] }];
291
+ }
292
+
293
+ return baseRequest;
294
+ }
295
+
296
+ /**
297
+ * Helper method to paginate through all results using HubSpot's after cursor (for List/GET APIs)
298
+ */
299
+ private async paginateList(
300
+ listFn: (params: any) => Promise<any>,
301
+ limit: number = 100
302
+ ): Promise<any[]> {
303
+ // Ensure limit doesn't exceed HubSpot's maximum
304
+ const safeLimit = Math.min(Math.max(limit, 1), 200);
305
+
306
+ const allResults: any[] = [];
307
+ let after: string | undefined = undefined;
308
+ let hasMore = true;
309
+
310
+ while (hasMore) {
311
+ await this.rateLimitDelay();
312
+
313
+ const params: any = { limit: safeLimit, after };
314
+ const response = await listFn(params);
315
+
316
+ // Standard v3 SDK responses have a 'results' array
317
+ const results = response.results;
318
+
319
+ if (results && Array.isArray(results) && results.length > 0) {
320
+ allResults.push(...results);
321
+ console.log(`📄 Fetched ${results.length} records (total: ${allResults.length})`);
322
+ }
323
+
324
+ // Standard v3 SDK pagination cursor, guard against stuck cursors
325
+ const nextAfter = response.paging?.next?.after;
326
+ if (!nextAfter || nextAfter === after || !results || results.length === 0) {
327
+ hasMore = false;
328
+ } else {
329
+ after = nextAfter;
330
+ }
331
+ }
332
+ return allResults;
333
+ }
334
+
335
+ async getRecentCompanies(limit: number = 100, updatedAfter?: string): Promise<any> {
336
+ return this.callWithRetry(async () => {
337
+ console.log('đŸĸ Fetching ALL companies with pagination...');
338
+
339
+ // Create search request with sort by lastmodifieddate
340
+ const baseRequest = {
341
+ sorts: ['lastmodifieddate:desc'],
342
+ properties: [
343
+ // Basic company info - enhanced
344
+ 'name', 'domain', 'website', 'phone', 'industry', 'company_type',
345
+ // Address info - comprehensive
346
+ 'address', 'address2', 'city', 'state', 'zip', 'country', 'country_code',
347
+ // Company details - expanded
348
+ 'description', 'numberofemployees', 'annualrevenue', 'type',
349
+ 'founded_year', 'business_type', 'revenue_range', 'employee_range',
350
+ 'target_account', 'ideal_customer_profile', 'account_tier',
351
+ // Contact info
352
+ 'owneremail', 'ownername', 'phone', 'owner_phone',
353
+ // Website and online presence - comprehensive
354
+ 'website', 'domain', 'blog_url', 'facebook_url', 'twitter_url',
355
+ 'linkedin_url', 'youtube_url', 'instagram_url', 'pinterest_url',
356
+ 'googleplus_url', 'klout_score', 'alexa_ranking', 'semrush_ranking',
357
+ // Company associations and details
358
+ 'hs_parent_company_id', 'hs_child_company_ids', 'hs_analytics_source',
359
+ 'hs_analytics_source_data_1', 'hs_analytics_source_data_2', 'hs_analytics_source_data_3',
360
+ 'hs_analytics_source_data_4', 'hs_analytics_source_raw',
361
+ // Custom properties - expanded
362
+ 'custom_property_1', 'custom_property_2', 'custom_property_3', 'custom_property_4', 'custom_property_5',
363
+ 'custom_property_6', 'custom_property_7', 'custom_property_8', 'custom_property_9', 'custom_property_10',
364
+ // Dates and timestamps - comprehensive
365
+ 'createdate', 'hs_lastmodifieddate', 'lastmodifieddate', 'hs_createdate',
366
+ 'first_conversion_date', 'last_conversion_date', 'hs_first_engagement_date',
367
+ 'hs_last_contacted', 'hs_last_activity_date', 'hs_first_contact_createdate',
368
+ // Additional fields - enhanced
369
+ 'hs_object_id', 'hs_unique_creation_key', 'hs_company_id', 'id',
370
+ 'is_deleted', 'hs_time_to_first_engagement', 'hs_time_to_close',
371
+ // Lead status and scoring
372
+ 'hs_lead_status', 'hs_lead_scoring', 'hs_predictivecontactscore_v2',
373
+ 'hs_predictivecontactscoring_v2', 'hs_ideal_customer_profile'
374
+ ]
375
+ };
376
+ this.applyUpdatedAfterFilter(baseRequest, updatedAfter, 'hs_lastmodifieddate');
377
+
378
+ // Use pagination to fetch ALL companies
379
+ const allCompanies = await this.paginateSearch(
380
+ (req) => this.client.crm.companies.searchApi.doSearch(req),
381
+ baseRequest,
382
+ limit
383
+ );
384
+
385
+ console.log(`✅ Total companies fetched: ${allCompanies.length}`);
386
+ return convertDatetimeFields(allCompanies);
387
+ }).catch((error: any) => {
388
+ console.error('Error getting recent companies:', error);
389
+ return { error: error.message };
390
+ });
391
+ }
392
+
393
+ async getRecentContacts(limit: number = 100, updatedAfter?: string): Promise<any> {
394
+ try {
395
+ console.log('📞 Fetching ALL contacts with pagination...');
396
+
397
+ // Create search request with sort by lastmodifieddate
398
+ const baseRequest = {
399
+ sorts: ['lastmodifieddate:desc'],
400
+ properties: [
401
+ // Basic info - enhanced
402
+ 'firstname', 'lastname', 'email', 'phone', 'mobilephone', 'jobtitle',
403
+ 'salutation', 'middle_name', 'prefix', 'suffix',
404
+ // Company info - comprehensive
405
+ 'company', 'website', 'company_name', 'company_website',
406
+ 'company_domain', 'company_industry', 'company_size',
407
+ // Address info - comprehensive
408
+ 'address', 'address2', 'city', 'state', 'zip', 'country', 'country_code',
409
+ // Contact properties - expanded
410
+ 'lifecyclestage', 'lead_status', 'hs_lead_status', 'contact_owner',
411
+ 'hs_contact_stage', 'hs_contact_status', 'hs_contact_source',
412
+ // Company association and details - enhanced
413
+ 'associatedcompanyid', 'associatedcompany_name', 'associatedcompany_domain',
414
+ 'associatedcompany_website', 'associatedcompany_industry', 'associatedcompany_size',
415
+ // Social media and online presence - comprehensive
416
+ 'linkedin_profile', 'twitter_profile', 'facebook_profile',
417
+ 'linkedin_url', 'twitter_url', 'facebook_url', 'instagram_url', 'youtube_url',
418
+ // Professional details - expanded
419
+ 'seniority', 'department', 'industry', 'job_function', 'job_role',
420
+ 'job_level', 'years_experience', 'education_level', 'school',
421
+ // Lead scoring and qualification - comprehensive
422
+ 'hs_lead_scoring', 'hs_lead_status', 'hs_analytics_source',
423
+ 'hs_analytics_source_data_1', 'hs_analytics_source_data_2', 'hs_analytics_source_data_3',
424
+ 'hs_analytics_source_data_4', 'hs_analytics_source_raw',
425
+ 'hs_predictivecontactscore_v2', 'hs_predictivecontactscoring_v2',
426
+ // Custom properties - expanded
427
+ 'custom_property_1', 'custom_property_2', 'custom_property_3', 'custom_property_4', 'custom_property_5',
428
+ 'custom_property_6', 'custom_property_7', 'custom_property_8', 'custom_property_9', 'custom_property_10',
429
+ // Dates and timestamps - comprehensive
430
+ 'createdate', 'hs_lastmodifieddate', 'lastmodifieddate', 'hs_createdate',
431
+ 'first_conversion_date', 'last_conversion_date', 'hs_first_engagement_date',
432
+ 'hs_last_contacted', 'hs_last_activity_date', 'hs_first_contact_createdate',
433
+ // Additional fields - enhanced
434
+ 'hs_object_id', 'hs_unique_creation_key', 'hs_contact_id', 'id',
435
+ 'is_deleted', 'hs_time_to_first_engagement', 'hs_contact_stage_probability'
436
+ ]
437
+ };
438
+ this.applyUpdatedAfterFilter(baseRequest, updatedAfter, 'hs_lastmodifieddate');
439
+
440
+ // Use pagination to fetch ALL contacts
441
+ const allContacts = await this.paginateSearch(
442
+ (req) => this.client.crm.contacts.searchApi.doSearch(req),
443
+ baseRequest,
444
+ limit
445
+ );
446
+
447
+ console.log(`✅ Total contacts fetched: ${allContacts.length}`);
448
+ return convertDatetimeFields(allContacts);
449
+ } catch (error: any) {
450
+ console.error('Error getting recent contacts:', error);
451
+ return { error: error.message };
452
+ }
453
+ }
454
+
455
+ async getAssociations(fromObjectType: string, fromObjectId: string, toObjectType: string): Promise<any[]> {
456
+ try {
457
+ const allAssociations: any[] = [];
458
+ let after: string | undefined = undefined;
459
+
460
+ while (true) {
461
+ await this.rateLimitDelay();
462
+ const response = await this.client.crm.associations.v4.basicApi.getPage(
463
+ fromObjectType,
464
+ fromObjectId,
465
+ toObjectType,
466
+ after,
467
+ 500
468
+ );
469
+
470
+ const results = response.results || [];
471
+ if (results.length > 0) {
472
+ allAssociations.push(...results);
473
+ }
474
+
475
+ const nextAfter = response.paging?.next?.after;
476
+ if (!nextAfter || nextAfter === after || results.length === 0) {
477
+ break;
478
+ }
479
+ after = nextAfter;
480
+ }
481
+
482
+ return allAssociations;
483
+ } catch (error: any) {
484
+ console.error(`Error fetching associations for ${fromObjectType}:${fromObjectId} -> ${toObjectType}:`, error);
485
+ throw new Error(`HubSpot API error: ${error.message}`);
486
+ }
487
+ }
488
+
489
+ async searchTasks(minutes: number = 30, limit: number = 50): Promise<any> {
490
+ try {
491
+ // Calculate the date range (past N minutes)
492
+ const endTime = new Date();
493
+ const startTime = new Date(endTime);
494
+ startTime.setMinutes(startTime.getMinutes() - minutes);
495
+ const startTimestamp = startTime.getTime();
496
+
497
+ console.log(`[MCP Debug] Searching tasks from ${startTime.toISOString()} to ${endTime.toISOString()}`);
498
+ console.log(`[MCP Debug] Timestamp filter: ${startTimestamp}`);
499
+
500
+ const baseRequest = {
501
+ filterGroups: [{
502
+ filters: [{
503
+ propertyName: 'hs_lastmodifieddate',
504
+ operator: 'GTE' as any,
505
+ value: startTimestamp.toString()
506
+ }]
507
+ }],
508
+ properties: ['hs_task_subject', 'hs_task_body', 'hs_task_status', 'hs_lastmodifieddate', 'hs_createdate'],
509
+ sorts: ['hs_lastmodifieddate:desc']
510
+ };
511
+
512
+ const allTasks = await this.paginateSearch(
513
+ (req) => this.client.crm.objects.tasks.searchApi.doSearch(req),
514
+ baseRequest,
515
+ limit
516
+ );
517
+
518
+ console.log(`[MCP Debug] Tasks found: ${allTasks.length}`);
519
+
520
+ return convertDatetimeFields(allTasks);
521
+
522
+ } catch (error: any) {
523
+ console.error('Error searching tasks:', error);
524
+ return { error: error.message };
525
+ }
526
+ }
527
+
528
+ async searchNotes(minutes: number = 30, limit: number = 50): Promise<any> {
529
+ try {
530
+ // Calculate the date range (past N minutes)
531
+ const endTime = new Date();
532
+ const startTime = new Date(endTime);
533
+ startTime.setMinutes(startTime.getMinutes() - minutes);
534
+ const startTimestamp = startTime.getTime();
535
+
536
+ console.log(`[MCP Debug] Searching notes from ${startTime.toISOString()} to ${endTime.toISOString()}`);
537
+ console.log(`[MCP Debug] Timestamp filter: ${startTimestamp}`);
538
+
539
+ const baseRequest = {
540
+ filterGroups: [{
541
+ filters: [{
542
+ propertyName: 'hs_lastmodifieddate',
543
+ operator: 'GTE' as any,
544
+ value: startTimestamp.toString()
545
+ }]
546
+ }],
547
+ properties: ['hs_note_body', 'hs_lastmodifieddate', 'hs_createdate'],
548
+ sorts: ['hs_lastmodifieddate:desc']
549
+ };
550
+
551
+ const allNotes = await this.paginateSearch(
552
+ (req) => this.client.crm.objects.notes.searchApi.doSearch(req),
553
+ baseRequest,
554
+ limit
555
+ );
556
+
557
+ console.log(`[MCP Debug] Notes found: ${allNotes.length}`);
558
+ return convertDatetimeFields(allNotes);
559
+
560
+ } catch (error: any) {
561
+ console.error('Error searching notes:', error);
562
+ return { error: error.message };
563
+ }
564
+ }
565
+
566
+ async searchCalls(minutes: number = 30, limit: number = 50): Promise<any> {
567
+ try {
568
+ // Calculate the date range (past N minutes)
569
+ const endTime = new Date();
570
+ const startTime = new Date(endTime);
571
+ startTime.setMinutes(startTime.getMinutes() - minutes);
572
+ const startTimestamp = startTime.getTime();
573
+
574
+ console.log(`[MCP Debug] Searching calls from ${startTime.toISOString()} to ${endTime.toISOString()}`);
575
+ console.log(`[MCP Debug] Timestamp filter: ${startTimestamp}`);
576
+
577
+ const baseRequest = {
578
+ filterGroups: [{
579
+ filters: [{
580
+ propertyName: 'hs_lastmodifieddate',
581
+ operator: 'GTE' as any,
582
+ value: startTimestamp.toString()
583
+ }]
584
+ }],
585
+ properties: ['hs_call_title', 'hs_call_body', 'hs_call_duration', 'hs_call_status', 'hs_lastmodifieddate', 'hs_createdate'],
586
+ sorts: ['hs_lastmodifieddate:desc']
587
+ };
588
+
589
+ const allCalls = await this.paginateSearch(
590
+ (req) => this.client.crm.objects.calls.searchApi.doSearch(req),
591
+ baseRequest,
592
+ limit
593
+ );
594
+
595
+ console.log(`[MCP Debug] Calls found: ${allCalls.length}`);
596
+ return convertDatetimeFields(allCalls);
597
+
598
+ } catch (error: any) {
599
+ console.error('Error searching calls:', error);
600
+ return { error: error.message };
601
+ }
602
+ }
603
+
604
+ async searchEmails(minutes: number = 30, limit: number = 50): Promise<any> {
605
+ try {
606
+ // Calculate the date range (past N minutes)
607
+ const endTime = new Date();
608
+ const startTime = new Date(endTime);
609
+ startTime.setMinutes(startTime.getMinutes() - minutes);
610
+ const startTimestamp = startTime.getTime();
611
+
612
+ console.log(`[MCP Debug] Searching emails from ${startTime.toISOString()} to ${endTime.toISOString()}`);
613
+ console.log(`[MCP Debug] Timestamp filter: ${startTimestamp}`);
614
+
615
+ const baseRequest = {
616
+ filterGroups: [{
617
+ filters: [{
618
+ propertyName: 'hs_lastmodifieddate',
619
+ operator: 'GTE' as any,
620
+ value: startTimestamp.toString()
621
+ }]
622
+ }],
623
+ properties: ['hs_email_subject', 'hs_email_text', 'hs_email_status', 'hs_lastmodifieddate', 'hs_createdate'],
624
+ sorts: ['hs_lastmodifieddate:desc']
625
+ };
626
+
627
+ const allEmails = await this.paginateSearch(
628
+ (req) => this.client.crm.objects.emails.searchApi.doSearch(req),
629
+ baseRequest,
630
+ limit
631
+ );
632
+
633
+ console.log(`[MCP Debug] Emails found: ${allEmails.length}`);
634
+ return convertDatetimeFields(allEmails);
635
+
636
+ } catch (error: any) {
637
+ console.error('Error searching emails:', error);
638
+ return { error: error.message };
639
+ }
640
+ }
641
+
642
+ async searchMeetings(minutes: number = 30, limit: number = 50): Promise<any> {
643
+ try {
644
+ // Calculate the date range (past N minutes)
645
+ const endTime = new Date();
646
+ const startTime = new Date(endTime);
647
+ startTime.setMinutes(startTime.getMinutes() - minutes);
648
+ const startTimestamp = startTime.getTime();
649
+
650
+ console.log(`[MCP Debug] Searching meetings from ${startTime.toISOString()} to ${endTime.toISOString()}`);
651
+ console.log(`[MCP Debug] Timestamp filter: ${startTimestamp}`);
652
+
653
+ const baseRequest = {
654
+ filterGroups: [{
655
+ filters: [{
656
+ propertyName: 'hs_lastmodifieddate',
657
+ operator: 'GTE' as any,
658
+ value: startTimestamp.toString()
659
+ }]
660
+ }],
661
+ properties: ['hs_meeting_title', 'hs_meeting_body', 'hs_meeting_start_time', 'hs_meeting_end_time', 'hs_lastmodifieddate', 'hs_createdate'],
662
+ sorts: ['hs_lastmodifieddate:desc']
663
+ };
664
+
665
+ const allMeetings = await this.paginateSearch(
666
+ (req) => this.client.crm.objects.meetings.searchApi.doSearch(req),
667
+ baseRequest,
668
+ limit
669
+ );
670
+
671
+ console.log(`[MCP Debug] Meetings found: ${allMeetings.length}`);
672
+ return convertDatetimeFields(allMeetings);
673
+
674
+ } catch (error: any) {
675
+ console.error('Error searching meetings:', error);
676
+ return { error: error.message };
677
+ }
678
+ }
679
+
680
+ async getRecentEngagements(limit: number = 50): Promise<any> {
681
+ try {
682
+ await this.rateLimitDelay();
683
+
684
+ console.log(`[MCP Debug] Searching for all engagements`);
685
+
686
+ // Ensure limit doesn't exceed HubSpot's maximum per search
687
+ const safeLimit = Math.min(Math.max(limit, 1), 200);
688
+
689
+ // Get all notes and tasks using CRM v3 API (no time filtering) with safe limits
690
+ const [notesResponse, tasksResponse] = await Promise.allSettled([
691
+ // Get all notes
692
+ this.client.crm.objects.notes.searchApi.doSearch({
693
+ properties: ['hs_note_body', 'hs_lastmodifieddate', 'hs_createdate'],
694
+ sorts: ['hs_lastmodifieddate:desc'],
695
+ limit: Math.floor(safeLimit / 2)
696
+ }),
697
+ // Get all tasks
698
+ this.client.crm.objects.tasks.searchApi.doSearch({
699
+ properties: ['hs_task_subject', 'hs_task_body', 'hs_task_status', 'hs_lastmodifieddate', 'hs_createdate'],
700
+ sorts: ['hs_lastmodifieddate:desc'],
701
+ limit: Math.floor(safeLimit / 2)
702
+ })
703
+ ]);
704
+
705
+ console.log(`[MCP Debug] Notes response status: ${notesResponse.status}`);
706
+ console.log(`[MCP Debug] Tasks response status: ${tasksResponse.status}`);
707
+
708
+ if (notesResponse.status === 'rejected') {
709
+ console.error(`[MCP Debug] Notes search failed:`, notesResponse.reason);
710
+ } else {
711
+ console.log(`[MCP Debug] Notes found: ${notesResponse.value?.results?.length || 0}`);
712
+ }
713
+
714
+ if (tasksResponse.status === 'rejected') {
715
+ console.error(`[MCP Debug] Tasks search failed:`, tasksResponse.reason);
716
+ } else {
717
+ console.log(`[MCP Debug] Tasks found: ${tasksResponse.value?.results?.length || 0}`);
718
+ }
719
+
720
+ // Format the engagements from CRM v3 responses
721
+ const formattedEngagements = [];
722
+
723
+ // Process notes
724
+ if (notesResponse.status === 'fulfilled' && notesResponse.value?.results) {
725
+ for (const note of notesResponse.value.results) {
726
+ formattedEngagements.push({
727
+ id: note.id,
728
+ type: 'NOTE',
729
+ engagement: {
730
+ id: note.id,
731
+ type: 'NOTE',
732
+ createdAt: note.properties?.hs_createdate,
733
+ lastUpdated: note.properties?.hs_lastmodifieddate
734
+ },
735
+ metadata: {
736
+ body: note.properties?.hs_note_body
737
+ },
738
+ properties: note.properties,
739
+ created_at: note.properties?.hs_createdate,
740
+ last_updated: note.properties?.hs_lastmodifieddate
741
+ });
742
+ }
743
+ }
744
+
745
+ // Process tasks
746
+ if (tasksResponse.status === 'fulfilled' && tasksResponse.value?.results) {
747
+ for (const task of tasksResponse.value.results) {
748
+ formattedEngagements.push({
749
+ id: task.id,
750
+ type: 'TASK',
751
+ engagement: {
752
+ id: task.id,
753
+ type: 'TASK',
754
+ createdAt: task.properties?.hs_createdate,
755
+ lastUpdated: task.properties?.hs_lastmodifieddate
756
+ },
757
+ metadata: {
758
+ subject: task.properties?.hs_task_subject,
759
+ body: task.properties?.hs_task_body,
760
+ status: task.properties?.hs_task_status
761
+ },
762
+ properties: task.properties,
763
+ created_at: task.properties?.hs_createdate,
764
+ last_updated: task.properties?.hs_lastmodifieddate
765
+ });
766
+ }
767
+ }
768
+
769
+ // Sort by last modified date (most recent first)
770
+ formattedEngagements.sort((a, b) => {
771
+ const aTime = new Date(a.last_updated || a.created_at || 0).getTime();
772
+ const bTime = new Date(b.last_updated || b.created_at || 0).getTime();
773
+ return bTime - aTime;
774
+ });
775
+
776
+ console.log(`[MCP Debug] Returning ${formattedEngagements.length} formatted engagements`);
777
+
778
+ return convertDatetimeFields(formattedEngagements);
779
+
780
+ } catch (error: any) {
781
+ console.error('Error getting recent engagements:', error);
782
+ return { error: error.message };
783
+ }
784
+ }
785
+
786
+
787
+ async createContact(
788
+ firstname: string,
789
+ lastname: string,
790
+ email?: string,
791
+ properties?: Record<string, any>
792
+ ): Promise<any> {
793
+ try {
794
+ // Search for existing contacts with same name and company
795
+ const company = properties?.company;
796
+
797
+ // Use type assertion to satisfy the HubSpot API client types
798
+ const searchRequest = {
799
+ filterGroups: [{
800
+ filters: [
801
+ {
802
+ propertyName: 'firstname',
803
+ operator: 'EQ',
804
+ value: firstname
805
+ } as any,
806
+ {
807
+ propertyName: 'lastname',
808
+ operator: 'EQ',
809
+ value: lastname
810
+ } as any
811
+ ]
812
+ }]
813
+ } as any;
814
+
815
+ // Add company filter if provided
816
+ if (company) {
817
+ searchRequest.filterGroups[0].filters.push({
818
+ propertyName: 'company',
819
+ operator: 'EQ',
820
+ value: company
821
+ } as any);
822
+ }
823
+
824
+ const searchResponse = await this.client.crm.contacts.searchApi.doSearch(searchRequest);
825
+
826
+ if (searchResponse.total > 0) {
827
+ // Contact already exists
828
+ return {
829
+ message: 'Contact already exists',
830
+ contact: searchResponse.results[0]
831
+ };
832
+ }
833
+
834
+ // If no existing contact found, proceed with creation
835
+ const contactProperties: Record<string, any> = {
836
+ firstname,
837
+ lastname
838
+ };
839
+
840
+ // Add email if provided
841
+ if (email) {
842
+ contactProperties.email = email;
843
+ }
844
+
845
+ // Add any additional properties
846
+ if (properties) {
847
+ Object.assign(contactProperties, properties);
848
+ }
849
+
850
+ // Create contact
851
+ const apiResponse = await this.client.crm.contacts.basicApi.create({
852
+ properties: contactProperties
853
+ });
854
+
855
+ return apiResponse;
856
+ } catch (error: any) {
857
+ console.error('Error creating contact:', error);
858
+ throw new Error(`HubSpot API error: ${error.message}`);
859
+ }
860
+ }
861
+
862
+ async createCompany(name: string, properties?: Record<string, any>): Promise<any> {
863
+ try {
864
+ await this.rateLimitDelay();
865
+ // Search for existing companies with same name
866
+ // Use type assertion to satisfy the HubSpot API client types
867
+ const searchRequest = {
868
+ filterGroups: [{
869
+ filters: [
870
+ {
871
+ propertyName: 'name',
872
+ operator: 'EQ',
873
+ value: name
874
+ } as any
875
+ ]
876
+ }]
877
+ } as any;
878
+
879
+ const searchResponse = await this.client.crm.companies.searchApi.doSearch(searchRequest);
880
+
881
+ if (searchResponse.total > 0) {
882
+ // Company already exists
883
+ return {
884
+ message: 'Company already exists',
885
+ company: searchResponse.results[0]
886
+ };
887
+ }
888
+
889
+ // If no existing company found, proceed with creation
890
+ const companyProperties: Record<string, any> = {
891
+ name
892
+ };
893
+
894
+ // Add any additional properties
895
+ if (properties) {
896
+ Object.assign(companyProperties, properties);
897
+ }
898
+
899
+ // Create company
900
+ const apiResponse = await this.client.crm.companies.basicApi.create({
901
+ properties: companyProperties
902
+ });
903
+
904
+ return apiResponse;
905
+ } catch (error: any) {
906
+ console.error('Error creating company:', error);
907
+ throw new Error(`HubSpot API error: ${error.message}`);
908
+ }
909
+ }
910
+
911
+ async createNote(body: string, associations?: any[]): Promise<any> {
912
+ try {
913
+ await this.rateLimitDelay();
914
+
915
+ // Create note properties
916
+ const noteProperties: Record<string, any> = {
917
+ hs_note_body: body
918
+ };
919
+
920
+ // Create the note using CRM v3 API
921
+ const noteResponse = await this.client.crm.objects.notes.basicApi.create({
922
+ properties: noteProperties,
923
+ associations: []
924
+ });
925
+
926
+ // If associations are provided, create them
927
+ if (associations && associations.length > 0) {
928
+ for (const association of associations) {
929
+ try {
930
+ await this.client.crm.associations.v4.basicApi.create(
931
+ 'notes',
932
+ noteResponse.id,
933
+ association.toObjectType,
934
+ association.toObjectId,
935
+ [{ associationCategory: association.category || 'USER_DEFINED', associationTypeId: association.typeId || 1 }]
936
+ );
937
+ } catch (associationError) {
938
+ console.warn('Failed to create association for note:', associationError);
939
+ // Continue with other associations
940
+ }
941
+ }
942
+ }
943
+
944
+ return {
945
+ id: noteResponse.id,
946
+ properties: noteResponse.properties,
947
+ associations: associations || []
948
+ };
949
+ } catch (error: any) {
950
+ console.error('Error creating note:', error);
951
+ throw new Error(`HubSpot API error: ${error.message}`);
952
+ }
953
+ }
954
+
955
+ async updateContact(
956
+ contactId: string,
957
+ properties: Record<string, any>
958
+ ): Promise<any> {
959
+ try {
960
+ // Check if contact exists
961
+ try {
962
+ await this.client.crm.contacts.basicApi.getById(contactId);
963
+ } catch (error: any) {
964
+ // If contact doesn't exist, return a message
965
+ if (error.statusCode === 404) {
966
+ return {
967
+ message: 'Contact not found, no update performed',
968
+ contactId
969
+ };
970
+ }
971
+ // For other errors, throw them to be caught by the outer try/catch
972
+ throw error;
973
+ }
974
+
975
+ // Update the contact
976
+ const apiResponse = await this.client.crm.contacts.basicApi.update(contactId, {
977
+ properties
978
+ });
979
+
980
+ return {
981
+ message: 'Contact updated successfully',
982
+ contactId,
983
+ properties
984
+ };
985
+ } catch (error: any) {
986
+ console.error('Error updating contact:', error);
987
+ throw new Error(`HubSpot API error: ${error.message}`);
988
+ }
989
+ }
990
+
991
+ async updateCompany(
992
+ companyId: string,
993
+ properties: Record<string, any>
994
+ ): Promise<any> {
995
+ try {
996
+ // Check if company exists
997
+ try {
998
+ await this.client.crm.companies.basicApi.getById(companyId);
999
+ } catch (error: any) {
1000
+ // If company doesn't exist, return a message
1001
+ if (error.statusCode === 404) {
1002
+ return {
1003
+ message: 'Company not found, no update performed',
1004
+ companyId
1005
+ };
1006
+ }
1007
+ // For other errors, throw them to be caught by the outer try/catch
1008
+ throw error;
1009
+ }
1010
+
1011
+ // Update the company
1012
+ const apiResponse = await this.client.crm.companies.basicApi.update(companyId, {
1013
+ properties
1014
+ });
1015
+
1016
+ return {
1017
+ message: 'Company updated successfully',
1018
+ companyId,
1019
+ properties
1020
+ };
1021
+ } catch (error: any) {
1022
+ console.error('Error updating company:', error);
1023
+ throw new Error(`HubSpot API error: ${error.message}`);
1024
+ }
1025
+ }
1026
+
1027
+ async getRecentDeals(limit: number = 100, updatedAfter?: string): Promise<any> {
1028
+ try {
1029
+ console.log('Fetching ALL deals with pagination...');
1030
+
1031
+ const baseRequest = {
1032
+ sorts: ['hs_lastmodifieddate:desc'],
1033
+ properties: [
1034
+ 'dealname', 'amount', 'dealstage', 'createdate', 'closedate',
1035
+ 'hubspot_owner_id', 'dealtype', 'pipeline', 'hs_analytics_source',
1036
+ 'hs_deal_stage_probability', 'days_to_close', 'num_contacted_notes'
1037
+ ]
1038
+ };
1039
+
1040
+ this.applyUpdatedAfterFilter(baseRequest, updatedAfter, 'hs_lastmodifieddate');
1041
+
1042
+ const allDeals = await this.paginateSearch(
1043
+ (req) => this.client.crm.deals.searchApi.doSearch(req),
1044
+ baseRequest,
1045
+ limit
1046
+ );
1047
+
1048
+ console.log(`?o. Total deals fetched: ${allDeals.length}`);
1049
+ return convertDatetimeFields(allDeals);
1050
+ } catch (error: any) {
1051
+ console.error('Error fetching deals:', error);
1052
+ throw new Error(`HubSpot API error: ${error.message}`);
1053
+ }
1054
+ }
1055
+
1056
+ async getCurrentUser(): Promise<any> {
1057
+ try {
1058
+ await this.rateLimitDelay();
1059
+ // Use the OAuth v1 access-tokens introspection endpoint (no Authorization header required)
1060
+ // https://api.hubapi.com/oauth/v1/access-tokens/{token}
1061
+ const resp = await fetch(`https://api.hubapi.com/oauth/v1/access-tokens/${this.accessToken}`);
1062
+
1063
+ if (!resp.ok) {
1064
+ // Return a minimal object instead of throwing to avoid noisy errors upstream
1065
+ return {
1066
+ userId: 'unknown',
1067
+ userEmail: 'unknown',
1068
+ hubId: null,
1069
+ scopes: [],
1070
+ error: `HTTP ${resp.status}: ${resp.statusText}`
1071
+ };
1072
+ }
1073
+
1074
+ const data = await resp.json();
1075
+ return {
1076
+ userId: data.user_id || data.user || 'unknown',
1077
+ userEmail: data.user_email || data.email || 'unknown',
1078
+ hubId: data.hub_id || data.hubId || null,
1079
+ scopes: data.scopes || []
1080
+ };
1081
+ } catch (error: any) {
1082
+ console.error('Error fetching current user:', error);
1083
+ // Graceful fallback to avoid breaking downstream consumers
1084
+ return {
1085
+ userId: 'unknown',
1086
+ userEmail: 'unknown',
1087
+ hubId: null,
1088
+ scopes: [],
1089
+ error: error?.message || 'unknown'
1090
+ };
1091
+ }
1092
+ }
1093
+
1094
+ async getRecentTickets(limit: number = 100): Promise<any> {
1095
+ try {
1096
+ console.log('?YZ? Fetching ALL tickets with pagination...');
1097
+
1098
+ const ticketProperties = [
1099
+ 'subject', 'content', 'hs_ticket_priority', 'hs_pipeline', 'hs_pipeline_stage',
1100
+ 'createdate', 'hs_lastmodifieddate', 'hubspot_owner_id',
1101
+ 'hs_ticket_category', 'hs_ticket_id', 'source_type',
1102
+ 'closed_date', 'first_agent_reply_date', 'time_to_close', 'time_to_first_agent_reply',
1103
+ 'hs_resolution', 'hs_custom_inbox', 'hs_was_imported',
1104
+ 'num_associated_conversations', 'num_notes', 'num_contacted_notes'
1105
+ ];
1106
+
1107
+ const baseRequest = {
1108
+ sorts: ['hs_lastmodifieddate:desc'],
1109
+ properties: ticketProperties
1110
+ };
1111
+
1112
+ const allTickets = await this.paginateSearch(
1113
+ (req) => this.client.crm.tickets.searchApi.doSearch(req),
1114
+ baseRequest,
1115
+ limit
1116
+ );
1117
+
1118
+ console.log(`?o. Total tickets fetched: ${allTickets.length}`);
1119
+ return convertDatetimeFields(allTickets);
1120
+ } catch (error: any) {
1121
+ console.error('Error fetching tickets:', error);
1122
+ throw new Error(`HubSpot API error: ${error.message}`);
1123
+ }
1124
+ }
1125
+
1126
+ async getRecentOrders(limit: number = 100, updatedAfter?: string): Promise<any[]> {
1127
+ try {
1128
+ const orderProperties = [
1129
+ 'hs_order_name', 'hs_source_store', 'hs_fulfillment_status',
1130
+ 'hs_currency_code', 'hs_total_price', 'hs_shipping_price',
1131
+ 'hs_tax_price', 'hs_discount_price', 'hs_createdate', 'hs_lastmodifieddate',
1132
+ 'hs_external_order_id', 'hs_order_status', 'hs_shipping_address_city',
1133
+ 'hubspot_owner_id', 'hs_payment_status', 'hs_order_currency_code'
1134
+ ];
1135
+ const searchRequest = {
1136
+ sorts: ['hs_lastmodifieddate:desc'],
1137
+ properties: orderProperties
1138
+ };
1139
+
1140
+ if (updatedAfter) {
1141
+ this.applyUpdatedAfterFilter(searchRequest, updatedAfter, 'hs_lastmodifieddate');
1142
+ }
1143
+
1144
+ // Note: Orders might not be available in all HubSpot portals
1145
+ // This depends on having Commerce Hub or ecommerce integrations
1146
+ const allOrders = await this.paginateSearch(
1147
+ (req) => this.client.crm.objects.searchApi.doSearch('orders', req),
1148
+ searchRequest,
1149
+ limit
1150
+ );
1151
+ return convertDatetimeFields(allOrders);
1152
+ } catch (error: any) {
1153
+ console.error('Error fetching orders:', error);
1154
+ // Return empty array if orders are not available rather than throwing
1155
+ return [];
1156
+ }
1157
+ }
1158
+
1159
+ async getOrders(limit: number = 100, updatedAfter?: string): Promise<any[]> {
1160
+ return this.getRecentOrders(limit, updatedAfter);
1161
+ }
1162
+
1163
+
1164
+ async getContactById(contactId: string): Promise<any> {
1165
+ try {
1166
+ await this.rateLimitDelay();
1167
+ const contact = await this.client.crm.contacts.basicApi.getById(contactId, [
1168
+ // Basic info - enhanced
1169
+ 'firstname', 'lastname', 'email', 'phone', 'mobilephone', 'jobtitle',
1170
+ 'salutation', 'middle_name', 'prefix', 'suffix',
1171
+ // Company info - comprehensive
1172
+ 'company', 'website', 'company_name', 'company_website',
1173
+ 'company_domain', 'company_industry', 'company_size',
1174
+ // Address info - comprehensive
1175
+ 'address', 'address2', 'city', 'state', 'zip', 'country', 'country_code',
1176
+ // Contact properties - expanded
1177
+ 'lifecyclestage', 'lead_status', 'hs_lead_status', 'contact_owner',
1178
+ 'hs_contact_stage', 'hs_contact_status', 'hs_contact_source',
1179
+ // Company association and details - enhanced
1180
+ 'associatedcompanyid', 'associatedcompany_name', 'associatedcompany_domain',
1181
+ 'associatedcompany_website', 'associatedcompany_industry', 'associatedcompany_size',
1182
+ // Social media and online presence - comprehensive
1183
+ 'linkedin_profile', 'twitter_profile', 'facebook_profile',
1184
+ 'linkedin_url', 'twitter_url', 'facebook_url', 'instagram_url', 'youtube_url',
1185
+ // Professional details - expanded
1186
+ 'seniority', 'department', 'industry', 'job_function', 'job_role',
1187
+ 'job_level', 'years_experience', 'education_level', 'school',
1188
+ // Lead scoring and qualification - comprehensive
1189
+ 'hs_lead_scoring', 'hs_lead_status', 'hs_analytics_source',
1190
+ 'hs_analytics_source_data_1', 'hs_analytics_source_data_2', 'hs_analytics_source_data_3',
1191
+ 'hs_analytics_source_data_4', 'hs_analytics_source_raw',
1192
+ 'hs_predictivecontactscore_v2', 'hs_predictivecontactscoring_v2',
1193
+ // Custom properties - expanded
1194
+ 'custom_property_1', 'custom_property_2', 'custom_property_3', 'custom_property_4', 'custom_property_5',
1195
+ 'custom_property_6', 'custom_property_7', 'custom_property_8', 'custom_property_9', 'custom_property_10',
1196
+ // Dates and timestamps - comprehensive
1197
+ 'createdate', 'hs_lastmodifieddate', 'lastmodifieddate', 'hs_createdate',
1198
+ 'first_conversion_date', 'last_conversion_date', 'hs_first_engagement_date',
1199
+ 'hs_last_contacted', 'hs_last_activity_date', 'hs_first_contact_createdate',
1200
+ // Additional fields - enhanced
1201
+ 'hs_object_id', 'hs_unique_creation_key', 'hs_contact_id', 'id',
1202
+ 'is_deleted', 'hs_time_to_first_engagement', 'hs_contact_stage_probability'
1203
+ ]);
1204
+ return convertDatetimeFields(contact);
1205
+ } catch (error: any) {
1206
+ console.error('Error fetching contact by ID:', error);
1207
+ throw new Error(`HubSpot API error: ${error.message}`);
1208
+ }
1209
+ }
1210
+
1211
+ async getCompanyById(companyId: string): Promise<any> {
1212
+ try {
1213
+ await this.rateLimitDelay();
1214
+ const company = await this.client.crm.companies.basicApi.getById(companyId, [
1215
+ // Basic company info - enhanced
1216
+ 'name', 'domain', 'website', 'phone', 'industry', 'company_type',
1217
+ // Address info - comprehensive
1218
+ 'address', 'address2', 'city', 'state', 'zip', 'country', 'country_code',
1219
+ // Company details - expanded
1220
+ 'description', 'numberofemployees', 'annualrevenue', 'type',
1221
+ 'founded_year', 'business_type', 'revenue_range', 'employee_range',
1222
+ 'target_account', 'ideal_customer_profile', 'account_tier',
1223
+ // Contact info
1224
+ 'owneremail', 'ownername', 'phone', 'owner_phone',
1225
+ // Website and online presence - comprehensive
1226
+ 'website', 'domain', 'blog_url', 'facebook_url', 'twitter_url',
1227
+ 'linkedin_url', 'youtube_url', 'instagram_url', 'pinterest_url',
1228
+ 'googleplus_url', 'klout_score', 'alexa_ranking', 'semrush_ranking',
1229
+ // Company associations and details
1230
+ 'hs_parent_company_id', 'hs_child_company_ids', 'hs_analytics_source',
1231
+ 'hs_analytics_source_data_1', 'hs_analytics_source_data_2', 'hs_analytics_source_data_3',
1232
+ 'hs_analytics_source_data_4', 'hs_analytics_source_raw',
1233
+ // Custom properties - expanded
1234
+ 'custom_property_1', 'custom_property_2', 'custom_property_3', 'custom_property_4', 'custom_property_5',
1235
+ 'custom_property_6', 'custom_property_7', 'custom_property_8', 'custom_property_9', 'custom_property_10',
1236
+ // Dates and timestamps - comprehensive
1237
+ 'createdate', 'hs_lastmodifieddate', 'lastmodifieddate', 'hs_createdate',
1238
+ 'first_conversion_date', 'last_conversion_date', 'hs_first_engagement_date',
1239
+ 'hs_last_contacted', 'hs_last_activity_date', 'hs_first_contact_createdate',
1240
+ // Additional fields - enhanced
1241
+ 'hs_object_id', 'hs_unique_creation_key', 'hs_company_id', 'id',
1242
+ 'is_deleted', 'hs_time_to_first_engagement', 'hs_time_to_close',
1243
+ // Lead status and scoring
1244
+ 'hs_lead_status', 'hs_lead_scoring', 'hs_predictivecontactscore_v2',
1245
+ 'hs_predictivecontactscoring_v2', 'hs_ideal_customer_profile'
1246
+ ]);
1247
+ return convertDatetimeFields(company);
1248
+ } catch (error: any) {
1249
+ console.error('Error fetching company by ID:', error);
1250
+ throw new Error(`HubSpot API error: ${error.message}`);
1251
+ }
1252
+ }
1253
+
1254
+ async getDealById(dealId: string): Promise<any> {
1255
+ try {
1256
+ await this.rateLimitDelay();
1257
+ const deal = await this.client.crm.deals.basicApi.getById(dealId, [
1258
+ 'dealname', 'amount', 'dealstage', 'pipeline', 'closedate', 'hs_lastmodifieddate', 'createdate'
1259
+ ]);
1260
+ return convertDatetimeFields(deal);
1261
+ } catch (error: any) {
1262
+ console.error('Error fetching deal by ID:', error);
1263
+ throw new Error(`HubSpot API error: ${error.message}`);
1264
+ }
1265
+ }
1266
+
1267
+ async getTicketById(ticketId: string): Promise<any> {
1268
+ try {
1269
+ await this.rateLimitDelay();
1270
+ const ticket = await this.client.crm.objects.basicApi.getById('tickets', ticketId, [
1271
+ 'subject', 'content', 'hs_ticket_priority', 'hs_ticket_category', 'hs_ticket_status', 'hs_lastmodifieddate', 'createdate'
1272
+ ]);
1273
+ return convertDatetimeFields(ticket);
1274
+ } catch (error: any) {
1275
+ console.error('Error fetching ticket by ID:', error);
1276
+ throw new Error(`HubSpot API error: ${error.message}`);
1277
+ }
1278
+ }
1279
+
1280
+
1281
+ // Helper method for graceful error handling of optional scopes
1282
+ private handleOptionalScopeError(error: any, scopeName: string, fallbackValue: any = []): any {
1283
+ const errorMessage = error.message?.toLowerCase() || '';
1284
+ const isPermissionError = errorMessage.includes('scope') ||
1285
+ errorMessage.includes('permission') ||
1286
+ errorMessage.includes('unauthorized') ||
1287
+ errorMessage.includes('forbidden') ||
1288
+ error.status === 403 ||
1289
+ error.status === 401;
1290
+
1291
+ if (isPermissionError) {
1292
+ console.error(`HubSpot scope missing: ${scopeName}. Unable to complete API call.`, {
1293
+ scope: scopeName,
1294
+ status: error?.status,
1295
+ category: error?.body?.category,
1296
+ message: error?.message
1297
+ });
1298
+ throw new HubSpotScopeError(scopeName, error);
1299
+ }
1300
+
1301
+ // Re-throw non-permission errors
1302
+ throw error;
1303
+ }
1304
+
1305
+ // Marketing Hub Methods
1306
+ async getForms(limit: number = 100): Promise<any[]> {
1307
+ try {
1308
+ console.log('📝 Fetching ALL forms with pagination...');
1309
+ const allForms = await this.paginateList(
1310
+ // Use the dedicated v3 marketing forms API client
1311
+ (params) => this.client.marketing.forms.formsApi.getPage(
1312
+ params.after,
1313
+ params.limit
1314
+ ),
1315
+ limit
1316
+ );
1317
+ console.log(`✅ Total forms fetched: ${allForms.length}`);
1318
+ return allForms;
1319
+ } catch (error: any) {
1320
+ console.error('Error fetching forms:', error);
1321
+ return this.handleOptionalScopeError(error, 'forms', []);
1322
+ }
1323
+ }
1324
+
1325
+ async getFormSubmissions(formId: string, limit: number = 100): Promise<any[]> {
1326
+ try {
1327
+ await this.rateLimitDelay();
1328
+ const response = await this.client.apiRequest({
1329
+ method: 'GET',
1330
+ path: `/forms/v2/submissions/forms/${formId}?limit=${limit}`
1331
+ });
1332
+ const data = await response.json();
1333
+ return data?.results || [];
1334
+ } catch (error: any) {
1335
+ console.error('Error fetching form submissions:', error);
1336
+ return this.handleOptionalScopeError(error, 'forms', []);
1337
+ }
1338
+ }
1339
+
1340
+ async getMarketingEmails(limit: number = 100, updatedAfter?: string): Promise<any[]> {
1341
+ try {
1342
+ console.log('📧 Fetching ALL marketing emails with pagination (v3)...');
1343
+
1344
+ const normalizedFilter = this.normalizeDateInput(updatedAfter);
1345
+ const allEmails = await this.paginateList(
1346
+ async (params) => {
1347
+ const query = new URLSearchParams();
1348
+ query.append('limit', params.limit.toString());
1349
+ if (params.after) {
1350
+ query.append('after', params.after);
1351
+ }
1352
+
1353
+ const path = `/marketing/v3/emails?${query.toString()}`;
1354
+ const response = await this.client.apiRequest({ method: 'GET', path });
1355
+ return await response.json();
1356
+ },
1357
+ limit
1358
+ );
1359
+
1360
+ const normalizedEmails = convertDatetimeFields(allEmails);
1361
+ const filteredEmails = filterRecordsByUpdatedAfter(
1362
+ normalizedEmails,
1363
+ normalizedFilter || undefined,
1364
+ (email) => email?.updatedAt,
1365
+ 'marketing emails'
1366
+ );
1367
+
1368
+ console.log(`✅ Total marketing emails fetched: ${filteredEmails.length}`);
1369
+ return filteredEmails;
1370
+ } catch (error: any) {
1371
+ console.error('Error fetching marketing emails:', error);
1372
+ return this.handleOptionalScopeError(error, 'marketing-email', []);
1373
+ }
1374
+ }
1375
+
1376
+ async getEmailEvents(limit: number = 100): Promise<any[]> {
1377
+ try {
1378
+ console.log('📊 Fetching marketing email events...');
1379
+ const allEvents = await this.paginateList(
1380
+ async (params) => {
1381
+ const path = `/marketing/v3/marketing-events/events?limit=${params.limit}${params.after ? `&after=${params.after}` : ''}`;
1382
+ const response = await this.client.apiRequest({ method: 'GET', path });
1383
+ return await response.json();
1384
+ },
1385
+ limit
1386
+ );
1387
+ console.log(`✅ Total email events fetched: ${allEvents.length}`);
1388
+ return allEvents;
1389
+ } catch (error: any) {
1390
+ console.error('Error fetching email events:', error);
1391
+ return this.handleOptionalScopeError(error, 'crm.objects.marketing_events.read', []);
1392
+ }
1393
+ }
1394
+
1395
+ async getCampaigns(limit: number = 100): Promise<any[]> {
1396
+ try {
1397
+ console.log('đŸ“ĸ Fetching ALL campaigns with pagination...');
1398
+ const allCampaigns = await this.paginateList(
1399
+ async (params) => {
1400
+ const path = `/marketing/v3/campaigns?limit=${params.limit}${params.after ? `&after=${params.after}` : ''}`;
1401
+ const response = await this.client.apiRequest({ method: 'GET', path });
1402
+ return await response.json();
1403
+ },
1404
+ limit
1405
+ );
1406
+ console.log(`✅ Total campaigns fetched: ${allCampaigns.length}`);
1407
+ return allCampaigns;
1408
+ } catch (error: any) {
1409
+ console.error('Error fetching campaigns:', error);
1410
+ return this.handleOptionalScopeError(error, 'marketing.campaigns.read', []);
1411
+ }
1412
+ }
1413
+
1414
+ async getAnalytics(startDate: string, endDate: string): Promise<any> {
1415
+ try {
1416
+ await this.rateLimitDelay();
1417
+ const path = `/analytics/v3/reports/domains/total-traffic?start_date=${startDate}&end_date=${endDate}`;
1418
+
1419
+ const response = await this.client.apiRequest({
1420
+ method: 'GET',
1421
+ path: path
1422
+ });
1423
+ return await response.json();
1424
+ } catch (error: any) {
1425
+ console.error('Error fetching analytics:', error);
1426
+ return this.handleOptionalScopeError(error, 'analytics.behavioral_events.send', {});
1427
+ }
1428
+ }
1429
+
1430
+ async getBlogPosts(limit: number = 100): Promise<any[]> {
1431
+ try {
1432
+ console.log('📝 Fetching ALL blog posts with pagination...');
1433
+ const allPosts = await this.paginateList(
1434
+ async (params) => {
1435
+ const path = `/cms/v3/blogs/posts?limit=${params.limit}${params.after ? `&after=${params.after}` : ''}`;
1436
+ const response = await this.client.apiRequest({ method: 'GET', path });
1437
+ return await response.json();
1438
+ },
1439
+ limit
1440
+ );
1441
+ console.log(`✅ Total blog posts fetched: ${allPosts.length}`);
1442
+ return allPosts;
1443
+ } catch (error: any) {
1444
+ console.error('Error fetching blog posts:', error);
1445
+ return this.handleOptionalScopeError(error, 'content', []);
1446
+ }
1447
+ }
1448
+
1449
+ async getLandingPages(limit: number = 100): Promise<any[]> {
1450
+ try {
1451
+ console.log('🏠 Fetching ALL landing pages with pagination...');
1452
+ const allPages = await this.paginateList(
1453
+ async (params) => {
1454
+ const path = `/cms/v3/pages/landing-pages?limit=${params.limit}${params.after ? `&after=${params.after}` : ''}`;
1455
+ const response = await this.client.apiRequest({ method: 'GET', path });
1456
+ return await response.json();
1457
+ },
1458
+ limit
1459
+ );
1460
+ console.log(`✅ Total landing pages fetched: ${allPages.length}`);
1461
+ return allPages;
1462
+ } catch (error: any) {
1463
+ console.error('Error fetching landing pages:', error);
1464
+ return this.handleOptionalScopeError(error, 'content', []);
1465
+ }
1466
+ }
1467
+ async getKnowledgeBase(limit: number = 100): Promise<any[]> {
1468
+ try {
1469
+ console.log('📚 Fetching ALL knowledge base articles with pagination...');
1470
+ const allArticles = await this.paginateList(
1471
+ async (params) => {
1472
+ const path = `/cms/v3/knowledge-base/articles?limit=${params.limit}${params.after ? `&after=${params.after}` : ''}`;
1473
+ const response = await this.client.apiRequest({ method: 'GET', path });
1474
+ return await response.json();
1475
+ },
1476
+ limit
1477
+ );
1478
+ console.log(`✅ Total knowledge base articles fetched: ${allArticles.length}`);
1479
+ return allArticles;
1480
+ } catch (error: any) {
1481
+ console.error('Error fetching knowledge base:', error);
1482
+ return this.handleOptionalScopeError(error, 'cms.knowledge_base.articles.read', []);
1483
+ }
1484
+ }
1485
+
1486
+ // Sales Hub Methods
1487
+ async getPipelines(objectType: string = 'deals'): Promise<any[]> {
1488
+ try {
1489
+ await this.rateLimitDelay();
1490
+ // Use the dedicated SDK method for pipelines
1491
+ const response = await this.client.crm.pipelines.pipelinesApi.getAll(objectType);
1492
+ return response.results || [];
1493
+ } catch (error: any) {
1494
+ console.error(`Error fetching pipelines for ${objectType}:`, error);
1495
+ // Handle cases where the object type might not support pipelines
1496
+ if (error.code === 404) {
1497
+ console.warn(`Pipelines for object type '${objectType}' not found.`);
1498
+ return [];
1499
+ }
1500
+ return this.handleOptionalScopeError(error, 'crm.pipelines.read', []);
1501
+ }
1502
+ }
1503
+
1504
+ async getProperties(objectType: string): Promise<any[]> {
1505
+ if (!objectType) {
1506
+ console.error('Error in getProperties: objectType was not provided.');
1507
+ return [];
1508
+ }
1509
+ try {
1510
+ await this.rateLimitDelay();
1511
+ // Use the dedicated SDK method for properties
1512
+ const response = await this.client.crm.properties.coreApi.getAll(objectType);
1513
+ return response.results || [];
1514
+ } catch (error: any) {
1515
+ console.error(`Error fetching properties for ${objectType}:`, error);
1516
+ return this.handleOptionalScopeError(error, 'crm.properties.read', []);
1517
+ }
1518
+ }
1519
+
1520
+ async getQuotes(limit: number = 100, updatedAfter?: string): Promise<any[]> {
1521
+ try {
1522
+ console.log('💰 Fetching ALL quotes with pagination...');
1523
+
1524
+ const quoteProperties = [
1525
+ 'hs_title', 'hs_expiration_date', 'hs_status', 'hs_quote_amount',
1526
+ 'hs_sender_firstname', 'hs_sender_lastname', 'hs_sender_email',
1527
+ 'hs_esign_enabled', 'hs_payment_enabled', 'hs_template_id',
1528
+ 'createdate', 'hs_lastmodifieddate', 'hubspot_owner_id',
1529
+ 'hs_quote_link', 'hs_pdf_download_link', 'hs_domain'
1530
+ ];
1531
+
1532
+ if (updatedAfter) {
1533
+ const searchRequest = {
1534
+ sorts: ['hs_lastmodifieddate:desc'],
1535
+ properties: quoteProperties
1536
+ };
1537
+ this.applyUpdatedAfterFilter(searchRequest, updatedAfter, 'hs_lastmodifieddate');
1538
+ const incrementalQuotes = await this.paginateSearch(
1539
+ (req) => this.client.crm.quotes.searchApi.doSearch(req),
1540
+ searchRequest,
1541
+ limit
1542
+ );
1543
+ console.log(`✅ Total quotes fetched (incremental): ${incrementalQuotes.length}`);
1544
+ return convertDatetimeFields(incrementalQuotes);
1545
+ }
1546
+
1547
+ const allQuotes = await this.paginateList(
1548
+ async (params) => {
1549
+ const props = quoteProperties.join(',');
1550
+ const path = `/crm/v3/objects/quotes?limit=${params.limit}&properties=${props}${params.after ? `&after=${params.after}` : ''}`;
1551
+ const response = await this.client.apiRequest({ method: 'GET', path });
1552
+ return await response.json();
1553
+ },
1554
+ limit
1555
+ );
1556
+ console.log(`✅ Total quotes fetched: ${allQuotes.length}`);
1557
+ return allQuotes;
1558
+ } catch (error: any) {
1559
+ console.error('Error fetching quotes:', error);
1560
+ return this.handleOptionalScopeError(error, 'crm.objects.quotes.read', []);
1561
+ }
1562
+ }
1563
+
1564
+ async getUsers(limit: number = 100): Promise<any[]> {
1565
+ try {
1566
+ console.log('đŸ‘Ĩ Fetching ALL users with pagination...');
1567
+ const allUsers = await this.paginateList(
1568
+ // Use the dedicated SDK method for users
1569
+ (params) => this.client.settings.users.usersApi.getPage(
1570
+ params.limit,
1571
+ params.after
1572
+ ),
1573
+ limit
1574
+ );
1575
+ console.log(`✅ Total users fetched: ${allUsers.length}`);
1576
+ return allUsers;
1577
+ } catch (error: any) {
1578
+ console.error('Error fetching users:', error);
1579
+ // This scope is usually available, but we can handle errors just in case.
1580
+ return this.handleOptionalScopeError(error, 'settings.users.read', []);
1581
+ }
1582
+ }
1583
+
1584
+ async getOwners(limit: number = 100): Promise<any[]> {
1585
+ try {
1586
+ console.log('🧑‍đŸ’ŧ Fetching ALL owners with pagination...');
1587
+ const allOwners = await this.paginateList(
1588
+ (params) => this.client.crm.owners.ownersApi.getPage(
1589
+ undefined,
1590
+ params.after,
1591
+ params.limit,
1592
+ false
1593
+ ),
1594
+ limit
1595
+ );
1596
+ console.log(`✅ Total owners fetched: ${allOwners.length}`);
1597
+ return allOwners;
1598
+ } catch (error: any) {
1599
+ console.error('Error fetching owners:', error);
1600
+ return this.handleOptionalScopeError(error, 'crm.objects.owners.read', []);
1601
+ }
1602
+ }
1603
+
1604
+ // Service Hub Methods
1605
+ async getConversations(limit: number = 100): Promise<any[]> {
1606
+ try {
1607
+ console.log('đŸ’Ŧ Fetching ALL conversations with pagination...');
1608
+ const allConversations = await this.paginateList(
1609
+ async (params) => {
1610
+ const path = `/conversations/v3/conversations/threads?limit=${params.limit}${params.after ? `&after=${params.after}` : ''}`;
1611
+ const response = await this.client.apiRequest({ method: 'GET', path });
1612
+ return await response.json();
1613
+ },
1614
+ limit
1615
+ );
1616
+ console.log(`✅ Total conversations fetched: ${allConversations.length}`);
1617
+ return allConversations;
1618
+ } catch (error: any) {
1619
+ console.error('Error fetching conversations:', error);
1620
+ return this.handleOptionalScopeError(error, 'conversations.read', []);
1621
+ }
1622
+ }
1623
+
1624
+
1625
+ async getCalls(limit: number = 100, updatedAfter?: string): Promise<any[]> {
1626
+ try {
1627
+ await this.rateLimitDelay();
1628
+ const callProperties = [
1629
+ 'hs_call_title', 'hs_call_body', 'hs_call_status', 'hs_call_direction',
1630
+ 'hs_call_disposition', 'hs_call_duration', 'hs_timestamp',
1631
+ 'hs_call_from_number', 'hs_call_to_number', 'hs_call_recording_url',
1632
+ 'createdate', 'hs_lastmodifieddate', 'hubspot_owner_id',
1633
+ 'hs_call_callee_object_id', 'hs_call_callee_object_type'
1634
+ ];
1635
+ const searchRequest = {
1636
+ sorts: ['hs_timestamp:desc'],
1637
+ limit,
1638
+ properties: callProperties
1639
+ };
1640
+ if (updatedAfter) {
1641
+ this.applyUpdatedAfterFilter(searchRequest, updatedAfter, 'hs_lastmodifieddate');
1642
+ }
1643
+ const response = await this.client.crm.objects.searchApi.doSearch('calls', searchRequest);
1644
+ const results = convertDatetimeFields(response.results || []);
1645
+ console.log(`Total calls fetched: ${results.length}`);
1646
+ return results;
1647
+ } catch (error: any) {
1648
+ console.error('Error fetching calls:', error);
1649
+ return this.handleOptionalScopeError(error, 'crm.objects.calls.read', []);
1650
+ }
1651
+ }
1652
+
1653
+ async getMeetings(limit: number = 100, updatedAfter?: string): Promise<any[]> {
1654
+ try {
1655
+ await this.rateLimitDelay();
1656
+ const meetingProperties = [
1657
+ 'hs_meeting_title', 'hs_meeting_body', 'hs_meeting_outcome',
1658
+ 'hs_meeting_start_time', 'hs_meeting_end_time', 'hs_timestamp',
1659
+ 'hs_meeting_external_url', 'hs_meeting_location', 'hs_meeting_source',
1660
+ 'createdate', 'hs_lastmodifieddate', 'hubspot_owner_id',
1661
+ 'hs_internal_meeting_notes', 'hs_meeting_ms_teams_payload'
1662
+ ];
1663
+ const searchRequest = {
1664
+ sorts: ['hs_meeting_start_time:desc'],
1665
+ limit,
1666
+ properties: meetingProperties
1667
+ };
1668
+ if (updatedAfter) {
1669
+ this.applyUpdatedAfterFilter(searchRequest, updatedAfter, 'hs_lastmodifieddate');
1670
+ }
1671
+ const response = await this.client.crm.objects.searchApi.doSearch('meetings', searchRequest);
1672
+ const results = convertDatetimeFields(response.results || []);
1673
+ console.log(`Total meetings fetched: ${results.length}`);
1674
+ return results;
1675
+ } catch (error: any) {
1676
+ console.error('Error fetching meetings:', error);
1677
+ return this.handleOptionalScopeError(error, 'crm.objects.meetings.read', []);
1678
+ }
1679
+ }
1680
+
1681
+ async getCustomObjects(objectType?: string, limit: number = 100): Promise<any[]> {
1682
+ const resolvedTypes: string[] = [];
1683
+ const schemaPropertiesByType = new Map<string, string[]>();
1684
+
1685
+ if (objectType && objectType !== 'custom_objects') {
1686
+ resolvedTypes.push(objectType);
1687
+ } else {
1688
+ try {
1689
+ console.log('Discovering custom object schemas...');
1690
+ await this.rateLimitDelay();
1691
+ const schemaResponse = await this.client.crm.schemas.coreApi.getAll();
1692
+ const customSchemas =
1693
+ schemaResponse?.results?.filter(schema =>
1694
+ typeof schema?.objectTypeId === 'string' &&
1695
+ schema.objectTypeId.startsWith('2-')
1696
+ ) || [];
1697
+
1698
+ for (const schema of customSchemas) {
1699
+ const typeId = schema.objectTypeId || schema.name;
1700
+ if (typeId) {
1701
+ resolvedTypes.push(typeId);
1702
+ const propertyNames = Array.isArray(schema.properties)
1703
+ ? schema.properties
1704
+ .map((property) => property?.name)
1705
+ .filter((name): name is string => Boolean(name))
1706
+ : [];
1707
+ if (propertyNames.length) {
1708
+ schemaPropertiesByType.set(typeId, propertyNames);
1709
+ }
1710
+ }
1711
+ }
1712
+
1713
+ if (resolvedTypes.length === 0) {
1714
+ console.log('No custom object schemas found for this portal.');
1715
+ return [];
1716
+ }
1717
+ } catch (error: any) {
1718
+ return this.handleOptionalScopeError(error, 'crm.schemas.custom.read', []);
1719
+ }
1720
+ }
1721
+
1722
+ try {
1723
+ const aggregated: any[] = [];
1724
+
1725
+ for (const typeId of resolvedTypes) {
1726
+ console.log(`Fetching custom objects for type ${typeId}...`);
1727
+ try {
1728
+ const objectsForType = await this.paginateList(
1729
+ (params) =>
1730
+ this.client.crm.objects.basicApi.getPage(
1731
+ typeId,
1732
+ params.limit,
1733
+ params.after,
1734
+ schemaPropertiesByType.get(typeId)
1735
+ ),
1736
+ limit
1737
+ );
1738
+
1739
+ const tagged = objectsForType.map(obj => ({
1740
+ ...obj,
1741
+ objectTypeId: typeId
1742
+ }));
1743
+
1744
+ aggregated.push(...tagged);
1745
+ } catch (error: any) {
1746
+ if (error?.status === 404 || error?.code === 404) {
1747
+ console.warn(`Custom object type '${typeId}' not found.`);
1748
+ continue;
1749
+ }
1750
+ throw error;
1751
+ }
1752
+ }
1753
+
1754
+ console.log(
1755
+ `Fetched ${aggregated.length} custom objects across ${resolvedTypes.length} type(s)`
1756
+ );
1757
+ return aggregated;
1758
+ } catch (error: any) {
1759
+ if (error?.status === 404 || error?.code === 404) {
1760
+ console.warn('Custom objects endpoint not found for this portal.');
1761
+ return [];
1762
+ }
1763
+ return this.handleOptionalScopeError(error, 'crm.objects.custom.read', []);
1764
+ }
1765
+ }
1766
+
1767
+ // CMS Hub Methods
1768
+ async getSitePages(limit: number = 100): Promise<any[]> {
1769
+ try {
1770
+ console.log('📄 Fetching ALL site pages with pagination...');
1771
+ const allPages = await this.paginateList(
1772
+ async (params) => {
1773
+ const path = `/cms/v3/pages/site-pages?limit=${params.limit}${params.after ? `&after=${params.after}` : ''}`;
1774
+ const response = await this.client.apiRequest({ method: 'GET', path });
1775
+ return await response.json();
1776
+ },
1777
+ limit
1778
+ );
1779
+ console.log(`✅ Total site pages fetched: ${allPages.length}`);
1780
+ return allPages;
1781
+ } catch (error: any) {
1782
+ console.error('Error fetching site pages:', error);
1783
+ return this.handleOptionalScopeError(error, 'content', []);
1784
+ }
1785
+ }
1786
+
1787
+ async getFiles(limit: number = 100): Promise<any[]> {
1788
+ try {
1789
+ console.log('📁 Fetching ALL files with pagination...');
1790
+ const pageSize = Math.min(Math.max(limit, 1), 200);
1791
+ const chunkThreshold = 9000; // stay well below HubSpot's 10k search cap
1792
+ const seenIds = new Set<string>();
1793
+ const allFiles: any[] = [];
1794
+ let createdAtCursor: string | undefined;
1795
+ let keepFetching = true;
1796
+
1797
+ while (keepFetching) {
1798
+ const segment = await this.fetchFilesSegment(pageSize, chunkThreshold, createdAtCursor);
1799
+
1800
+ if (segment.results.length === 0) {
1801
+ break;
1802
+ }
1803
+
1804
+ for (const file of segment.results) {
1805
+ if (file?.id && !seenIds.has(file.id)) {
1806
+ seenIds.add(file.id);
1807
+ allFiles.push(file);
1808
+ }
1809
+ }
1810
+
1811
+ if (!segment.reachedThreshold) {
1812
+ keepFetching = false;
1813
+ } else if (segment.lastCreatedAt) {
1814
+ const nextStart = new Date(segment.lastCreatedAt);
1815
+ nextStart.setMilliseconds(nextStart.getMilliseconds() + 1);
1816
+ createdAtCursor = nextStart.toISOString();
1817
+ } else {
1818
+ keepFetching = false;
1819
+ }
1820
+ }
1821
+
1822
+ console.log(`✅ Total files fetched: ${allFiles.length}`);
1823
+ return allFiles;
1824
+ } catch (error: any) {
1825
+ console.error('Error fetching files:', error);
1826
+ return this.handleOptionalScopeError(error, 'files', []);
1827
+ }
1828
+ }
1829
+
1830
+ private async fetchFilesSegment(
1831
+ pageSize: number,
1832
+ chunkThreshold: number,
1833
+ createdAtGte?: string
1834
+ ): Promise<{ results: any[]; reachedThreshold: boolean; lastCreatedAt?: string }> {
1835
+ const collected: any[] = [];
1836
+ let reachedThreshold = false;
1837
+ let lastCreatedAt: string | undefined;
1838
+ let after: string | undefined;
1839
+ let hasMore = true;
1840
+
1841
+ while (hasMore) {
1842
+ await this.rateLimitDelay();
1843
+ const createdAtDate = createdAtGte ? new Date(createdAtGte) : undefined;
1844
+ const response = await this.client.files.filesApi.doSearch(
1845
+ undefined,
1846
+ after,
1847
+ undefined,
1848
+ pageSize,
1849
+ ['createdAt'],
1850
+ undefined,
1851
+ undefined,
1852
+ undefined,
1853
+ createdAtDate
1854
+ );
1855
+
1856
+ const results = response.results || [];
1857
+ if (results.length === 0) {
1858
+ break;
1859
+ }
1860
+
1861
+ collected.push(...results);
1862
+ lastCreatedAt = results[results.length - 1]?.createdAt;
1863
+
1864
+ if (!response.paging?.next?.after) {
1865
+ hasMore = false;
1866
+ } else {
1867
+ after = response.paging.next.after;
1868
+ }
1869
+
1870
+ if (collected.length >= chunkThreshold) {
1871
+ reachedThreshold = true;
1872
+ hasMore = false;
1873
+ }
1874
+ }
1875
+
1876
+ return { results: collected, reachedThreshold, lastCreatedAt };
1877
+ }
1878
+
1879
+ async getHubDbTables(limit: number = 100): Promise<any[]> {
1880
+ try {
1881
+ console.log('🗄 Fetching ALL HubDB tables with pagination...');
1882
+ const allTables = await this.paginateList(
1883
+ (params) => this.client.cms.hubdb.tablesApi.getAllTables(undefined, params.after, params.limit),
1884
+ limit
1885
+ );
1886
+ console.log(`✅ Total HubDB tables fetched: ${allTables.length}`);
1887
+ return allTables;
1888
+ } catch (error: any) {
1889
+ console.error('Error fetching HubDB tables:', error);
1890
+ return this.handleOptionalScopeError(error, 'hubdb', []);
1891
+ }
1892
+ }
1893
+
1894
+ // Commerce Hub Methods
1895
+ async getProducts(limit: number = 100, updatedAfter?: string): Promise<any[]> {
1896
+ try {
1897
+ console.log('đŸ“Ļ Fetching ALL products with pagination...');
1898
+
1899
+ const productProperties = [
1900
+ 'name', 'description', 'price', 'hs_sku', 'hs_cost_of_goods_sold',
1901
+ 'hs_recurring_billing_period', 'hs_product_type', 'hs_url',
1902
+ 'createdate', 'hs_lastmodifieddate', 'hs_folder_id',
1903
+ 'tax', 'hs_images', 'hs_avatar_filemanager_key'
1904
+ ];
1905
+
1906
+ if (updatedAfter) {
1907
+ const searchRequest = {
1908
+ sorts: ['hs_lastmodifieddate:desc'],
1909
+ properties: productProperties
1910
+ };
1911
+ this.applyUpdatedAfterFilter(searchRequest, updatedAfter, 'hs_lastmodifieddate');
1912
+ const incrementalProducts = await this.paginateSearch(
1913
+ (req) => this.client.crm.products.searchApi.doSearch(req),
1914
+ searchRequest,
1915
+ limit
1916
+ );
1917
+ const normalizedProducts = convertDatetimeFields(incrementalProducts);
1918
+ return filterRecordsByUpdatedAfter(
1919
+ normalizedProducts,
1920
+ updatedAfter,
1921
+ (record) => record?.updatedAt,
1922
+ 'products'
1923
+ );
1924
+ }
1925
+
1926
+ const allProducts = await this.paginateList(
1927
+ (params) =>
1928
+ this.client.crm.products.basicApi.getPage(
1929
+ params.limit,
1930
+ params.after,
1931
+ productProperties
1932
+ ),
1933
+ limit
1934
+ );
1935
+ const normalizedProducts = convertDatetimeFields(allProducts);
1936
+ return normalizedProducts;
1937
+ } catch (error: any) {
1938
+ console.error('Error fetching products:', error);
1939
+ return this.handleOptionalScopeError(error, 'crm.objects.products.read', []);
1940
+ }
1941
+ }
1942
+
1943
+ async getLineItems(limit: number = 100, updatedAfter?: string): Promise<any[]> {
1944
+ try {
1945
+ console.log('📋 Fetching ALL line items with pagination...');
1946
+
1947
+ const lineItemProperties = [
1948
+ 'name', 'quantity', 'price', 'amount', 'discount',
1949
+ 'hs_sku', 'hs_product_id', 'hs_line_item_currency_code',
1950
+ 'createdate', 'hs_lastmodifieddate',
1951
+ 'hs_recurring_billing_period', 'hs_term_in_months', 'tax'
1952
+ ];
1953
+
1954
+ if (updatedAfter) {
1955
+ const searchRequest = {
1956
+ sorts: ['hs_lastmodifieddate:desc'],
1957
+ properties: lineItemProperties
1958
+ };
1959
+ this.applyUpdatedAfterFilter(searchRequest, updatedAfter, 'hs_lastmodifieddate');
1960
+ const incrementalLineItems = await this.paginateSearch(
1961
+ (req) => this.client.crm.lineItems.searchApi.doSearch(req),
1962
+ searchRequest,
1963
+ limit
1964
+ );
1965
+ console.log(`✅ Total line items fetched (incremental): ${incrementalLineItems.length}`);
1966
+ return convertDatetimeFields(incrementalLineItems);
1967
+ }
1968
+
1969
+ const allLineItems = await this.paginateList(
1970
+ (params) => this.client.crm.lineItems.basicApi.getPage(
1971
+ params.limit,
1972
+ params.after,
1973
+ lineItemProperties
1974
+ ),
1975
+ limit
1976
+ );
1977
+ console.log(`✅ Total line items fetched: ${allLineItems.length}`);
1978
+ return allLineItems;
1979
+ } catch (error: any) {
1980
+ console.error('Error fetching line items:', error);
1981
+ return this.handleOptionalScopeError(error, 'crm.objects.line_items.read', []);
1982
+ }
1983
+ }
1984
+
1985
+ // Enterprise Methods
1986
+ async searchObjects(objectType: string, query: string, limit: number = 100): Promise<any[]> {
1987
+ try {
1988
+ await this.rateLimitDelay();
1989
+ const response = await this.client.apiRequest({
1990
+ method: 'POST',
1991
+ path: `/crm/v3/objects/${objectType}/search`,
1992
+ body: {
1993
+ query: query,
1994
+ limit: limit,
1995
+ sorts: [{ propertyName: "createdate", direction: "DESCENDING" }]
1996
+ }
1997
+ });
1998
+ const data = await response.json();
1999
+ return data?.results || [];
2000
+ } catch (error: any) {
2001
+ console.error('Error searching objects:', error);
2002
+ throw new Error(`HubSpot API error: ${error.message}`);
2003
+ }
2004
+ }
2005
+
2006
+ async getLists(limit: number = 100): Promise<any[]> {
2007
+ try {
2008
+ console.log('📄 Fetching ALL lists with search pagination...');
2009
+ const pageSize = Math.min(Math.max(limit, 1), 100);
2010
+ const allLists: any[] = [];
2011
+ let offset: number | undefined = undefined;
2012
+ let hasMore = true;
2013
+
2014
+ while (hasMore) {
2015
+ await this.rateLimitDelay();
2016
+
2017
+ const body: any = { limit: pageSize };
2018
+ if (typeof offset === 'number') {
2019
+ body.offset = offset;
2020
+ }
2021
+
2022
+ const response = await this.client.apiRequest({
2023
+ method: 'POST',
2024
+ path: `/crm/v3/lists/search`,
2025
+ body
2026
+ });
2027
+
2028
+ const data = await response.json();
2029
+ const pageLists = Array.isArray(data?.lists) ? data.lists : [];
2030
+ if (pageLists.length > 0) {
2031
+ allLists.push(...pageLists);
2032
+ console.log(`📄 Fetched ${pageLists.length} lists (total: ${allLists.length})`);
2033
+ }
2034
+
2035
+ hasMore = Boolean(data?.hasMore);
2036
+ offset = data?.offset;
2037
+
2038
+ if (!hasMore || typeof offset !== 'number') {
2039
+ break;
2040
+ }
2041
+ }
2042
+
2043
+ console.log(`✅ Total lists fetched: ${allLists.length}`);
2044
+ return allLists;
2045
+ } catch (error: any) {
2046
+ console.error('Error fetching lists:', error);
2047
+ throw new Error(`HubSpot API error: ${error.message}`);
2048
+ }
2049
+ }
2050
+
2051
+ async getWorkflows(limit: number = 100): Promise<any[]> {
2052
+ try {
2053
+ console.log('?sT??? Fetching workflows from automation API with pagination...');
2054
+
2055
+ const allWorkflows = await this.paginateList(
2056
+ async (params) => {
2057
+ const query = new URLSearchParams();
2058
+ query.append('limit', params.limit.toString());
2059
+ if (params.after) {
2060
+ query.append('after', params.after);
2061
+ }
2062
+
2063
+ const response = await this.client.apiRequest({
2064
+ method: 'GET',
2065
+ path: `/automation/v3/workflows?${query.toString()}`
2066
+ });
2067
+
2068
+ const data = await response.json();
2069
+ const results = Array.isArray(data?.workflows)
2070
+ ? data.workflows
2071
+ : Array.isArray(data?.results)
2072
+ ? data.results
2073
+ : [];
2074
+
2075
+ return {
2076
+ results,
2077
+ paging: data?.paging
2078
+ };
2079
+ },
2080
+ limit
2081
+ );
2082
+
2083
+ console.log(`?o. Total workflows fetched: ${allWorkflows.length}`);
2084
+ return allWorkflows;
2085
+ } catch (error: any) {
2086
+ console.error('Error fetching workflows:', error);
2087
+ return this.handleOptionalScopeError(error, 'automation', []);
2088
+ }
2089
+ }
2090
+
2091
+ async getDomains(limit: number = 10): Promise<any[]> {
2092
+ try {
2093
+ console.log('?Y"? Fetching ALL domains with pagination...');
2094
+
2095
+ const allDomains = await this.paginateList(
2096
+ async (params) => {
2097
+ const query = new URLSearchParams();
2098
+ query.append('limit', params.limit.toString());
2099
+ if (params.after) {
2100
+ query.append('after', params.after);
2101
+ }
2102
+
2103
+ const response = await this.client.apiRequest({
2104
+ method: 'GET',
2105
+ path: `/cms/v3/domains?${query.toString()}`
2106
+ });
2107
+
2108
+ return await response.json();
2109
+ },
2110
+ limit
2111
+ );
2112
+
2113
+ console.log(`?o. Total domains fetched: ${allDomains.length}`);
2114
+ return allDomains;
2115
+ } catch (error: any) {
2116
+ console.error('Error fetching domains:', error);
2117
+ return this.handleOptionalScopeError(error, 'content', []);
2118
+ }
2119
+ }
2120
+ }