domain-search-mcp 1.2.8 → 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 +656 -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
+ }
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);
3598
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,109 @@ 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
+
4427
4909
  #### Startup-Type Optimized Parameters
4428
4910
 
4429
4911
  Different startup types require different tool parameter configurations for optimal results:
@@ -4903,6 +5385,180 @@ console.log(formatStartupReport(result));
4903
5385
  └──────────────────────────────────────────────────────────────┘
4904
5386
  ```
4905
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
+
4906
5562
  ## Security
4907
5563
 
4908
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.8",
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",