domain-search-mcp 1.1.4 → 1.2.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.
@@ -18,6 +18,8 @@ import {
18
18
  getSynonyms,
19
19
  getIndustryTerms,
20
20
  } from '../utils/semantic-engine.js';
21
+ import { godaddyMcpAdapter, type GodaddySuggestion } from '../registrars/index.js';
22
+ import { logger } from '../utils/logger.js';
21
23
  import type { DomainResult } from '../types.js';
22
24
 
23
25
  /**
@@ -101,14 +103,16 @@ export const suggestDomainsSmartTool = {
101
103
  description: `AI-powered domain name suggestion engine.
102
104
 
103
105
  Generate creative, brandable domain names from keywords or business descriptions.
104
- Uses semantic analysis, synonym expansion, and industry-specific vocabulary.
106
+ Combines our semantic engine with GoDaddy's AI suggestions for maximum coverage.
105
107
 
106
108
  Features:
109
+ - Dual-source suggestions: Our semantic engine + GoDaddy AI
107
110
  - Understands natural language queries ("coffee shop in seattle")
108
111
  - Auto-detects industry for contextual suggestions
109
112
  - Generates portmanteau/blended names (instagram = instant + telegram)
110
113
  - Applies modern naming patterns (ly, ify, io, hub, etc.)
111
114
  - Filters premium domains by default
115
+ - Pre-verified availability via GoDaddy
112
116
 
113
117
  Examples:
114
118
  - suggest_domains_smart("ai customer service") → AI-themed suggestions
@@ -206,6 +210,7 @@ interface SmartSuggestion {
206
210
  privacy_included: boolean;
207
211
  score: number;
208
212
  category: 'standard' | 'premium' | 'auction' | 'unavailable';
213
+ source: 'semantic_engine' | 'godaddy_suggest' | 'both';
209
214
  }
210
215
 
211
216
  /**
@@ -217,7 +222,11 @@ interface SuggestDomainsSmartResponse {
217
222
  detected_industry: string | null;
218
223
  tld: string;
219
224
  style: string;
220
- total_generated: number;
225
+ sources: {
226
+ semantic_engine: number;
227
+ godaddy_suggest: number;
228
+ merged: number;
229
+ };
221
230
  total_checked: number;
222
231
  results: {
223
232
  available: SmartSuggestion[];
@@ -243,97 +252,215 @@ export async function executeSuggestDomainsSmart(
243
252
  const detectedWords = segmentWords(normalizedQuery);
244
253
  const detectedIndustry = industry || detectIndustry(detectedWords);
245
254
 
246
- // Generate smart suggestions
247
- const rawSuggestions = generateSmartSuggestions(normalizedQuery, {
248
- maxSuggestions: max_suggestions * 4, // Generate extra for filtering
255
+ // Track source statistics
256
+ const sourceStats = {
257
+ semantic_engine: 0,
258
+ godaddy_suggest: 0,
259
+ merged: 0,
260
+ };
261
+
262
+ // ========================================
263
+ // STEP 1: Generate suggestions from BOTH sources in parallel
264
+ // ========================================
265
+
266
+ // Source 1: Our semantic engine
267
+ const semanticSuggestions = generateSmartSuggestions(normalizedQuery, {
268
+ maxSuggestions: max_suggestions * 3,
249
269
  includePortmanteau: style === 'creative' || style === 'brandable',
250
270
  includeSynonyms: style !== 'short',
251
271
  includeIndustryTerms: !!detectedIndustry,
252
272
  industry: detectedIndustry || undefined,
253
273
  });
274
+ sourceStats.semantic_engine = semanticSuggestions.length;
275
+
276
+ // Source 2: GoDaddy's AI suggestions (parallel call)
277
+ let godaddySuggestions: GodaddySuggestion[] = [];
278
+ try {
279
+ godaddySuggestions = await godaddyMcpAdapter.suggestDomains(query, {
280
+ tlds: [tld],
281
+ limit: max_suggestions * 2,
282
+ });
283
+ sourceStats.godaddy_suggest = godaddySuggestions.length;
284
+ logger.debug('GoDaddy suggestions received', {
285
+ count: godaddySuggestions.length,
286
+ sample: godaddySuggestions.slice(0, 3).map(s => s.domain),
287
+ });
288
+ } catch (error) {
289
+ // GoDaddy might fail - continue with just semantic suggestions
290
+ logger.warn('GoDaddy suggestions failed, using semantic engine only', {
291
+ error: error instanceof Error ? error.message : 'unknown',
292
+ });
293
+ }
254
294
 
255
- // Apply style filter
256
- const styledSuggestions = applyStyleFilter(rawSuggestions, style, normalizedQuery);
257
-
258
- // Limit candidates for availability check
259
- const candidates = styledSuggestions.slice(0, max_suggestions * 2);
295
+ // ========================================
296
+ // STEP 2: Merge and deduplicate suggestions
297
+ // ========================================
260
298
 
261
- // Check availability in parallel batches
262
- const BATCH_SIZE = 5;
263
- const results: Array<{ name: string; result: DomainResult | null }> = [];
299
+ // Track which domains came from which source
300
+ const domainSources = new Map<string, 'semantic_engine' | 'godaddy_suggest' | 'both'>();
264
301
 
265
- for (let i = 0; i < candidates.length; i += BATCH_SIZE) {
266
- const batch = candidates.slice(i, i + BATCH_SIZE);
267
- const batchResults = await Promise.all(
268
- batch.map(async (name) => {
269
- try {
270
- const response = await searchDomain(name, [tld]);
271
- const result = response.results.find((r) => r.domain === `${name}.${tld}`);
272
- return { name, result: result || null };
273
- } catch {
274
- return { name, result: null };
275
- }
276
- }),
277
- );
278
- results.push(...batchResults);
302
+ // Add semantic suggestions (need availability check)
303
+ const styledSuggestions = applyStyleFilter(semanticSuggestions, style, normalizedQuery);
304
+ for (const name of styledSuggestions) {
305
+ const fullDomain = `${name}.${tld}`.toLowerCase();
306
+ domainSources.set(fullDomain, 'semantic_engine');
307
+ }
279
308
 
280
- // Early exit if we have enough available
281
- const availableCount = results.filter(r => r.result?.available && !r.result?.premium).length;
282
- if (availableCount >= max_suggestions && !include_premium) {
283
- break;
309
+ // Add GoDaddy suggestions (already have availability)
310
+ for (const gs of godaddySuggestions) {
311
+ const fullDomain = gs.domain.toLowerCase();
312
+ if (domainSources.has(fullDomain)) {
313
+ domainSources.set(fullDomain, 'both'); // Found in both sources
314
+ sourceStats.merged++;
315
+ } else {
316
+ domainSources.set(fullDomain, 'godaddy_suggest');
284
317
  }
285
318
  }
286
319
 
287
- // Categorize results
320
+ // ========================================
321
+ // STEP 3: Check availability for semantic suggestions
322
+ // (GoDaddy suggestions already have availability info)
323
+ // ========================================
324
+
288
325
  const available: SmartSuggestion[] = [];
289
326
  const premium: SmartSuggestion[] = [];
290
327
  let unavailableCount = 0;
328
+ let totalChecked = 0;
291
329
 
292
- for (const { name, result } of results) {
293
- if (!result) {
294
- unavailableCount++;
295
- continue;
296
- }
297
-
298
- const isPremium = result.premium || isPremiumPrice(tld, result.price_first_year);
330
+ // First, add pre-checked GoDaddy suggestions (no API call needed!)
331
+ for (const gs of godaddySuggestions) {
332
+ const fullDomain = gs.domain.toLowerCase();
333
+ const source = domainSources.get(fullDomain) || 'godaddy_suggest';
334
+ const name = fullDomain.replace(`.${tld}`, '');
299
335
 
300
336
  const suggestion: SmartSuggestion = {
301
- domain: `${name}.${tld}`,
302
- available: result.available,
303
- price_first_year: result.price_first_year,
304
- price_renewal: result.price_renewal,
305
- registrar: result.registrar,
306
- premium: result.premium || false,
307
- premium_detected: isPremiumPrice(tld, result.price_first_year),
308
- privacy_included: result.privacy_included || false,
337
+ domain: fullDomain,
338
+ available: gs.available,
339
+ price_first_year: null, // GoDaddy doesn't provide pricing
340
+ price_renewal: null,
341
+ registrar: 'godaddy',
342
+ premium: gs.premium,
343
+ premium_detected: gs.premium,
344
+ privacy_included: false,
309
345
  score: scoreDomainName(name, normalizedQuery),
310
- category: !result.available
346
+ category: !gs.available
311
347
  ? 'unavailable'
312
- : isPremium
348
+ : gs.premium
313
349
  ? 'premium'
350
+ : gs.auction
351
+ ? 'auction'
314
352
  : 'standard',
353
+ source,
315
354
  };
316
355
 
317
- if (!result.available) {
356
+ if (!gs.available) {
318
357
  unavailableCount++;
319
- } else if (isPremium) {
320
- premium.push(suggestion);
358
+ } else if (gs.premium || gs.auction) {
359
+ if (include_premium) {
360
+ premium.push(suggestion);
361
+ }
321
362
  } else {
322
363
  available.push(suggestion);
323
364
  }
324
365
  }
325
366
 
326
- // Sort by score
327
- available.sort((a, b) => b.score - a.score);
367
+ // Then, check semantic suggestions that weren't in GoDaddy results
368
+ const semanticOnlyCandidates = styledSuggestions
369
+ .filter(name => {
370
+ const fullDomain = `${name}.${tld}`.toLowerCase();
371
+ return domainSources.get(fullDomain) === 'semantic_engine';
372
+ })
373
+ .slice(0, max_suggestions); // Limit API calls
374
+
375
+ const BATCH_SIZE = 5;
376
+ for (let i = 0; i < semanticOnlyCandidates.length; i += BATCH_SIZE) {
377
+ const batch = semanticOnlyCandidates.slice(i, i + BATCH_SIZE);
378
+ const batchResults = await Promise.all(
379
+ batch.map(async (name) => {
380
+ try {
381
+ const response = await searchDomain(name, [tld]);
382
+ const result = response.results.find((r) => r.domain === `${name}.${tld}`);
383
+ return { name, result: result || null };
384
+ } catch {
385
+ return { name, result: null };
386
+ }
387
+ }),
388
+ );
389
+
390
+ for (const { name, result } of batchResults) {
391
+ totalChecked++;
392
+ if (!result) {
393
+ unavailableCount++;
394
+ continue;
395
+ }
396
+
397
+ const isPremiumDomain = result.premium || isPremiumPrice(tld, result.price_first_year);
398
+ const fullDomain = `${name}.${tld}`.toLowerCase();
399
+
400
+ const suggestion: SmartSuggestion = {
401
+ domain: fullDomain,
402
+ available: result.available,
403
+ price_first_year: result.price_first_year,
404
+ price_renewal: result.price_renewal,
405
+ registrar: result.registrar,
406
+ premium: result.premium || false,
407
+ premium_detected: isPremiumPrice(tld, result.price_first_year),
408
+ privacy_included: result.privacy_included || false,
409
+ score: scoreDomainName(name, normalizedQuery),
410
+ category: !result.available
411
+ ? 'unavailable'
412
+ : isPremiumDomain
413
+ ? 'premium'
414
+ : 'standard',
415
+ source: 'semantic_engine',
416
+ };
417
+
418
+ if (!result.available) {
419
+ unavailableCount++;
420
+ } else if (isPremiumDomain) {
421
+ if (include_premium) {
422
+ premium.push(suggestion);
423
+ }
424
+ } else {
425
+ available.push(suggestion);
426
+ }
427
+ }
428
+
429
+ // Early exit if we have enough available
430
+ if (available.length >= max_suggestions && !include_premium) {
431
+ break;
432
+ }
433
+ }
434
+
435
+ // ========================================
436
+ // STEP 4: Sort and finalize results
437
+ // ========================================
438
+
439
+ // Sort by score, prefer 'both' source items (validated by multiple sources)
440
+ available.sort((a, b) => {
441
+ // Boost 'both' source items
442
+ const aBoost = a.source === 'both' ? 2 : 0;
443
+ const bBoost = b.source === 'both' ? 2 : 0;
444
+ return (b.score + bBoost) - (a.score + aBoost);
445
+ });
328
446
  premium.sort((a, b) => b.score - a.score);
329
447
 
330
448
  // Limit results
331
449
  const finalAvailable = available.slice(0, max_suggestions);
332
450
  const finalPremium = include_premium ? premium.slice(0, Math.floor(max_suggestions / 2)) : [];
333
451
 
334
- // Generate insights
452
+ // ========================================
453
+ // STEP 5: Generate insights
454
+ // ========================================
455
+
335
456
  const insights: string[] = [];
336
457
 
458
+ // Source info
459
+ insights.push(`🔍 Sources: Semantic Engine (${sourceStats.semantic_engine}) + GoDaddy AI (${sourceStats.godaddy_suggest})`);
460
+ if (sourceStats.merged > 0) {
461
+ insights.push(`🔗 ${sourceStats.merged} suggestions found in both sources`);
462
+ }
463
+
337
464
  if (detectedIndustry) {
338
465
  insights.push(`🎯 Detected industry: ${detectedIndustry}`);
339
466
  }
@@ -345,8 +472,9 @@ export async function executeSuggestDomainsSmart(
345
472
  if (finalAvailable.length > 0) {
346
473
  insights.push(`✅ Found ${finalAvailable.length} available domain${finalAvailable.length > 1 ? 's' : ''}`);
347
474
  const best = finalAvailable[0]!;
348
- const priceStr = best.price_first_year !== null ? `$${best.price_first_year}/yr` : 'price unknown';
349
- insights.push(`⭐ Top pick: ${best.domain} (${priceStr})`);
475
+ const priceStr = best.price_first_year !== null ? `$${best.price_first_year}/yr` : 'via ' + best.registrar;
476
+ const sourceStr = best.source === 'both' ? ' (verified by both sources)' : '';
477
+ insights.push(`⭐ Top pick: ${best.domain} (${priceStr})${sourceStr}`);
350
478
  } else {
351
479
  insights.push(`❌ No standard-priced domains available`);
352
480
  }
@@ -376,8 +504,8 @@ export async function executeSuggestDomainsSmart(
376
504
  detected_industry: detectedIndustry,
377
505
  tld,
378
506
  style,
379
- total_generated: rawSuggestions.length,
380
- total_checked: results.length,
507
+ sources: sourceStats,
508
+ total_checked: totalChecked + godaddySuggestions.length,
381
509
  results: {
382
510
  available: finalAvailable,
383
511
  premium: finalPremium,