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}