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