domain-search-mcp 1.2.7 → 1.2.9

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.
Files changed (3) hide show
  1. package/CLAUDE.md +91 -0
  2. package/README.md +1135 -0
  3. package/package.json +1 -1
package/CLAUDE.md ADDED
@@ -0,0 +1,91 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Build & Development Commands
6
+
7
+ ```bash
8
+ npm run build # Compile TypeScript to dist/
9
+ npm run dev # Watch mode with tsx
10
+ npm start # Run compiled server
11
+ npm test # Run all tests
12
+ npm run test:unit # Run unit tests only
13
+ npm run coverage # Run tests with coverage
14
+ ```
15
+
16
+ Run a single test file:
17
+ ```bash
18
+ npx jest tests/unit/cache.test.ts
19
+ npx jest --testPathPattern="validators"
20
+ ```
21
+
22
+ ## Architecture Overview
23
+
24
+ This is an MCP (Model Context Protocol) server for domain availability searches. It aggregates data from multiple registrars and fallback sources.
25
+
26
+ ### Core Flow
27
+
28
+ ```
29
+ server.ts (MCP Server)
30
+
31
+ tools/*.ts (Tool definitions + executors)
32
+
33
+ services/domain-search.ts (Orchestration layer)
34
+
35
+ registrars/*.ts (API adapters) → Porkbun, Namecheap, GoDaddy
36
+ or
37
+ fallbacks/*.ts (RDAP, WHOIS)
38
+ ```
39
+
40
+ ### Key Components
41
+
42
+ **Tools** (`src/tools/`): Each MCP tool has three exports:
43
+ - `*Tool`: Tool definition (name, description, schema)
44
+ - `*Schema`: Zod validation schema
45
+ - `execute*`: Execution function
46
+
47
+ **Registrar Adapters** (`src/registrars/`): All extend `RegistrarAdapter` base class which provides:
48
+ - Token bucket rate limiting
49
+ - Retry with exponential backoff
50
+ - Timeout handling
51
+ - Standardized `DomainResult` creation
52
+
53
+ **Domain Search Service** (`src/services/domain-search.ts`): Orchestrates source selection:
54
+ 1. Porkbun API (if configured)
55
+ 2. Namecheap API (if configured)
56
+ 3. GoDaddy MCP (always available, no auth)
57
+ 4. RDAP fallback
58
+ 5. WHOIS last resort
59
+
60
+ **Utilities** (`src/utils/`):
61
+ - `cache.ts`: TTL-based in-memory cache
62
+ - `errors.ts`: Structured error types with retry hints
63
+ - `premium-analyzer.ts`: Domain quality scoring and premium detection
64
+ - `semantic-engine.ts`: AI-powered domain name generation
65
+
66
+ ### Type System
67
+
68
+ Core types in `src/types.ts`:
69
+ - `DomainResult`: Complete domain availability/pricing info
70
+ - `SearchResponse`: Results + insights + next_steps
71
+ - `DataSource`: Enum of where data came from
72
+ - `Config`: Environment configuration shape
73
+
74
+ ### Configuration
75
+
76
+ Server works without API keys (falls back to RDAP/WHOIS). For pricing data, configure in `.env`:
77
+ - `PORKBUN_API_KEY` + `PORKBUN_API_SECRET`: Fast with pricing
78
+ - `NAMECHEAP_API_KEY` + `NAMECHEAP_API_USER`: Requires IP whitelist
79
+
80
+ ### Adding a New Tool
81
+
82
+ 1. Create `src/tools/new_tool.ts` with schema, tool definition, and executor
83
+ 2. Export from `src/tools/index.ts`
84
+ 3. Add to `TOOLS` array and `executeToolCall` switch in `server.ts`
85
+
86
+ ### Adding a New Registrar
87
+
88
+ 1. Create `src/registrars/new_registrar.ts` extending `RegistrarAdapter`
89
+ 2. Implement `search()`, `getTldInfo()`, `isEnabled()`
90
+ 3. Export from `src/registrars/index.ts`
91
+ 4. Add to source selection logic in `services/domain-search.ts`
package/README.md CHANGED
@@ -3595,7 +3595,386 @@ function formatReportForPresentation(report: AcquisitionReport): string {
3595
3595
  // Usage example
3596
3596
  const report = await completeDomainAcquisition("techstartup");
3597
3597
  console.log(report.presentation);
3598
+ ```
3599
+
3600
+ #### Finalized Brand Acquisition Wrapper
3601
+
3602
+ Production-ready wrapper with explicit workflow orchestration and edge case handling:
3603
+
3604
+ ```typescript
3605
+ import { searchDomain, checkSocials, suggestDomains, compareRegistrars } from 'domain-search-mcp';
3606
+
3607
+ /**
3608
+ * FINALIZED BRAND ACQUISITION WORKFLOW
3609
+ *
3610
+ * Integrates search_domain, check_socials, and suggest_domains in a single
3611
+ * coordinated workflow with comprehensive edge case handling.
3612
+ *
3613
+ * Workflow Steps:
3614
+ * 1. Parallel domain + social checks with coordinated timeouts
3615
+ * 2. Handle partial availability (some domains available, some not)
3616
+ * 3. Generate alternatives for unavailable domains
3617
+ * 4. Compare pricing across registrars
3618
+ * 5. Compile actionable report
3619
+ */
3620
+ async function brandAcquisitionWorkflow(
3621
+ brandName: string,
3622
+ options: {
3623
+ tlds?: string[];
3624
+ socialPlatforms?: string[];
3625
+ timeoutMs?: number;
3626
+ maxRetries?: number;
3627
+ } = {}
3628
+ ): Promise<BrandAcquisitionResult> {
3629
+ const {
3630
+ tlds = ["com", "io", "dev", "app", "co"],
3631
+ socialPlatforms = ["github", "twitter", "instagram", "npm", "linkedin"],
3632
+ timeoutMs = 15000,
3633
+ maxRetries = 3
3634
+ } = options;
3635
+
3636
+ const startTime = Date.now();
3637
+ const errors: WorkflowError[] = [];
3638
+
3639
+ // ═══════════════════════════════════════════════════════════════════════════
3640
+ // STEP 1: Simultaneous Platform Checking with Coordinated Timeouts
3641
+ // ═══════════════════════════════════════════════════════════════════════════
3642
+
3643
+ // Create AbortController for coordinated timeout across all parallel checks
3644
+ const controller = new AbortController();
3645
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
3646
+
3647
+ try {
3648
+ // Run domain and social checks in parallel with shared timeout
3649
+ const [domainOutcome, socialOutcome] = await Promise.allSettled([
3650
+ // Domain check with per-TLD timeout handling
3651
+ executeWithTimeout(
3652
+ searchDomain({ domain_name: brandName, tlds }),
3653
+ timeoutMs * 0.8, // 80% of total timeout for domains
3654
+ "domain_search"
3655
+ ),
3656
+ // Social check with platform-specific handling
3657
+ executeWithTimeout(
3658
+ checkSocials({ name: brandName, platforms: socialPlatforms }),
3659
+ timeoutMs * 0.8, // 80% of total timeout for socials
3660
+ "social_check"
3661
+ )
3662
+ ]);
3663
+
3664
+ clearTimeout(timeoutId);
3665
+
3666
+ // ═══════════════════════════════════════════════════════════════════════════
3667
+ // STEP 2: Handle Partial Availability Scenarios
3668
+ // ═══════════════════════════════════════════════════════════════════════════
3669
+
3670
+ // Process domain results with partial failure handling
3671
+ let domainResults: DomainResult[] = [];
3672
+ if (domainOutcome.status === "fulfilled") {
3673
+ domainResults = domainOutcome.value.results;
3674
+ } else {
3675
+ errors.push({
3676
+ stage: "domain_search",
3677
+ error: domainOutcome.reason?.message || "Domain search failed",
3678
+ recoverable: true,
3679
+ fallback: "Will retry individual TLDs"
3680
+ });
3681
+
3682
+ // Fallback: Try each TLD individually
3683
+ domainResults = await retryIndividualTlds(brandName, tlds, maxRetries);
3684
+ }
3685
+
3686
+ // Categorize domain results
3687
+ const availableDomains = domainResults.filter(r => r.available === true);
3688
+ const takenDomains = domainResults.filter(r => r.available === false);
3689
+ const failedDomains = domainResults.filter(r => r.error);
3690
+
3691
+ // Process social results with per-platform handling
3692
+ let socialResults: SocialResult[] = [];
3693
+ if (socialOutcome.status === "fulfilled") {
3694
+ socialResults = socialOutcome.value.platforms || socialOutcome.value.results;
3695
+ } else {
3696
+ errors.push({
3697
+ stage: "social_check",
3698
+ error: socialOutcome.reason?.message || "Social check failed",
3699
+ recoverable: true,
3700
+ fallback: "Manual verification required for all platforms"
3701
+ });
3702
+
3703
+ // Provide manual verification links for all platforms
3704
+ socialResults = socialPlatforms.map(platform => ({
3705
+ platform,
3706
+ available: null,
3707
+ confidence: "unverified",
3708
+ manual_check_url: getSocialProfileUrl(platform, brandName),
3709
+ error: "Automated check failed"
3710
+ }));
3711
+ }
3712
+
3713
+ // ═══════════════════════════════════════════════════════════════════════════
3714
+ // STEP 3: Generate Alternatives for Unavailable Domains
3715
+ // ═══════════════════════════════════════════════════════════════════════════
3716
+
3717
+ let alternatives: DomainSuggestion[] = [];
3718
+
3719
+ // Only generate alternatives if primary .com is taken
3720
+ const comAvailable = availableDomains.some(d => d.domain.endsWith('.com'));
3721
+ if (!comAvailable && takenDomains.some(d => d.domain.endsWith('.com'))) {
3722
+ try {
3723
+ const suggestions = await suggestDomains({
3724
+ base_name: brandName,
3725
+ tld: "com",
3726
+ max_suggestions: 10,
3727
+ variants: ["prefixes", "suffixes", "hyphen"]
3728
+ });
3729
+ alternatives = suggestions.suggestions.filter(s => s.available);
3730
+ } catch (error) {
3731
+ errors.push({
3732
+ stage: "suggest_domains",
3733
+ error: error.message,
3734
+ recoverable: false,
3735
+ fallback: "No alternatives generated"
3736
+ });
3737
+ }
3738
+ }
3739
+
3740
+ // ═══════════════════════════════════════════════════════════════════════════
3741
+ // STEP 4: Compare Pricing for Available Domains
3742
+ // ═══════════════════════════════════════════════════════════════════════════
3743
+
3744
+ const pricingComparisons = await Promise.allSettled(
3745
+ availableDomains.slice(0, 5).map(async (d) => {
3746
+ const [name, tld] = d.domain.split('.');
3747
+ const comparison = await compareRegistrars({ domain: name, tld });
3748
+ return { domain: d.domain, comparison };
3749
+ })
3750
+ );
3751
+
3752
+ const pricing = pricingComparisons
3753
+ .filter((p): p is PromiseFulfilledResult<any> => p.status === "fulfilled")
3754
+ .map(p => p.value);
3755
+
3756
+ // ═══════════════════════════════════════════════════════════════════════════
3757
+ // STEP 5: Compile Final Report
3758
+ // ═══════════════════════════════════════════════════════════════════════════
3759
+
3760
+ const executionTime = Date.now() - startTime;
3761
+
3762
+ return {
3763
+ success: errors.length === 0,
3764
+ brandName,
3765
+ executionTime,
3766
+
3767
+ // Domain availability summary
3768
+ domains: {
3769
+ available: availableDomains.map(d => ({
3770
+ domain: d.domain,
3771
+ price: d.price_first_year,
3772
+ registrar: d.registrar,
3773
+ bestPrice: pricing.find(p => p.domain === d.domain)?.comparison?.best_first_year
3774
+ })),
3775
+ taken: takenDomains.map(d => d.domain),
3776
+ failed: failedDomains.map(d => ({ domain: d.domain, error: d.error })),
3777
+ alternatives: alternatives
3778
+ },
3779
+
3780
+ // Social media summary with confidence levels
3781
+ socials: {
3782
+ available: socialResults.filter(s => s.available === true),
3783
+ taken: socialResults.filter(s => s.available === false),
3784
+ unverified: socialResults.filter(s => s.available === null),
3785
+ highConfidence: socialResults.filter(s => s.confidence === "high"),
3786
+ needsManualCheck: socialResults.filter(s =>
3787
+ s.confidence === "low" || s.confidence === "unverified"
3788
+ )
3789
+ },
3790
+
3791
+ // Actionable recommendations
3792
+ recommendations: generateRecommendations(
3793
+ availableDomains,
3794
+ takenDomains,
3795
+ socialResults,
3796
+ alternatives,
3797
+ pricing
3798
+ ),
3799
+
3800
+ // Error tracking for debugging
3801
+ errors,
3802
+
3803
+ // Partial success indicator
3804
+ partialSuccess: errors.length > 0 && (availableDomains.length > 0 || socialResults.length > 0)
3805
+ };
3806
+
3807
+ } finally {
3808
+ clearTimeout(timeoutId);
3809
+ }
3810
+ }
3811
+
3812
+ // ═══════════════════════════════════════════════════════════════════════════
3813
+ // EDGE CASE HANDLING UTILITIES
3814
+ // ═══════════════════════════════════════════════════════════════════════════
3815
+
3816
+ /**
3817
+ * Execute a promise with timeout, returning a structured result
3818
+ */
3819
+ async function executeWithTimeout<T>(
3820
+ promise: Promise<T>,
3821
+ timeoutMs: number,
3822
+ operationName: string
3823
+ ): Promise<T> {
3824
+ return Promise.race([
3825
+ promise,
3826
+ new Promise<never>((_, reject) =>
3827
+ setTimeout(
3828
+ () => reject(new Error(`${operationName} timed out after ${timeoutMs}ms`)),
3829
+ timeoutMs
3830
+ )
3831
+ )
3832
+ ]);
3833
+ }
3834
+
3835
+ /**
3836
+ * Retry individual TLDs when bulk search fails
3837
+ * Implements staggered retry to avoid rate limiting
3838
+ */
3839
+ async function retryIndividualTlds(
3840
+ brandName: string,
3841
+ tlds: string[],
3842
+ maxRetries: number
3843
+ ): Promise<DomainResult[]> {
3844
+ const results: DomainResult[] = [];
3845
+
3846
+ for (const tld of tlds) {
3847
+ let lastError: Error | null = null;
3848
+
3849
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
3850
+ try {
3851
+ // Stagger requests to avoid rate limiting
3852
+ if (attempt > 1) {
3853
+ await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt - 1)));
3854
+ }
3855
+
3856
+ const result = await searchDomain({
3857
+ domain_name: brandName,
3858
+ tlds: [tld]
3859
+ });
3860
+
3861
+ if (result.results[0]) {
3862
+ results.push(result.results[0]);
3863
+ break;
3864
+ }
3865
+ } catch (error) {
3866
+ lastError = error;
3867
+ if (attempt === maxRetries) {
3868
+ results.push({
3869
+ domain: `${brandName}.${tld}`,
3870
+ available: null,
3871
+ error: lastError.message,
3872
+ source: "retry_failed"
3873
+ });
3874
+ }
3875
+ }
3876
+ }
3877
+ }
3598
3878
 
3879
+ return results;
3880
+ }
3881
+
3882
+ /**
3883
+ * Get social profile URL for manual verification
3884
+ */
3885
+ function getSocialProfileUrl(platform: string, username: string): string {
3886
+ const urls: Record<string, string> = {
3887
+ github: `https://github.com/${username}`,
3888
+ twitter: `https://twitter.com/${username}`,
3889
+ instagram: `https://instagram.com/${username}`,
3890
+ linkedin: `https://linkedin.com/company/${username}`,
3891
+ npm: `https://npmjs.com/~${username}`,
3892
+ tiktok: `https://tiktok.com/@${username}`,
3893
+ reddit: `https://reddit.com/user/${username}`
3894
+ };
3895
+ return urls[platform] || `https://${platform}.com/${username}`;
3896
+ }
3897
+
3898
+ /**
3899
+ * Generate actionable recommendations based on results
3900
+ */
3901
+ function generateRecommendations(
3902
+ available: DomainResult[],
3903
+ taken: DomainResult[],
3904
+ socials: SocialResult[],
3905
+ alternatives: DomainSuggestion[],
3906
+ pricing: any[]
3907
+ ): string[] {
3908
+ const recommendations: string[] = [];
3909
+
3910
+ // Domain recommendations
3911
+ if (available.length > 0) {
3912
+ const cheapest = available.sort((a, b) =>
3913
+ (a.price_first_year || 999) - (b.price_first_year || 999)
3914
+ )[0];
3915
+ recommendations.push(
3916
+ `Register ${cheapest.domain} ($${cheapest.price_first_year}/yr) - best value`
3917
+ );
3918
+ } else if (alternatives.length > 0) {
3919
+ recommendations.push(
3920
+ `Primary domains taken. Top alternative: ${alternatives[0].domain}`
3921
+ );
3922
+ }
3923
+
3924
+ // Social media recommendations
3925
+ const availableSocials = socials.filter(s => s.available === true);
3926
+ const takenSocials = socials.filter(s => s.available === false);
3927
+
3928
+ if (availableSocials.length > 0) {
3929
+ recommendations.push(
3930
+ `Secure handles on: ${availableSocials.map(s => s.platform).join(", ")}`
3931
+ );
3932
+ }
3933
+
3934
+ if (takenSocials.length > 0) {
3935
+ recommendations.push(
3936
+ `Consider alternative usernames for: ${takenSocials.map(s => s.platform).join(", ")}`
3937
+ );
3938
+ }
3939
+
3940
+ // Manual verification recommendations
3941
+ const needsManual = socials.filter(s => s.confidence === "low" || s.available === null);
3942
+ if (needsManual.length > 0) {
3943
+ recommendations.push(
3944
+ `Manually verify: ${needsManual.map(s => s.platform).join(", ")}`
3945
+ );
3946
+ }
3947
+
3948
+ return recommendations;
3949
+ }
3950
+
3951
+ // ═══════════════════════════════════════════════════════════════════════════
3952
+ // USAGE EXAMPLE
3953
+ // ═══════════════════════════════════════════════════════════════════════════
3954
+
3955
+ const result = await brandAcquisitionWorkflow("nexatech", {
3956
+ tlds: ["com", "io", "dev"],
3957
+ socialPlatforms: ["github", "twitter", "npm"],
3958
+ timeoutMs: 10000,
3959
+ maxRetries: 2
3960
+ });
3961
+
3962
+ console.log(`Brand: ${result.brandName}`);
3963
+ console.log(`Success: ${result.success}`);
3964
+ console.log(`Execution time: ${result.executionTime}ms`);
3965
+ console.log(`Available domains: ${result.domains.available.length}`);
3966
+ console.log(`Available socials: ${result.socials.available.length}`);
3967
+ console.log(`Recommendations:`, result.recommendations);
3968
+
3969
+ // Handle partial success
3970
+ if (result.partialSuccess) {
3971
+ console.log("⚠️ Partial success - some checks failed:");
3972
+ result.errors.forEach(e => console.log(` - ${e.stage}: ${e.error}`));
3973
+ }
3974
+ ```
3975
+
3976
+ **Example output:**
3977
+ ```
3599
3978
  // Example output:
3600
3979
  // ════════════════════════════════════════════════════════════
3601
3980
  // BRAND VALIDATION REPORT: TECHSTARTUP
@@ -4424,6 +4803,762 @@ async function domainResearchPipeline(businessIdea: string) {
4424
4803
  const research = await domainResearchPipeline("ai-powered code review tool");
4425
4804
  ```
4426
4805
 
4806
+ #### Tool Parameter Optimization Reference
4807
+
4808
+ Quick reference table for optimizing each tool based on startup context:
4809
+
4810
+ | Tool | Parameter | Tech Startup | E-commerce | Creative | Fintech |
4811
+ |------|-----------|--------------|------------|----------|---------|
4812
+ | **search_domain** | `tlds` | `["io","dev","app"]` | `["com","co","shop"]` | `["design","studio","io"]` | `["com","io","finance"]` |
4813
+ | **tld_info** | `detailed` | `true` (need restrictions) | `false` (basic info) | `true` (pricing focus) | `true` (compliance check) |
4814
+ | **suggest_domains** | `style` | `"brandable"` | `"brandable"` | `"creative"` | `"brandable"` |
4815
+ | **suggest_domains** | `industry` | `"tech"` | `"ecommerce"` | `"creative"` | `"finance"` |
4816
+ | **suggest_domains** | `max_suggestions` | `15-20` | `10-15` | `20-25` | `10` |
4817
+ | **check_socials** | `platforms` | `["github","npm","twitter"]` | `["instagram","tiktok"]` | `["instagram","dribbble"]` | `["linkedin","twitter"]` |
4818
+ | **compare_registrars** | Priority | Performance | Best price | Design focus | Security |
4819
+
4820
+ #### Direct tld_info Usage for Startup Research
4821
+
4822
+ The `tld_info` tool provides critical context for startup domain decisions. Use it directly to gather TLD-specific insights:
4823
+
4824
+ ```typescript
4825
+ import { tldInfo } from 'domain-search-mcp';
4826
+
4827
+ // Example: Get detailed TLD information for tech startup decision-making
4828
+ async function analyzeTldsForStartup(startupType: string) {
4829
+ // Define TLDs relevant to startup type
4830
+ const tldsByType = {
4831
+ tech: ["io", "dev", "app", "sh", "ai"],
4832
+ ecommerce: ["com", "shop", "store", "co"],
4833
+ creative: ["design", "studio", "art", "io"],
4834
+ fintech: ["com", "io", "finance", "money"]
4835
+ };
4836
+
4837
+ const tldsToAnalyze = tldsByType[startupType] || tldsByType.tech;
4838
+ const analysis = [];
4839
+
4840
+ // Fetch detailed info for each TLD using tld_info directly
4841
+ for (const tld of tldsToAnalyze) {
4842
+ const info = await tldInfo({ tld, detailed: true });
4843
+
4844
+ analysis.push({
4845
+ tld: info.tld,
4846
+ description: info.description,
4847
+ priceRange: {
4848
+ min: info.price_range.min,
4849
+ max: info.price_range.max,
4850
+ currency: "USD"
4851
+ },
4852
+ typicalUse: info.typical_use,
4853
+ popularity: info.popularity,
4854
+ restrictions: info.restrictions || [],
4855
+ recommendation: info.recommendation,
4856
+ // Calculate suitability score based on startup type
4857
+ suitabilityScore: calculateTldSuitability(info, startupType)
4858
+ });
4859
+ }
4860
+
4861
+ // Sort by suitability score
4862
+ return analysis.sort((a, b) => b.suitabilityScore - a.suitabilityScore);
4863
+ }
4864
+
4865
+ function calculateTldSuitability(tldData: TldInfoResult, startupType: string): number {
4866
+ let score = 50; // Base score
4867
+
4868
+ // Price factor
4869
+ if (tldData.price_range.min < 15) score += 20;
4870
+ else if (tldData.price_range.min < 30) score += 10;
4871
+
4872
+ // Popularity factor
4873
+ if (tldData.popularity === "Very High") score += 15;
4874
+ else if (tldData.popularity === "High") score += 10;
4875
+
4876
+ // Startup type specific bonuses
4877
+ if (startupType === "tech" && ["io", "dev", "app"].includes(tldData.tld)) score += 15;
4878
+ if (startupType === "ecommerce" && tldData.tld === "com") score += 20;
4879
+ if (startupType === "creative" && ["design", "studio"].includes(tldData.tld)) score += 15;
4880
+
4881
+ // Restriction penalty
4882
+ if (tldData.restrictions && tldData.restrictions.length > 0) score -= 10;
4883
+
4884
+ return Math.min(100, Math.max(0, score));
4885
+ }
4886
+
4887
+ // Usage
4888
+ const tldAnalysis = await analyzeTldsForStartup("tech");
4889
+ console.log("TLD Analysis for Tech Startup:");
4890
+ tldAnalysis.forEach(tld => {
4891
+ console.log(` .${tld.tld}: Score ${tld.suitabilityScore}/100`);
4892
+ console.log(` ${tld.description}`);
4893
+ console.log(` Price: $${tld.priceRange.min}-$${tld.priceRange.max}/yr`);
4894
+ });
4895
+
4896
+ // Output:
4897
+ // TLD Analysis for Tech Startup:
4898
+ // .io: Score 85/100
4899
+ // Popular with tech startups and SaaS companies
4900
+ // Price: $32-$44/yr
4901
+ // .dev: Score 80/100
4902
+ // Google-operated TLD for developers
4903
+ // Price: $12-$16/yr
4904
+ // .app: Score 75/100
4905
+ // Mobile and web applications
4906
+ // Price: $14-$18/yr
4907
+ ```
4908
+
4909
+ #### Startup-Type Optimized Parameters
4910
+
4911
+ Different startup types require different tool parameter configurations for optimal results:
4912
+
4913
+ ```typescript
4914
+ // Startup type definitions with optimized tool parameters
4915
+ const STARTUP_TYPE_CONFIGS = {
4916
+ tech: {
4917
+ // Tech startups: developer tools, SaaS, APIs
4918
+ tlds: ["io", "dev", "app", "com", "sh"],
4919
+ suggestStyle: "brandable" as const,
4920
+ socialPlatforms: ["github", "twitter", "npm", "pypi", "producthunt"],
4921
+ priceThreshold: 50, // Tech startups accept higher TLD costs
4922
+ industryHint: "tech" as const
4923
+ },
4924
+ ecommerce: {
4925
+ // E-commerce: retail, marketplace, DTC brands
4926
+ tlds: ["com", "co", "shop", "store", "io"],
4927
+ suggestStyle: "brandable" as const,
4928
+ socialPlatforms: ["instagram", "twitter", "tiktok", "pinterest"],
4929
+ priceThreshold: 25, // Lower threshold, .com preferred
4930
+ industryHint: "ecommerce" as const
4931
+ },
4932
+ creative: {
4933
+ // Creative agencies: design, media, content
4934
+ tlds: ["design", "studio", "io", "co", "com"],
4935
+ suggestStyle: "creative" as const,
4936
+ socialPlatforms: ["instagram", "twitter", "dribbble", "behance"],
4937
+ priceThreshold: 35,
4938
+ industryHint: "creative" as const
4939
+ },
4940
+ fintech: {
4941
+ // Financial technology: payments, banking, crypto
4942
+ tlds: ["com", "io", "finance", "money", "app"],
4943
+ suggestStyle: "brandable" as const,
4944
+ socialPlatforms: ["twitter", "linkedin", "github"],
4945
+ priceThreshold: 100, // Premium domains acceptable
4946
+ industryHint: "finance" as const
4947
+ },
4948
+ healthcare: {
4949
+ // Healthcare & wellness startups
4950
+ tlds: ["health", "care", "com", "io", "app"],
4951
+ suggestStyle: "descriptive" as const,
4952
+ socialPlatforms: ["twitter", "linkedin", "facebook"],
4953
+ priceThreshold: 40,
4954
+ industryHint: "health" as const
4955
+ }
4956
+ };
4957
+
4958
+ type StartupType = keyof typeof STARTUP_TYPE_CONFIGS;
4959
+ ```
4960
+
4961
+ #### Complete Startup Research Pipeline with Error Recovery
4962
+
4963
+ Production-ready pipeline with comprehensive error handling and retry logic:
4964
+
4965
+ ```typescript
4966
+ import {
4967
+ searchDomain,
4968
+ tldInfo,
4969
+ suggestDomainsSmart,
4970
+ checkSocials,
4971
+ compareRegistrars
4972
+ } from 'domain-search-mcp';
4973
+
4974
+ interface PipelineResult {
4975
+ success: boolean;
4976
+ startupName: string;
4977
+ startupType: StartupType;
4978
+ domains: DomainRecommendation[];
4979
+ socialAnalysis: SocialAnalysis[];
4980
+ tldContext: TldContext[];
4981
+ errors: PipelineError[];
4982
+ executionTime: number;
4983
+ }
4984
+
4985
+ interface PipelineError {
4986
+ stage: string;
4987
+ error: string;
4988
+ recoverable: boolean;
4989
+ fallbackUsed?: string;
4990
+ }
4991
+
4992
+ // Retry wrapper with exponential backoff
4993
+ async function withRetry<T>(
4994
+ fn: () => Promise<T>,
4995
+ options: { maxRetries: number; baseDelay: number; stageName: string }
4996
+ ): Promise<{ result: T | null; error: PipelineError | null }> {
4997
+ let lastError: Error | null = null;
4998
+
4999
+ for (let attempt = 0; attempt <= options.maxRetries; attempt++) {
5000
+ try {
5001
+ const result = await fn();
5002
+ return { result, error: null };
5003
+ } catch (err) {
5004
+ lastError = err as Error;
5005
+
5006
+ if (attempt < options.maxRetries) {
5007
+ const delay = options.baseDelay * Math.pow(2, attempt);
5008
+ console.log(`⚠️ ${options.stageName} failed, retrying in ${delay}ms...`);
5009
+ await new Promise(r => setTimeout(r, delay));
5010
+ }
5011
+ }
5012
+ }
5013
+
5014
+ return {
5015
+ result: null,
5016
+ error: {
5017
+ stage: options.stageName,
5018
+ error: lastError?.message || 'Unknown error',
5019
+ recoverable: false
5020
+ }
5021
+ };
5022
+ }
5023
+
5024
+ async function startupDomainResearchPipeline(
5025
+ startupName: string,
5026
+ startupType: StartupType = 'tech'
5027
+ ): Promise<PipelineResult> {
5028
+ const startTime = Date.now();
5029
+ const config = STARTUP_TYPE_CONFIGS[startupType];
5030
+ const errors: PipelineError[] = [];
5031
+
5032
+ console.log(`\n🚀 Starting domain research for "${startupName}" (${startupType} startup)`);
5033
+ console.log(` Preferred TLDs: ${config.tlds.join(', ')}`);
5034
+ console.log(` Social platforms: ${config.socialPlatforms.join(', ')}`);
5035
+
5036
+ // Stage 1: Get TLD context with error recovery
5037
+ console.log('\n📋 Stage 1: Fetching TLD information...');
5038
+ const tldResults: TldContext[] = [];
5039
+
5040
+ for (const tld of config.tlds) {
5041
+ const { result, error } = await withRetry(
5042
+ () => tldInfo({ tld, detailed: true }),
5043
+ { maxRetries: 2, baseDelay: 1000, stageName: `tld_info(${tld})` }
5044
+ );
5045
+
5046
+ if (result) {
5047
+ tldResults.push({
5048
+ tld: result.tld,
5049
+ description: result.description,
5050
+ priceRange: result.price_range,
5051
+ typicalUse: result.typical_use,
5052
+ restrictions: result.restrictions,
5053
+ recommendation: result.recommendation
5054
+ });
5055
+ console.log(` ✅ .${tld}: ${result.description.substring(0, 50)}...`);
5056
+ } else if (error) {
5057
+ errors.push(error);
5058
+ console.log(` ⚠️ .${tld}: Failed to fetch info`);
5059
+ }
5060
+ }
5061
+
5062
+ // Stage 2: Generate smart suggestions with startup context
5063
+ console.log('\n💡 Stage 2: Generating domain suggestions...');
5064
+
5065
+ const { result: suggestions, error: suggestError } = await withRetry(
5066
+ () => suggestDomainsSmart({
5067
+ query: startupName,
5068
+ tld: config.tlds[0], // Primary TLD
5069
+ style: config.suggestStyle,
5070
+ industry: config.industryHint,
5071
+ max_suggestions: 20,
5072
+ include_premium: config.priceThreshold > 50
5073
+ }),
5074
+ { maxRetries: 2, baseDelay: 1500, stageName: 'suggest_domains_smart' }
5075
+ );
5076
+
5077
+ if (suggestError) {
5078
+ errors.push(suggestError);
5079
+ console.log(' ⚠️ Smart suggestions failed, falling back to basic search');
5080
+ }
5081
+
5082
+ // Stage 3: Check availability across all preferred TLDs
5083
+ console.log('\n🔍 Stage 3: Checking domain availability...');
5084
+
5085
+ const { result: availability, error: searchError } = await withRetry(
5086
+ () => searchDomain({
5087
+ domain_name: startupName,
5088
+ tlds: config.tlds
5089
+ }),
5090
+ { maxRetries: 3, baseDelay: 1000, stageName: 'search_domain' }
5091
+ );
5092
+
5093
+ if (searchError) {
5094
+ errors.push(searchError);
5095
+ }
5096
+
5097
+ // Stage 4: Social media availability with platform-specific handling
5098
+ console.log('\n🌐 Stage 4: Checking social media availability...');
5099
+
5100
+ const { result: socialResults, error: socialError } = await withRetry(
5101
+ () => checkSocials({
5102
+ name: startupName.toLowerCase().replace(/[^a-z0-9]/g, ''),
5103
+ platforms: config.socialPlatforms
5104
+ }),
5105
+ { maxRetries: 2, baseDelay: 2000, stageName: 'check_socials' }
5106
+ );
5107
+
5108
+ // Process social results with confidence levels
5109
+ const socialAnalysis: SocialAnalysis[] = [];
5110
+ if (socialResults) {
5111
+ for (const platform of socialResults.platforms) {
5112
+ socialAnalysis.push({
5113
+ platform: platform.platform,
5114
+ available: platform.available,
5115
+ confidence: platform.confidence || 'unknown',
5116
+ url: platform.profile_url,
5117
+ recommendation: getSocialRecommendation(platform, startupType)
5118
+ });
5119
+
5120
+ const status = platform.available ? '✅' : '❌';
5121
+ const conf = platform.confidence ? ` (${platform.confidence})` : '';
5122
+ console.log(` ${status} ${platform.platform}${conf}`);
5123
+ }
5124
+ } else if (socialError) {
5125
+ errors.push({
5126
+ ...socialError,
5127
+ fallbackUsed: 'Manual verification recommended'
5128
+ });
5129
+ }
5130
+
5131
+ // Stage 5: Price comparison for available domains
5132
+ console.log('\n💰 Stage 5: Comparing registrar pricing...');
5133
+
5134
+ const domains: DomainRecommendation[] = [];
5135
+ const availableDomains = availability?.results.filter(r => r.available) || [];
5136
+
5137
+ for (const domain of availableDomains.slice(0, 5)) {
5138
+ const domainName = domain.domain.split('.')[0];
5139
+ const tld = domain.domain.split('.').pop()!;
5140
+
5141
+ const { result: pricing } = await withRetry(
5142
+ () => compareRegistrars({ domain: domainName, tld }),
5143
+ { maxRetries: 1, baseDelay: 1000, stageName: `compare_registrars(${domain.domain})` }
5144
+ );
5145
+
5146
+ const tldContext = tldResults.find(t => t.tld === tld);
5147
+
5148
+ domains.push({
5149
+ domain: domain.domain,
5150
+ available: true,
5151
+ priceFirstYear: pricing?.best_first_year?.price || domain.price_first_year,
5152
+ priceRenewal: pricing?.best_renewal?.price,
5153
+ bestRegistrar: pricing?.best_first_year?.registrar || domain.registrar,
5154
+ tldInfo: tldContext,
5155
+ withinBudget: (domain.price_first_year || 0) <= config.priceThreshold,
5156
+ score: calculateDomainScore(domain, tldContext, socialAnalysis, config)
5157
+ });
5158
+
5159
+ console.log(` ${domain.domain}: $${domain.price_first_year}/yr`);
5160
+ }
5161
+
5162
+ // Sort by score
5163
+ domains.sort((a, b) => (b.score || 0) - (a.score || 0));
5164
+
5165
+ const executionTime = Date.now() - startTime;
5166
+
5167
+ console.log(`\n✨ Research complete in ${executionTime}ms`);
5168
+ console.log(` ${domains.length} available domains found`);
5169
+ console.log(` ${errors.length} errors encountered`);
5170
+
5171
+ return {
5172
+ success: errors.length === 0,
5173
+ startupName,
5174
+ startupType,
5175
+ domains,
5176
+ socialAnalysis,
5177
+ tldContext: tldResults,
5178
+ errors,
5179
+ executionTime
5180
+ };
5181
+ }
5182
+
5183
+ // Helper: Calculate domain score based on multiple factors
5184
+ function calculateDomainScore(
5185
+ domain: any,
5186
+ tldContext: TldContext | undefined,
5187
+ social: SocialAnalysis[],
5188
+ config: typeof STARTUP_TYPE_CONFIGS[StartupType]
5189
+ ): number {
5190
+ let score = 50; // Base score
5191
+
5192
+ // Price factor (lower is better, up to threshold)
5193
+ const price = domain.price_first_year || 0;
5194
+ if (price <= config.priceThreshold * 0.5) score += 20;
5195
+ else if (price <= config.priceThreshold) score += 10;
5196
+ else score -= 10;
5197
+
5198
+ // TLD preference factor
5199
+ const tldIndex = config.tlds.indexOf(domain.domain.split('.').pop());
5200
+ if (tldIndex === 0) score += 15; // Primary TLD
5201
+ else if (tldIndex <= 2) score += 10;
5202
+ else if (tldIndex > -1) score += 5;
5203
+
5204
+ // Social availability factor
5205
+ const availableSocials = social.filter(s => s.available).length;
5206
+ score += availableSocials * 5;
5207
+
5208
+ // Length factor (shorter is better)
5209
+ const nameLength = domain.domain.split('.')[0].length;
5210
+ if (nameLength <= 6) score += 10;
5211
+ else if (nameLength <= 10) score += 5;
5212
+ else if (nameLength > 15) score -= 5;
5213
+
5214
+ return Math.max(0, Math.min(100, score));
5215
+ }
5216
+
5217
+ // Helper: Get platform-specific recommendations
5218
+ function getSocialRecommendation(
5219
+ platform: any,
5220
+ startupType: StartupType
5221
+ ): string {
5222
+ if (platform.available) {
5223
+ return `Register @${platform.name} immediately to secure brand consistency`;
5224
+ }
5225
+
5226
+ const alternatives: Record<string, string> = {
5227
+ twitter: 'Consider @get[name], @[name]app, or @[name]hq',
5228
+ github: 'Use organization account or add suffix like -app, -io',
5229
+ instagram: 'Try [name].official, get[name], or [name]_app',
5230
+ npm: 'Use scoped package @[org]/[name]',
5231
+ linkedin: 'Create company page with full business name'
5232
+ };
5233
+
5234
+ return alternatives[platform.platform] || 'Consider alternative naming';
5235
+ }
5236
+
5237
+ // Usage with different startup types
5238
+ const techStartupReport = await startupDomainResearchPipeline("codeflow", "tech");
5239
+ const ecommerceReport = await startupDomainResearchPipeline("shopwave", "ecommerce");
5240
+ const creativeReport = await startupDomainResearchPipeline("pixelcraft", "creative");
5241
+ ```
5242
+
5243
+ #### Formatted Report Output by Startup Type
5244
+
5245
+ Generate customized reports based on startup category:
5246
+
5247
+ ```typescript
5248
+ function formatStartupReport(result: PipelineResult): string {
5249
+ const { startupName, startupType, domains, socialAnalysis, tldContext, errors } = result;
5250
+
5251
+ let report = `
5252
+ ╔══════════════════════════════════════════════════════════════╗
5253
+ ║ DOMAIN RESEARCH REPORT: ${startupName.toUpperCase().padEnd(35)}║
5254
+ ║ Type: ${startupType.toUpperCase()} STARTUP${' '.repeat(45 - startupType.length)}║
5255
+ ║ Generated: ${new Date().toISOString().split('T')[0]}${' '.repeat(40)}║
5256
+ ╚══════════════════════════════════════════════════════════════╝
5257
+ `;
5258
+
5259
+ // TLD Analysis Section
5260
+ report += `\n┌─ TLD ANALYSIS ${'─'.repeat(46)}┐\n`;
5261
+ for (const tld of tldContext) {
5262
+ report += `│ .${tld.tld.padEnd(6)} │ ${tld.description.substring(0, 40).padEnd(40)} │\n`;
5263
+ report += `│ │ Price: $${tld.priceRange.min}-$${tld.priceRange.max}/yr`.padEnd(52) + `│\n`;
5264
+ }
5265
+ report += `└${'─'.repeat(60)}┘\n`;
5266
+
5267
+ // Domain Recommendations Section
5268
+ report += `\n┌─ TOP DOMAIN RECOMMENDATIONS ${'─'.repeat(31)}┐\n`;
5269
+ report += `│ Rank │ Domain${' '.repeat(20)}│ Price │ Score │\n`;
5270
+ report += `├──────┼${'-'.repeat(26)}┼────────┼───────┤\n`;
5271
+
5272
+ domains.slice(0, 5).forEach((d, i) => {
5273
+ const rank = `#${i + 1}`.padEnd(4);
5274
+ const domain = d.domain.padEnd(25);
5275
+ const price = `$${d.priceFirstYear}`.padEnd(6);
5276
+ const score = `${d.score}/100`;
5277
+ report += `│ ${rank} │ ${domain} │ ${price} │ ${score.padEnd(5)} │\n`;
5278
+ });
5279
+ report += `└${'─'.repeat(60)}┘\n`;
5280
+
5281
+ // Social Media Section
5282
+ report += `\n┌─ SOCIAL MEDIA AVAILABILITY ${'─'.repeat(32)}┐\n`;
5283
+ for (const social of socialAnalysis) {
5284
+ const status = social.available ? '✅ Available' : '❌ Taken';
5285
+ const conf = social.confidence !== 'unknown' ? ` (${social.confidence})` : '';
5286
+ report += `│ ${social.platform.padEnd(12)} │ ${status}${conf}`.padEnd(48) + `│\n`;
5287
+ if (!social.available) {
5288
+ report += `│ │ → ${social.recommendation.substring(0, 35)}`.padEnd(48) + `│\n`;
5289
+ }
5290
+ }
5291
+ report += `└${'─'.repeat(60)}┘\n`;
5292
+
5293
+ // Recommendations by startup type
5294
+ report += `\n┌─ ${startupType.toUpperCase()} STARTUP RECOMMENDATIONS ${'─'.repeat(26)}┐\n`;
5295
+
5296
+ const typeRecommendations: Record<StartupType, string[]> = {
5297
+ tech: [
5298
+ '→ Prioritize .io or .dev for developer credibility',
5299
+ '→ Secure GitHub org and npm package name',
5300
+ '→ Consider .sh for CLI tools'
5301
+ ],
5302
+ ecommerce: [
5303
+ '→ .com is essential for consumer trust',
5304
+ '→ Secure Instagram and TikTok handles',
5305
+ '→ Consider .shop or .store as secondary'
5306
+ ],
5307
+ creative: [
5308
+ '→ .design or .studio signals creative focus',
5309
+ '→ Instagram presence is critical',
5310
+ '→ Shorter names work better for branding'
5311
+ ],
5312
+ fintech: [
5313
+ '→ .com required for financial credibility',
5314
+ '→ Avoid hyphens - trust signals matter',
5315
+ '→ LinkedIn company page essential'
5316
+ ],
5317
+ healthcare: [
5318
+ '→ .health TLD builds trust',
5319
+ '→ Avoid playful names - professionalism matters',
5320
+ '→ Verify regulatory compliance for domain use'
5321
+ ]
5322
+ };
5323
+
5324
+ for (const rec of typeRecommendations[startupType]) {
5325
+ report += `│ ${rec.padEnd(58)} │\n`;
5326
+ }
5327
+ report += `└${'─'.repeat(60)}┘\n`;
5328
+
5329
+ // Error summary if any
5330
+ if (errors.length > 0) {
5331
+ report += `\n⚠️ WARNINGS: ${errors.length} issues encountered during research\n`;
5332
+ errors.forEach(e => {
5333
+ report += ` • ${e.stage}: ${e.error}\n`;
5334
+ });
5335
+ }
5336
+
5337
+ return report;
5338
+ }
5339
+
5340
+ // Generate and print formatted report
5341
+ const result = await startupDomainResearchPipeline("nexaflow", "tech");
5342
+ console.log(formatStartupReport(result));
5343
+ ```
5344
+
5345
+ **Sample Output:**
5346
+ ```
5347
+ ╔══════════════════════════════════════════════════════════════╗
5348
+ ║ DOMAIN RESEARCH REPORT: NEXAFLOW ║
5349
+ ║ Type: TECH STARTUP ║
5350
+ ║ Generated: 2024-01-15 ║
5351
+ ╚══════════════════════════════════════════════════════════════╝
5352
+
5353
+ ┌─ TLD ANALYSIS ──────────────────────────────────────────────┐
5354
+ │ .io │ Popular with tech startups and SaaS │
5355
+ │ │ Price: $32-$44/yr │
5356
+ │ .dev │ Google TLD for developers │
5357
+ │ │ Price: $12-$16/yr │
5358
+ │ .app │ Mobile and web applications │
5359
+ │ │ Price: $14-$18/yr │
5360
+ └──────────────────────────────────────────────────────────────┘
5361
+
5362
+ ┌─ TOP DOMAIN RECOMMENDATIONS ─────────────────────────────────┐
5363
+ │ Rank │ Domain │ Price │ Score │
5364
+ ├──────┼──────────────────────────┼────────┼───────┤
5365
+ │ #1 │ nexaflow.dev │ $14.95 │ 85/100│
5366
+ │ #2 │ nexaflow.io │ $39.95 │ 78/100│
5367
+ │ #3 │ nexaflow.app │ $14.95 │ 75/100│
5368
+ │ #4 │ nexaflow.com │ $9.95 │ 72/100│
5369
+ │ #5 │ nexaflow.sh │ $24.95 │ 65/100│
5370
+ └──────────────────────────────────────────────────────────────┘
5371
+
5372
+ ┌─ SOCIAL MEDIA AVAILABILITY ──────────────────────────────────┐
5373
+ │ github │ ✅ Available (high) │
5374
+ │ twitter │ ❌ Taken (high) │
5375
+ │ │ → Consider @getnexaflow, @nexaflowapp │
5376
+ │ npm │ ✅ Available (high) │
5377
+ │ pypi │ ✅ Available (high) │
5378
+ │ producthunt │ ✅ Available (medium) │
5379
+ └──────────────────────────────────────────────────────────────┘
5380
+
5381
+ ┌─ TECH STARTUP RECOMMENDATIONS ───────────────────────────────┐
5382
+ │ → Prioritize .io or .dev for developer credibility │
5383
+ │ → Secure GitHub org and npm package name │
5384
+ │ → Consider .sh for CLI tools │
5385
+ └──────────────────────────────────────────────────────────────┘
5386
+ ```
5387
+
5388
+ #### Advanced Error Recovery: Circuit Breaker Pattern
5389
+
5390
+ For production startup research pipelines, implement a circuit breaker to handle API failures gracefully:
5391
+
5392
+ ```typescript
5393
+ /**
5394
+ * Circuit Breaker for Domain Research Pipeline
5395
+ *
5396
+ * States:
5397
+ * - CLOSED: Normal operation, requests flow through
5398
+ * - OPEN: Too many failures, requests fail fast
5399
+ * - HALF_OPEN: Testing if service recovered
5400
+ */
5401
+ class CircuitBreaker {
5402
+ private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
5403
+ private failureCount = 0;
5404
+ private successCount = 0;
5405
+ private lastFailureTime = 0;
5406
+
5407
+ constructor(
5408
+ private readonly options: {
5409
+ failureThreshold: number; // Failures before opening circuit
5410
+ successThreshold: number; // Successes to close circuit
5411
+ timeout: number; // Time before trying half-open (ms)
5412
+ fallback?: () => Promise<any>; // Fallback function when open
5413
+ }
5414
+ ) {}
5415
+
5416
+ async execute<T>(operation: () => Promise<T>, operationName: string): Promise<T> {
5417
+ // Check if circuit should transition from OPEN to HALF_OPEN
5418
+ if (this.state === 'OPEN') {
5419
+ if (Date.now() - this.lastFailureTime >= this.options.timeout) {
5420
+ this.state = 'HALF_OPEN';
5421
+ console.log(`⚡ Circuit ${operationName}: OPEN → HALF_OPEN (testing recovery)`);
5422
+ } else {
5423
+ // Circuit is open, fail fast or use fallback
5424
+ if (this.options.fallback) {
5425
+ console.log(`⚡ Circuit ${operationName}: OPEN (using fallback)`);
5426
+ return this.options.fallback();
5427
+ }
5428
+ throw new Error(`Circuit breaker is OPEN for ${operationName}`);
5429
+ }
5430
+ }
5431
+
5432
+ try {
5433
+ const result = await operation();
5434
+ this.onSuccess(operationName);
5435
+ return result;
5436
+ } catch (error) {
5437
+ this.onFailure(operationName);
5438
+ throw error;
5439
+ }
5440
+ }
5441
+
5442
+ private onSuccess(operationName: string) {
5443
+ if (this.state === 'HALF_OPEN') {
5444
+ this.successCount++;
5445
+ if (this.successCount >= this.options.successThreshold) {
5446
+ this.state = 'CLOSED';
5447
+ this.failureCount = 0;
5448
+ this.successCount = 0;
5449
+ console.log(`✅ Circuit ${operationName}: HALF_OPEN → CLOSED (recovered)`);
5450
+ }
5451
+ } else {
5452
+ this.failureCount = 0; // Reset on success in CLOSED state
5453
+ }
5454
+ }
5455
+
5456
+ private onFailure(operationName: string) {
5457
+ this.failureCount++;
5458
+ this.lastFailureTime = Date.now();
5459
+
5460
+ if (this.state === 'HALF_OPEN') {
5461
+ this.state = 'OPEN';
5462
+ this.successCount = 0;
5463
+ console.log(`❌ Circuit ${operationName}: HALF_OPEN → OPEN (still failing)`);
5464
+ } else if (this.failureCount >= this.options.failureThreshold) {
5465
+ this.state = 'OPEN';
5466
+ console.log(`❌ Circuit ${operationName}: CLOSED → OPEN (threshold reached)`);
5467
+ }
5468
+ }
5469
+
5470
+ getState() {
5471
+ return { state: this.state, failures: this.failureCount, successes: this.successCount };
5472
+ }
5473
+ }
5474
+
5475
+ // Create circuit breakers for each tool in the pipeline
5476
+ const circuitBreakers = {
5477
+ search_domain: new CircuitBreaker({
5478
+ failureThreshold: 3,
5479
+ successThreshold: 2,
5480
+ timeout: 30000,
5481
+ fallback: async () => ({ results: [], fromFallback: true })
5482
+ }),
5483
+ tld_info: new CircuitBreaker({
5484
+ failureThreshold: 5,
5485
+ successThreshold: 1,
5486
+ timeout: 60000,
5487
+ fallback: async () => ({ tld: "unknown", description: "Info unavailable" })
5488
+ }),
5489
+ suggest_domains: new CircuitBreaker({
5490
+ failureThreshold: 3,
5491
+ successThreshold: 2,
5492
+ timeout: 30000
5493
+ }),
5494
+ check_socials: new CircuitBreaker({
5495
+ failureThreshold: 5,
5496
+ successThreshold: 1,
5497
+ timeout: 60000,
5498
+ fallback: async () => ({ platforms: [], error: "Social check unavailable" })
5499
+ })
5500
+ };
5501
+
5502
+ // Usage in startup research pipeline
5503
+ async function resilientStartupResearch(startupName: string, startupType: StartupType) {
5504
+ const config = STARTUP_TYPE_CONFIGS[startupType];
5505
+ const results = {
5506
+ tldAnalysis: [] as any[],
5507
+ domains: [] as any[],
5508
+ socials: null as any,
5509
+ errors: [] as string[]
5510
+ };
5511
+
5512
+ // Step 1: TLD Analysis with circuit breaker
5513
+ for (const tld of config.tlds) {
5514
+ try {
5515
+ const info = await circuitBreakers.tld_info.execute(
5516
+ () => tldInfo({ tld, detailed: true }),
5517
+ `tld_info(${tld})`
5518
+ );
5519
+ results.tldAnalysis.push(info);
5520
+ } catch (error) {
5521
+ results.errors.push(`tld_info(${tld}): ${error.message}`);
5522
+ }
5523
+ }
5524
+
5525
+ // Step 2: Domain search with circuit breaker
5526
+ try {
5527
+ const searchResult = await circuitBreakers.search_domain.execute(
5528
+ () => searchDomain({ domain_name: startupName, tlds: config.tlds }),
5529
+ 'search_domain'
5530
+ );
5531
+ results.domains = searchResult.results;
5532
+ } catch (error) {
5533
+ results.errors.push(`search_domain: ${error.message}`);
5534
+ }
5535
+
5536
+ // Step 3: Social check with circuit breaker
5537
+ try {
5538
+ results.socials = await circuitBreakers.check_socials.execute(
5539
+ () => checkSocials({ name: startupName, platforms: config.socialPlatforms }),
5540
+ 'check_socials'
5541
+ );
5542
+ } catch (error) {
5543
+ results.errors.push(`check_socials: ${error.message}`);
5544
+ }
5545
+
5546
+ // Log circuit breaker states for monitoring
5547
+ console.log('Circuit Breaker States:', {
5548
+ search_domain: circuitBreakers.search_domain.getState(),
5549
+ tld_info: circuitBreakers.tld_info.getState(),
5550
+ check_socials: circuitBreakers.check_socials.getState()
5551
+ });
5552
+
5553
+ return results;
5554
+ }
5555
+
5556
+ // Example usage with automatic recovery
5557
+ const result = await resilientStartupResearch("nexaflow", "tech");
5558
+ // If APIs fail repeatedly, circuit opens and uses fallbacks
5559
+ // After timeout, circuit tests recovery with half-open state
5560
+ ```
5561
+
4427
5562
  ## Security
4428
5563
 
4429
5564
  - API keys are never logged (automatic secret masking)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domain-search-mcp",
3
- "version": "1.2.7",
3
+ "version": "1.2.9",
4
4
  "description": "Fast domain availability aggregator MCP server. Check availability across Porkbun, Namecheap, RDAP, and WHOIS. Compare pricing. Get suggestions.",
5
5
  "main": "dist/server.js",
6
6
  "types": "dist/server.d.ts",