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.
- package/.env.example +52 -0
- package/Dockerfile +15 -0
- package/LICENSE +21 -0
- package/README.md +426 -0
- package/SECURITY.md +252 -0
- package/dist/config.d.ts +25 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +117 -0
- package/dist/config.js.map +1 -0
- package/dist/fallbacks/index.d.ts +6 -0
- package/dist/fallbacks/index.d.ts.map +1 -0
- package/dist/fallbacks/index.js +14 -0
- package/dist/fallbacks/index.js.map +1 -0
- package/dist/fallbacks/rdap.d.ts +18 -0
- package/dist/fallbacks/rdap.d.ts.map +1 -0
- package/dist/fallbacks/rdap.js +339 -0
- package/dist/fallbacks/rdap.js.map +1 -0
- package/dist/fallbacks/whois.d.ts +27 -0
- package/dist/fallbacks/whois.d.ts.map +1 -0
- package/dist/fallbacks/whois.js +219 -0
- package/dist/fallbacks/whois.js.map +1 -0
- package/dist/registrars/base.d.ts +89 -0
- package/dist/registrars/base.d.ts.map +1 -0
- package/dist/registrars/base.js +203 -0
- package/dist/registrars/base.js.map +1 -0
- package/dist/registrars/index.d.ts +7 -0
- package/dist/registrars/index.d.ts.map +1 -0
- package/dist/registrars/index.js +15 -0
- package/dist/registrars/index.js.map +1 -0
- package/dist/registrars/namecheap.d.ts +69 -0
- package/dist/registrars/namecheap.d.ts.map +1 -0
- package/dist/registrars/namecheap.js +307 -0
- package/dist/registrars/namecheap.js.map +1 -0
- package/dist/registrars/porkbun.d.ts +63 -0
- package/dist/registrars/porkbun.d.ts.map +1 -0
- package/dist/registrars/porkbun.js +299 -0
- package/dist/registrars/porkbun.js.map +1 -0
- package/dist/server.d.ts +19 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +209 -0
- package/dist/server.js.map +1 -0
- package/dist/services/domain-search.d.ts +40 -0
- package/dist/services/domain-search.d.ts.map +1 -0
- package/dist/services/domain-search.js +438 -0
- package/dist/services/domain-search.js.map +1 -0
- package/dist/services/index.d.ts +5 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +11 -0
- package/dist/services/index.js.map +1 -0
- package/dist/tools/bulk_search.d.ts +72 -0
- package/dist/tools/bulk_search.d.ts.map +1 -0
- package/dist/tools/bulk_search.js +108 -0
- package/dist/tools/bulk_search.js.map +1 -0
- package/dist/tools/check_socials.d.ts +71 -0
- package/dist/tools/check_socials.d.ts.map +1 -0
- package/dist/tools/check_socials.js +357 -0
- package/dist/tools/check_socials.js.map +1 -0
- package/dist/tools/compare_registrars.d.ts +80 -0
- package/dist/tools/compare_registrars.d.ts.map +1 -0
- package/dist/tools/compare_registrars.js +116 -0
- package/dist/tools/compare_registrars.js.map +1 -0
- package/dist/tools/index.d.ts +10 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +31 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/search_domain.d.ts +61 -0
- package/dist/tools/search_domain.d.ts.map +1 -0
- package/dist/tools/search_domain.js +81 -0
- package/dist/tools/search_domain.js.map +1 -0
- package/dist/tools/suggest_domains.d.ts +82 -0
- package/dist/tools/suggest_domains.d.ts.map +1 -0
- package/dist/tools/suggest_domains.js +227 -0
- package/dist/tools/suggest_domains.js.map +1 -0
- package/dist/tools/tld_info.d.ts +56 -0
- package/dist/tools/tld_info.d.ts.map +1 -0
- package/dist/tools/tld_info.js +273 -0
- package/dist/tools/tld_info.js.map +1 -0
- package/dist/types.d.ts +193 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/cache.d.ts +81 -0
- package/dist/utils/cache.d.ts.map +1 -0
- package/dist/utils/cache.js +192 -0
- package/dist/utils/cache.js.map +1 -0
- package/dist/utils/errors.d.ts +87 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +191 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/index.d.ts +8 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +24 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logger.d.ts +27 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +132 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/premium-analyzer.d.ts +33 -0
- package/dist/utils/premium-analyzer.d.ts.map +1 -0
- package/dist/utils/premium-analyzer.js +273 -0
- package/dist/utils/premium-analyzer.js.map +1 -0
- package/dist/utils/validators.d.ts +53 -0
- package/dist/utils/validators.d.ts.map +1 -0
- package/dist/utils/validators.js +159 -0
- package/dist/utils/validators.js.map +1 -0
- package/docs/marketing/devto-post.md +135 -0
- package/docs/marketing/hackernews.md +42 -0
- package/docs/marketing/producthunt.md +109 -0
- package/docs/marketing/reddit-post.md +59 -0
- package/docs/marketing/twitter-thread.md +105 -0
- package/examples/bulk-search-50-domains.ts +131 -0
- package/examples/cli-interactive.ts +280 -0
- package/examples/compare-registrars.ts +78 -0
- package/examples/search-single-domain.ts +54 -0
- package/examples/suggest-names.ts +110 -0
- package/glama.json +6 -0
- package/jest.config.js +35 -0
- package/package.json +62 -0
- package/smithery.yaml +36 -0
- package/src/config.ts +121 -0
- package/src/fallbacks/index.ts +6 -0
- package/src/fallbacks/rdap.ts +407 -0
- package/src/fallbacks/whois.ts +250 -0
- package/src/registrars/base.ts +264 -0
- package/src/registrars/index.ts +7 -0
- package/src/registrars/namecheap.ts +378 -0
- package/src/registrars/porkbun.ts +380 -0
- package/src/server.ts +276 -0
- package/src/services/domain-search.ts +567 -0
- package/src/services/index.ts +9 -0
- package/src/tools/bulk_search.ts +142 -0
- package/src/tools/check_socials.ts +467 -0
- package/src/tools/compare_registrars.ts +162 -0
- package/src/tools/index.ts +45 -0
- package/src/tools/search_domain.ts +93 -0
- package/src/tools/suggest_domains.ts +284 -0
- package/src/tools/tld_info.ts +294 -0
- package/src/types.ts +289 -0
- package/src/utils/cache.ts +238 -0
- package/src/utils/errors.ts +262 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/logger.ts +162 -0
- package/src/utils/premium-analyzer.ts +303 -0
- package/src/utils/validators.ts +193 -0
- package/tests/premium-analyzer.test.ts +310 -0
- package/tests/unit/cache.test.ts +123 -0
- package/tests/unit/errors.test.ts +190 -0
- package/tests/unit/tld-info.test.ts +62 -0
- package/tests/unit/tools.test.ts +200 -0
- package/tests/unit/validators.test.ts +146 -0
- 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';
|