domain-search-mcp 1.1.0 → 1.1.2

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 (49) hide show
  1. package/README.md +57 -1
  2. package/context7.json +4 -0
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +1 -0
  5. package/dist/config.js.map +1 -1
  6. package/dist/registrars/godaddy-mcp.d.ts +67 -0
  7. package/dist/registrars/godaddy-mcp.d.ts.map +1 -0
  8. package/dist/registrars/godaddy-mcp.js +301 -0
  9. package/dist/registrars/godaddy-mcp.js.map +1 -0
  10. package/dist/registrars/index.d.ts +1 -0
  11. package/dist/registrars/index.d.ts.map +1 -1
  12. package/dist/registrars/index.js +4 -1
  13. package/dist/registrars/index.js.map +1 -1
  14. package/dist/server.d.ts +1 -0
  15. package/dist/server.d.ts.map +1 -1
  16. package/dist/server.js +11 -0
  17. package/dist/server.js.map +1 -1
  18. package/dist/services/domain-search.d.ts +5 -4
  19. package/dist/services/domain-search.d.ts.map +1 -1
  20. package/dist/services/domain-search.js +24 -8
  21. package/dist/services/domain-search.js.map +1 -1
  22. package/dist/tools/check_socials.d.ts.map +1 -1
  23. package/dist/tools/check_socials.js +21 -6
  24. package/dist/tools/check_socials.js.map +1 -1
  25. package/dist/tools/index.d.ts +1 -0
  26. package/dist/tools/index.d.ts.map +1 -1
  27. package/dist/tools/index.js +5 -1
  28. package/dist/tools/index.js.map +1 -1
  29. package/dist/tools/suggest_domains_smart.d.ts +113 -0
  30. package/dist/tools/suggest_domains_smart.d.ts.map +1 -0
  31. package/dist/tools/suggest_domains_smart.js +311 -0
  32. package/dist/tools/suggest_domains_smart.js.map +1 -0
  33. package/dist/types.d.ts +2 -0
  34. package/dist/types.d.ts.map +1 -1
  35. package/dist/utils/semantic-engine.d.ts +59 -0
  36. package/dist/utils/semantic-engine.d.ts.map +1 -0
  37. package/dist/utils/semantic-engine.js +427 -0
  38. package/dist/utils/semantic-engine.js.map +1 -0
  39. package/package.json +1 -1
  40. package/src/config.ts +1 -0
  41. package/src/registrars/godaddy-mcp.ts +380 -0
  42. package/src/registrars/index.ts +1 -0
  43. package/src/server.ts +16 -0
  44. package/src/services/domain-search.ts +25 -9
  45. package/src/tools/check_socials.ts +22 -6
  46. package/src/tools/index.ts +7 -0
  47. package/src/tools/suggest_domains_smart.ts +392 -0
  48. package/src/types.ts +2 -0
  49. package/src/utils/semantic-engine.ts +483 -0
@@ -0,0 +1,380 @@
1
+ /**
2
+ * GoDaddy MCP Adapter.
3
+ *
4
+ * Uses GoDaddy's public MCP endpoint for domain availability checks.
5
+ * No API key or reseller account required!
6
+ *
7
+ * Endpoint: https://api.godaddy.com/v1/domains/mcp
8
+ * Protocol: JSON-RPC 2.0 over HTTP (Streamable HTTP transport)
9
+ *
10
+ * Features:
11
+ * - Free availability checking (no auth)
12
+ * - Bulk checking up to 1000 domains
13
+ * - Premium/auction domain detection
14
+ *
15
+ * Limitations:
16
+ * - No pricing information
17
+ * - Rate limits not documented (be conservative)
18
+ */
19
+
20
+ import { z } from 'zod';
21
+ import { RegistrarAdapter, RateLimiter } from './base.js';
22
+ import type { DomainResult, TLDInfo } from '../types.js';
23
+ import { logger } from '../utils/logger.js';
24
+ import { RegistrarApiError } from '../utils/errors.js';
25
+
26
+ /**
27
+ * GoDaddy MCP endpoint.
28
+ */
29
+ const GODADDY_MCP_ENDPOINT = 'https://api.godaddy.com/v1/domains/mcp';
30
+
31
+ /**
32
+ * JSON-RPC request ID counter.
33
+ */
34
+ let jsonRpcId = 1;
35
+
36
+ /**
37
+ * Response schema for MCP tool call.
38
+ */
39
+ const McpResponseSchema = z.object({
40
+ jsonrpc: z.literal('2.0'),
41
+ id: z.number(),
42
+ result: z.object({
43
+ content: z.array(z.object({
44
+ type: z.string(),
45
+ text: z.string(),
46
+ })),
47
+ isError: z.boolean().optional(),
48
+ }).optional(),
49
+ error: z.object({
50
+ code: z.number(),
51
+ message: z.string(),
52
+ }).optional(),
53
+ });
54
+
55
+ /**
56
+ * Parse availability from GoDaddy MCP text response.
57
+ * The response is markdown-formatted text with different formats for single vs bulk queries.
58
+ */
59
+ interface ParsedAvailability {
60
+ available: boolean;
61
+ premium: boolean;
62
+ auction: boolean;
63
+ }
64
+
65
+ function parseAvailabilityResponse(text: string, domain: string): ParsedAvailability {
66
+ const normalizedDomain = domain.toLowerCase();
67
+ const normalizedText = text.toLowerCase();
68
+
69
+ // Default: unavailable
70
+ const result: ParsedAvailability = {
71
+ available: false,
72
+ premium: false,
73
+ auction: false,
74
+ };
75
+
76
+ // ==== SINGLE DOMAIN FORMAT ====
77
+ // Format: "STATUS: ✅ AVAILABLE" or "AVAILABILITY: Standard registration available"
78
+ if (normalizedText.includes('status:') || normalizedText.includes('availability:')) {
79
+ // Check for explicit availability indicators
80
+ if (
81
+ normalizedText.includes('status: ✅ available') ||
82
+ normalizedText.includes('✅ available') ||
83
+ normalizedText.includes('standard registration available') ||
84
+ normalizedText.includes('purchasable: yes')
85
+ ) {
86
+ result.available = true;
87
+
88
+ // Check if premium
89
+ if (normalizedText.includes('type: premium') || normalizedText.includes('premium domain')) {
90
+ result.premium = true;
91
+ }
92
+ // Check if auction
93
+ if (normalizedText.includes('type: auction') || normalizedText.includes('auction domain')) {
94
+ result.auction = true;
95
+ }
96
+ return result;
97
+ }
98
+
99
+ // Explicit unavailable
100
+ if (
101
+ normalizedText.includes('status: ❌') ||
102
+ normalizedText.includes('not available') ||
103
+ normalizedText.includes('already registered') ||
104
+ normalizedText.includes('purchasable: no')
105
+ ) {
106
+ result.available = false;
107
+ return result;
108
+ }
109
+ }
110
+
111
+ // ==== BULK DOMAIN FORMAT ====
112
+ // Check if domain appears in available section
113
+ // GoDaddy formats: "✅ **AVAILABLE DOMAINS" or "✅ **STANDARD SUGGESTIONS"
114
+ const availableMatch = text.match(/✅\s*\*\*(?:AVAILABLE|STANDARD)[^]*?(?=(?:💎|⚠️|❌|\*\*[A-Z])|$)/i);
115
+ if (availableMatch && availableMatch[0].toLowerCase().includes(normalizedDomain)) {
116
+ result.available = true;
117
+ return result;
118
+ }
119
+
120
+ // Check premium section
121
+ // GoDaddy format: "💎 **PREMIUM DOMAINS"
122
+ const premiumMatch = text.match(/💎\s*\*\*PREMIUM[^]*?(?=(?:⚠️|❌|\*\*[A-Z])|$)/i);
123
+ if (premiumMatch && premiumMatch[0].toLowerCase().includes(normalizedDomain)) {
124
+ result.available = true;
125
+ result.premium = true;
126
+ return result;
127
+ }
128
+
129
+ // Check auction section
130
+ // GoDaddy format: "🔨 **AUCTION DOMAINS" or similar
131
+ const auctionMatch = text.match(/🔨\s*\*\*AUCTION[^]*?(?=(?:💎|⚠️|❌|\*\*[A-Z])|$)/i);
132
+ if (auctionMatch && auctionMatch[0].toLowerCase().includes(normalizedDomain)) {
133
+ result.available = true;
134
+ result.auction = true;
135
+ return result;
136
+ }
137
+
138
+ // Check unavailable section
139
+ // GoDaddy format: "❌ **UNAVAILABLE DOMAINS"
140
+ const unavailableMatch = text.match(/❌\s*\*\*UNAVAILABLE[^]*?(?=(?:💎|⚠️|\*\*[A-Z])|$)/i);
141
+ if (unavailableMatch && unavailableMatch[0].toLowerCase().includes(normalizedDomain)) {
142
+ result.available = false;
143
+ return result;
144
+ }
145
+
146
+ // ==== FALLBACK: LINE-BY-LINE ANALYSIS ====
147
+ const lines = text.split('\n');
148
+ for (const line of lines) {
149
+ const lowerLine = line.toLowerCase();
150
+
151
+ // Check for domain-specific lines or general status
152
+ if (lowerLine.includes(normalizedDomain) || lowerLine.includes('status') || lowerLine.includes('available')) {
153
+ // Premium indicators
154
+ if (lowerLine.includes('premium')) {
155
+ result.available = true;
156
+ result.premium = true;
157
+ return result;
158
+ }
159
+ // Auction indicators
160
+ if (lowerLine.includes('auction')) {
161
+ result.available = true;
162
+ result.auction = true;
163
+ return result;
164
+ }
165
+ // Available indicators (must check before unavailable since "unavailable" contains "available")
166
+ if (
167
+ (lowerLine.includes('✅') && lowerLine.includes('available')) ||
168
+ lowerLine.includes('register at') ||
169
+ lowerLine.includes('can be registered')
170
+ ) {
171
+ result.available = true;
172
+ return result;
173
+ }
174
+ // Unavailable indicators
175
+ if (lowerLine.includes('❌') || lowerLine.includes('unavailable') || lowerLine.includes('not available')) {
176
+ result.available = false;
177
+ return result;
178
+ }
179
+ }
180
+ }
181
+
182
+ return result;
183
+ }
184
+
185
+ /**
186
+ * GoDaddy MCP Adapter.
187
+ *
188
+ * Uses GoDaddy's public MCP endpoint - no authentication required!
189
+ */
190
+ export class GodaddyMcpAdapter extends RegistrarAdapter {
191
+ readonly name = 'GoDaddy';
192
+ readonly id = 'godaddy';
193
+
194
+ constructor() {
195
+ // Conservative rate limit - GoDaddy doesn't document their limits
196
+ // Using 30/min to be safe (they say "excessive requests may be throttled")
197
+ super(30);
198
+ }
199
+
200
+ /**
201
+ * Check if GoDaddy MCP is enabled.
202
+ * Always enabled since no API key needed!
203
+ */
204
+ isEnabled(): boolean {
205
+ return true;
206
+ }
207
+
208
+ /**
209
+ * Search for domain availability using GoDaddy MCP.
210
+ */
211
+ async search(domain: string, tld: string): Promise<DomainResult> {
212
+ const fullDomain = `${domain}.${tld}`;
213
+
214
+ return this.retryWithBackoff(async () => {
215
+ const text = await this.callMcpTool('domains_check_availability', {
216
+ domains: fullDomain,
217
+ });
218
+
219
+ const parsed = parseAvailabilityResponse(text, fullDomain);
220
+
221
+ return this.createResult(domain, tld, {
222
+ available: parsed.available,
223
+ premium: parsed.premium,
224
+ price_first_year: null, // GoDaddy MCP doesn't provide pricing
225
+ price_renewal: null,
226
+ privacy_included: false, // Unknown
227
+ source: 'godaddy_api',
228
+ premium_reason: parsed.premium
229
+ ? 'Premium domain (GoDaddy)'
230
+ : parsed.auction
231
+ ? 'Auction domain (GoDaddy)'
232
+ : undefined,
233
+ });
234
+ }, `check ${fullDomain}`);
235
+ }
236
+
237
+ /**
238
+ * Bulk check multiple domains at once.
239
+ * GoDaddy MCP supports up to 1000 domains per request!
240
+ */
241
+ async bulkSearch(domains: string[]): Promise<Map<string, ParsedAvailability>> {
242
+ const results = new Map<string, ParsedAvailability>();
243
+
244
+ // GoDaddy accepts comma-separated domains
245
+ const domainList = domains.join(', ');
246
+
247
+ const text = await this.callMcpTool('domains_check_availability', {
248
+ domains: domainList,
249
+ });
250
+
251
+ // Parse results for each domain
252
+ for (const domain of domains) {
253
+ const parsed = parseAvailabilityResponse(text, domain);
254
+ results.set(domain.toLowerCase(), parsed);
255
+ }
256
+
257
+ return results;
258
+ }
259
+
260
+ /**
261
+ * Get TLD info - not supported by GoDaddy MCP.
262
+ */
263
+ async getTldInfo(_tld: string): Promise<TLDInfo | null> {
264
+ return null;
265
+ }
266
+
267
+ /**
268
+ * Call a GoDaddy MCP tool via JSON-RPC.
269
+ */
270
+ private async callMcpTool(
271
+ toolName: string,
272
+ args: Record<string, unknown>,
273
+ ): Promise<string> {
274
+ const requestId = jsonRpcId++;
275
+
276
+ const payload = {
277
+ jsonrpc: '2.0',
278
+ method: 'tools/call',
279
+ params: {
280
+ name: toolName,
281
+ arguments: args,
282
+ },
283
+ id: requestId,
284
+ };
285
+
286
+ logger.debug('GoDaddy MCP request', {
287
+ tool: toolName,
288
+ args,
289
+ request_id: requestId,
290
+ });
291
+
292
+ try {
293
+ const response = await this.withTimeout(
294
+ fetch(GODADDY_MCP_ENDPOINT, {
295
+ method: 'POST',
296
+ headers: {
297
+ 'Content-Type': 'application/json',
298
+ 'Accept': 'application/json, text/event-stream',
299
+ },
300
+ body: JSON.stringify(payload),
301
+ }),
302
+ `GoDaddy MCP ${toolName}`,
303
+ 15000, // 15 second timeout
304
+ );
305
+
306
+ if (!response.ok) {
307
+ throw new RegistrarApiError(
308
+ 'GoDaddy MCP',
309
+ `HTTP ${response.status}: ${response.statusText}`,
310
+ );
311
+ }
312
+
313
+ // Response is SSE format: "event: message\ndata: {...}"
314
+ const rawText = await response.text();
315
+
316
+ // Extract JSON from SSE format
317
+ const dataMatch = rawText.match(/data:\s*(\{.*\})/s);
318
+ if (!dataMatch) {
319
+ throw new RegistrarApiError(
320
+ 'GoDaddy MCP',
321
+ 'Invalid response format - expected SSE',
322
+ );
323
+ }
324
+
325
+ const jsonStr = dataMatch[1];
326
+ const parsed = JSON.parse(jsonStr!);
327
+
328
+ // Validate response
329
+ const validated = McpResponseSchema.parse(parsed);
330
+
331
+ if (validated.error) {
332
+ throw new RegistrarApiError(
333
+ 'GoDaddy MCP',
334
+ `RPC Error ${validated.error.code}: ${validated.error.message}`,
335
+ );
336
+ }
337
+
338
+ if (!validated.result || validated.result.isError) {
339
+ throw new RegistrarApiError(
340
+ 'GoDaddy MCP',
341
+ 'Tool call returned error',
342
+ );
343
+ }
344
+
345
+ // Extract text content
346
+ const textContent = validated.result.content.find(c => c.type === 'text');
347
+ if (!textContent) {
348
+ throw new RegistrarApiError(
349
+ 'GoDaddy MCP',
350
+ 'No text content in response',
351
+ );
352
+ }
353
+
354
+ logger.debug('GoDaddy MCP response', {
355
+ request_id: requestId,
356
+ text_length: textContent.text.length,
357
+ });
358
+
359
+ return textContent.text;
360
+ } catch (error) {
361
+ if (error instanceof RegistrarApiError) {
362
+ throw error;
363
+ }
364
+
365
+ if (error instanceof Error) {
366
+ if (error.name === 'AbortError' || error.message.includes('timeout')) {
367
+ throw error;
368
+ }
369
+ throw new RegistrarApiError('GoDaddy MCP', error.message);
370
+ }
371
+
372
+ throw new RegistrarApiError('GoDaddy MCP', 'Unknown network error');
373
+ }
374
+ }
375
+ }
376
+
377
+ /**
378
+ * Singleton instance.
379
+ */
380
+ export const godaddyMcpAdapter = new GodaddyMcpAdapter();
@@ -5,3 +5,4 @@
5
5
  export { RegistrarAdapter } from './base.js';
6
6
  export { PorkbunAdapter, porkbunAdapter } from './porkbun.js';
7
7
  export { NamecheapAdapter, namecheapAdapter } from './namecheap.js';
8
+ export { GodaddyMcpAdapter, godaddyMcpAdapter } from './godaddy-mcp.js';
package/src/server.ts CHANGED
@@ -10,6 +10,7 @@
10
10
  * - bulk_search: Check many domains at once
11
11
  * - compare_registrars: Compare pricing across registrars
12
12
  * - suggest_domains: Generate available name variations
13
+ * - suggest_domains_smart: AI-powered domain suggestions with semantic analysis
13
14
  * - tld_info: Get TLD information and recommendations
14
15
  * - check_socials: Check social handle availability
15
16
  *
@@ -36,6 +37,8 @@ import {
36
37
  executeCompareRegistrars,
37
38
  suggestDomainsTool,
38
39
  executeSuggestDomains,
40
+ suggestDomainsSmartTool,
41
+ executeSuggestDomainsSmart,
39
42
  tldInfoTool,
40
43
  executeTldInfo,
41
44
  checkSocialsTool,
@@ -57,6 +60,7 @@ const TOOLS: Tool[] = [
57
60
  bulkSearchTool as Tool,
58
61
  compareRegistrarsTool as Tool,
59
62
  suggestDomainsTool as Tool,
63
+ suggestDomainsSmartTool as Tool,
60
64
  tldInfoTool as Tool,
61
65
  checkSocialsTool as Tool,
62
66
  ];
@@ -187,6 +191,18 @@ async function executeToolCall(
187
191
  max_suggestions: (args.max_suggestions as number) || 10,
188
192
  });
189
193
 
194
+ case 'suggest_domains_smart':
195
+ return executeSuggestDomainsSmart({
196
+ query: args.query as string,
197
+ tld: (args.tld as string) || 'com',
198
+ industry: args.industry as
199
+ | 'tech' | 'startup' | 'finance' | 'health' | 'food' | 'creative' | 'ecommerce' | 'education' | 'gaming' | 'social'
200
+ | undefined,
201
+ style: (args.style as 'brandable' | 'descriptive' | 'short' | 'creative') || 'brandable',
202
+ max_suggestions: (args.max_suggestions as number) || 15,
203
+ include_premium: (args.include_premium as boolean) || false,
204
+ });
205
+
190
206
  case 'tld_info':
191
207
  return executeTldInfo({
192
208
  tld: args.tld as string,
@@ -2,10 +2,11 @@
2
2
  * Domain Search Service.
3
3
  *
4
4
  * Orchestrates domain availability checks across multiple sources:
5
- * 1. Porkbun (primary, if configured)
6
- * 2. Namecheap (secondary, if configured)
7
- * 3. RDAP (fallback, always available)
8
- * 4. WHOIS (last resort, always available)
5
+ * 1. Porkbun (primary, if configured - has pricing)
6
+ * 2. Namecheap (secondary, if configured - has pricing)
7
+ * 3. GoDaddy MCP (always available - no auth, no pricing, great availability data)
8
+ * 4. RDAP (fallback, always available)
9
+ * 5. WHOIS (last resort, always available)
9
10
  *
10
11
  * Handles:
11
12
  * - Smart source selection based on availability and configuration
@@ -28,7 +29,7 @@ import {
28
29
  buildDomain,
29
30
  } from '../utils/validators.js';
30
31
  import { domainCache, domainCacheKey, getOrCompute } from '../utils/cache.js';
31
- import { porkbunAdapter, namecheapAdapter } from '../registrars/index.js';
32
+ import { porkbunAdapter, namecheapAdapter, godaddyMcpAdapter } from '../registrars/index.js';
32
33
  import { checkRdap, isRdapAvailable } from '../fallbacks/rdap.js';
33
34
  import { checkWhois, isWhoisAvailable } from '../fallbacks/whois.js';
34
35
  import {
@@ -125,7 +126,7 @@ async function searchSingleDomain(
125
126
  const triedSources: string[] = [];
126
127
 
127
128
  // Check cache first
128
- for (const source of ['porkbun', 'namecheap', 'rdap', 'whois'] as const) {
129
+ for (const source of ['porkbun', 'namecheap', 'godaddy', 'rdap', 'whois'] as const) {
129
130
  const cacheKey = domainCacheKey(fullDomain, source);
130
131
  const cached = domainCache.get(cacheKey);
131
132
  if (cached) {
@@ -182,6 +183,14 @@ async function searchSingleDomain(
182
183
 
183
184
  /**
184
185
  * Build the priority list of sources to try.
186
+ *
187
+ * Priority order:
188
+ * 1. Preferred registrars (if specified)
189
+ * 2. Porkbun (has pricing, best API)
190
+ * 3. Namecheap (has pricing)
191
+ * 4. GoDaddy MCP (free, no pricing but good availability data)
192
+ * 5. RDAP (free, no pricing)
193
+ * 6. WHOIS (slowest fallback)
185
194
  */
186
195
  function buildSourcePriority(
187
196
  tld: string,
@@ -196,21 +205,25 @@ function buildSourcePriority(
196
205
  sources.push('porkbun');
197
206
  } else if (registrar === 'namecheap' && config.namecheap.enabled) {
198
207
  sources.push('namecheap');
208
+ } else if (registrar === 'godaddy') {
209
+ sources.push('godaddy');
199
210
  }
200
211
  }
201
212
  } else {
202
- // Default priority: Porkbun first (better API), then Namecheap
213
+ // Default priority: Porkbun first (best API with pricing), then Namecheap, then GoDaddy
203
214
  if (config.porkbun.enabled) sources.push('porkbun');
204
215
  if (config.namecheap.enabled) sources.push('namecheap');
216
+ // GoDaddy MCP is always available (no auth needed)
217
+ sources.push('godaddy');
205
218
  }
206
219
 
207
220
  // Always add fallbacks
208
221
  if (isRdapAvailable(tld)) sources.push('rdap');
209
222
  if (isWhoisAvailable(tld)) sources.push('whois');
210
223
 
211
- // If no registrar APIs, RDAP should be first
224
+ // If no registrar APIs, GoDaddy MCP and RDAP should be first
212
225
  if (sources.length === 0) {
213
- sources.push('rdap', 'whois');
226
+ sources.push('godaddy', 'rdap', 'whois');
214
227
  }
215
228
 
216
229
  return sources;
@@ -231,6 +244,9 @@ async function trySource(
231
244
  case 'namecheap':
232
245
  return namecheapAdapter.search(domain, tld);
233
246
 
247
+ case 'godaddy':
248
+ return godaddyMcpAdapter.search(domain, tld);
249
+
234
250
  case 'rdap':
235
251
  return checkRdap(domain, tld);
236
252
 
@@ -112,14 +112,15 @@ const PLATFORM_CONFIGS: Record<SocialPlatform, PlatformConfig> = {
112
112
  // MEDIUM CONFIDENCE (Status codes, some edge cases)
113
113
  // ─────────────────────────────────────────────────────────────────────────
114
114
  twitter: {
115
- url: 'https://twitter.com/{}',
115
+ // Use oembed API - Twitter SPA returns 200 for all direct requests
116
+ url: 'https://publish.twitter.com/oembed?url=https://twitter.com/{}',
116
117
  profileUrl: 'https://twitter.com/{}',
117
118
  errorType: 'status_code',
118
119
  errorCode: 404,
119
- confidence: 'medium',
120
- method: 'HEAD',
120
+ confidence: 'high', // oembed API is reliable
121
+ method: 'GET',
121
122
  headers: {
122
- 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
123
+ 'User-Agent': 'Domain-Search-MCP/1.0',
123
124
  },
124
125
  regexCheck: /^[A-Za-z0-9_]{1,15}$/,
125
126
  },
@@ -226,8 +227,8 @@ export const checkSocialsTool = {
226
227
  description: `Check if a username is available on social media and developer platforms.
227
228
 
228
229
  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)
230
+ - HIGH: GitHub, npm, PyPI, Reddit, Twitter/X (reliable public APIs)
231
+ - MEDIUM: YouTube, ProductHunt (status code based)
231
232
  - LOW: Instagram, LinkedIn, TikTok (block automated checks - verify manually)
232
233
 
233
234
  Returns availability status with confidence indicator.
@@ -295,6 +296,20 @@ async function checkPlatform(
295
296
  maxRedirects: 0,
296
297
  });
297
298
 
299
+ // Handle rate limiting (429) - return uncertain instead of false negative
300
+ if (response.status === 429) {
301
+ logger.debug(`Rate limited on ${platform}`, { username });
302
+ return {
303
+ platform,
304
+ handle: username,
305
+ available: false,
306
+ url: profileUrl,
307
+ checked_at: new Date().toISOString(),
308
+ confidence: 'low', // Can't be sure due to rate limit
309
+ error: 'Rate limited - please try again later',
310
+ };
311
+ }
312
+
298
313
  let available = false;
299
314
 
300
315
  // Determine availability based on errorType
@@ -342,6 +357,7 @@ async function checkPlatform(
342
357
  url: profileUrl,
343
358
  checked_at: new Date().toISOString(),
344
359
  confidence: 'low',
360
+ error: error instanceof Error ? error.message : 'Unknown error',
345
361
  };
346
362
  }
347
363
  }
@@ -30,6 +30,13 @@ export {
30
30
  type SuggestDomainsInput,
31
31
  } from './suggest_domains.js';
32
32
 
33
+ export {
34
+ suggestDomainsSmartTool,
35
+ suggestDomainsSmartSchema,
36
+ executeSuggestDomainsSmart,
37
+ type SuggestDomainsSmartInput,
38
+ } from './suggest_domains_smart.js';
39
+
33
40
  export {
34
41
  tldInfoTool,
35
42
  tldInfoSchema,