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.
- package/CLAUDE.md +91 -0
- package/README.md +656 -0
- 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.
|
|
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",
|