Skip to main content

dnslib/mcp/
server.rs

1use std::sync::Arc;
2
3use rmcp::{
4    ErrorData as McpError, ServerHandler,
5    handler::server::{router::tool::ToolRouter, wrapper::Parameters},
6    model::*,
7    tool, tool_handler, tool_router,
8};
9
10use crate::{
11    control_plane::{
12        config::AppConfig,
13        policy::{Policy, PolicyRule},
14        transfer,
15    },
16    mcp::{
17        helpers::mcp_err,
18        params::*,
19        tools::{
20            access_lists, cache as cache_tools, logs as logs_tools, records as record_tools,
21            resolve as resolve_tools, settings as settings_tools, stats as stats_tools,
22            sync as sync_tools, zones as zone_tools,
23        },
24    },
25    vendors::runtime::VendorClient,
26};
27
28// ─── Server state ─────────────────────────────────────────────────────────────
29
30#[derive(Clone)]
31pub struct DnsServer {
32    config: Arc<AppConfig>,
33    cli_access: Arc<Vec<PolicyRule>>,
34    cli_allow_zone: Arc<Vec<String>>,
35    startup_info: String,
36    #[allow(dead_code)]
37    tool_router: ToolRouter<Self>,
38}
39
40impl DnsServer {
41    /// Construct a `DnsServer` from the given application configuration and CLI-derived policy inputs.
42    ///
43    /// The created server stores the provided `config`, `cli_access`, and `cli_allow_zone` (each wrapped in `Arc`)
44    /// and computes a human-readable `startup_info` message that either lists available server IDs or instructs
45    /// how to add a server when none are configured.
46    ///
47    /// # Examples
48    ///
49    /// ```ignore
50    /// // Create a DnsServer from an AppConfig and CLI policy inputs.
51    /// // (Fields shown here are illustrative; construct AppConfig/PolicyRule as appropriate in real code.)
52    /// let config = AppConfig { servers: vec![] }; // or whatever constructor is available
53    /// let cli_access = Vec::<PolicyRule>::new();
54    /// let cli_allow_zone = Vec::<String>::new();
55    /// let server = DnsServer::new(config, cli_access, cli_allow_zone);
56    /// ```
57    pub fn new(
58        config: AppConfig,
59        cli_access: Vec<PolicyRule>,
60        cli_allow_zone: Vec<String>,
61    ) -> Self {
62        let startup_info = if config.servers.is_empty() {
63            " No DNS servers configured. Run `dns config add` to add one, then restart the MCP server.".to_string()
64        } else {
65            let ids: Vec<&str> = config.servers.iter().map(|s| s.id.as_str()).collect();
66            format!(
67                " Available servers: {}. Pass `server_id` to every tool.",
68                ids.join(", ")
69            )
70        };
71
72        Self {
73            config: Arc::new(config),
74            cli_access: Arc::new(cli_access),
75            cli_allow_zone: Arc::new(cli_allow_zone),
76            startup_info,
77            tool_router: Self::tool_router(),
78        }
79    }
80
81    /// Resolve a configured DNS backend by its identifier and produce a client and policy for calling it.
82    ///
83    /// Looks up `server_id` case-insensitively in the server list, constructs a `VendorClient` for that
84    /// server, and builds a `Policy` using the CLI-provided access and allow-zone rules. If the server
85    /// cannot be found, returns a configuration error advising the caller to list available server IDs.
86    ///
87    /// # Parameters
88    ///
89    /// - `server_id`: Case-insensitive identifier of the configured server to resolve.
90    ///
91    /// # Returns
92    ///
93    /// A `(VendorClient, Policy)` pair for the matched server.
94    ///
95    /// # Errors
96    ///
97    /// Returns a configuration `Error` if no server with the given `server_id` exists.
98    ///
99    /// # Examples
100    ///
101    /// ```ignore
102    /// # use std::sync::Arc;
103    /// # use crate::mcp::server::DnsServer;
104    /// # use crate::config::AppConfig;
105    /// // Given a DnsServer `srv` and a server id:
106    /// // let srv = DnsServer::new(app_config, vec![], vec![]);
107    /// // let (client, policy) = srv.resolve_server("primary")?;
108    /// ```
109    fn resolve_server(
110        &self,
111        server_id: &str,
112    ) -> crate::core::error::Result<(VendorClient, Policy)> {
113        let server = self
114            .config
115            .servers
116            .iter()
117            .find(|s| s.id.eq_ignore_ascii_case(server_id))
118            .ok_or_else(|| {
119                crate::core::error::Error::config(format!(
120                    "no server named '{server_id}' — call dns_list_servers to see available IDs"
121                ))
122            })?;
123        let client = VendorClient::from_server(server)?;
124        let policy = Policy::for_server(server, &self.cli_access, &self.cli_allow_zone)?;
125        Ok((client, policy))
126    }
127
128    fn show_settings_secrets(&self, server_id: &str) -> crate::core::error::Result<bool> {
129        self.config
130            .servers
131            .iter()
132            .find(|s| s.id.eq_ignore_ascii_case(server_id))
133            .map(|server| server.mcp.show_settings_secrets)
134            .ok_or_else(|| {
135                crate::core::error::Error::config(format!(
136                    "no server named '{server_id}' — call dns_list_servers to see available IDs"
137                ))
138            })
139    }
140}
141
142// ─── Tools ───────────────────────────────────────────────────────────────────
143
144#[tool_router]
145impl DnsServer {
146    // ── Server management ─────────────────────────────────────────────────
147
148    /// List configured DNS servers with their `id`, `vendor`, and `base_url`.
149    ///
150    /// Returns a JSON object with a `servers` array where each element is an object containing:
151    /// - `id`: the server identifier
152    /// - `vendor`: the vendor name formatted with `Debug`
153    /// - `base_url`: the server base URL, or the string `"(default)"` when no base URL is configured
154    ///
155    /// # Examples
156    ///
157    /// ```ignore
158    /// use serde_json::json;
159    ///
160    /// let expected_shape = json!({
161    ///     "servers": [ { "id": "example", "vendor": "VendorA", "base_url": "(default)" } ]
162    /// });
163    ///
164    /// assert!(expected_shape.get("servers").is_some());
165    /// ```
166    #[tool(description = "List all DNS servers defined in the config file. \
167    Shows each server's ID, vendor, and base URL. \
168    Call this first to discover server IDs — pass `server_id` to every other tool.")]
169    async fn dns_list_servers(&self) -> Result<CallToolResult, McpError> {
170        tracing::info!(tool = "dns_list_servers", "MCP tool invoked");
171
172        let servers: Vec<serde_json::Value> = self
173            .config
174            .servers
175            .iter()
176            .map(|s| {
177                serde_json::json!({
178                    "id": s.id,
179                    "vendor": format!("{:?}", s.vendor),
180                    "base_url": s.base_url.as_deref().unwrap_or("(default)"),
181                })
182            })
183            .collect();
184
185        Ok(crate::mcp::helpers::json_result(serde_json::json!({
186            "servers": servers,
187        })))
188    }
189
190    // ── Zones ─────────────────────────────────────────────────────────────
191
192    /// List authoritative zones hosted on the specified DNS server.
193    ///
194    /// The `server_id` field of the provided parameters selects which configured backend to query.
195    ///
196    /// # Returns
197    ///
198    /// `CallToolResult` containing a JSON object with the zones list on success, or an `McpError` on failure.
199    ///
200    /// # Examples
201    ///
202    /// ```ignore
203    /// # async fn example(server: &crate::mcp::DnsServer) -> Result<(), crate::core::error::McpError> {
204    /// let params = crate::mcp::ListZonesParams { server_id: "primary".into(), ..Default::default() };
205    /// let result = server.dns_list_zones(crate::mcp::Parameters(params)).await?;
206    /// println!("{:?}", result);
207    /// # Ok(())
208    /// # }
209    /// ```
210    #[tool(
211        description = "List all authoritative zones hosted on the DNS server. \
212    Use `server_id` from dns_list_servers."
213    )]
214    async fn dns_list_zones(
215        &self,
216        Parameters(p): Parameters<ListZonesParams>,
217    ) -> Result<CallToolResult, McpError> {
218        tracing::info!(tool = "dns_list_zones", "MCP tool invoked");
219        let (client, policy) = self.resolve_server(&p.server_id).map_err(mcp_err)?;
220        zone_tools::handle_list_zones(&client, &policy, p).await
221    }
222
223    /// Create a new DNS zone on a configured server.
224    ///
225    /// Supports zone types: Primary, Secondary, Stub, and Forwarder. The `server_id` field in the parameters
226    /// must reference one of the configured servers (discoverable via `dns_list_servers`).
227    ///
228    /// # Parameters
229    ///
230    /// - `p`: `CreateZoneParams` containing the zone definition and the target `server_id`.
231    ///
232    /// # Returns
233    ///
234    /// `CallToolResult` describing the outcome of the creation operation.
235    ///
236    /// # Examples
237    ///
238    /// ```ignore
239    /// # async fn example(dns_server: &crate::mcp::server::DnsServer) -> Result<(), crate::core::error::McpError> {
240    /// let params = crate::mcp::tools::zones::CreateZoneParams {
241    ///     server_id: "primary-1".into(),
242    ///     zone: "example.com".into(),
243    ///     zone_type: Some("Primary".into()),
244    ///     // ... other fields ...
245    /// };
246    /// let result = dns_server.dns_create_zone(crate::mcp::tools::Parameters(params)).await?;
247    /// # Ok(())
248    /// # }
249    /// ```
250    #[tool(
251        description = "Create a new DNS zone. Types: Primary, Secondary, Stub, Forwarder. \
252    Use `server_id` from dns_list_servers."
253    )]
254    async fn dns_create_zone(
255        &self,
256        Parameters(p): Parameters<CreateZoneParams>,
257    ) -> Result<CallToolResult, McpError> {
258        tracing::info!(tool = "dns_create_zone", "MCP tool invoked");
259        tracing::debug!(zone = %p.zone, "tool invoked");
260        let (client, policy) = self.resolve_server(&p.server_id).map_err(mcp_err)?;
261        zone_tools::handle_create_zone(&client, &policy, p).await
262    }
263
264    /// Deletes the specified DNS zone on the configured backend server.
265    ///
266    /// The `p` parameter must include `server_id` to select a configured DNS backend and `zone` to
267    /// identify the zone to remove. This operation is destructive and cannot be undone.
268    ///
269    /// # Parameters
270    ///
271    /// - `p`: `ZoneParams` containing `server_id` and the `zone` name to delete.
272    ///
273    /// # Returns
274    ///
275    /// `Ok(CallToolResult)` on success, `Err(McpError)` if the server cannot be resolved or deletion fails.
276    ///
277    /// # Examples
278    ///
279    /// ```ignore
280    /// # async fn example(server: &crate::mcp::server::DnsServer) -> Result<(), crate::mcp::error::McpError> {
281    /// use crate::mcp::params::ZoneParams;
282    /// use crate::mcp::server::Parameters;
283    ///
284    /// let params = ZoneParams { server_id: "primary".into(), zone: "example.com".into() };
285    /// let _result = server.dns_delete_zone(Parameters(params)).await?;
286    /// # Ok(())
287    /// # }
288    /// ```
289    #[tool(
290        description = "Delete a DNS zone. This is destructive and cannot be undone. \
291    Use `server_id` from dns_list_servers."
292    )]
293    async fn dns_delete_zone(
294        &self,
295        Parameters(p): Parameters<ZoneParams>,
296    ) -> Result<CallToolResult, McpError> {
297        tracing::info!(tool = "dns_delete_zone", "MCP tool invoked");
298        tracing::debug!(zone = %p.zone, "tool invoked");
299        let (client, policy) = self.resolve_server(&p.server_id).map_err(mcp_err)?;
300        zone_tools::handle_delete_zone(&client, &policy, p).await
301    }
302
303    /// Enables a previously disabled DNS zone on the specified server.
304    ///
305    /// The `server_id` must match one of the IDs returned by `dns_list_servers`.
306    ///
307    /// # Examples
308    ///
309    /// ```ignore
310    /// // Example usage (async context):
311    /// // dns_enable_zone(&server, Parameters(ZoneParams { server_id: "prod-dns".into(), zone: "example.com".into() })).await?;
312    /// ```
313    #[tool(description = "Enable a previously disabled DNS zone. \
314    Use `server_id` from dns_list_servers.")]
315    async fn dns_enable_zone(
316        &self,
317        Parameters(p): Parameters<ZoneParams>,
318    ) -> Result<CallToolResult, McpError> {
319        tracing::info!(tool = "dns_enable_zone", "MCP tool invoked");
320        tracing::debug!(zone = %p.zone, "tool invoked");
321        let (client, policy) = self.resolve_server(&p.server_id).map_err(mcp_err)?;
322        zone_tools::handle_enable_zone(&client, &policy, p).await
323    }
324
325    /// Disable a DNS zone so it stops responding to queries.
326    ///
327    /// The `server_id` field in `ZoneParams` selects which configured backend to operate on;
328    /// discover valid IDs with `dns_list_servers`.
329    ///
330    /// # Examples
331    ///
332    /// ```ignore
333    /// // Disable zone "example.com" on server "primary"
334    /// let params = ZoneParams { server_id: "primary".into(), zone: "example.com".into() };
335    /// let _res = dns_server.dns_disable_zone(Parameters(params)).await;
336    /// ```
337    ///
338    /// Returns `Ok(CallToolResult)` on success, `Err(McpError)` on failure.
339    #[tool(description = "Disable a DNS zone so it stops responding to queries. \
340    Use `server_id` from dns_list_servers.")]
341    async fn dns_disable_zone(
342        &self,
343        Parameters(p): Parameters<ZoneParams>,
344    ) -> Result<CallToolResult, McpError> {
345        tracing::info!(tool = "dns_disable_zone", "MCP tool invoked");
346        tracing::debug!(zone = %p.zone, "tool invoked");
347        let (client, policy) = self.resolve_server(&p.server_id).map_err(mcp_err)?;
348        zone_tools::handle_disable_zone(&client, &policy, p).await
349    }
350
351    /// Imports an RFC 1035 zone file into an existing zone.
352    ///
353    /// This replaces or merges the zone's records according to the `overwrite_zone` flag:
354    /// - If `overwrite_zone` is `true`, existing records for the zone are deleted before import.
355    /// - If `overwrite_zone` is `false`, records from the zone file are added/updated alongside existing records.
356    ///
357    /// The `server_id` parameter selects which configured DNS backend to target (see `dns_list_servers`).
358    ///
359    /// # Examples
360    ///
361    /// ```ignore
362    /// use crate::mcp::params::ImportZoneFileParams;
363    /// // construct params with `zone`, `content`, `overwrite_zone`, and `server_id`
364    /// let params = ImportZoneFileParams { zone: "example.com".into(), content: "$ORIGIN example.com.\n...".into(), overwrite_zone: true, server_id: "primary".into() };
365    /// // `srv` is a `DnsServer` instance; call from an async context
366    /// let _res = tokio::runtime::Runtime::new().unwrap().block_on(async { srv.dns_import_zone_file(Parameters(params)).await });
367    /// ```
368    #[tool(
369        description = "Import a zone file (RFC 1035 format) into an existing zone. \
370    Pass the full zone file text in `content`. Use `overwrite_zone: true` for a clean \
371    replace that deletes all existing records first. \
372    Use `server_id` from dns_list_servers."
373    )]
374    async fn dns_import_zone_file(
375        &self,
376        Parameters(p): Parameters<ImportZoneFileParams>,
377    ) -> Result<CallToolResult, McpError> {
378        tracing::info!(tool = "dns_import_zone_file", "MCP tool invoked");
379        tracing::debug!(zone = %p.zone, "tool invoked");
380        let (client, policy) = self.resolve_server(&p.server_id).map_err(mcp_err)?;
381        zone_tools::handle_import_zone_file(&client, &policy, p).await
382    }
383
384    /// Export a DNS zone in BIND (RFC 1035) zone file format.
385    ///
386    /// The specified `server_id` selects which configured DNS backend to query; use `dns_list_servers` to discover available IDs.
387    /// The returned result contains the complete zone file text suitable for saving to disk or importing into another DNS provider.
388    ///
389    /// # Examples
390    ///
391    /// ```ignore
392    /// # async fn example(server: &crate::mcp::server::DnsServer) -> Result<(), crate::core::error::McpError> {
393    /// use crate::mcp::tools::Parameters;
394    /// use crate::mcp::params::ExportZoneFileParams;
395    ///
396    /// let params = ExportZoneFileParams { server_id: "primary".into(), zone: "example.com".into() };
397    /// let res = server.dns_export_zone_file(Parameters(params)).await?;
398    /// // `res` contains the exported zone file text (BIND format)
399    /// # Ok(())
400    /// # }
401    /// ```
402    #[tool(
403        description = "Export a DNS zone as a BIND-format (RFC 1035) zone file. \
404    Returns the full zone file text, which can be saved to disk or imported into another DNS provider. \
405    Use `server_id` from dns_list_servers."
406    )]
407    async fn dns_export_zone_file(
408        &self,
409        Parameters(p): Parameters<ExportZoneFileParams>,
410    ) -> Result<CallToolResult, McpError> {
411        tracing::info!(tool = "dns_export_zone_file", "MCP tool invoked");
412        tracing::debug!(zone = %p.zone, "tool invoked");
413        let (client, policy) = self.resolve_server(&p.server_id).map_err(mcp_err)?;
414        zone_tools::handle_export_zone_file(&client, &policy, p).await
415    }
416
417    #[tool(description = "Copy a zone from one configured server to another. \
418    Reads from `from`, writes to `to`, and respects each server's MCP permissions and allowed zones.")]
419    async fn dns_transfer_zone(
420        &self,
421        Parameters(p): Parameters<TransferZoneParams>,
422    ) -> Result<CallToolResult, McpError> {
423        tracing::info!(tool = "dns_transfer_zone", "MCP tool invoked");
424        let (_, from_policy) = self.resolve_server(&p.from).map_err(mcp_err)?;
425        let (_, to_policy) = self.resolve_server(&p.to).map_err(mcp_err)?;
426        let check = from_policy
427            .check_read()
428            .and(from_policy.check_zone(&p.zone))
429            .and(to_policy.check_write())
430            .and(to_policy.check_zone(&p.zone));
431        Ok(
432            crate::mcp::helpers::run_json("dns_transfer_zone", check, async move {
433                let result = transfer::transfer_zone(
434                    Some(&self.config),
435                    &p.zone,
436                    &p.from,
437                    &p.to,
438                    p.overwrite,
439                    p.overwrite_zone,
440                )
441                .await?;
442                serde_json::to_value(result).map_err(|e| {
443                    crate::core::error::Error::parse(format!(
444                        "could not serialise zone transfer result: {e}"
445                    ))
446                })
447            })
448            .await,
449        )
450    }
451
452    // ── Records ───────────────────────────────────────────────────────────
453
454    /// List DNS records for a domain, returning typed records including writable and DNSSEC types.
455    ///
456    /// Returns a JSON result containing the domain's DNS records suitable for display and editing.
457    /// The returned set includes writable record types (for example: `A`, `AAAA`, `MX`, etc.)
458    /// and read-only DNSSEC records (`DNSKEY`, `RRSIG`, `NSEC`, `NSEC3`).
459    ///
460    /// # Examples
461    ///
462    /// ```ignore
463    /// // Example (pseudo): call the tool with a server_id and domain.
464    /// // let srv = DnsServer::new(...);
465    /// // let params = ListRecordsParams { server_id: "primary".into(), domain: "example.com".into(), zone: None };
466    /// // let result = srv.dns_list_records(Parameters(params)).await?;
467    /// ```
468    #[tool(
469        description = "List all DNS records for a domain. Returns typed records including writable types (A, AAAA, MX, etc.) and read-only DNSSEC types (DNSKEY, RRSIG, NSEC, NSEC3). \
470    Use `server_id` from dns_list_servers."
471    )]
472    async fn dns_list_records(
473        &self,
474        Parameters(p): Parameters<ListRecordsParams>,
475    ) -> Result<CallToolResult, McpError> {
476        tracing::info!(tool = "dns_list_records", "MCP tool invoked");
477        tracing::debug!(domain = ?p.domain, zone = ?p.zone, "tool invoked");
478        let (client, policy) = self.resolve_server(&p.server_id).map_err(mcp_err)?;
479        record_tools::handle_list_records(&client, &policy, p).await
480    }
481
482    /// Adds a DNS record to a zone on the specified server.
483    ///
484    /// The operation applies the provided `record` (typed union: e.g. `A`, `MX`, `TXT`) to `zone`/`domain` on the server identified by `server_id`.
485    ///
486    /// # Examples
487    ///
488    /// ```ignore
489    /// # use crate::mcp::server::DnsServer;
490    /// # use crate::mcp::params::{AddRecordParams, Record};
491    /// # use crate::mcp::tools::Parameters;
492    /// # async fn _example(server: &DnsServer) {
493    /// let params = AddRecordParams {
494    ///     server_id: "primary".to_string(),
495    ///     zone: "example.com".to_string(),
496    ///     domain: "www".to_string(),
497    ///     record: Record::A { ip: "1.2.3.4".to_string() },
498    /// };
499    /// let result = server.dns_add_record(Parameters(params)).await;
500    /// assert!(result.is_ok());
501    /// # }
502    /// ```
503    #[tool(
504        description = "Add a DNS record. The `record` field is a typed union: {\"type\":\"A\",\"ip\":\"1.2.3.4\"}, {\"type\":\"MX\",\"exchange\":\"mail.example.com\",\"preference\":10}, {\"type\":\"TXT\",\"text\":\"...\"}, etc. \
505    Use `server_id` from dns_list_servers."
506    )]
507    async fn dns_add_record(
508        &self,
509        Parameters(p): Parameters<AddRecordParams>,
510    ) -> Result<CallToolResult, McpError> {
511        tracing::info!(tool = "dns_add_record", "MCP tool invoked");
512        tracing::debug!(zone = %p.zone, domain = %p.domain, "tool invoked");
513        let (client, policy) = self.resolve_server(&p.server_id).map_err(mcp_err)?;
514        record_tools::handle_add_record(&client, &policy, p).await
515    }
516
517    /// Delete one or more DNS records for a configured server.
518    ///
519    /// The `server_id` field in the parameters selects which configured backend to use (see `dns_list_servers`).
520    /// If only `type` is provided, all records of that type for the specified domain are deleted; providing value fields
521    /// (for example an IP address for an A record) narrows the deletion to matching records.
522    ///
523    /// # Examples
524    ///
525    /// ```ignore
526    /// # async fn example(srv: &DnsServer) {
527    /// let params = DeleteRecordParams {
528    ///     server_id: "primary".into(),
529    ///     zone: "example.com".into(),
530    ///     domain: "www".into(),
531    ///     r#type: "A".into(),
532    ///     ip_address: Some("1.2.3.4".into()),
533    ///     ..Default::default()
534    /// };
535    /// let res = srv.dns_delete_record(Parameters(params)).await;
536    /// assert!(res.is_ok());
537    /// # }
538    /// ```
539    #[tool(
540        description = "Delete DNS record(s). Only `type` is required \u{2014} omitting value fields \
541    deletes ALL records of that type for the domain. \
542    e.g. {\"type\":\"A\"} deletes all A records; {\"type\":\"A\",\"ipAddress\":\"1.2.3.4\"} deletes one specific record. \
543    Use `server_id` from dns_list_servers."
544    )]
545    async fn dns_delete_record(
546        &self,
547        Parameters(p): Parameters<DeleteRecordParams>,
548    ) -> Result<CallToolResult, McpError> {
549        tracing::info!(tool = "dns_delete_record", "MCP tool invoked");
550        tracing::debug!(zone = %p.zone, domain = %p.domain, "tool invoked");
551        let (client, policy) = self.resolve_server(&p.server_id).map_err(mcp_err)?;
552        record_tools::handle_delete_record(&client, &policy, p).await
553    }
554
555    // ── Cache ─────────────────────────────────────────────────────────────
556
557    /// List entries in the DNS cache for a specific configured server and domain.
558    ///
559    /// If the `domain` parameter is an empty string, the root cache is listed. Use a `server_id` obtained from `dns_list_servers`.
560    ///
561    /// # Parameters
562    ///
563    /// - `p`: Parameters containing `server_id` (the configured server to query) and `domain` (the domain to list).
564    ///
565    /// # Returns
566    ///
567    /// `CallToolResult` containing the cache entries for the specified server and domain, or an `McpError` on failure.
568    ///
569    /// # Examples
570    ///
571    /// ```ignore
572    /// // Async context required:
573    /// // let params = Parameters(DomainParams { server_id: "primary".into(), domain: "".into() });
574    /// // let result = dns_server.dns_list_cache(params).await?;
575    /// ```
576    #[tool(
577        description = "Browse the DNS cache. Pass an empty string for domain to list the root. \
578    Use `server_id` from dns_list_servers."
579    )]
580    async fn dns_list_cache(
581        &self,
582        Parameters(p): Parameters<DomainParams>,
583    ) -> Result<CallToolResult, McpError> {
584        tracing::info!(tool = "dns_list_cache", "MCP tool invoked");
585        tracing::debug!(domain = %p.domain, "tool invoked");
586        let (client, policy) = self.resolve_server(&p.server_id).map_err(mcp_err)?;
587        cache_tools::handle_list_cache(&client, &policy, p).await
588    }
589
590    /// Evicts the DNS cache for a specific domain on the targeted server.
591    ///
592    /// The `server_id` field in the parameters selects which configured DNS backend to operate on;
593    /// discover available IDs with `dns_list_servers`.
594    ///
595    /// # Returns
596    ///
597    /// `CallToolResult` on success, `McpError` on failure.
598    ///
599    /// # Examples
600    ///
601    /// ```ignore
602    /// # use crate::mcp::server::DnsServer;
603    /// # use crate::mcp::params::DomainParams;
604    /// # use crate::mcp::tools::Parameters;
605    /// # async fn example(server: &DnsServer) {
606    /// let params = DomainParams { server_id: "primary".into(), domain: "example.com".into() };
607    /// let res = server.dns_delete_cache_zone(Parameters(params)).await;
608    /// match res {
609    ///     Ok(result) => println!("Evicted cache: {:?}", result),
610    ///     Err(err) => eprintln!("Failed to evict cache: {:?}", err),
611    /// }
612    /// # }
613    /// ```
614    #[tool(description = "Evict a specific domain from the DNS cache. \
615    Use `server_id` from dns_list_servers.")]
616    async fn dns_delete_cache_zone(
617        &self,
618        Parameters(p): Parameters<DomainParams>,
619    ) -> Result<CallToolResult, McpError> {
620        tracing::info!(tool = "dns_delete_cache_zone", "MCP tool invoked");
621        tracing::debug!(domain = %p.domain, "tool invoked");
622        let (client, policy) = self.resolve_server(&p.server_id).map_err(mcp_err)?;
623        cache_tools::handle_delete_cache_zone(&client, &policy, p).await
624    }
625
626    /// Flushes the DNS cache for the configured server identified by `server_id`.
627    ///
628    /// Use a `server_id` returned by `dns_list_servers` to target the correct backend.
629    ///
630    /// Returns a `CallToolResult` on success, or an `McpError` if the server cannot be resolved or the flush fails.
631    ///
632    /// # Examples
633    ///
634    /// ```ignore
635    /// # use crate::mcp::server::DnsServer;
636    /// # use crate::mcp::params::ServerScopeParams;
637    /// # async fn example(server: &DnsServer) -> Result<(), crate::core::error::McpError> {
638    /// let params = ServerScopeParams { server_id: "main".to_string() };
639    /// let result = server.dns_flush_cache(crate::mcp::tools::Parameters(params)).await?;
640    /// # Ok(())
641    /// # }
642    /// ```
643    #[tool(
644        description = "Flush the entire DNS cache, forcing all records to be resolved fresh. \
645    Use `server_id` from dns_list_servers."
646    )]
647    async fn dns_flush_cache(
648        &self,
649        Parameters(p): Parameters<ServerScopeParams>,
650    ) -> Result<CallToolResult, McpError> {
651        tracing::info!(tool = "dns_flush_cache", "MCP tool invoked");
652        let (client, policy) = self.resolve_server(&p.server_id).map_err(mcp_err)?;
653        cache_tools::handle_flush_cache(&client, &policy).await
654    }
655
656    // ── Stats ─────────────────────────────────────────────────────────────
657
658    /// Fetches dashboard statistics for a configured DNS server.
659    ///
660    /// Requests metrics for the server identified by `server_id`. The `stats_type` selects the
661    /// time range and defaults to `"LastDay"` when unset. Valid `stats_type` values are
662    /// `"LastHour"`, `"LastDay"`, `"LastWeek"`, `"LastMonth"`, and `"LastYear"`.
663    ///
664    /// # Parameters
665    ///
666    /// - `p.server_id` — Identifier of a configured DNS server (discoverable via `dns_list_servers`).
667    /// - `p.stats_type` — Optional time range for the statistics; defaults to `"LastDay"`.
668    ///
669    /// # Returns
670    ///
671    /// `CallToolResult` containing the requested statistics payload.
672    ///
673    /// # Examples
674    ///
675    /// ```ignore
676    /// # async fn example(dns_server: &crate::mcp::server::DnsServer) {
677    /// use crate::mcp::params::{Parameters, StatsParams};
678    ///
679    /// let params = Parameters(StatsParams {
680    ///     server_id: "primary".to_string(),
681    ///     stats_type: None,
682    /// });
683    ///
684    /// let result = dns_server.dns_get_stats(params).await;
685    /// assert!(result.is_ok());
686    /// # }
687    /// ```
688    #[tool(
689        description = "Get dashboard statistics. stats_type: LastHour, LastDay, LastWeek, LastMonth, LastYear. \
690    Use `server_id` from dns_list_servers."
691    )]
692    async fn dns_get_stats(
693        &self,
694        Parameters(p): Parameters<StatsParams>,
695    ) -> Result<CallToolResult, McpError> {
696        tracing::info!(tool = "dns_get_stats", "MCP tool invoked");
697        tracing::debug!(
698            stats_type = p.stats_type.as_deref().unwrap_or("LastDay"),
699            "tool invoked"
700        );
701        let (client, policy) = self.resolve_server(&p.server_id).map_err(mcp_err)?;
702        stats_tools::handle_get_stats(&client, &policy, p).await
703    }
704
705    // ── Blocked ───────────────────────────────────────────────────────────
706
707    /// List manually blocked domain names for a configured server.
708    ///
709    /// Resolves the target server by `server_id` and returns the blocked-domain list as a tool call result.
710    ///
711    /// # Examples
712    ///
713    /// ```ignore
714    /// # use crate::mcp::server::DnsServer;
715    /// # use crate::mcp::params::ServerScopeParams;
716    /// # use crate::mcp::tools::Parameters;
717    /// # async fn example(server: &DnsServer) -> Result<(), crate::core::error::McpError> {
718    /// let params = Parameters(ServerScopeParams { server_id: "primary".into() });
719    /// let res = server.dns_list_blocked_zones(params).await?;
720    /// println!("{}", res);
721    /// # Ok(())
722    /// # }
723    /// ```
724    #[tool(description = "List all manually blocked domains. \
725    Use `server_id` from dns_list_servers.")]
726    async fn dns_list_blocked_zones(
727        &self,
728        Parameters(p): Parameters<ServerScopeParams>,
729    ) -> Result<CallToolResult, McpError> {
730        tracing::info!(tool = "dns_list_blocked_zones", "MCP tool invoked");
731        let (client, policy) = self.resolve_server(&p.server_id).map_err(mcp_err)?;
732        access_lists::handle_list_blocked(&client, &policy).await
733    }
734
735    /// Adds a domain to the specified server's blocked list so that the DNS server will refuse to resolve it.
736    ///
737    /// The `DomainParams.server_id` selects which configured DNS server to affect (discoverable via `dns_list_servers`).
738    ///
739    /// # Returns
740    ///
741    /// `Ok(CallToolResult)` on success, `Err(McpError)` on failure.
742    ///
743    /// # Examples
744    ///
745    /// ```ignore
746    /// # use crate::mcp::params::DomainParams;
747    /// # async fn example(server: &crate::mcp::server::DnsServer) {
748    /// let params = DomainParams { server_id: "default".into(), domain: "example.com".into() };
749    /// let res = server.dns_add_blocked_zone(crate::mcp::Parameters(params)).await;
750    /// assert!(res.is_ok());
751    /// # }
752    /// ```
753    #[tool(
754        description = "Block a domain, causing the DNS server to refuse to resolve it. \
755    Use `server_id` from dns_list_servers."
756    )]
757    async fn dns_add_blocked_zone(
758        &self,
759        Parameters(p): Parameters<DomainParams>,
760    ) -> Result<CallToolResult, McpError> {
761        tracing::info!(tool = "dns_add_blocked_zone", "MCP tool invoked");
762        tracing::debug!(domain = %p.domain, "tool invoked");
763        let (client, policy) = self.resolve_server(&p.server_id).map_err(mcp_err)?;
764        access_lists::handle_add_blocked(&client, &policy, p).await
765    }
766
767    /// Remove a domain from a server's manual blocked (deny) list.
768    ///
769    /// The `server_id` identifies which configured DNS backend to operate on; use
770    /// `dns_list_servers` to discover available IDs.
771    ///
772    /// # Examples
773    ///
774    /// ```ignore
775    /// # use crate::mcp::server::DnsServer;
776    /// # use crate::mcp::params::DomainParams;
777    /// # use crate::mcp::tools::CallToolResult;
778    /// # use crate::mcp::error::McpError;
779    /// # use axum::extract::Parameters;
780    /// # #[tokio::main]
781    /// # async fn main() -> Result<(), McpError> {
782    /// let server = /* construct or obtain a DnsServer instance */;
783    /// let params = DomainParams { server_id: "primary".into(), domain: "example.com".into() };
784    /// let result: CallToolResult = server.dns_delete_blocked_zone(Parameters(params)).await?;
785    /// # Ok(()) }
786    /// ```
787    #[tool(description = "Unblock a domain. \
788    Use `server_id` from dns_list_servers.")]
789    async fn dns_delete_blocked_zone(
790        &self,
791        Parameters(p): Parameters<DomainParams>,
792    ) -> Result<CallToolResult, McpError> {
793        tracing::info!(tool = "dns_delete_blocked_zone", "MCP tool invoked");
794        tracing::debug!(domain = %p.domain, "tool invoked");
795        let (client, policy) = self.resolve_server(&p.server_id).map_err(mcp_err)?;
796        access_lists::handle_delete_blocked(&client, &policy, p).await
797    }
798
799    // ── Allowed ───────────────────────────────────────────────────────────
800
801    /// List whitelisted (allowed) domains for the specified DNS server.
802    ///
803    /// # Returns
804    ///
805    /// `Ok(CallToolResult)` containing the allowed domains list, `Err(McpError)` on failure.
806    ///
807    /// # Examples
808    ///
809    /// ```ignore
810    /// // In an async context:
811    /// let params = ServerScopeParams { server_id: "example-server".to_string() };
812    /// let result = server.dns_list_allowed_zones(Parameters(params)).await;
813    /// assert!(result.is_ok());
814    /// ```
815    #[tool(description = "List all whitelisted domains. \
816    Use `server_id` from dns_list_servers.")]
817    async fn dns_list_allowed_zones(
818        &self,
819        Parameters(p): Parameters<ServerScopeParams>,
820    ) -> Result<CallToolResult, McpError> {
821        tracing::info!(tool = "dns_list_allowed_zones", "MCP tool invoked");
822        let (client, policy) = self.resolve_server(&p.server_id).map_err(mcp_err)?;
823        access_lists::handle_list_allowed(&client, &policy).await
824    }
825
826    /// Adds a domain to a server's allow list so it will be permitted even if present on a block list.
827    ///
828    /// The `DomainParams.server_id` selects which configured DNS server to act on (discoverable via `dns_list_servers`), and `DomainParams.domain` is the domain to allow.
829    ///
830    /// # Examples
831    ///
832    /// ```ignore
833    /// // Prepare parameters and call the tool
834    /// let params = DomainParams { server_id: "prod".into(), domain: "example.com".into() };
835    /// let result = dns_server.dns_add_allowed_zone(Parameters(params)).await?;
836    /// ```
837    #[tool(
838        description = "Whitelist a domain, allowing it even if it appears on a block list. \
839    Use `server_id` from dns_list_servers."
840    )]
841    async fn dns_add_allowed_zone(
842        &self,
843        Parameters(p): Parameters<DomainParams>,
844    ) -> Result<CallToolResult, McpError> {
845        tracing::info!(tool = "dns_add_allowed_zone", "MCP tool invoked");
846        tracing::debug!(domain = %p.domain, "tool invoked");
847        let (client, policy) = self.resolve_server(&p.server_id).map_err(mcp_err)?;
848        access_lists::handle_add_allowed(&client, &policy, p).await
849    }
850
851    /// Removes a domain from a server's allow list (whitelist).
852    ///
853    /// # Examples
854    ///
855    /// ```ignore
856    /// # use crate::mcp::server::DnsServer;
857    /// # use crate::mcp::params::DomainParams;
858    /// # async fn example(srv: &DnsServer) {
859    /// let params = DomainParams { server_id: "server1".into(), domain: "example.com".into() };
860    /// // Call the tool and await the result
861    /// let _ = srv.dns_delete_allowed_zone(crate::mcp::server::Parameters(params)).await;
862    /// # }
863    /// ```
864    #[tool(description = "Remove a domain from the whitelist. \
865    Use `server_id` from dns_list_servers.")]
866    async fn dns_delete_allowed_zone(
867        &self,
868        Parameters(p): Parameters<DomainParams>,
869    ) -> Result<CallToolResult, McpError> {
870        tracing::info!(tool = "dns_delete_allowed_zone", "MCP tool invoked");
871        tracing::debug!(domain = %p.domain, "tool invoked");
872        let (client, policy) = self.resolve_server(&p.server_id).map_err(mcp_err)?;
873        access_lists::handle_delete_allowed(&client, &policy, p).await
874    }
875
876    // ── Settings ──────────────────────────────────────────────────────────
877
878    /// Retrieve the active DNS configuration for the specified server.
879    ///
880    /// The `server_id` in `ServerScopeParams` must identify one of the configured servers (see `dns_list_servers`).
881    ///
882    /// On success returns a `CallToolResult` containing the server settings; on failure returns an `McpError`.
883    ///
884    /// # Examples
885    ///
886    /// ```ignore
887    /// # async fn example_usage(server: &crate::mcp::server::DnsServer) {
888    /// use axum::extract::Parameters;
889    /// use crate::mcp::params::ServerScopeParams;
890    ///
891    /// let params = ServerScopeParams { server_id: "primary".to_string() };
892    /// let res = server.dns_get_settings(Parameters(params)).await;
893    /// // `res` is `Ok(CallToolResult)` on success
894    /// # }
895    /// ```
896    #[tool(description = "Get the current DNS server configuration. \
897    Use `server_id` from dns_list_servers.")]
898    async fn dns_get_settings(
899        &self,
900        Parameters(p): Parameters<ServerScopeParams>,
901    ) -> Result<CallToolResult, McpError> {
902        tracing::info!(tool = "dns_get_settings", "MCP tool invoked");
903        let (client, policy) = self.resolve_server(&p.server_id).map_err(mcp_err)?;
904        let show_secrets = self.show_settings_secrets(&p.server_id).map_err(mcp_err)?;
905        settings_tools::handle_get_settings(&client, &policy, show_secrets).await
906    }
907
908    // ── Logs ──────────────────────────────────────────────────────────────
909
910    /// Retrieve DNS server logs from the specified configured backend.
911    #[tool(
912        description = "Get DNS server logs. Use `server_id` from dns_list_servers. \
913    Optional filters: lines, start, end, and level."
914    )]
915    async fn dns_logs(
916        &self,
917        Parameters(p): Parameters<LogsParams>,
918    ) -> Result<CallToolResult, McpError> {
919        tracing::info!(tool = "dns_logs", "MCP tool invoked");
920        let (client, policy) = self.resolve_server(&p.server_id).map_err(mcp_err)?;
921        logs_tools::handle_logs(&client, &policy, p).await
922    }
923
924    // ── Sync ──────────────────────────────────────────────────────────────
925
926    #[tool(description = "Sync records between two configured servers. \
927    Dry-run by default; set `apply` to true to write changes.")]
928    async fn dns_sync(
929        &self,
930        Parameters(p): Parameters<SyncParams>,
931    ) -> Result<CallToolResult, McpError> {
932        tracing::info!(tool = "dns_sync", "MCP tool invoked");
933        let profile = p.profile.as_deref().and_then(|name| {
934            self.config
935                .sync
936                .iter()
937                .find(|profile| profile.name.eq_ignore_ascii_case(name))
938        });
939        let from_id = p
940            .from
941            .as_deref()
942            .or_else(|| profile.map(|profile| profile.from.as_str()))
943            .ok_or_else(|| {
944                mcp_err(crate::core::error::Error::parse(
945                    "sync requires a source server: name a profile or pass from",
946                ))
947            })?;
948        let to_id =
949            p.to.as_deref()
950                .or_else(|| profile.map(|profile| profile.to.as_str()))
951                .ok_or_else(|| {
952                    mcp_err(crate::core::error::Error::parse(
953                        "sync requires a destination server: name a profile or pass to",
954                    ))
955                })?;
956        let (_, from_policy) = self.resolve_server(from_id).map_err(mcp_err)?;
957        let (_, to_policy) = self.resolve_server(to_id).map_err(mcp_err)?;
958        sync_tools::handle_sync(&self.config, &from_policy, &to_policy, p).await
959    }
960
961    // ── Direct DNS resolution (mirrors `dns query`) ───────────────────────
962
963    /// Resolve a name directly via the system resolver, a configured
964    /// `[[servers]]` entry, or any ad-hoc nameserver. Supports DNS,
965    /// DoT, DoH, and (with the `doq` Cargo feature) DoQ.
966    ///
967    /// Mirrors the `dns query` CLI subcommand and returns the same
968    /// stable JSON shape (`query`, `target`, `results` array — one
969    /// entry per transport). When `all_transports = true` or
970    /// `transports` lists multiple entries, the tool fans out across
971    /// every requested block in precedence order doh → dot → dns →
972    /// doq; the response's `results` length reflects the actual
973    /// transports queried.
974    #[tool(
975        description = "Resolve a name directly against the system resolver, a configured \
976    `[[servers]]` entry (via `server_id`), or any ad-hoc nameserver (via `at`). Supports DNS, \
977    DoT, DoH, and (with the `doq` build feature) DoQ. When `server_id` is given, transport \
978    selection follows `transports` / `all_transports`; otherwise the scheme on `at` chooses, \
979    or the system resolver is used. Returns the same JSON shape as `dns query --json` — a \
980    `results` array with one entry per transport queried."
981    )]
982    async fn dns_resolve(
983        &self,
984        Parameters(p): Parameters<ResolveParams>,
985    ) -> Result<CallToolResult, McpError> {
986        tracing::info!(tool = "dns_resolve", "MCP tool invoked");
987        resolve_tools::handle_resolve(&self.config, &self.cli_access, &self.cli_allow_zone, p).await
988    }
989}
990
991// ─── ServerHandler ────────────────────────────────────────────────────────────
992
993#[tool_handler]
994impl ServerHandler for DnsServer {
995    /// Builds the ServerInfo metadata describing this DNS MCP server.
996    ///
997    /// The returned `ServerInfo` contains the protocol version, enabled capabilities,
998    /// human-facing instructions (including the server's startup info), and implementation
999    /// metadata with the implementation name set to `"dns"`.
1000    ///
1001    /// # Examples
1002    ///
1003    /// ```ignore
1004    /// use std::sync::Arc;
1005    ///
1006    /// // Construct a server (example uses default/empty inputs for brevity).
1007    /// let config = AppConfig::default();
1008    /// let server = DnsServer::new(config, Vec::new(), Vec::new());
1009    /// let info = server.get_info();
1010    /// assert_eq!(info.server_info.name, "dns");
1011    /// ```
1012    fn get_info(&self) -> ServerInfo {
1013        let base = "MCP server for DNS management. Manages zones, records, cache, stats, \
1014                    and block/allow lists. Confirm before calling any destructive tool.";
1015
1016        let mut info = ServerInfo::default();
1017        info.protocol_version = ProtocolVersion::V_2024_11_05;
1018        info.capabilities = ServerCapabilities::builder().enable_tools().build();
1019        info.instructions = Some(format!("{base}{}", self.startup_info));
1020
1021        let mut impl_info = Implementation::from_build_env();
1022        impl_info.name = "dns".into();
1023        info.server_info = impl_info;
1024
1025        info
1026    }
1027}