1use serde::de::DeserializeOwned;
2use serde::{Deserialize, Serialize};
3use thiserror::Error;
4use tokio::io::{self, AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter};
5
6use ipnet::Ipv4Net;
7use serde_json::json;
8use serde_json::Value;
9
10#[derive(Serialize, Deserialize, Debug)]
11struct JsonRpcRequest {
12 jsonrpc: String,
13 method: String,
14 params: Option<serde_json::Value>,
15 id: Option<serde_json::Value>,
16}
17
18#[derive(Serialize, Deserialize, Debug)]
19struct JsonRpcResponse {
20 jsonrpc: String,
21 result: Option<serde_json::Value>,
22 error: Option<JsonRpcError>,
23 id: Option<serde_json::Value>,
24}
25
26#[derive(Serialize, Deserialize, Debug)]
27struct JsonRpcError {
28 code: i32,
29 message: String,
30}
31
32#[derive(Debug, Error)]
33enum RpcError {
34 #[error("Invalid Request: {0}")]
35 InvalidRequest(String),
36 #[error("Not initialized")]
37 NotInitialized,
38 #[error("Method not found")]
39 MethodNotFound,
40 #[error("Invalid params: {0}")]
41 InvalidParams(String),
42 #[error("{0}")]
43 ToolError(String),
44 #[error("{0}")]
45 Internal(String),
46}
47
48impl RpcError {
49 fn code(&self) -> i32 {
50 match self {
51 RpcError::InvalidRequest(_) => -32600,
52 RpcError::NotInitialized => -32002,
53 RpcError::MethodNotFound => -32601,
54 RpcError::InvalidParams(_) => -32602,
55 RpcError::ToolError(_) => -32000,
56 RpcError::Internal(_) => -32603,
57 }
58 }
59}
60
61#[derive(Debug, Default)]
62struct ServerState {
63 initialized: bool,
64}
65
66fn clamp_concurrency(max_concurrent: Option<usize>, default: usize) -> usize {
67 let c = max_concurrent.unwrap_or(default);
68 c.clamp(1, 4096)
69}
70
71fn clamp_timeout_ms(timeout_ms: Option<u64>, default: u64) -> u64 {
72 let t = timeout_ms.unwrap_or(default);
74 t.clamp(10, 10 * 60 * 1000)
75}
76
77fn parse_params<T: DeserializeOwned>(val: Value) -> Result<T, RpcError> {
78 serde_json::from_value(val).map_err(|e| RpcError::InvalidParams(e.to_string()))
79}
80
81const MAX_SUBNET_ADDRESSES: u64 = 1 << 16; const MAX_PORTS_PER_REQUEST: usize = 4096;
83
84fn validate_subnet(subnet: &str) -> Result<(), RpcError> {
85 let net: Ipv4Net = subnet
86 .parse()
87 .map_err(|e| RpcError::InvalidParams(format!("invalid subnet '{subnet}': {e}")))?;
88 let prefix = net.prefix_len() as u32;
89 let total = 1u64
90 .checked_shl(32u32.saturating_sub(prefix))
91 .unwrap_or(u64::MAX);
92 if total > MAX_SUBNET_ADDRESSES {
93 return Err(RpcError::InvalidParams(format!(
94 "subnet too large: {subnet} (max /16)"
95 )));
96 }
97 Ok(())
98}
99
100fn normalize_ports(mut ports: Option<Vec<u16>>) -> Result<Option<Vec<u16>>, RpcError> {
101 let Some(mut ps) = ports.take() else {
102 return Ok(None);
103 };
104 if ps.is_empty() {
105 return Ok(None);
106 }
107 if ps.len() > MAX_PORTS_PER_REQUEST {
108 return Err(RpcError::InvalidParams(format!(
109 "too many ports requested ({} > {})",
110 ps.len(),
111 MAX_PORTS_PER_REQUEST
112 )));
113 }
114 if ps.contains(&0) {
115 return Err(RpcError::InvalidParams("port 0 is invalid".to_string()));
116 }
117 ps.sort_unstable();
118 ps.dedup();
119 Ok(Some(ps))
120}
121
122#[derive(Deserialize)]
123struct InitializeParams {
124 #[serde(rename = "protocolVersion")]
125 protocol_version: Option<String>,
126 #[allow(dead_code)]
127 capabilities: Option<serde_json::Value>,
128 #[allow(dead_code)]
129 #[serde(rename = "clientInfo")]
130 client_info: Option<serde_json::Value>,
131}
132
133#[derive(Deserialize)]
134struct DiscoverParams {
135 subnet: Option<String>,
136 #[serde(rename = "resolveHostnames")]
137 resolve_hostnames: Option<bool>,
138 timeout: Option<u64>,
139 #[serde(rename = "maxConcurrent")]
140 max_concurrent: Option<usize>,
141}
142
143#[derive(Deserialize)]
144struct PingHostParams {
145 host: String,
146 #[serde(default)]
150 count: Option<u32>,
151 timeout: Option<u64>,
152 #[serde(rename = "maxConcurrent")]
153 max_concurrent: Option<usize>,
154}
155
156#[derive(Deserialize)]
157struct ScanParams {
158 host: String,
159 ports: Option<Vec<u16>>,
160 timeout: Option<u64>,
161 #[serde(rename = "maxConcurrent")]
162 max_concurrent: Option<usize>,
163}
164
165#[derive(Deserialize)]
166struct DnsParams {
167 host: String,
168 #[serde(rename = "type")]
169 record_type: Option<String>,
170}
171
172#[derive(Deserialize)]
173struct SweepParams {
174 subnet: Option<String>,
175 ports: Option<Vec<u16>>,
176 #[serde(rename = "resolveHostnames")]
177 resolve_hostnames: Option<bool>,
178 timeout: Option<u64>,
179 #[serde(rename = "maxConcurrent")]
180 max_concurrent: Option<usize>,
181}
182
183#[derive(Deserialize)]
184#[cfg_attr(not(feature = "pcap"), allow(dead_code))]
185struct PcapParams {
186 interface: String,
187 filter: Option<String>,
188 duration: Option<u64>,
189 #[serde(rename = "outputFile")]
190 output_file: Option<String>,
191 #[serde(rename = "maxPackets")]
192 max_packets: Option<u64>,
193}
194
195#[cfg(feature = "mdns")]
196#[derive(Deserialize, Default)]
197struct MdnsParams {
198 #[serde(default)]
199 timeout_ms: Option<u64>,
200 #[serde(default)]
201 service_types: Option<Vec<String>>,
202}
203
204fn init_tracing() {
213 use tracing_subscriber::{fmt, EnvFilter};
214
215 let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
216
217 let _ = fmt()
218 .with_writer(std::io::stderr)
219 .with_env_filter(filter)
220 .with_target(false)
221 .json()
222 .with_current_span(false)
223 .with_span_list(false)
224 .try_init();
225}
226
227pub async fn run_server() -> anyhow::Result<()> {
228 init_tracing();
229 tracing::info!(
230 version = env!("CARGO_PKG_VERSION"),
231 pcap = cfg!(feature = "pcap"),
232 "netscli MCP server starting"
233 );
234
235 let stdin = io::stdin();
236 let stdout = io::stdout();
237 let mut reader = BufReader::new(stdin).lines();
238 let mut writer = BufWriter::new(stdout);
239 let mut state = ServerState::default();
240 let mut requests_handled: u64 = 0;
241
242 while let Some(line) = reader.next_line().await? {
243 if line.trim().is_empty() {
244 continue;
245 }
246
247 let request = match serde_json::from_str::<JsonRpcRequest>(&line) {
248 Ok(req) => req,
249 Err(e) => {
250 tracing::warn!(error = %e, "json parse error");
251 let response = JsonRpcResponse {
253 jsonrpc: "2.0".to_string(),
254 result: None,
255 error: Some(JsonRpcError {
256 code: -32700,
257 message: "Parse error".to_string(),
258 }),
259 id: Some(serde_json::Value::Null),
260 };
261 let response_str = serde_json::to_string(&response)?;
262 writer.write_all(response_str.as_bytes()).await?;
263 writer.write_all(b"\n").await?;
264 writer.flush().await?;
265 continue;
266 }
267 };
268
269 if request.id.is_none() {
271 tracing::debug!(method = %request.method, "notification");
272 if request.method == "notifications/initialized" {
273 state.initialized = true;
274 }
275 continue;
276 }
277
278 let response = handle_request(&mut state, request).await;
279 let response_str = serde_json::to_string(&response)?;
280 writer.write_all(response_str.as_bytes()).await?;
281 writer.write_all(b"\n").await?;
282 writer.flush().await?;
283 requests_handled += 1;
284 }
285
286 tracing::info!(requests_handled, "netscli MCP server shutting down");
287 Ok(())
288}
289
290async fn handle_request(state: &mut ServerState, req: JsonRpcRequest) -> JsonRpcResponse {
291 let id = req.id.clone();
292 let method = req.method.clone();
293 let tool_name: Option<String> = if method == "tools/call" {
296 req.params
297 .as_ref()
298 .and_then(|p| p.get("name"))
299 .and_then(|n| n.as_str())
300 .map(|s| s.to_string())
301 } else {
302 None
303 };
304
305 let start = std::time::Instant::now();
306 let mut response = JsonRpcResponse {
307 jsonrpc: "2.0".to_string(),
308 result: None,
309 error: None,
310 id,
311 };
312
313 match handle_request_inner(state, &req).await {
314 Ok(val) => {
315 response.result = Some(val);
316 }
317 Err(e) => {
318 response.error = Some(JsonRpcError {
319 code: e.code(),
320 message: e.to_string(),
321 });
322 }
323 }
324
325 let duration_ms = start.elapsed().as_millis() as u64;
326 match response.error.as_ref() {
327 None => tracing::info!(
328 method = %method,
329 tool = tool_name.as_deref(),
330 duration_ms,
331 "ok"
332 ),
333 Some(err) => tracing::warn!(
334 method = %method,
335 tool = tool_name.as_deref(),
336 duration_ms,
337 code = err.code,
338 message = %err.message,
339 "error"
340 ),
341 }
342
343 response
344}
345
346pub fn tools_list() -> serde_json::Value {
347 let tools = vec![
348 json!({
349 "name": "discover_network",
350 "description": "Discover live hosts on a network subnet",
351 "inputSchema": {
352 "type": "object",
353 "properties": {
354 "subnet": { "type": "string", "default": "192.168.1.0/24" },
355 "resolveHostnames": { "type": "boolean", "default": false },
356 "timeout": { "type": "number", "default": 1000 },
357 "maxConcurrent": { "type": "number", "default": 256 }
358 }
359 }
360 }),
361 json!({
362 "name": "scan_ports",
363 "description": "Scan TCP ports on a host",
364 "inputSchema": {
365 "type": "object",
366 "properties": {
367 "host": { "type": "string" },
368 "ports": { "type": "array", "items": { "type": "number" } },
369 "timeout": { "type": "number", "default": 500 },
370 "maxConcurrent": { "type": "number", "default": 256 }
371 },
372 "required": ["host"]
373 }
374 }),
375 json!({
376 "name": "ping_host",
377 "description": "Ping a host (ICMP with TCP-connect fallback). Returns a PingSummary with aggregate loss and min/avg/max RTT when count > 1.",
378 "inputSchema": {
379 "type": "object",
380 "properties": {
381 "host": { "type": "string" },
382 "count": { "type": "number", "default": 1, "minimum": 1, "maximum": 256 },
383 "timeout": { "type": "number", "default": 1000 },
384 "maxConcurrent": { "type": "number", "default": 64 }
385 },
386 "required": ["host"]
387 }
388 }),
389 json!({
390 "name": "dns_lookup",
391 "description": "DNS lookup (A, AAAA, CNAME, MX, NS, TXT, SRV, PTR, SOA, CAA, or ALL/ANY for every record type)",
392 "inputSchema": {
393 "type": "object",
394 "properties": {
395 "host": { "type": "string" },
396 "type": {
397 "type": "string",
398 "default": "A",
399 "enum": ["A", "AAAA", "CNAME", "MX", "NS", "TXT", "SRV", "PTR", "SOA", "CAA", "ALL", "ANY"]
400 }
401 },
402 "required": ["host"]
403 }
404 }),
405 json!({
406 "name": "get_arp_table",
407 "description": "Get ARP/neighbor table with vendor information",
408 "inputSchema": {
409 "type": "object",
410 "properties": {}
411 }
412 }),
413 json!({
414 "name": "inspect_host",
415 "description": "Inspect a host (ping + port scan + optional DNS resolution)",
416 "inputSchema": {
417 "type": "object",
418 "properties": {
419 "host": { "type": "string" },
420 "ports": { "type": "array", "items": { "type": "number" } }
421 },
422 "required": ["host"]
423 }
424 }),
425 json!({
426 "name": "sweep_network",
427 "description": "Sweep a network (discover hosts then scan ports)",
428 "inputSchema": {
429 "type": "object",
430 "properties": {
431 "subnet": { "type": "string", "default": "192.168.1.0/24" },
432 "ports": { "type": "array", "items": { "type": "number" } },
433 "resolveHostnames": { "type": "boolean", "default": false },
434 "timeout": { "type": "number", "default": 500 },
435 "maxConcurrent": { "type": "number", "default": 256 }
436 }
437 }
438 }),
439 json!({
440 "name": "list_network_interfaces",
441 "description": "List network interfaces with details",
442 "inputSchema": {
443 "type": "object",
444 "properties": {}
445 }
446 }),
447 ];
448
449 #[cfg(feature = "pcap")]
450 let tools = {
451 let mut tools = tools;
452 tools.push(json!({
453 "name": "capture_pcap",
454 "description": "Capture network packets to a PCAP file (may require root/admin)",
455 "inputSchema": {
456 "type": "object",
457 "properties": {
458 "interface": { "type": "string" },
459 "filter": { "type": "string" },
460 "duration": { "type": "number", "default": 10 },
461 "outputFile": { "type": "string", "default": "capture.pcap" },
462 "maxPackets": { "type": "number" }
463 },
464 "required": ["interface"]
465 }
466 }));
467 tools
468 };
469
470 #[cfg(feature = "mdns")]
471 let tools = {
472 let mut tools = tools;
473 tools.push(json!({
474 "name": "discover_mdns",
475 "description": "Discover devices on the local network via mDNS/DNS-SD (Bonjour). Returns services with their hostnames, resolved IPs, ports, and TXT properties. Much friendlier than IP-based discovery for named devices like printers, Chromecasts, or Homebridge accessories.",
476 "inputSchema": {
477 "type": "object",
478 "properties": {
479 "timeout_ms": {
480 "type": "number",
481 "default": 3000,
482 "description": "How long to browse for responses. 3000-5000ms is typical; many devices re-announce on a multi-second cadence."
483 },
484 "service_types": {
485 "type": "array",
486 "items": { "type": "string" },
487 "description": "Explicit service types to browse (e.g. [\"_http._tcp.local.\", \"_airplay._tcp.local.\"]). Omit to use a curated default set."
488 }
489 }
490 }
491 }));
492 tools
493 };
494
495 json!({ "tools": tools })
496}
497
498fn mcp_tool_result_text(val: serde_json::Value) -> serde_json::Value {
499 json!({
500 "content": [
501 {
502 "type": "text",
503 "text": serde_json::to_string_pretty(&val).unwrap_or_else(|_| "<serialization error>".to_string())
504 }
505 ]
506 })
507}
508
509async fn op_discover(p: DiscoverParams) -> Result<Vec<netscli_core::Host>, RpcError> {
512 if let Some(ref subnet) = p.subnet {
513 validate_subnet(subnet)?;
514 }
515 let concurrency = clamp_concurrency(p.max_concurrent, netscli_core::DEFAULT_CONCURRENCY);
516 let timeout_ms = clamp_timeout_ms(p.timeout, netscli_core::DEFAULT_PING_TIMEOUT_MS);
517 let cfg = netscli_core::OpsConfig {
518 concurrency,
519 ping_timeout_ms: timeout_ms,
520 dns_timeout_ms: timeout_ms,
521 ..Default::default()
522 };
523 let ops = netscli_core::Ops::new(cfg);
524 let (_subnet, hosts) = ops
525 .discover_ipv4(p.subnet, p.resolve_hostnames.unwrap_or(false))
526 .await
527 .map_err(|e| RpcError::ToolError(e.to_string()))?;
528 Ok(hosts)
529}
530
531async fn op_scan_ports(p: ScanParams) -> Result<Vec<netscli_core::PortResult>, RpcError> {
532 let ports = normalize_ports(p.ports)?;
533 let concurrency = clamp_concurrency(p.max_concurrent, netscli_core::DEFAULT_CONCURRENCY);
534 let timeout_ms = clamp_timeout_ms(p.timeout, netscli_core::DEFAULT_SCAN_TIMEOUT_MS);
535 let cfg = netscli_core::OpsConfig {
536 concurrency,
537 scan_timeout_ms: timeout_ms,
538 ..Default::default()
539 };
540 let ops = netscli_core::Ops::new(cfg);
541 let (_ip, res) = ops
542 .scan_ports(&p.host, ports)
543 .await
544 .map_err(|e| RpcError::ToolError(e.to_string()))?;
545 Ok(res)
546}
547
548async fn op_inspect_host(p: ScanParams) -> Result<netscli_core::InspectResult, RpcError> {
549 let ports = normalize_ports(p.ports)?;
550 let concurrency = clamp_concurrency(p.max_concurrent, netscli_core::DEFAULT_CONCURRENCY);
551 let timeout_ms = clamp_timeout_ms(p.timeout, netscli_core::DEFAULT_SCAN_TIMEOUT_MS);
552 let cfg = netscli_core::OpsConfig {
553 concurrency,
554 scan_timeout_ms: timeout_ms,
555 ping_timeout_ms: timeout_ms,
556 dns_timeout_ms: timeout_ms,
557 };
558 let ops = netscli_core::Ops::new(cfg);
559 ops.inspect_host(p.host, ports)
560 .await
561 .map_err(|e| RpcError::ToolError(e.to_string()))
562}
563
564async fn op_sweep(p: SweepParams) -> Result<Vec<netscli_core::SweepEntry>, RpcError> {
565 if let Some(ref subnet) = p.subnet {
566 validate_subnet(subnet)?;
567 }
568 let ports = normalize_ports(p.ports)?;
569 let concurrency = clamp_concurrency(p.max_concurrent, netscli_core::DEFAULT_CONCURRENCY);
570 let timeout_ms = clamp_timeout_ms(p.timeout, netscli_core::DEFAULT_SCAN_TIMEOUT_MS);
571 let cfg = netscli_core::OpsConfig {
572 concurrency,
573 scan_timeout_ms: timeout_ms,
574 ping_timeout_ms: timeout_ms,
575 dns_timeout_ms: timeout_ms,
576 };
577 let ops = netscli_core::Ops::new(cfg);
578 let (_subnet, res) = ops
579 .sweep_ipv4(p.subnet, ports, p.resolve_hostnames.unwrap_or(false))
580 .await
581 .map_err(|e| RpcError::ToolError(e.to_string()))?;
582 Ok(res)
583}
584
585async fn op_ping_host(p: PingHostParams) -> Result<netscli_core::PingSummary, RpcError> {
586 if p.host.trim().is_empty() {
587 return Err(RpcError::InvalidParams("host is required".to_string()));
588 }
589 let _ = p.max_concurrent; let count = p.count.unwrap_or(1).clamp(1, 256);
591 let timeout_ms = clamp_timeout_ms(p.timeout, netscli_core::DEFAULT_PING_TIMEOUT_MS);
592
593 let cfg = netscli_core::OpsConfig {
596 ping_timeout_ms: timeout_ms,
597 dns_timeout_ms: timeout_ms,
598 ..Default::default()
599 };
600 let ops = netscli_core::Ops::new(cfg);
601 ops.ping_host_summary(&p.host, count)
602 .await
603 .map_err(|e| RpcError::ToolError(e.to_string()))
604}
605
606async fn op_dns_lookup(p: DnsParams) -> Result<Vec<netscli_core::dns::DnsRecord>, RpcError> {
607 if let Some(ref t) = p.record_type {
608 let upper = t.to_uppercase();
609 if upper != "ALL"
610 && upper != "ANY"
611 && netscli_core::dns::parse_record_type(&upper).is_none()
612 {
613 return Err(RpcError::InvalidParams(format!(
614 "unsupported record type: {t}"
615 )));
616 }
617 }
618 let ops = netscli_core::Ops::default();
619 ops.dns_lookup(&p.host, p.record_type)
620 .await
621 .map_err(|e| RpcError::ToolError(e.to_string()))
622}
623
624fn op_get_arp_table() -> Result<Vec<netscli_core::ArpEntry>, RpcError> {
625 let ops = netscli_core::Ops::default();
626 ops.get_arp_table()
627 .map_err(|e| RpcError::ToolError(e.to_string()))
628}
629
630fn op_list_interfaces() -> Vec<netscli_core::InterfaceInfo> {
631 let ops = netscli_core::Ops::default();
632 ops.list_interfaces()
633}
634
635#[cfg(feature = "pcap")]
636async fn op_capture_pcap(p: PcapParams) -> Result<netscli_core::PcapResult, RpcError> {
637 if p.interface.trim().is_empty() {
638 return Err(RpcError::InvalidParams("interface is required".to_string()));
639 }
640 let duration = p.duration.map(|s| s.clamp(1, 60 * 60));
641 let max_packets = match p.max_packets {
642 Some(0) => None,
643 Some(n) => {
644 if n > (usize::MAX as u64) {
645 return Err(RpcError::InvalidParams("maxPackets too large".to_string()));
646 }
647 Some(n as usize)
648 }
649 None => None,
650 };
651 let ops = netscli_core::Ops::default();
652 ops.capture_pcap_async(p.interface, p.filter, duration, p.output_file, max_packets)
653 .await
654 .map_err(|e| RpcError::ToolError(e.to_string()))
655}
656
657#[cfg(feature = "mdns")]
658async fn op_discover_mdns(p: MdnsParams) -> Result<Vec<netscli_core::MdnsService>, RpcError> {
659 let timeout_ms = p.timeout_ms.unwrap_or(3000).clamp(100, 30_000);
662 let service_types = p.service_types.unwrap_or_default();
663 let ops = netscli_core::Ops::default();
664 ops.discover_mdns(&service_types, std::time::Duration::from_millis(timeout_ms))
665 .await
666 .map_err(|e| RpcError::ToolError(e.to_string()))
667}
668
669#[cfg(not(feature = "pcap"))]
670async fn op_capture_pcap(_p: PcapParams) -> Result<netscli_core::PcapResult, RpcError> {
671 Err(RpcError::ToolError(
672 "pcap support disabled at compile time".to_string(),
673 ))
674}
675
676async fn handle_request_inner(
677 state: &mut ServerState,
678 req: &JsonRpcRequest,
679) -> Result<serde_json::Value, RpcError> {
680 if req.jsonrpc != "2.0" {
681 return Err(RpcError::InvalidRequest(format!(
682 "expected jsonrpc='2.0', got '{}'",
683 req.jsonrpc
684 )));
685 }
686
687 if !state.initialized && req.method != "initialize" && req.method != "tools/list" {
689 return Err(RpcError::NotInitialized);
690 }
691
692 let params = req.params.clone().unwrap_or(Value::Null);
693
694 match req.method.as_str() {
695 "initialize" => {
697 let p: InitializeParams = parse_params(params)?;
698 let protocol = p
699 .protocol_version
700 .unwrap_or_else(|| "2024-11-05".to_string());
701 state.initialized = true;
702 Ok(json!({
703 "protocolVersion": protocol,
704 "capabilities": { "tools": {} },
705 "serverInfo": { "name": "netscli", "version": env!("CARGO_PKG_VERSION") }
706 }))
707 }
708 "tools/list" => Ok(tools_list()),
709 "tools/call" => {
710 #[derive(Deserialize)]
711 struct ToolCallParams {
712 name: String,
713 #[serde(default, rename = "arguments")]
714 args: serde_json::Value,
715 }
716
717 let p: ToolCallParams = parse_params(params)?;
718 let args = if p.args.is_null() {
719 Value::Object(serde_json::Map::new())
720 } else {
721 p.args
722 };
723
724 let output = match p.name.as_str() {
725 "discover_network" => {
726 let p: DiscoverParams = parse_params(args)?;
727 let hosts = op_discover(p).await?;
728 json!(hosts)
729 }
730 "scan_ports" => {
731 let p: ScanParams = parse_params(args)?;
732 let res = op_scan_ports(p).await?;
733 let open: Vec<_> = res.into_iter().filter(|r| r.open).collect();
734 json!(open)
735 }
736 "ping_host" => {
737 let p: PingHostParams = parse_params(args)?;
738 let res = op_ping_host(p).await?;
739 json!(res)
740 }
741 "dns_lookup" => {
742 let p: DnsParams = parse_params(args)?;
743 let res = op_dns_lookup(p).await?;
744 json!(res)
745 }
746 "get_arp_table" => {
747 json!(op_get_arp_table()?)
748 }
749 "inspect_host" => {
750 let p: ScanParams = parse_params(args)?;
751 let res = op_inspect_host(p).await?;
752 json!(res)
753 }
754 "sweep_network" => {
755 let p: SweepParams = parse_params(args)?;
756 let res = op_sweep(p).await?;
757 json!(res)
758 }
759 "list_network_interfaces" => {
760 json!(op_list_interfaces())
761 }
762 "capture_pcap" => {
763 let p: PcapParams = parse_params(args)?;
764 let res = op_capture_pcap(p).await?;
765 json!(res)
766 }
767 #[cfg(feature = "mdns")]
768 "discover_mdns" => {
769 let p: MdnsParams = parse_params(args)?;
770 let res = op_discover_mdns(p).await?;
771 json!(res)
772 }
773 other => {
774 return Err(RpcError::InvalidParams(format!("Unknown tool: {other}")));
775 }
776 };
777
778 Ok(mcp_tool_result_text(output))
779 }
780
781 "discover_network" => {
783 let p: DiscoverParams = parse_params(params)?;
784 let hosts = op_discover(p).await?;
785 serde_json::to_value(hosts).map_err(|e| RpcError::Internal(e.to_string()))
786 }
787 "scan_ports" => {
788 let p: ScanParams = parse_params(params)?;
789 let res = op_scan_ports(p).await?;
790 serde_json::to_value(res).map_err(|e| RpcError::Internal(e.to_string()))
791 }
792 "ping_host" => {
793 let p: PingHostParams = parse_params(params)?;
794 let res = op_ping_host(p).await?;
795 serde_json::to_value(res).map_err(|e| RpcError::Internal(e.to_string()))
796 }
797 "dns_lookup" => {
798 let p: DnsParams = parse_params(params)?;
799 let res = op_dns_lookup(p).await?;
800 serde_json::to_value(res).map_err(|e| RpcError::Internal(e.to_string()))
801 }
802 "get_arp_table" => {
803 serde_json::to_value(op_get_arp_table()?).map_err(|e| RpcError::Internal(e.to_string()))
804 }
805 "inspect_host" => {
806 let p: ScanParams = parse_params(params)?;
807 let res = op_inspect_host(p).await?;
808 serde_json::to_value(res).map_err(|e| RpcError::Internal(e.to_string()))
809 }
810 "sweep_network" => {
811 let p: SweepParams = parse_params(params)?;
812 let res = op_sweep(p).await?;
813 serde_json::to_value(res).map_err(|e| RpcError::Internal(e.to_string()))
814 }
815 "list_network_interfaces" => serde_json::to_value(op_list_interfaces())
816 .map_err(|e| RpcError::Internal(e.to_string())),
817 "capture_pcap" => {
818 let p: PcapParams = parse_params(params)?;
819 let res = op_capture_pcap(p).await?;
820 serde_json::to_value(res).map_err(|e| RpcError::Internal(e.to_string()))
821 }
822 #[cfg(feature = "mdns")]
823 "discover_mdns" => {
824 let p: MdnsParams = parse_params(params)?;
825 let res = op_discover_mdns(p).await?;
826 serde_json::to_value(res).map_err(|e| RpcError::Internal(e.to_string()))
827 }
828 _ => Err(RpcError::MethodNotFound),
829 }
830}