domain-search-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/.env.example +52 -0
  2. package/Dockerfile +15 -0
  3. package/LICENSE +21 -0
  4. package/README.md +426 -0
  5. package/SECURITY.md +252 -0
  6. package/dist/config.d.ts +25 -0
  7. package/dist/config.d.ts.map +1 -0
  8. package/dist/config.js +117 -0
  9. package/dist/config.js.map +1 -0
  10. package/dist/fallbacks/index.d.ts +6 -0
  11. package/dist/fallbacks/index.d.ts.map +1 -0
  12. package/dist/fallbacks/index.js +14 -0
  13. package/dist/fallbacks/index.js.map +1 -0
  14. package/dist/fallbacks/rdap.d.ts +18 -0
  15. package/dist/fallbacks/rdap.d.ts.map +1 -0
  16. package/dist/fallbacks/rdap.js +339 -0
  17. package/dist/fallbacks/rdap.js.map +1 -0
  18. package/dist/fallbacks/whois.d.ts +27 -0
  19. package/dist/fallbacks/whois.d.ts.map +1 -0
  20. package/dist/fallbacks/whois.js +219 -0
  21. package/dist/fallbacks/whois.js.map +1 -0
  22. package/dist/registrars/base.d.ts +89 -0
  23. package/dist/registrars/base.d.ts.map +1 -0
  24. package/dist/registrars/base.js +203 -0
  25. package/dist/registrars/base.js.map +1 -0
  26. package/dist/registrars/index.d.ts +7 -0
  27. package/dist/registrars/index.d.ts.map +1 -0
  28. package/dist/registrars/index.js +15 -0
  29. package/dist/registrars/index.js.map +1 -0
  30. package/dist/registrars/namecheap.d.ts +69 -0
  31. package/dist/registrars/namecheap.d.ts.map +1 -0
  32. package/dist/registrars/namecheap.js +307 -0
  33. package/dist/registrars/namecheap.js.map +1 -0
  34. package/dist/registrars/porkbun.d.ts +63 -0
  35. package/dist/registrars/porkbun.d.ts.map +1 -0
  36. package/dist/registrars/porkbun.js +299 -0
  37. package/dist/registrars/porkbun.js.map +1 -0
  38. package/dist/server.d.ts +19 -0
  39. package/dist/server.d.ts.map +1 -0
  40. package/dist/server.js +209 -0
  41. package/dist/server.js.map +1 -0
  42. package/dist/services/domain-search.d.ts +40 -0
  43. package/dist/services/domain-search.d.ts.map +1 -0
  44. package/dist/services/domain-search.js +438 -0
  45. package/dist/services/domain-search.js.map +1 -0
  46. package/dist/services/index.d.ts +5 -0
  47. package/dist/services/index.d.ts.map +1 -0
  48. package/dist/services/index.js +11 -0
  49. package/dist/services/index.js.map +1 -0
  50. package/dist/tools/bulk_search.d.ts +72 -0
  51. package/dist/tools/bulk_search.d.ts.map +1 -0
  52. package/dist/tools/bulk_search.js +108 -0
  53. package/dist/tools/bulk_search.js.map +1 -0
  54. package/dist/tools/check_socials.d.ts +71 -0
  55. package/dist/tools/check_socials.d.ts.map +1 -0
  56. package/dist/tools/check_socials.js +357 -0
  57. package/dist/tools/check_socials.js.map +1 -0
  58. package/dist/tools/compare_registrars.d.ts +80 -0
  59. package/dist/tools/compare_registrars.d.ts.map +1 -0
  60. package/dist/tools/compare_registrars.js +116 -0
  61. package/dist/tools/compare_registrars.js.map +1 -0
  62. package/dist/tools/index.d.ts +10 -0
  63. package/dist/tools/index.d.ts.map +1 -0
  64. package/dist/tools/index.js +31 -0
  65. package/dist/tools/index.js.map +1 -0
  66. package/dist/tools/search_domain.d.ts +61 -0
  67. package/dist/tools/search_domain.d.ts.map +1 -0
  68. package/dist/tools/search_domain.js +81 -0
  69. package/dist/tools/search_domain.js.map +1 -0
  70. package/dist/tools/suggest_domains.d.ts +82 -0
  71. package/dist/tools/suggest_domains.d.ts.map +1 -0
  72. package/dist/tools/suggest_domains.js +227 -0
  73. package/dist/tools/suggest_domains.js.map +1 -0
  74. package/dist/tools/tld_info.d.ts +56 -0
  75. package/dist/tools/tld_info.d.ts.map +1 -0
  76. package/dist/tools/tld_info.js +273 -0
  77. package/dist/tools/tld_info.js.map +1 -0
  78. package/dist/types.d.ts +193 -0
  79. package/dist/types.d.ts.map +1 -0
  80. package/dist/types.js +9 -0
  81. package/dist/types.js.map +1 -0
  82. package/dist/utils/cache.d.ts +81 -0
  83. package/dist/utils/cache.d.ts.map +1 -0
  84. package/dist/utils/cache.js +192 -0
  85. package/dist/utils/cache.js.map +1 -0
  86. package/dist/utils/errors.d.ts +87 -0
  87. package/dist/utils/errors.d.ts.map +1 -0
  88. package/dist/utils/errors.js +191 -0
  89. package/dist/utils/errors.js.map +1 -0
  90. package/dist/utils/index.d.ts +8 -0
  91. package/dist/utils/index.d.ts.map +1 -0
  92. package/dist/utils/index.js +24 -0
  93. package/dist/utils/index.js.map +1 -0
  94. package/dist/utils/logger.d.ts +27 -0
  95. package/dist/utils/logger.d.ts.map +1 -0
  96. package/dist/utils/logger.js +132 -0
  97. package/dist/utils/logger.js.map +1 -0
  98. package/dist/utils/premium-analyzer.d.ts +33 -0
  99. package/dist/utils/premium-analyzer.d.ts.map +1 -0
  100. package/dist/utils/premium-analyzer.js +273 -0
  101. package/dist/utils/premium-analyzer.js.map +1 -0
  102. package/dist/utils/validators.d.ts +53 -0
  103. package/dist/utils/validators.d.ts.map +1 -0
  104. package/dist/utils/validators.js +159 -0
  105. package/dist/utils/validators.js.map +1 -0
  106. package/docs/marketing/devto-post.md +135 -0
  107. package/docs/marketing/hackernews.md +42 -0
  108. package/docs/marketing/producthunt.md +109 -0
  109. package/docs/marketing/reddit-post.md +59 -0
  110. package/docs/marketing/twitter-thread.md +105 -0
  111. package/examples/bulk-search-50-domains.ts +131 -0
  112. package/examples/cli-interactive.ts +280 -0
  113. package/examples/compare-registrars.ts +78 -0
  114. package/examples/search-single-domain.ts +54 -0
  115. package/examples/suggest-names.ts +110 -0
  116. package/glama.json +6 -0
  117. package/jest.config.js +35 -0
  118. package/package.json +62 -0
  119. package/smithery.yaml +36 -0
  120. package/src/config.ts +121 -0
  121. package/src/fallbacks/index.ts +6 -0
  122. package/src/fallbacks/rdap.ts +407 -0
  123. package/src/fallbacks/whois.ts +250 -0
  124. package/src/registrars/base.ts +264 -0
  125. package/src/registrars/index.ts +7 -0
  126. package/src/registrars/namecheap.ts +378 -0
  127. package/src/registrars/porkbun.ts +380 -0
  128. package/src/server.ts +276 -0
  129. package/src/services/domain-search.ts +567 -0
  130. package/src/services/index.ts +9 -0
  131. package/src/tools/bulk_search.ts +142 -0
  132. package/src/tools/check_socials.ts +467 -0
  133. package/src/tools/compare_registrars.ts +162 -0
  134. package/src/tools/index.ts +45 -0
  135. package/src/tools/search_domain.ts +93 -0
  136. package/src/tools/suggest_domains.ts +284 -0
  137. package/src/tools/tld_info.ts +294 -0
  138. package/src/types.ts +289 -0
  139. package/src/utils/cache.ts +238 -0
  140. package/src/utils/errors.ts +262 -0
  141. package/src/utils/index.ts +8 -0
  142. package/src/utils/logger.ts +162 -0
  143. package/src/utils/premium-analyzer.ts +303 -0
  144. package/src/utils/validators.ts +193 -0
  145. package/tests/premium-analyzer.test.ts +310 -0
  146. package/tests/unit/cache.test.ts +123 -0
  147. package/tests/unit/errors.test.ts +190 -0
  148. package/tests/unit/tld-info.test.ts +62 -0
  149. package/tests/unit/tools.test.ts +200 -0
  150. package/tests/unit/validators.test.ts +146 -0
  151. package/tsconfig.json +25 -0
@@ -0,0 +1,93 @@
1
+ /**
2
+ * search_domain Tool - Primary Domain Search.
3
+ *
4
+ * This is the main entry point for domain availability checks.
5
+ * Designed for magic first moment: works with zero configuration.
6
+ */
7
+
8
+ import { z } from 'zod';
9
+ import type { SearchResponse } from '../types.js';
10
+ import { searchDomain } from '../services/domain-search.js';
11
+ import { wrapError } from '../utils/errors.js';
12
+
13
+ /**
14
+ * Input schema for search_domain.
15
+ */
16
+ export const searchDomainSchema = z.object({
17
+ domain_name: z
18
+ .string()
19
+ .min(1)
20
+ .max(63)
21
+ .describe(
22
+ "The domain name you want to check (e.g., 'vibecoding', 'myapp'). Don't include the extension (.com, .io, etc.)",
23
+ ),
24
+ tlds: z
25
+ .array(z.string())
26
+ .optional()
27
+ .describe(
28
+ "TLD extensions to check (e.g., ['com', 'io', 'dev']). Defaults to ['com', 'io', 'dev'] if not specified.",
29
+ ),
30
+ registrars: z
31
+ .array(z.string())
32
+ .optional()
33
+ .describe(
34
+ "Optional: specific registrars to check (e.g., ['porkbun', 'namecheap']). Leave empty to auto-select.",
35
+ ),
36
+ });
37
+
38
+ export type SearchDomainInput = z.infer<typeof searchDomainSchema>;
39
+
40
+ /**
41
+ * Tool definition for MCP.
42
+ */
43
+ export const searchDomainTool = {
44
+ name: 'search_domain',
45
+ description: `Search for domain availability and pricing across multiple TLDs.
46
+
47
+ Returns:
48
+ - Availability status for each domain
49
+ - Pricing (first year and renewal)
50
+ - Whether WHOIS privacy is included
51
+ - Human-readable insights and next steps
52
+
53
+ Examples:
54
+ - search_domain("vibecoding") → checks vibecoding.com, .io, .dev
55
+ - search_domain("myapp", ["com", "io"]) → checks specific TLDs`,
56
+ inputSchema: {
57
+ type: 'object',
58
+ properties: {
59
+ domain_name: {
60
+ type: 'string',
61
+ description:
62
+ "The domain name to search (e.g., 'vibecoding'). No extension needed.",
63
+ },
64
+ tlds: {
65
+ type: 'array',
66
+ items: { type: 'string' },
67
+ description:
68
+ "TLD extensions to check (e.g., ['com', 'io', 'dev']). Defaults to ['com', 'io', 'dev'].",
69
+ },
70
+ registrars: {
71
+ type: 'array',
72
+ items: { type: 'string' },
73
+ description:
74
+ "Optional: specific registrars to check. Leave empty to auto-select.",
75
+ },
76
+ },
77
+ required: ['domain_name'],
78
+ },
79
+ };
80
+
81
+ /**
82
+ * Execute the search_domain tool.
83
+ */
84
+ export async function executeSearchDomain(
85
+ input: SearchDomainInput,
86
+ ): Promise<SearchResponse> {
87
+ try {
88
+ const { domain_name, tlds, registrars } = searchDomainSchema.parse(input);
89
+ return await searchDomain(domain_name, tlds, registrars);
90
+ } catch (error) {
91
+ throw wrapError(error);
92
+ }
93
+ }
@@ -0,0 +1,284 @@
1
+ /**
2
+ * suggest_domains Tool - Domain Name Suggestions.
3
+ *
4
+ * Generate and check variations of a base domain name.
5
+ * Helps find available alternatives when the primary name is taken.
6
+ */
7
+
8
+ import { z } from 'zod';
9
+ import { searchDomain } from '../services/domain-search.js';
10
+ import { validateDomainName } from '../utils/validators.js';
11
+ import { wrapError } from '../utils/errors.js';
12
+ import type { DomainResult } from '../types.js';
13
+
14
+ /**
15
+ * Input schema for suggest_domains.
16
+ */
17
+ export const suggestDomainsSchema = z.object({
18
+ base_name: z
19
+ .string()
20
+ .min(1)
21
+ .describe("The base domain name to generate variations from (e.g., 'vibecoding')."),
22
+ tld: z
23
+ .string()
24
+ .optional()
25
+ .default('com')
26
+ .describe("TLD to check suggestions against (e.g., 'com'). Defaults to 'com'."),
27
+ variants: z
28
+ .array(z.enum(['hyphen', 'numbers', 'abbreviations', 'synonyms', 'prefixes', 'suffixes']))
29
+ .optional()
30
+ .describe(
31
+ "Types of variations to generate. Defaults to all types.",
32
+ ),
33
+ max_suggestions: z
34
+ .number()
35
+ .int()
36
+ .min(1)
37
+ .max(50)
38
+ .optional()
39
+ .default(10)
40
+ .describe("Maximum number of suggestions to return (1-50). Defaults to 10."),
41
+ });
42
+
43
+ export type SuggestDomainsInput = z.infer<typeof suggestDomainsSchema>;
44
+
45
+ /**
46
+ * Tool definition for MCP.
47
+ */
48
+ export const suggestDomainsTool = {
49
+ name: 'suggest_domains',
50
+ description: `Generate and check availability of domain name variations.
51
+
52
+ Creates variations like:
53
+ - Hyphenated: vibe-coding
54
+ - With numbers: vibecoding1, vibecoding2
55
+ - Prefixes: getvibecoding, tryvibecoding
56
+ - Suffixes: vibecodingapp, vibecodinghq
57
+
58
+ Returns only available suggestions, ranked by quality.
59
+
60
+ Example:
61
+ - suggest_domains("vibecoding") → finds available variations`,
62
+ inputSchema: {
63
+ type: 'object',
64
+ properties: {
65
+ base_name: {
66
+ type: 'string',
67
+ description: "The base domain name to generate variations from.",
68
+ },
69
+ tld: {
70
+ type: 'string',
71
+ description: "TLD to check (e.g., 'com'). Defaults to 'com'.",
72
+ },
73
+ variants: {
74
+ type: 'array',
75
+ items: {
76
+ type: 'string',
77
+ enum: ['hyphen', 'numbers', 'abbreviations', 'synonyms', 'prefixes', 'suffixes'],
78
+ },
79
+ description: "Types of variations to generate. Defaults to all.",
80
+ },
81
+ max_suggestions: {
82
+ type: 'number',
83
+ description: "Maximum suggestions to return (1-50). Defaults to 10.",
84
+ },
85
+ },
86
+ required: ['base_name'],
87
+ },
88
+ };
89
+
90
+ /**
91
+ * Generate domain name variations.
92
+ */
93
+ function generateVariations(
94
+ baseName: string,
95
+ variantTypes: string[],
96
+ ): string[] {
97
+ const variations = new Set<string>();
98
+
99
+ // Always include the original
100
+ variations.add(baseName);
101
+
102
+ if (variantTypes.includes('hyphen') || variantTypes.length === 0) {
103
+ // Add hyphens at word boundaries (heuristic: before capital letters or common words)
104
+ const hyphenated = baseName.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
105
+ if (hyphenated !== baseName) {
106
+ variations.add(hyphenated);
107
+ }
108
+
109
+ // Try hyphen in the middle
110
+ if (baseName.length >= 6) {
111
+ const mid = Math.floor(baseName.length / 2);
112
+ variations.add(baseName.slice(0, mid) + '-' + baseName.slice(mid));
113
+ }
114
+ }
115
+
116
+ if (variantTypes.includes('numbers') || variantTypes.length === 0) {
117
+ // Add numbers
118
+ for (let i = 1; i <= 3; i++) {
119
+ variations.add(`${baseName}${i}`);
120
+ }
121
+ variations.add(`${baseName}io`);
122
+ variations.add(`${baseName}app`);
123
+ }
124
+
125
+ if (variantTypes.includes('prefixes') || variantTypes.length === 0) {
126
+ const prefixes = ['get', 'try', 'use', 'my', 'the', 'go', 'hey', 'hi'];
127
+ for (const prefix of prefixes) {
128
+ variations.add(`${prefix}${baseName}`);
129
+ }
130
+ }
131
+
132
+ if (variantTypes.includes('suffixes') || variantTypes.length === 0) {
133
+ const suffixes = ['app', 'hq', 'io', 'labs', 'dev', 'ai', 'hub', 'now'];
134
+ for (const suffix of suffixes) {
135
+ variations.add(`${baseName}${suffix}`);
136
+ }
137
+ }
138
+
139
+ if (variantTypes.includes('abbreviations') || variantTypes.length === 0) {
140
+ // Remove vowels (except first letter)
141
+ const abbreviated = baseName[0] + baseName.slice(1).replace(/[aeiou]/gi, '');
142
+ if (abbreviated.length >= 3 && abbreviated !== baseName) {
143
+ variations.add(abbreviated);
144
+ }
145
+ }
146
+
147
+ return Array.from(variations);
148
+ }
149
+
150
+ /**
151
+ * Score a domain suggestion.
152
+ * Higher score = better suggestion.
153
+ */
154
+ function scoreSuggestion(
155
+ original: string,
156
+ suggestion: string,
157
+ result: DomainResult,
158
+ ): number {
159
+ let score = 50; // Base score
160
+
161
+ // Prefer shorter names
162
+ score -= suggestion.length;
163
+
164
+ // Prefer exact match
165
+ if (suggestion === original) score += 20;
166
+
167
+ // Prefer no hyphens
168
+ if (!suggestion.includes('-')) score += 5;
169
+
170
+ // Prefer no numbers
171
+ if (!/\d/.test(suggestion)) score += 5;
172
+
173
+ // Prefer cheaper prices
174
+ if (result.price_first_year !== null) {
175
+ if (result.price_first_year <= 10) score += 10;
176
+ else if (result.price_first_year <= 15) score += 5;
177
+ }
178
+
179
+ // Penalize premiums
180
+ if (result.premium) score -= 20;
181
+
182
+ // Bonus for privacy included
183
+ if (result.privacy_included) score += 5;
184
+
185
+ return score;
186
+ }
187
+
188
+ /**
189
+ * Response format for suggestions.
190
+ */
191
+ interface SuggestDomainsResponse {
192
+ base_name: string;
193
+ tld: string;
194
+ total_checked: number;
195
+ available_count: number;
196
+ suggestions: Array<{
197
+ domain: string;
198
+ price_first_year: number | null;
199
+ registrar: string;
200
+ score: number;
201
+ }>;
202
+ insights: string[];
203
+ }
204
+
205
+ /**
206
+ * Execute the suggest_domains tool.
207
+ */
208
+ export async function executeSuggestDomains(
209
+ input: SuggestDomainsInput,
210
+ ): Promise<SuggestDomainsResponse> {
211
+ try {
212
+ const { base_name, tld, variants, max_suggestions } =
213
+ suggestDomainsSchema.parse(input);
214
+
215
+ const normalizedBase = validateDomainName(base_name);
216
+ const variantTypes = variants || [];
217
+
218
+ // Generate variations
219
+ const variations = generateVariations(normalizedBase, variantTypes);
220
+
221
+ // Limit to max + some buffer for unavailable ones
222
+ const toCheck = variations.slice(0, max_suggestions * 2);
223
+
224
+ // Check availability for all variations
225
+ const results: Array<{ name: string; result: DomainResult | null }> = [];
226
+
227
+ for (const name of toCheck) {
228
+ try {
229
+ const response = await searchDomain(name, [tld]);
230
+ const result = response.results.find((r) => r.domain === `${name}.${tld}`);
231
+ results.push({ name, result: result || null });
232
+ } catch {
233
+ results.push({ name, result: null });
234
+ }
235
+ }
236
+
237
+ // Filter to available and score them
238
+ const available = results
239
+ .filter((r) => r.result?.available)
240
+ .map((r) => ({
241
+ name: r.name,
242
+ result: r.result!,
243
+ score: scoreSuggestion(normalizedBase, r.name, r.result!),
244
+ }))
245
+ .sort((a, b) => b.score - a.score)
246
+ .slice(0, max_suggestions);
247
+
248
+ const insights: string[] = [];
249
+
250
+ if (available.length > 0) {
251
+ insights.push(
252
+ `✅ Found ${available.length} available variation${available.length > 1 ? 's' : ''}`,
253
+ );
254
+
255
+ const best = available[0]!;
256
+ insights.push(
257
+ `⭐ Top suggestion: ${best.name}.${tld} ($${best.result.price_first_year ?? 'unknown'}/year)`,
258
+ );
259
+ } else {
260
+ insights.push(
261
+ `❌ No available variations found for ${normalizedBase}.${tld}`,
262
+ );
263
+ insights.push(
264
+ '💡 Try a different TLD or modify the base name more significantly',
265
+ );
266
+ }
267
+
268
+ return {
269
+ base_name: normalizedBase,
270
+ tld,
271
+ total_checked: toCheck.length,
272
+ available_count: available.length,
273
+ suggestions: available.map((a) => ({
274
+ domain: `${a.name}.${tld}`,
275
+ price_first_year: a.result.price_first_year,
276
+ registrar: a.result.registrar,
277
+ score: a.score,
278
+ })),
279
+ insights,
280
+ };
281
+ } catch (error) {
282
+ throw wrapError(error);
283
+ }
284
+ }
@@ -0,0 +1,294 @@
1
+ /**
2
+ * tld_info Tool - TLD Information.
3
+ *
4
+ * Get information about a Top Level Domain (TLD).
5
+ * Includes pricing, restrictions, and recommendations.
6
+ */
7
+
8
+ import { z } from 'zod';
9
+ import type { TLDInfo } from '../types.js';
10
+ import { validateTld } from '../utils/validators.js';
11
+ import { wrapError } from '../utils/errors.js';
12
+ import { tldCache, tldCacheKey } from '../utils/cache.js';
13
+
14
+ /**
15
+ * Input schema for tld_info.
16
+ */
17
+ export const tldInfoSchema = z.object({
18
+ tld: z
19
+ .string()
20
+ .min(2)
21
+ .max(63)
22
+ .describe("The TLD to get information about (e.g., 'com', 'io', 'dev')."),
23
+ detailed: z
24
+ .boolean()
25
+ .optional()
26
+ .default(false)
27
+ .describe("Whether to include detailed information. Defaults to false."),
28
+ });
29
+
30
+ export type TldInfoInput = z.infer<typeof tldInfoSchema>;
31
+
32
+ /**
33
+ * Tool definition for MCP.
34
+ */
35
+ export const tldInfoTool = {
36
+ name: 'tld_info',
37
+ description: `Get information about a Top Level Domain (TLD).
38
+
39
+ Returns:
40
+ - Description and typical use case
41
+ - Price range
42
+ - Any special restrictions
43
+ - Popularity and recommendations
44
+
45
+ Example:
46
+ - tld_info("io") → info about .io domains`,
47
+ inputSchema: {
48
+ type: 'object',
49
+ properties: {
50
+ tld: {
51
+ type: 'string',
52
+ description: "The TLD to get info about (e.g., 'com', 'io', 'dev').",
53
+ },
54
+ detailed: {
55
+ type: 'boolean',
56
+ description: "Include detailed information. Defaults to false.",
57
+ },
58
+ },
59
+ required: ['tld'],
60
+ },
61
+ };
62
+
63
+ /**
64
+ * Static TLD database.
65
+ */
66
+ const TLD_DATABASE: Record<string, TLDInfo> = {
67
+ com: {
68
+ tld: 'com',
69
+ description: 'Commercial - the most recognized TLD worldwide',
70
+ typical_use: 'Businesses, commercial websites, general purpose',
71
+ price_range: { min: 8.88, max: 15.99, currency: 'USD' },
72
+ renewal_price_typical: 12.99,
73
+ restrictions: [],
74
+ popularity: 'high',
75
+ category: 'generic',
76
+ },
77
+ io: {
78
+ tld: 'io',
79
+ description: 'British Indian Ocean Territory - popular with tech startups',
80
+ typical_use: 'Tech startups, SaaS products, developer tools',
81
+ price_range: { min: 29.88, max: 59.99, currency: 'USD' },
82
+ renewal_price_typical: 44.99,
83
+ restrictions: [],
84
+ popularity: 'high',
85
+ category: 'country',
86
+ },
87
+ dev: {
88
+ tld: 'dev',
89
+ description: 'Developer - for software developers and their projects',
90
+ typical_use: 'Developer portfolios, tools, documentation sites',
91
+ price_range: { min: 10.18, max: 19.99, currency: 'USD' },
92
+ renewal_price_typical: 14.99,
93
+ restrictions: ['Requires HTTPS (HSTS preloaded)'],
94
+ popularity: 'medium',
95
+ category: 'new',
96
+ },
97
+ app: {
98
+ tld: 'app',
99
+ description: 'Application - for mobile and web applications',
100
+ typical_use: 'Mobile apps, web applications, software products',
101
+ price_range: { min: 11.18, max: 19.99, currency: 'USD' },
102
+ renewal_price_typical: 14.99,
103
+ restrictions: ['Requires HTTPS (HSTS preloaded)'],
104
+ popularity: 'medium',
105
+ category: 'new',
106
+ },
107
+ co: {
108
+ tld: 'co',
109
+ description: 'Colombia / Company - popular alternative to .com',
110
+ typical_use: 'Companies, startups, short URLs',
111
+ price_range: { min: 9.48, max: 29.99, currency: 'USD' },
112
+ renewal_price_typical: 24.99,
113
+ restrictions: [],
114
+ popularity: 'high',
115
+ category: 'country',
116
+ },
117
+ net: {
118
+ tld: 'net',
119
+ description: 'Network - originally for network providers',
120
+ typical_use: 'Technology companies, network services, ISPs',
121
+ price_range: { min: 9.88, max: 14.99, currency: 'USD' },
122
+ renewal_price_typical: 12.99,
123
+ restrictions: [],
124
+ popularity: 'high',
125
+ category: 'generic',
126
+ },
127
+ org: {
128
+ tld: 'org',
129
+ description: 'Organization - for non-profits and communities',
130
+ typical_use: 'Non-profit organizations, open source projects, communities',
131
+ price_range: { min: 9.88, max: 14.99, currency: 'USD' },
132
+ renewal_price_typical: 12.99,
133
+ restrictions: [],
134
+ popularity: 'high',
135
+ category: 'generic',
136
+ },
137
+ ai: {
138
+ tld: 'ai',
139
+ description: 'Anguilla / Artificial Intelligence - trending for AI projects',
140
+ typical_use: 'AI/ML projects, tech startups, research',
141
+ price_range: { min: 49.88, max: 99.99, currency: 'USD' },
142
+ renewal_price_typical: 79.99,
143
+ restrictions: [],
144
+ popularity: 'medium',
145
+ category: 'country',
146
+ },
147
+ xyz: {
148
+ tld: 'xyz',
149
+ description: 'XYZ - for the next generation of internet users',
150
+ typical_use: 'Creative projects, personal sites, unconventional brands',
151
+ price_range: { min: 1.99, max: 12.99, currency: 'USD' },
152
+ renewal_price_typical: 12.99,
153
+ restrictions: [],
154
+ popularity: 'medium',
155
+ category: 'new',
156
+ },
157
+ me: {
158
+ tld: 'me',
159
+ description: 'Montenegro - popular for personal brands',
160
+ typical_use: 'Personal websites, portfolios, URL shorteners',
161
+ price_range: { min: 9.88, max: 19.99, currency: 'USD' },
162
+ renewal_price_typical: 17.99,
163
+ restrictions: [],
164
+ popularity: 'medium',
165
+ category: 'country',
166
+ },
167
+ sh: {
168
+ tld: 'sh',
169
+ description: 'Saint Helena - popular with developers (shell scripts)',
170
+ typical_use: 'Developer tools, CLI applications, tech projects',
171
+ price_range: { min: 29.88, max: 49.99, currency: 'USD' },
172
+ renewal_price_typical: 44.99,
173
+ restrictions: [],
174
+ popularity: 'low',
175
+ category: 'country',
176
+ },
177
+ cc: {
178
+ tld: 'cc',
179
+ description: 'Cocos Islands - often used as "Creative Commons"',
180
+ typical_use: 'Creative projects, alternative to .com',
181
+ price_range: { min: 9.88, max: 24.99, currency: 'USD' },
182
+ renewal_price_typical: 19.99,
183
+ restrictions: [],
184
+ popularity: 'low',
185
+ category: 'country',
186
+ },
187
+ };
188
+
189
+ /**
190
+ * Response format for TLD info.
191
+ */
192
+ interface TldInfoResponse extends TLDInfo {
193
+ insights: string[];
194
+ recommendation: string;
195
+ }
196
+
197
+ /**
198
+ * Execute the tld_info tool.
199
+ */
200
+ export async function executeTldInfo(
201
+ input: TldInfoInput,
202
+ ): Promise<TldInfoResponse> {
203
+ try {
204
+ const { tld, detailed } = tldInfoSchema.parse(input);
205
+ const normalizedTld = validateTld(tld);
206
+
207
+ // Check cache
208
+ const cacheKey = tldCacheKey(normalizedTld);
209
+ const cached = tldCache.get(cacheKey);
210
+ if (cached) {
211
+ return formatResponse(cached, detailed);
212
+ }
213
+
214
+ // Look up in database
215
+ const info = TLD_DATABASE[normalizedTld];
216
+
217
+ if (!info) {
218
+ // Return generic info for unknown TLDs
219
+ const genericInfo: TLDInfo = {
220
+ tld: normalizedTld,
221
+ description: `${normalizedTld.toUpperCase()} domain extension`,
222
+ typical_use: 'General purpose',
223
+ price_range: { min: 10, max: 50, currency: 'USD' },
224
+ renewal_price_typical: 20,
225
+ restrictions: ['Check registrar for specific restrictions'],
226
+ popularity: 'low',
227
+ category: 'generic',
228
+ };
229
+
230
+ return formatResponse(genericInfo, detailed);
231
+ }
232
+
233
+ // Cache the result
234
+ tldCache.set(cacheKey, info);
235
+
236
+ return formatResponse(info, detailed);
237
+ } catch (error) {
238
+ throw wrapError(error);
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Format the response with insights.
244
+ */
245
+ function formatResponse(info: TLDInfo, detailed: boolean): TldInfoResponse {
246
+ const insights: string[] = [];
247
+ let recommendation = '';
248
+
249
+ // Generate insights
250
+ if (info.popularity === 'high') {
251
+ insights.push(`✅ .${info.tld} is highly recognized and trusted`);
252
+ } else if (info.popularity === 'medium') {
253
+ insights.push(`💡 .${info.tld} is gaining popularity in specific niches`);
254
+ } else {
255
+ insights.push(`⚠️ .${info.tld} is less common - may need more brand building`);
256
+ }
257
+
258
+ if (info.restrictions.length > 0) {
259
+ insights.push(`⚠️ Special requirements: ${info.restrictions.join(', ')}`);
260
+ }
261
+
262
+ if (info.price_range.min <= 10) {
263
+ insights.push(`💰 Budget-friendly starting at $${info.price_range.min}/year`);
264
+ } else if (info.price_range.min >= 40) {
265
+ insights.push(`💸 Premium pricing starting at $${info.price_range.min}/year`);
266
+ }
267
+
268
+ // Generate recommendation
269
+ switch (info.tld) {
270
+ case 'com':
271
+ recommendation = 'Best for mainstream businesses and maximum recognition';
272
+ break;
273
+ case 'io':
274
+ recommendation = 'Perfect for tech startups and SaaS products';
275
+ break;
276
+ case 'dev':
277
+ recommendation = 'Ideal for developers and tech portfolios (requires HTTPS)';
278
+ break;
279
+ case 'app':
280
+ recommendation = 'Great for mobile/web applications (requires HTTPS)';
281
+ break;
282
+ case 'ai':
283
+ recommendation = 'Trending choice for AI/ML projects, but pricey';
284
+ break;
285
+ default:
286
+ recommendation = `Good choice for ${info.typical_use.toLowerCase()}`;
287
+ }
288
+
289
+ return {
290
+ ...info,
291
+ insights,
292
+ recommendation,
293
+ };
294
+ }