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,262 @@
1
+ /**
2
+ * Custom Error Classes for Domain Search MCP.
3
+ *
4
+ * These errors are designed to be:
5
+ * 1. User-friendly (clear messages for non-developers)
6
+ * 2. Actionable (suggest what to do next)
7
+ * 3. Informative (include context for debugging)
8
+ */
9
+
10
+ /**
11
+ * Base error class for all domain search errors.
12
+ */
13
+ export class DomainSearchError extends Error {
14
+ /** Machine-readable error code */
15
+ readonly code: string;
16
+ /** User-friendly message */
17
+ readonly userMessage: string;
18
+ /** Can this operation be retried? */
19
+ readonly retryable: boolean;
20
+ /** Suggested action for the user */
21
+ readonly suggestedAction?: string;
22
+
23
+ constructor(
24
+ code: string,
25
+ message: string,
26
+ userMessage: string,
27
+ options?: {
28
+ retryable?: boolean;
29
+ suggestedAction?: string;
30
+ cause?: Error;
31
+ },
32
+ ) {
33
+ super(message);
34
+ this.name = 'DomainSearchError';
35
+ this.code = code;
36
+ this.userMessage = userMessage;
37
+ this.retryable = options?.retryable ?? false;
38
+ this.suggestedAction = options?.suggestedAction;
39
+ if (options?.cause) {
40
+ this.cause = options.cause;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Convert to a plain object for JSON responses.
46
+ */
47
+ toJSON(): object {
48
+ return {
49
+ code: this.code,
50
+ message: this.userMessage,
51
+ retryable: this.retryable,
52
+ suggestedAction: this.suggestedAction,
53
+ };
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Error when a domain name is invalid.
59
+ */
60
+ export class InvalidDomainError extends DomainSearchError {
61
+ constructor(domain: string, reason: string) {
62
+ super(
63
+ 'INVALID_DOMAIN',
64
+ `Invalid domain: ${domain} - ${reason}`,
65
+ `The domain "${domain}" is not valid: ${reason}`,
66
+ {
67
+ retryable: false,
68
+ suggestedAction:
69
+ 'Check the domain name for typos or invalid characters.',
70
+ },
71
+ );
72
+ this.name = 'InvalidDomainError';
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Error when a TLD is not supported.
78
+ */
79
+ export class UnsupportedTldError extends DomainSearchError {
80
+ constructor(tld: string, availableTlds: string[]) {
81
+ const suggestion =
82
+ availableTlds.length > 0
83
+ ? `Try one of these: ${availableTlds.slice(0, 5).join(', ')}`
84
+ : 'Contact support for TLD availability.';
85
+
86
+ super(
87
+ 'UNSUPPORTED_TLD',
88
+ `TLD not supported: .${tld}`,
89
+ `The TLD ".${tld}" is not supported for searching.`,
90
+ {
91
+ retryable: false,
92
+ suggestedAction: suggestion,
93
+ },
94
+ );
95
+ this.name = 'UnsupportedTldError';
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Error when an API rate limit is hit.
101
+ */
102
+ export class RateLimitError extends DomainSearchError {
103
+ /** When to retry (Unix timestamp) */
104
+ readonly retryAfter?: number;
105
+
106
+ constructor(registrar: string, retryAfterSeconds?: number) {
107
+ super(
108
+ 'RATE_LIMIT',
109
+ `Rate limit hit for ${registrar}`,
110
+ `Too many requests to ${registrar}. Please slow down.`,
111
+ {
112
+ retryable: true,
113
+ suggestedAction: retryAfterSeconds
114
+ ? `Wait ${retryAfterSeconds} seconds before trying again.`
115
+ : 'Wait a moment and try again, or check fewer domains at once.',
116
+ },
117
+ );
118
+ this.name = 'RateLimitError';
119
+ if (retryAfterSeconds) {
120
+ this.retryAfter = Date.now() + retryAfterSeconds * 1000;
121
+ }
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Error when a registrar API fails.
127
+ */
128
+ export class RegistrarApiError extends DomainSearchError {
129
+ /** HTTP status code if available */
130
+ readonly statusCode?: number;
131
+
132
+ constructor(
133
+ registrar: string,
134
+ message: string,
135
+ statusCode?: number,
136
+ cause?: Error,
137
+ ) {
138
+ const isServerError = statusCode !== undefined && statusCode >= 500;
139
+
140
+ super(
141
+ 'REGISTRAR_API_ERROR',
142
+ `${registrar} API error: ${message}`,
143
+ isServerError
144
+ ? `${registrar} is experiencing issues. We'll try another source.`
145
+ : `Could not check with ${registrar}: ${message}`,
146
+ {
147
+ retryable: isServerError,
148
+ suggestedAction: isServerError
149
+ ? 'The system will automatically try alternative sources.'
150
+ : `Check your ${registrar} API configuration.`,
151
+ cause,
152
+ },
153
+ );
154
+ this.name = 'RegistrarApiError';
155
+ this.statusCode = statusCode;
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Error when API credentials are missing or invalid.
161
+ */
162
+ export class AuthenticationError extends DomainSearchError {
163
+ constructor(registrar: string, reason?: string) {
164
+ super(
165
+ 'AUTH_ERROR',
166
+ `Authentication failed for ${registrar}: ${reason || 'Invalid credentials'}`,
167
+ `Could not authenticate with ${registrar}.`,
168
+ {
169
+ retryable: false,
170
+ suggestedAction: `Check your ${registrar.toUpperCase()}_API_KEY and ${registrar.toUpperCase()}_API_SECRET in your .env file.`,
171
+ },
172
+ );
173
+ this.name = 'AuthenticationError';
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Error when no data source is available.
179
+ */
180
+ export class NoSourceAvailableError extends DomainSearchError {
181
+ constructor(domain: string, triedSources: string[]) {
182
+ super(
183
+ 'NO_SOURCE_AVAILABLE',
184
+ `No source available for ${domain}. Tried: ${triedSources.join(', ')}`,
185
+ `Could not check availability for "${domain}". All sources failed.`,
186
+ {
187
+ retryable: true,
188
+ suggestedAction:
189
+ 'Try again in a few minutes, or check the domain manually at a registrar website.',
190
+ },
191
+ );
192
+ this.name = 'NoSourceAvailableError';
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Error when a network request times out.
198
+ */
199
+ export class TimeoutError extends DomainSearchError {
200
+ constructor(operation: string, timeoutMs: number) {
201
+ super(
202
+ 'TIMEOUT',
203
+ `Operation timed out: ${operation} (${timeoutMs}ms)`,
204
+ `The request took too long to complete.`,
205
+ {
206
+ retryable: true,
207
+ suggestedAction: 'Try again - this might be a temporary network issue.',
208
+ },
209
+ );
210
+ this.name = 'TimeoutError';
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Error when a required configuration is missing.
216
+ */
217
+ export class ConfigurationError extends DomainSearchError {
218
+ constructor(missing: string, howToFix: string) {
219
+ super(
220
+ 'CONFIG_ERROR',
221
+ `Missing configuration: ${missing}`,
222
+ `Server configuration is incomplete.`,
223
+ {
224
+ retryable: false,
225
+ suggestedAction: howToFix,
226
+ },
227
+ );
228
+ this.name = 'ConfigurationError';
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Convert any error to a DomainSearchError.
234
+ */
235
+ export function wrapError(error: unknown): DomainSearchError {
236
+ if (error instanceof DomainSearchError) {
237
+ return error;
238
+ }
239
+
240
+ if (error instanceof Error) {
241
+ return new DomainSearchError(
242
+ 'UNKNOWN_ERROR',
243
+ error.message,
244
+ 'An unexpected error occurred.',
245
+ {
246
+ retryable: true,
247
+ suggestedAction: 'Try again or contact support if the issue persists.',
248
+ cause: error,
249
+ },
250
+ );
251
+ }
252
+
253
+ return new DomainSearchError(
254
+ 'UNKNOWN_ERROR',
255
+ String(error),
256
+ 'An unexpected error occurred.',
257
+ {
258
+ retryable: true,
259
+ suggestedAction: 'Try again or contact support if the issue persists.',
260
+ },
261
+ );
262
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Utility Exports.
3
+ */
4
+
5
+ export * from './logger.js';
6
+ export * from './errors.js';
7
+ export * from './cache.js';
8
+ export * from './validators.js';
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Structured JSON Logger with Secret Masking.
3
+ *
4
+ * - Outputs JSON for easy parsing
5
+ * - Masks API keys and secrets automatically
6
+ * - Includes request IDs for tracing
7
+ */
8
+
9
+ import { config } from '../config.js';
10
+
11
+ type LogLevel = 'debug' | 'info' | 'warn' | 'error';
12
+
13
+ interface LogEntry {
14
+ timestamp: string;
15
+ level: LogLevel;
16
+ message: string;
17
+ request_id?: string;
18
+ [key: string]: unknown;
19
+ }
20
+
21
+ /**
22
+ * Patterns that look like API keys or secrets.
23
+ * These will be masked in log output.
24
+ */
25
+ const SECRET_PATTERNS = [
26
+ // Long alphanumeric strings (likely API keys)
27
+ /\b[a-zA-Z0-9]{32,}\b/g,
28
+ // Patterns that look like secrets
29
+ /(?:api[_-]?key|secret|password|token)[\s:="']+[^\s"']+/gi,
30
+ ];
31
+
32
+ /**
33
+ * Mask sensitive data in a value.
34
+ */
35
+ function maskSecrets(value: unknown): unknown {
36
+ if (typeof value === 'string') {
37
+ let masked = value;
38
+ for (const pattern of SECRET_PATTERNS) {
39
+ masked = masked.replace(pattern, '[REDACTED]');
40
+ }
41
+ return masked;
42
+ }
43
+
44
+ if (Array.isArray(value)) {
45
+ return value.map(maskSecrets);
46
+ }
47
+
48
+ if (value && typeof value === 'object') {
49
+ const maskedObj: Record<string, unknown> = {};
50
+ for (const [key, val] of Object.entries(value)) {
51
+ // Always mask keys that look like secrets
52
+ const lowerKey = key.toLowerCase();
53
+ if (
54
+ lowerKey.includes('secret') ||
55
+ lowerKey.includes('password') ||
56
+ lowerKey.includes('apikey') ||
57
+ lowerKey.includes('api_key') ||
58
+ lowerKey.includes('token')
59
+ ) {
60
+ maskedObj[key] = '[REDACTED]';
61
+ } else {
62
+ maskedObj[key] = maskSecrets(val);
63
+ }
64
+ }
65
+ return maskedObj;
66
+ }
67
+
68
+ return value;
69
+ }
70
+
71
+ /**
72
+ * Log level priority for filtering.
73
+ */
74
+ const LOG_LEVELS: Record<LogLevel, number> = {
75
+ debug: 0,
76
+ info: 1,
77
+ warn: 2,
78
+ error: 3,
79
+ };
80
+
81
+ /**
82
+ * Should this level be logged?
83
+ */
84
+ function shouldLog(level: LogLevel): boolean {
85
+ return LOG_LEVELS[level] >= LOG_LEVELS[config.logLevel];
86
+ }
87
+
88
+ /**
89
+ * Generate a unique request ID.
90
+ */
91
+ export function generateRequestId(): string {
92
+ return `req_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
93
+ }
94
+
95
+ /**
96
+ * Current request context (for tracing).
97
+ */
98
+ let currentRequestId: string | undefined;
99
+
100
+ export function setRequestId(id: string): void {
101
+ currentRequestId = id;
102
+ }
103
+
104
+ export function clearRequestId(): void {
105
+ currentRequestId = undefined;
106
+ }
107
+
108
+ /**
109
+ * Core logging function.
110
+ */
111
+ function log(
112
+ level: LogLevel,
113
+ message: string,
114
+ data?: Record<string, unknown>,
115
+ ): void {
116
+ if (!shouldLog(level)) return;
117
+
118
+ const entry: LogEntry = {
119
+ timestamp: new Date().toISOString(),
120
+ level,
121
+ message,
122
+ };
123
+
124
+ if (currentRequestId) {
125
+ entry.request_id = currentRequestId;
126
+ }
127
+
128
+ // Add and mask additional data
129
+ if (data) {
130
+ const masked = maskSecrets(data) as Record<string, unknown>;
131
+ Object.assign(entry, masked);
132
+ }
133
+
134
+ // Output to stderr (MCP servers use stdout for protocol)
135
+ console.error(JSON.stringify(entry));
136
+ }
137
+
138
+ /**
139
+ * Logger instance with convenience methods.
140
+ */
141
+ export const logger = {
142
+ debug: (message: string, data?: Record<string, unknown>) =>
143
+ log('debug', message, data),
144
+ info: (message: string, data?: Record<string, unknown>) =>
145
+ log('info', message, data),
146
+ warn: (message: string, data?: Record<string, unknown>) =>
147
+ log('warn', message, data),
148
+ error: (message: string, data?: Record<string, unknown>) =>
149
+ log('error', message, data),
150
+
151
+ /**
152
+ * Log an error with stack trace.
153
+ */
154
+ logError: (message: string, error: Error, data?: Record<string, unknown>) => {
155
+ log('error', message, {
156
+ ...data,
157
+ error_name: error.name,
158
+ error_message: error.message,
159
+ error_stack: error.stack,
160
+ });
161
+ },
162
+ };