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,467 @@
1
+ /**
2
+ * check_socials Tool - Social Handle Availability.
3
+ *
4
+ * Check if a username is available across social platforms.
5
+ * Uses Sherlock-style detection for accurate results.
6
+ *
7
+ * Detection methods:
8
+ * - status_code: 404 = available, 200 = taken
9
+ * - message: Check response body for error indicators
10
+ * - api: Use platform's public API
11
+ */
12
+
13
+ import { z } from 'zod';
14
+ import axios from 'axios';
15
+ import type { SocialPlatform, SocialHandleResult } from '../types.js';
16
+ import { wrapError } from '../utils/errors.js';
17
+ import { logger } from '../utils/logger.js';
18
+
19
+ // ═══════════════════════════════════════════════════════════════════════════
20
+ // Platform Configuration (Sherlock-style)
21
+ // ═══════════════════════════════════════════════════════════════════════════
22
+
23
+ /**
24
+ * Detection method types (inspired by Sherlock).
25
+ */
26
+ type ErrorType = 'status_code' | 'message' | 'api';
27
+
28
+ /**
29
+ * Platform configuration for username checking.
30
+ */
31
+ interface PlatformConfig {
32
+ /** URL to check (use {} for username placeholder) */
33
+ url: string;
34
+ /** Public profile URL */
35
+ profileUrl: string;
36
+ /** Detection method */
37
+ errorType: ErrorType;
38
+ /** For message type: strings that indicate username is available */
39
+ errorMsg?: string[];
40
+ /** For status_code type: HTTP status that means available */
41
+ errorCode?: number;
42
+ /** Expected confidence level */
43
+ confidence: 'high' | 'medium' | 'low';
44
+ /** HTTP method */
45
+ method: 'GET' | 'HEAD';
46
+ /** Custom headers */
47
+ headers?: Record<string, string>;
48
+ /** Username regex validation */
49
+ regexCheck?: RegExp;
50
+ }
51
+
52
+ /**
53
+ * Platform configurations using Sherlock-style detection.
54
+ * HIGH confidence = public API or reliable status codes
55
+ * MEDIUM confidence = status codes but may have edge cases
56
+ * LOW confidence = platforms that block automated checks
57
+ */
58
+ const PLATFORM_CONFIGS: Record<SocialPlatform, PlatformConfig> = {
59
+ // ─────────────────────────────────────────────────────────────────────────
60
+ // HIGH CONFIDENCE (Public APIs)
61
+ // ─────────────────────────────────────────────────────────────────────────
62
+ github: {
63
+ url: 'https://api.github.com/users/{}',
64
+ profileUrl: 'https://github.com/{}',
65
+ errorType: 'status_code',
66
+ errorCode: 404,
67
+ confidence: 'high',
68
+ method: 'GET',
69
+ headers: {
70
+ 'Accept': 'application/vnd.github.v3+json',
71
+ 'User-Agent': 'Domain-Search-MCP/1.0',
72
+ },
73
+ regexCheck: /^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$/i,
74
+ },
75
+
76
+ npm: {
77
+ url: 'https://registry.npmjs.org/{}',
78
+ profileUrl: 'https://www.npmjs.com/~{}',
79
+ errorType: 'status_code',
80
+ errorCode: 404,
81
+ confidence: 'high',
82
+ method: 'GET',
83
+ headers: {
84
+ 'Accept': 'application/json',
85
+ },
86
+ regexCheck: /^[a-z0-9][a-z0-9._-]*$/i,
87
+ },
88
+
89
+ pypi: {
90
+ url: 'https://pypi.org/user/{}/',
91
+ profileUrl: 'https://pypi.org/user/{}/',
92
+ errorType: 'status_code',
93
+ errorCode: 404,
94
+ confidence: 'high',
95
+ method: 'GET',
96
+ },
97
+
98
+ reddit: {
99
+ url: 'https://www.reddit.com/user/{}/about.json',
100
+ profileUrl: 'https://reddit.com/user/{}',
101
+ errorType: 'message',
102
+ errorMsg: ['"error": 404'],
103
+ confidence: 'high',
104
+ method: 'GET',
105
+ headers: {
106
+ 'User-Agent': 'Domain-Search-MCP/1.0 (checking username availability)',
107
+ },
108
+ regexCheck: /^[A-Za-z0-9_-]{3,20}$/,
109
+ },
110
+
111
+ // ─────────────────────────────────────────────────────────────────────────
112
+ // MEDIUM CONFIDENCE (Status codes, some edge cases)
113
+ // ─────────────────────────────────────────────────────────────────────────
114
+ twitter: {
115
+ url: 'https://twitter.com/{}',
116
+ profileUrl: 'https://twitter.com/{}',
117
+ errorType: 'status_code',
118
+ errorCode: 404,
119
+ confidence: 'medium',
120
+ method: 'HEAD',
121
+ headers: {
122
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
123
+ },
124
+ regexCheck: /^[A-Za-z0-9_]{1,15}$/,
125
+ },
126
+
127
+ youtube: {
128
+ url: 'https://www.youtube.com/@{}',
129
+ profileUrl: 'https://youtube.com/@{}',
130
+ errorType: 'status_code',
131
+ errorCode: 404,
132
+ confidence: 'medium',
133
+ method: 'HEAD',
134
+ headers: {
135
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
136
+ },
137
+ },
138
+
139
+ producthunt: {
140
+ url: 'https://www.producthunt.com/@{}',
141
+ profileUrl: 'https://producthunt.com/@{}',
142
+ errorType: 'status_code',
143
+ errorCode: 404,
144
+ confidence: 'medium',
145
+ method: 'HEAD',
146
+ headers: {
147
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
148
+ },
149
+ },
150
+
151
+ // ─────────────────────────────────────────────────────────────────────────
152
+ // LOW CONFIDENCE (Aggressive bot protection)
153
+ // ─────────────────────────────────────────────────────────────────────────
154
+ instagram: {
155
+ url: 'https://www.instagram.com/{}/',
156
+ profileUrl: 'https://instagram.com/{}',
157
+ errorType: 'status_code',
158
+ errorCode: 404,
159
+ confidence: 'low', // Instagram aggressively blocks automated checks
160
+ method: 'HEAD',
161
+ headers: {
162
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
163
+ },
164
+ regexCheck: /^[a-zA-Z0-9_.]{1,30}$/,
165
+ },
166
+
167
+ linkedin: {
168
+ url: 'https://www.linkedin.com/in/{}',
169
+ profileUrl: 'https://linkedin.com/in/{}',
170
+ errorType: 'status_code',
171
+ errorCode: 404,
172
+ confidence: 'low', // LinkedIn requires auth
173
+ method: 'HEAD',
174
+ headers: {
175
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
176
+ },
177
+ },
178
+
179
+ tiktok: {
180
+ url: 'https://www.tiktok.com/@{}',
181
+ profileUrl: 'https://tiktok.com/@{}',
182
+ errorType: 'status_code',
183
+ errorCode: 404,
184
+ confidence: 'low', // TikTok blocks automated checks
185
+ method: 'HEAD',
186
+ headers: {
187
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
188
+ },
189
+ regexCheck: /^[a-zA-Z0-9_.]{2,24}$/,
190
+ },
191
+ };
192
+
193
+ // ═══════════════════════════════════════════════════════════════════════════
194
+ // Zod Schema
195
+ // ═══════════════════════════════════════════════════════════════════════════
196
+
197
+ const ALL_PLATFORMS = [
198
+ 'github', 'twitter', 'instagram', 'linkedin', 'tiktok',
199
+ 'reddit', 'youtube', 'npm', 'pypi', 'producthunt',
200
+ ] as const;
201
+
202
+ /**
203
+ * Input schema for check_socials.
204
+ */
205
+ export const checkSocialsSchema = z.object({
206
+ name: z
207
+ .string()
208
+ .min(1)
209
+ .max(30)
210
+ .describe("The username/handle to check (e.g., 'vibecoding')."),
211
+ platforms: z
212
+ .array(z.enum(ALL_PLATFORMS))
213
+ .optional()
214
+ .describe(
215
+ "Platforms to check. Defaults to ['github', 'twitter', 'reddit', 'npm'].",
216
+ ),
217
+ });
218
+
219
+ export type CheckSocialsInput = z.infer<typeof checkSocialsSchema>;
220
+
221
+ /**
222
+ * Tool definition for MCP.
223
+ */
224
+ export const checkSocialsTool = {
225
+ name: 'check_socials',
226
+ description: `Check if a username is available on social media and developer platforms.
227
+
228
+ Supports 10 platforms with varying confidence levels:
229
+ - HIGH: GitHub, npm, PyPI, Reddit (reliable public APIs)
230
+ - MEDIUM: Twitter/X, YouTube, ProductHunt (status code based)
231
+ - LOW: Instagram, LinkedIn, TikTok (block automated checks - verify manually)
232
+
233
+ Returns availability status with confidence indicator.
234
+
235
+ Example:
236
+ - check_socials("vibecoding") → checks GitHub, Twitter, Reddit, npm
237
+ - check_socials("myapp", ["github", "npm", "pypi"]) → developer platforms only`,
238
+ inputSchema: {
239
+ type: 'object',
240
+ properties: {
241
+ name: {
242
+ type: 'string',
243
+ description: "The username/handle to check.",
244
+ },
245
+ platforms: {
246
+ type: 'array',
247
+ items: {
248
+ type: 'string',
249
+ enum: ALL_PLATFORMS,
250
+ },
251
+ description:
252
+ "Platforms to check. Defaults to ['github', 'twitter', 'reddit', 'npm'].",
253
+ },
254
+ },
255
+ required: ['name'],
256
+ },
257
+ };
258
+
259
+ // ═══════════════════════════════════════════════════════════════════════════
260
+ // Platform Checking Logic
261
+ // ═══════════════════════════════════════════════════════════════════════════
262
+
263
+ /**
264
+ * Check a single platform using Sherlock-style detection.
265
+ */
266
+ async function checkPlatform(
267
+ username: string,
268
+ platform: SocialPlatform,
269
+ ): Promise<SocialHandleResult> {
270
+ const config = PLATFORM_CONFIGS[platform];
271
+ const url = config.url.replace('{}', username);
272
+ const profileUrl = config.profileUrl.replace('{}', username);
273
+
274
+ // Validate username format if regex provided
275
+ if (config.regexCheck && !config.regexCheck.test(username)) {
276
+ return {
277
+ platform,
278
+ handle: username,
279
+ available: false,
280
+ url: profileUrl,
281
+ checked_at: new Date().toISOString(),
282
+ confidence: 'high', // High confidence it's invalid
283
+ };
284
+ }
285
+
286
+ try {
287
+ const response = await axios({
288
+ method: config.method,
289
+ url,
290
+ timeout: 8000,
291
+ validateStatus: () => true, // Don't throw on any status
292
+ headers: {
293
+ ...config.headers,
294
+ },
295
+ maxRedirects: 0,
296
+ });
297
+
298
+ let available = false;
299
+
300
+ // Determine availability based on errorType
301
+ switch (config.errorType) {
302
+ case 'status_code':
303
+ available = response.status === (config.errorCode ?? 404);
304
+ break;
305
+
306
+ case 'message':
307
+ if (config.errorMsg && typeof response.data === 'string') {
308
+ available = config.errorMsg.some((msg) =>
309
+ response.data.includes(msg),
310
+ );
311
+ } else if (config.errorMsg && typeof response.data === 'object') {
312
+ const dataStr = JSON.stringify(response.data);
313
+ available = config.errorMsg.some((msg) => dataStr.includes(msg));
314
+ }
315
+ break;
316
+
317
+ case 'api':
318
+ // API-specific logic would go here
319
+ available = response.status === 404;
320
+ break;
321
+ }
322
+
323
+ return {
324
+ platform,
325
+ handle: username,
326
+ available,
327
+ url: profileUrl,
328
+ checked_at: new Date().toISOString(),
329
+ confidence: config.confidence,
330
+ };
331
+ } catch (error) {
332
+ logger.debug(`Failed to check ${platform}`, {
333
+ username,
334
+ error: error instanceof Error ? error.message : String(error),
335
+ });
336
+
337
+ // Return uncertain result on error
338
+ return {
339
+ platform,
340
+ handle: username,
341
+ available: false, // Assume taken if we can't check
342
+ url: profileUrl,
343
+ checked_at: new Date().toISOString(),
344
+ confidence: 'low',
345
+ };
346
+ }
347
+ }
348
+
349
+ // ═══════════════════════════════════════════════════════════════════════════
350
+ // Response Types
351
+ // ═══════════════════════════════════════════════════════════════════════════
352
+
353
+ /**
354
+ * Response format for social checks.
355
+ */
356
+ interface CheckSocialsResponse {
357
+ name: string;
358
+ results: SocialHandleResult[];
359
+ summary: {
360
+ available: number;
361
+ taken: number;
362
+ uncertain: number;
363
+ };
364
+ insights: string[];
365
+ }
366
+
367
+ // ═══════════════════════════════════════════════════════════════════════════
368
+ // Main Execution
369
+ // ═══════════════════════════════════════════════════════════════════════════
370
+
371
+ /**
372
+ * Execute the check_socials tool.
373
+ */
374
+ export async function executeCheckSocials(
375
+ input: CheckSocialsInput,
376
+ ): Promise<CheckSocialsResponse> {
377
+ try {
378
+ const { name, platforms } = checkSocialsSchema.parse(input);
379
+
380
+ // Default platforms: mix of social and developer platforms
381
+ const platformsToCheck: SocialPlatform[] = platforms || [
382
+ 'github',
383
+ 'twitter',
384
+ 'reddit',
385
+ 'npm',
386
+ ];
387
+
388
+ // Normalize username (lowercase, remove special chars)
389
+ const normalizedName = name.toLowerCase().replace(/[^a-z0-9_-]/g, '');
390
+
391
+ // Check all platforms in parallel (max 5 concurrent)
392
+ const results = await Promise.all(
393
+ platformsToCheck.map((p) => checkPlatform(normalizedName, p)),
394
+ );
395
+
396
+ // Categorize results by confidence
397
+ const highConfidence = results.filter((r) => r.confidence === 'high');
398
+ const available = results.filter(
399
+ (r) => r.available && r.confidence !== 'low',
400
+ );
401
+ const taken = results.filter((r) => !r.available && r.confidence !== 'low');
402
+ const uncertain = results.filter((r) => r.confidence === 'low');
403
+
404
+ // Generate insights
405
+ const insights: string[] = [];
406
+
407
+ if (available.length > 0) {
408
+ insights.push(
409
+ `✅ "${normalizedName}" is available on: ${available.map((r) => r.platform).join(', ')}`,
410
+ );
411
+ }
412
+
413
+ if (taken.length > 0) {
414
+ insights.push(
415
+ `❌ "${normalizedName}" is taken on: ${taken.map((r) => r.platform).join(', ')}`,
416
+ );
417
+ }
418
+
419
+ if (uncertain.length > 0) {
420
+ insights.push(
421
+ `⚠️ Could not reliably check: ${uncertain.map((r) => r.platform).join(', ')} (verify manually)`,
422
+ );
423
+ }
424
+
425
+ // Developer-focused insight
426
+ const devPlatforms = results.filter((r) =>
427
+ ['github', 'npm', 'pypi'].includes(r.platform),
428
+ );
429
+ const allDevAvailable = devPlatforms.every((r) => r.available);
430
+ if (devPlatforms.length > 0 && allDevAvailable) {
431
+ insights.push(
432
+ `🛠️ Great for developers! "${normalizedName}" is available on all dev platforms`,
433
+ );
434
+ }
435
+
436
+ // Branding consistency advice
437
+ const allAvailable = results.every((r) => r.available);
438
+ const allTaken = results.every((r) => !r.available);
439
+
440
+ if (allAvailable) {
441
+ insights.push(
442
+ `🎉 Perfect! "${normalizedName}" is available everywhere - grab it now!`,
443
+ );
444
+ } else if (allTaken) {
445
+ insights.push(
446
+ `💡 Try variations: ${normalizedName}hq, ${normalizedName}app, get${normalizedName}, ${normalizedName}io`,
447
+ );
448
+ } else if (available.length > 0 && taken.length > 0) {
449
+ insights.push(
450
+ '💡 For consistent branding, consider a name available on all platforms',
451
+ );
452
+ }
453
+
454
+ return {
455
+ name: normalizedName,
456
+ results,
457
+ summary: {
458
+ available: available.length,
459
+ taken: taken.length,
460
+ uncertain: uncertain.length,
461
+ },
462
+ insights,
463
+ };
464
+ } catch (error) {
465
+ throw wrapError(error);
466
+ }
467
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * compare_registrars Tool - Price Comparison.
3
+ *
4
+ * Compare pricing across multiple registrars for a specific domain.
5
+ * Helps find the best deal.
6
+ */
7
+
8
+ import { z } from 'zod';
9
+ import { compareRegistrars } from '../services/domain-search.js';
10
+ import { validateDomainName, validateTld } from '../utils/validators.js';
11
+ import { wrapError } from '../utils/errors.js';
12
+ import type { DomainResult } from '../types.js';
13
+
14
+ /**
15
+ * Input schema for compare_registrars.
16
+ */
17
+ export const compareRegistrarsSchema = z.object({
18
+ domain: z
19
+ .string()
20
+ .min(1)
21
+ .describe("The domain name to compare (e.g., 'vibecoding')."),
22
+ tld: z
23
+ .string()
24
+ .describe("The TLD extension (e.g., 'com', 'io')."),
25
+ registrars: z
26
+ .array(z.string())
27
+ .optional()
28
+ .describe(
29
+ "Registrars to compare (e.g., ['porkbun', 'namecheap']). Defaults to all available.",
30
+ ),
31
+ });
32
+
33
+ export type CompareRegistrarsInput = z.infer<typeof compareRegistrarsSchema>;
34
+
35
+ /**
36
+ * Tool definition for MCP.
37
+ */
38
+ export const compareRegistrarsTool = {
39
+ name: 'compare_registrars',
40
+ description: `Compare domain pricing across multiple registrars.
41
+
42
+ Checks the same domain at different registrars to find:
43
+ - Best first year price
44
+ - Best renewal price
45
+ - Overall recommendation
46
+
47
+ Returns pricing comparison and a recommendation.
48
+
49
+ Example:
50
+ - compare_registrars("vibecoding", "com") → compares Porkbun vs Namecheap`,
51
+ inputSchema: {
52
+ type: 'object',
53
+ properties: {
54
+ domain: {
55
+ type: 'string',
56
+ description: "The domain name to compare (without extension).",
57
+ },
58
+ tld: {
59
+ type: 'string',
60
+ description: "The TLD extension (e.g., 'com', 'io').",
61
+ },
62
+ registrars: {
63
+ type: 'array',
64
+ items: { type: 'string' },
65
+ description:
66
+ "Registrars to compare. Defaults to ['porkbun', 'namecheap'].",
67
+ },
68
+ },
69
+ required: ['domain', 'tld'],
70
+ },
71
+ };
72
+
73
+ /**
74
+ * Response format for registrar comparison.
75
+ */
76
+ interface CompareRegistrarsResponse {
77
+ domain: string;
78
+ what_happened: string;
79
+ comparison_count: number;
80
+ comparisons: DomainResult[];
81
+ best_first_year: {
82
+ registrar: string;
83
+ price: number;
84
+ currency: string;
85
+ } | null;
86
+ best_renewal: {
87
+ registrar: string;
88
+ price: number;
89
+ currency: string;
90
+ } | null;
91
+ recommendation: string;
92
+ insights: string[];
93
+ }
94
+
95
+ /**
96
+ * Execute the compare_registrars tool.
97
+ */
98
+ export async function executeCompareRegistrars(
99
+ input: CompareRegistrarsInput,
100
+ ): Promise<CompareRegistrarsResponse> {
101
+ try {
102
+ const { domain, tld, registrars } = compareRegistrarsSchema.parse(input);
103
+
104
+ const normalizedDomain = validateDomainName(domain);
105
+ const normalizedTld = validateTld(tld);
106
+ const fullDomain = `${normalizedDomain}.${normalizedTld}`;
107
+
108
+ const result = await compareRegistrars(
109
+ normalizedDomain,
110
+ normalizedTld,
111
+ registrars,
112
+ );
113
+
114
+ const insights: string[] = [];
115
+
116
+ // Generate insights
117
+ if (result.best_first_year && result.best_renewal) {
118
+ if (result.best_first_year.registrar === result.best_renewal.registrar) {
119
+ insights.push(
120
+ `✅ ${result.best_first_year.registrar} wins on both first year and renewal`,
121
+ );
122
+ } else {
123
+ insights.push(
124
+ `💡 Split strategy: ${result.best_first_year.registrar} for year 1, consider transfer to ${result.best_renewal.registrar} later`,
125
+ );
126
+ }
127
+ }
128
+
129
+ // Check for privacy inclusion
130
+ const withPrivacy = result.comparisons.filter((r) => r.privacy_included);
131
+ if (withPrivacy.length > 0) {
132
+ insights.push(
133
+ `🔒 ${withPrivacy.map((r) => r.registrar).join(', ')} include free WHOIS privacy`,
134
+ );
135
+ }
136
+
137
+ // Premium warning
138
+ const premiums = result.comparisons.filter((r) => r.premium);
139
+ if (premiums.length > 0) {
140
+ insights.push(
141
+ `⚠️ This is a premium domain at: ${premiums.map((r) => r.registrar).join(', ')}`,
142
+ );
143
+ }
144
+
145
+ return {
146
+ domain: fullDomain,
147
+ what_happened: `Compared pricing across ${result.comparisons.length} registrars`,
148
+ comparison_count: result.comparisons.length,
149
+ comparisons: result.comparisons,
150
+ best_first_year: result.best_first_year
151
+ ? { ...result.best_first_year, currency: 'USD' }
152
+ : null,
153
+ best_renewal: result.best_renewal
154
+ ? { ...result.best_renewal, currency: 'USD' }
155
+ : null,
156
+ recommendation: result.recommendation,
157
+ insights,
158
+ };
159
+ } catch (error) {
160
+ throw wrapError(error);
161
+ }
162
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Tool Exports.
3
+ */
4
+
5
+ export {
6
+ searchDomainTool,
7
+ searchDomainSchema,
8
+ executeSearchDomain,
9
+ type SearchDomainInput,
10
+ } from './search_domain.js';
11
+
12
+ export {
13
+ bulkSearchTool,
14
+ bulkSearchSchema,
15
+ executeBulkSearch,
16
+ type BulkSearchInput,
17
+ } from './bulk_search.js';
18
+
19
+ export {
20
+ compareRegistrarsTool,
21
+ compareRegistrarsSchema,
22
+ executeCompareRegistrars,
23
+ type CompareRegistrarsInput,
24
+ } from './compare_registrars.js';
25
+
26
+ export {
27
+ suggestDomainsTool,
28
+ suggestDomainsSchema,
29
+ executeSuggestDomains,
30
+ type SuggestDomainsInput,
31
+ } from './suggest_domains.js';
32
+
33
+ export {
34
+ tldInfoTool,
35
+ tldInfoSchema,
36
+ executeTldInfo,
37
+ type TldInfoInput,
38
+ } from './tld_info.js';
39
+
40
+ export {
41
+ checkSocialsTool,
42
+ checkSocialsSchema,
43
+ executeCheckSocials,
44
+ type CheckSocialsInput,
45
+ } from './check_socials.js';