Skip to content

Commit 980ad92

Browse files
authored
Merge pull request #3 from cfdude/dev
feat: implement TLD caching with filtering and pagination
2 parents a6199c2 + e84c87c commit 980ad92

File tree

4 files changed

+190
-4
lines changed

4 files changed

+190
-4
lines changed

src/index.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
import dotenv from 'dotenv';
1212
import { NamecheapClient } from './namecheap-client.js';
1313
import { namecheapTools } from './tools.js';
14+
import { TldCache } from './tld-cache.js';
1415
import type {
1516
DomainsListParams,
1617
DomainsCheckParams,
@@ -50,6 +51,7 @@ if (!API_KEY || !API_USER || !CLIENT_IP) {
5051
class NamecheapMcpServer {
5152
private server: Server;
5253
private namecheapClient: NamecheapClient;
54+
private tldCache: TldCache;
5355

5456
constructor() {
5557
this.server = new Server(
@@ -71,6 +73,8 @@ class NamecheapMcpServer {
7173
USE_SANDBOX
7274
);
7375

76+
this.tldCache = new TldCache(this.namecheapClient);
77+
7478
this.setupHandlers();
7579

7680
this.server.onerror = (error) => process.stderr.write(`[MCP Error] ${error}\n`);
@@ -237,7 +241,15 @@ class NamecheapMcpServer {
237241
}
238242

239243
private async handleDomainsGetTldList(args: DomainsGetTldListParams) {
240-
const result = await this.namecheapClient.domainsGetTldList(args);
244+
// Use the TldCache to get filtered and paginated results
245+
const result = await this.tldCache.getTlds({
246+
search: args.search,
247+
registerable: args.registerable,
248+
page: args.page,
249+
pageSize: args.pageSize,
250+
sortBy: args.sortBy,
251+
});
252+
241253
return {
242254
content: [
243255
{

src/tld-cache.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { NamecheapClient } from './namecheap-client.js';
2+
3+
interface TldInfo {
4+
name: string;
5+
isApiRegisterable: boolean;
6+
isRenewalAllowed: boolean;
7+
minRegisterYears: number;
8+
maxRegisterYears: number;
9+
minRenewYears: number;
10+
maxRenewYears: number;
11+
isTransferrable: boolean;
12+
transferLockDays: number;
13+
isPrivacyProtectionAllowed: boolean;
14+
isIdnSupported: boolean;
15+
supportedIdnLanguages?: string[];
16+
categories?: string[];
17+
isPremium?: boolean;
18+
}
19+
20+
export class TldCache {
21+
private cache: TldInfo[] | null = null;
22+
private cacheTimestamp: number = 0;
23+
private readonly CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours
24+
private readonly client: NamecheapClient;
25+
26+
constructor(client: NamecheapClient) {
27+
this.client = client;
28+
}
29+
30+
private async fetchAndParseTldList(): Promise<TldInfo[]> {
31+
const response = await this.client.domainsGetTldList();
32+
const tlds: TldInfo[] = [];
33+
34+
// Parse the XML response to extract TLD information
35+
if (response && typeof response === 'object' && 'raw' in response && typeof response.raw === 'string') {
36+
// Simple regex parsing for demonstration
37+
// In production, use a proper XML parser
38+
const tldMatches = response.raw.matchAll(/<Tld Name="([^"]+)"[^>]*>/g);
39+
40+
for (const match of tldMatches) {
41+
const tldElement = match[0];
42+
const name = match[1];
43+
44+
tlds.push({
45+
name,
46+
isApiRegisterable: tldElement.includes('IsApiRegisterable="true"'),
47+
isRenewalAllowed: tldElement.includes('IsApiRenewalAllowed="true"'),
48+
minRegisterYears: parseInt(tldElement.match(/MinRegisterYears="(\d+)"/)?.[1] || '1'),
49+
maxRegisterYears: parseInt(tldElement.match(/MaxRegisterYears="(\d+)"/)?.[1] || '10'),
50+
minRenewYears: parseInt(tldElement.match(/MinRenewYears="(\d+)"/)?.[1] || '1'),
51+
maxRenewYears: parseInt(tldElement.match(/MaxRenewYears="(\d+)"/)?.[1] || '10'),
52+
isTransferrable: !tldElement.includes('IsApiTransferrable="false"'),
53+
transferLockDays: parseInt(tldElement.match(/TransferLockDays="(\d+)"/)?.[1] || '60'),
54+
isPrivacyProtectionAllowed: !tldElement.includes('IsPrivacyProtectionAllowed="false"'),
55+
isIdnSupported: tldElement.includes('IsIdnSupported="true"'),
56+
isPremium: tldElement.includes('IsPremiumTLD="true"'),
57+
});
58+
}
59+
}
60+
61+
return tlds;
62+
}
63+
64+
private async ensureCache(): Promise<TldInfo[]> {
65+
const now = Date.now();
66+
67+
if (!this.cache || (now - this.cacheTimestamp) > this.CACHE_DURATION) {
68+
this.cache = await this.fetchAndParseTldList();
69+
this.cacheTimestamp = now;
70+
}
71+
72+
return this.cache;
73+
}
74+
75+
async getTlds(options?: {
76+
search?: string;
77+
category?: string;
78+
registerable?: boolean;
79+
page?: number;
80+
pageSize?: number;
81+
sortBy?: 'name' | 'popularity';
82+
}): Promise<{
83+
tlds: TldInfo[];
84+
totalCount: number;
85+
page: number;
86+
pageSize: number;
87+
totalPages: number;
88+
}> {
89+
const allTlds = await this.ensureCache();
90+
91+
// Apply filters
92+
let filteredTlds = [...allTlds];
93+
94+
if (options?.search) {
95+
const searchLower = options.search.toLowerCase();
96+
filteredTlds = filteredTlds.filter(tld =>
97+
tld.name.toLowerCase().includes(searchLower)
98+
);
99+
}
100+
101+
if (options?.registerable !== undefined) {
102+
filteredTlds = filteredTlds.filter(tld =>
103+
tld.isApiRegisterable === options.registerable
104+
);
105+
}
106+
107+
if (options?.category) {
108+
filteredTlds = filteredTlds.filter(tld =>
109+
tld.categories?.includes(options.category!)
110+
);
111+
}
112+
113+
// Sort
114+
if (options?.sortBy === 'name') {
115+
filteredTlds.sort((a, b) => a.name.localeCompare(b.name));
116+
}
117+
118+
// Paginate
119+
const page = options?.page || 1;
120+
const pageSize = options?.pageSize || 50;
121+
const totalCount = filteredTlds.length;
122+
const totalPages = Math.ceil(totalCount / pageSize);
123+
const startIndex = (page - 1) * pageSize;
124+
const endIndex = startIndex + pageSize;
125+
126+
const paginatedTlds = filteredTlds.slice(startIndex, endIndex);
127+
128+
return {
129+
tlds: paginatedTlds,
130+
totalCount,
131+
page,
132+
pageSize,
133+
totalPages,
134+
};
135+
}
136+
137+
async refreshCache(): Promise<void> {
138+
this.cache = null;
139+
this.cacheTimestamp = 0;
140+
await this.ensureCache();
141+
}
142+
}

src/tools.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,10 +198,36 @@ export const namecheapTools: Tool[] = [
198198
},
199199
{
200200
name: 'namecheap_domains_gettldlist',
201-
description: 'Get a list of all supported TLDs',
201+
description: 'Get a list of all supported TLDs with filtering and pagination',
202202
inputSchema: {
203203
type: 'object',
204-
properties: {},
204+
properties: {
205+
search: {
206+
type: 'string',
207+
description: 'Search for TLDs containing this text (e.g., "com", "org", "tech")',
208+
},
209+
registerable: {
210+
type: 'boolean',
211+
description: 'Filter to only show TLDs that can be registered via API',
212+
},
213+
page: {
214+
type: 'number',
215+
description: 'Page number for pagination (default: 1)',
216+
default: 1,
217+
},
218+
pageSize: {
219+
type: 'number',
220+
description: 'Number of TLDs per page (default: 50, max: 200)',
221+
default: 50,
222+
maximum: 200,
223+
},
224+
sortBy: {
225+
type: 'string',
226+
enum: ['name', 'popularity'],
227+
description: 'Sort TLDs by name or popularity',
228+
default: 'name',
229+
},
230+
},
205231
},
206232
},
207233
{

src/types.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,13 @@ export interface DomainsCreateParams {
9090
eapFee?: number;
9191
}
9292

93-
export type DomainsGetTldListParams = Record<string, never>;
93+
export interface DomainsGetTldListParams {
94+
search?: string;
95+
registerable?: boolean;
96+
page?: number;
97+
pageSize?: number;
98+
sortBy?: 'name' | 'popularity';
99+
}
94100

95101
export interface DomainsSetContactsParams {
96102
domainName: string;

0 commit comments

Comments
 (0)