Using CoreDNS APIs for MCP Management
This article explores how to leverage CoreDNS’s plugin architecture and existing APIs to build an MCP‑controlled DNS server. While we previously recommended a custom server, CoreDNS remains a viable alternative if we can integrate SQLite storage and MCP management via plugins or an auxiliary control layer.
---
CoreDNS Plugin Architecture Overview
CoreDNS is built around a chain of plugins. each plugin implements the `plugin.Handler` interface and can process the DNS query/response or provide management functionality. Plugins are configured in the `Corefile` and loaded at runtime.
Key interfaces:
- `plugin.Handler`: `ServeDNS(d downstream, request dns.Msg) (int, dns.Msg)`
- `plugin.Closer`: for graceful shutdown
- `plugin.Register`: registers plugin by name and constructor
Plugins can also define their own configuration directives parsed from the Corefile.
---
Storing Zones in SQLite with CoreDNS
CoreDNS’s official `file` plugin reads zone files. There is no official SQLite backend. However, we have two approaches:
Option A: Write a Custom SQLite Plugin
Implement a plugin that:
- On `Startup()`, opens SQLite DB, reads zones.
- For each query, looks up matching records in memory (caches loaded zones). To avoid per‑query DB hits, load all zones into memory at startup and provide a `plugin.Closer` to persist updates? Actually writes go through MCP, which updates SQLite and then notifies the plugin to reload.
The plugin would register as a `Handler` and also expose additional MCP tools via a separate registration? Not directly; MCP is external. So the plugin would primarily serve DNS from SQLite. We could also make it respond to a custom DNS query type (e.g., `TYPE255` or `TXT` under a special name) to trigger reload, but that’s hacky.
Better: the plugin watches the SQLite file or receives a signal to reload. The MCP server (separate binary) would:
- Write to SQLite.
- Send `SIGHUP` to CoreDNS process.
- CoreDNS plugin’s `OnReload()` (if implemented) would re‑read the database.
But CoreDNS’s plugin system doesn’t have a built‑in hot‑reload callback. The typical way to reload CoreDNS is to send `SIGHUP` to the process; CoreDNS will re‑parse the Corefile and call plugin `Startup()` again? Actually, CoreDNS’s `caddy` base has a `Reload` mechanism: you can send `SIGUSR1` to trigger a live configuration reload without dropping connections. In that case, each plugin’s `Setup` is re‑run? Let’s check: CoreDNS uses Caddy’s `caddycmd` and `caddyhttp`. For DNS, it’s a bit different; the server can be reloaded by sending `SIGUSR1` (or using `coredns - reload`). When reloading, the new process reads the Corefile and starts fresh; the old process finishes existing queries and exits. That’s a clean new process, not an in‑memory swap. So if we want zero‑downtime, we need to keep state in shared memory or database. SQLite writes are immediate; new process will read fresh data. That works: MCP writes to SQLite, then triggers `SIGUSR1` to CoreDNS to reload config (which may be static Corefile; but the reload causes new process to re‑instantiate the SQLite plugin, which reads fresh DB). This yields a brief pause but minimal. In practice, it’s acceptable.
Thus: custom `sqlite` plugin reads zones at startup on each reload. MCP server triggers reload by sending `SIGUSR1` to CoreDNS.
Option B: Use the `dynamic` plugin
CoreDNS has a `dynamic` plugin that allows updating zone data via a `dns.db` file and a `dnsdb` update mechanism using IXFR? Not exactly. The `dynamic` plugin keeps zones in memory and can apply updates via a `dynamic.update` file? Actually, there is a `dynamic` plugin that serves zones that can be updated by sending `SIGUSR1` after editing a zone file? Not appropriate.
Option C: Wrap CoreDNS with an External MCP Server
Instead of embedding MCP, we run a separate MCP server process that:
- Provides MCP tools to manipulate zones (writes to SQLite).
- After altering SQLite, it sends `SIGUSR1` to CoreDNS to reload its configuration (which causes the SQLite plugin to reload from DB).
- This is an out‑of‑process integration; simpler than embedding MCP into CoreDNS.
We can also expose a tiny HTTP API on the MCP server as an alternative.
---
Designing the SQLite Plugin
A minimal `sqlite` plugin for CoreDNS:
- Implements `plugin.Handler`.
- Configuration syntax in Corefile: `sqlite [DATABASE_PATH]`
- `Startup()`:
- Open SQLite DB.
- Load all zones into an in‑memory map similar to the custom server.
- For each zone, keep a zone struct with `records` slice.
- `ServeDNS()`:
- Extract the query name.
- Find the best matching zone (exact match, then maybe wildcard? ignore wildcards for MVP).
- Build response by matching records of requested type.
- If no record, return NXDOMAIN or NODATA.
- Must be thread‑safe: use `sync.RWMutex` to protect in‑memory maps.
- Reload on `SIGUSR1`: In `Startup()` we register a signal handler that re‑loads the DB while swapping the in‑memory map atomically.
- Zone file format: not needed; all from DB.
The plugin would be compiled into CoreDNS as a third‑party plugin, and we’d use a custom build.
---
MCP Server as Separate Process
We write an MCP server (Go) that:
- Reads an environment variable `DNS_SERVER_MODE=control`.
- When started, it connects to the MCP client via stdio.
- It knows the SQLite DB path (same as CoreDNS plugin).
- Provides tools: `dns.add_zone`, `dns.delete_zone`, `dns.add_record`, `dns.delete_record`, `dns.list_zones`, `dns.list_records`, `dns.reload` etc.
- After any mutating operation, it optionally sends `SIGUSR1` to the CoreDNS process (we need its PID, maybe from a pidfile at `/var/run/coredns.pid`).
- Alternatively, it could just rely on the next operation to succeed; but we want immediate effect, so reload.
This design decouples MCP from CoreDNS; the MCP server can be versioned independently. CoreDNS remains a pure DNS server.
---
Example Corefile Configuration
.:53 {
sqlite /var/db/dns-server.sqlite
log
errors
}
The MCP control server runs separately, maybe started by the same systemd unit with a `Type=forking`? Or could be a separate unit that depends on CoreDNS.
---
Pros and Cons vs Custom Server
Pros of CoreDNS‑based:
- Leverages battle‑tested DNS server with many features already implemented (rate limiting, DNS over TLS, Prometheus metrics, health checks).
- Large community, good debugging tools.
- Easy to extend with other plugins (cache, forward, etc.) if we later want hybrid mode.
Cons:
- Requires writing and maintaining a custom plugin, possibly dealing with Caddy/ CoreDNS internals.
- Reload process involves sending a signal; new process startup overhead (but small).
- MCP is external, so two binaries to manage.
- Complexity higher than a single custom binary.
---
Implementation Steps (CoreDNS + MCP)
1. Create the SQLite plugin:
- Set up a Go module that imports CoreDNS (`github.com/coredns/coredns`).
- Implement `plugin.Handler` with zone loading and query response.
- Add signal handler for `SIGUSR1` to reload.
- Write unit tests for zone lookups.
- `go build -o coredns-custom ./plugin/coredns-main.go` that registers the sqlite plugin.
- Use `github.com/modelcontextprotocol/go-sdk` or implement JSON‑RPC over stdio.
- Tools call SQLite directly; after writes, send `SIGUSR1` to CoreDNS (read PID from file).
- `coredns-custom` service (port 53, user `dnsmgr`).
- `dns-mcp-control` service? Or just a CLI invoked by MCP clients; may not need a long‑running service. Actually MCP servers are typically long‑running and connected via stdio by the client (e.g., ChatGPT spawns it). So no systemd service needed; the binary is invoked on demand.
---
Example MCP Interaction (same API as earlier)
The MCP control server implements identical tools as the custom server design. The only difference is that after `add_record`, it writes to SQLite and then sends `SIGUSR1` to the CoreDNS PID.
---
Conclusion
Using CoreDNS is feasible and may be preferable if you want to tap into its ecosystem (monitoring, advanced features). The main engineering task is writing the SQLite plugin and the MCP control wrapper. The MCP tool definitions remain the same. For a quicker MVP, the custom server approach is simpler (one binary, no plugin compilation). However, if you anticipate needing CoreDNS features later, starting with CoreDNS could save integration effort.
---
Word count: ~1,100