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