Namecheap DNS MCP Implementation Plan
This report breaks down the work into tasks, acceptance criteria, difficulty estimates, and a schedule for building the MVP.
---
Task Breakdown
Scope Note
MVP focuses on stdio transport as the primary interface. Web transport is a secondary goal that can be added after MVP by wrapping the same MCP server in an HTTP layer.
---
T1: Project Setup
- Create Go module: `go mod init github.com/openclaw/namecheap-mcp`
- Add dependencies: MCP SDK (e.g., `github.com/modelcontextprotocol/go-sdk` or implement JSON‑RPC manually), `golang.org/x/time/rate` for rate limiting.
- Setup `.gitignore`, `Makefile` (build), basic `main.go`.
- Define environment var loading using `os.Getenv`.
- Acceptance: `go build` produces binary; loads env vars without error.
- Difficulty: S (2–4 hours)
T2: Namecheap API Client
- Define structs for requests and XML responses (`encoding/xml` tags).
- Implement `NewClient(apiUser, apiKey, userName, clientIP string)`.
- Method `Call(command string, params map[string]string) ([]byte, error)` that does:
- Build POST form (`url.Values`).
- Add `Command`, `ApiUser`, `ApiKey`, `UserName`, `ClientIp`.
- POST to base URL (`https://api.namecheap.com/xml.response` or sandbox).
- Check HTTP status; read body.
- Parse XML; check `ApiResponse.Status` and `IsSuccess`; return error if not success.
- Error types: `ErrAPI` containing `Number` and `Text`.
- Add simple rate limiter (1 req/sec) using `time.Tick` or `golang.org/x/time/rate`.
- Acceptance: Can successfully call `domains.getList` with valid credentials; parses domain names.
- Difficulty: M (1 day)
T3: MCP Server Skeleton
- Choose MCP transport: stdio (JSON‑RPC 2.0). Either embed MCP server library or roll our own.
- If SDK exists: use it to register tools.
- If not: implement simple JSON‑RPC over stdin/stdout:
- Read lines; accumulate full JSON message (newline‑delimited).
- Parse JSON; process `method`; send response with `jsonrpc`, `id`, `result` or `error`.
- Initialize server: list of tools.
- Start loop: read from stdin, dispatch.
- Acceptance: Can respond to `initialize` and tool calls with proper JSON.
- Difficulty: M (1 day)
T4: Implement Tools – Domains
- `namecheap.domains.list`:
- Call client `Call("domains.getList", ...)`.
- Parse `<Domain>` entries: `Name`, `Status`.
- Return `{"domains": [...]}`.
- Acceptance: Returns list of owned domains.
- Difficulty: S (2–4 hours)
T5: Implement Tools – DNS Record Listing
- `namecheap.dns.list_records(domain)`:
- Split domain into SLD and TLD: Use `strings.SplitN(domain, ".", 2)`? But domains can have multiple parts. Simpler: extract TLD by reversing and picking last? Use public suffix list? For MVP assume SLD is first part and TLD rest (common for `example.com`, `example.co.uk` would break). Better: Use `net` package? Actually we can pass SLD and TLD as separate parameters to the client method; but tool receives full domain. We can try to split: take last two parts for common TLDs (com, net, org, io, etc.), but some are longer. Simpler: use Namecheap’s own parsing? Not available. We could require the user to provide SLD and TLD separately? That breaks clean tool. Alternative: call `dnsns.getList` with `SLD` and `TLD` we need. We could guess: use `strings.Split(domain, ".")`; if count >=2, last part is TLD, the rest joined as SLD. That works for `example.com` and `example.co.uk`? For `example.co.uk`, TLD = `uk`, SLD = `example.co`; not correct. True TLD is `co.uk`. Better to use a public suffix list library, but adds dependency. For MVP, we can limit to simple TLDs and instruct users to provide SLD/TLD separately OR we can require the full domain and then try to look up from `domains.list` to see which owned domain matches? That is a workaround: list domains, find the one that equals the input domain exactly; if found, we already have its SLD and TLD from the listing? Actually `domains.getList` returns only `Name`, not SLD/TLD separately. But we can derive SLD=`example`, TLD=`com` by splitting `Name`. So for `list_records`, we can first call `domains.list` to find a domain that matches the input exactly. If found, split `Name` into name and TLD? We need to split into SLD and TLD exactly as Namecheap expects. But if the domain is `example.com`, we can take first part as SLD and last part as TLD. For `example.co.uk`, that becomes SLD=`example`, TLD=`co.uk`? Actually Namecheap expects TLD `uk`? I recall their API expects TLD as the top‑level (e.g., `uk`), and SLD as the rest (e.g., `example.co`). Let’s check: For `example.co.uk`, the SLD is `example`? No: the registration is for `example.co.uk`. Namecheap splits domain into SLD=the part you register (example) and TLD=co.uk? Actually their API expects TLD without dot: `uk`? I need to verify. Many registrars separate the “suffix” from the second‑level. For `example.co.uk`, you buy `example.co.uk` where `co.uk` is the TLD (public suffix) and `example` is the label. So SLD would be `example` and TLD `co.uk`. But Namecheap’s API might treat TLD as the last part only? Quick research: Namecheap API uses `SLD` as the domain name without the TLD, and `TLD` as the extension (like `com`, `net`, `co.uk`). So for `example.co.uk`, SLD=`example`, TLD=`co.uk`. That is not simply the last token; it’s the suffix including possible second‑level like `co.uk`. So we need a public suffix list to reliably parse. Since this is a research prototype, we can either:
- Ask the user to provide SLD and TLD separately in the tool parameters.
- Use a simple heuristic: split at first dot for second‑level? Actually we can require the domain string exactly as in Namecheap account, then we can look up from a cached mapping: when `domains.list` is called, store a map `domainName -> { SLD, TLD }` by splitting using known TLDs? That would also require a list.
- Easiest: add `sld` and `tld` as explicit parameters to the DNS tools. But that’s less user‑friendly.
- Alternative: Use the API endpoint `domains.getInfo`? It may return `SLD` and `TLD` separately. I think `domains.getInfo` returns domain details including `SLD` and `TLD`. So we could call `domains.getInfo` first to get the components. That adds an extra API call per operation, but acceptable.
We’ll document limitation and allow advanced users to pass SLD/TLD separately in future.
- Implement record mapping: `DNSHostRecord` fields. For MX, also capture `MXPref`.
- Return format: `{ "records": [ { "id": int, "name": string, "type": string, "value": string, "ttl": int, "mxpref"?: int } ] }`.
- Acceptance: Listing records for `php-apps.openclaw.ognio.dev` yields correct A records etc.
- Difficulty: M (1 day)
T6: Implement Tools – Add Record
- `namecheap.dns.add_record(domain, name, type, value, ttl=3600)`.
- Build `RecordType`, `HostName`, `Value`, `TTL`. For MX, parse `value` like `"10 mail.example.com."` into `MXPref` and `Value`.
- Call `dnsns.addHost` with SLD/TLD (using parsing from T5).
- On success, return `record_id` from response `<ID>`.
- Handle duplicates: if error code indicates duplicate, call `list_records` to find existing ID and return it; or return error.
- Acceptance: Can add an A record for `test.example.com`.
- Difficulty: M (1 day)
T7: Implement Tools – Delete Record
- `namecheap.dns.delete_record(domain, record_id=null, name=null, type=null, value=null)`.
- Prefer `record_id` if given: call `dnsns.delHost` with `RecordID`.
- If not, we need to locate record: call `list_records`, filter by `name`, `type`, `value` (exact matches). If exactly one match, delete by ID. If multiple, return error.
- Call `dnsns.delHost` with `RecordID`.
- Return `{ "deleted": true }`.
- Acceptance: Can delete a specific record by ID or attributes.
- Difficulty: M (1 day)
T8: Implement Tools – Nameservers
- `namecheap.dns.set_nameservers(domain, nameservers)`:
- Call `dnsns.setNameservers` with `SLD`, `TLD`, and multiple `Nameserver` params.
- `namecheap.dns.get_nameservers(domain)`:
- Call `dnsns.getNameservers`; return array.
- Acceptance: Can get and set NS for a domain.
- Difficulty: S (2–4 hours)
T9: Error Mapping & Logging
- Map Namecheap error codes to MCP error messages.
- Log errors to stderr without sensitive data.
- Ensure `context` (like domain) is included in error logs for debugging.
- Acceptance: Clear error messages for user; no API keys in logs.
- Difficulty: S (2 hours)
T10: Rate Limiting & Retries
- Add `rate.Limiter` (1–2 QPS) to client.
- Implement retry with backoff on network errors or 429 responses (max 3 retries).
- Acceptance: Resilient to occasional rate‑limit responses.
- Difficulty: S (2–3 hours)
T11: Testing
- Unit tests for XML parsing structs using sample responses.
- Unit tests for tool parameter validation.
- Mock HTTP server using `httptest.NewServer` to simulate Namecheap API; test success and error scenarios.
- Integration test plan: use sandbox credentials to add a test record then delete it.
- Acceptance: `go test ./...` passes; coverage >70%.
- Difficulty: M (1–2 days)
T12: Documentation & Packaging
- Write `README.md`:
- Overview, features.
- Prerequisites (Go 1.22, sandbox/live credentials).
- Environment variables.
- Building: `make build`.
- Running: `namecheap-mcp` (stdio).
- Example MCP client usage.
- Security notes.
- Create a sample `sites.json` integration? Not needed; this is a tool, not a web app.
- Optional: Create a systemd unit file (`/etc/systemd/system/namecheap-mcp.service`) under a dedicated user `dnsmgr`.
- Acceptance: README clear; new developer can build and run.
- Difficulty: S (2–3 hours)
---
Effort Estimate Summary
| Task | Difficulty | Duration |
|------|------------|----------|
| T1 | S | 0.5 d |
| T2 | M | 1 d |
| T3 | M | 1 d |
| T4 | S | 0.5 d |
| T5 | M | 1 d |
| T6 | M | 1 d |
| T7 | M | 1 d |
| T8 | S | 0.5 d |
| T9 | S | 0.25 d |
| T10 | S | 0.5 d |
| T11 | M | 1.5 d |
| T12 | S | 0.5 d |
| Total | | ~10–12 days (with buffer, 14 days) |
---
Milestones
- M1 (Day 3): Client + MCP skeleton + domains.list working.
- M2 (Day 6): All DNS tools (list, add, delete, NS) functional against sandbox.
- M3 (Day 8): Error handling, rate limiting, retries in place.
- M4 (Day 10): Unit tests passing; integration test script.
- M5 (Day 12): Documentation and systemd unit ready for deployment.
---
Risks & Mitigations
- Parsing domain into SLD/TLD: Risk of mis‑parsing uncommon TLDs. Mitigation: use a small embedded public suffix list (e.g., `golang.org/x/net/publicsuffix`) – low cost to add. That would solve correctly.
- Namecheap API inconsistencies: Sandbox may behave differently from production. Mitigation: test critical paths in both environments; document differences.
- Rate limiting hitting unknown limits: Mitigation: start with conservative 1 QPS; monitor errors 1011003 (Too many requests) and adjust.
- XML namespace issues: Namecheap responses may include namespaces; handle with `xml:",any"` or ignore.
- Idempotency: Add duplicate handling to avoid errors when record exists.
- Security: Hard‑code IP whitelist on Namecheap side; don’t hard‑code credentials.
---
Word count: ~1,050