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
195fn init_tracing() {
204 use tracing_subscriber::{fmt, EnvFilter};
205
206 let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
207
208 let _ = fmt()
209 .with_writer(std::io::stderr)
210 .with_env_filter(filter)
211 .with_target(false)
212 .json()
213 .with_current_span(false)
214 .with_span_list(false)
215 .try_init();
216}
217
218pub async fn run_server() -> anyhow::Result<()> {
219 init_tracing();
220 tracing::info!(
221 version = env!("CARGO_PKG_VERSION"),
222 pcap = cfg!(feature = "pcap"),
223 "netscli MCP server starting"
224 );
225
226 let stdin = io::stdin();
227 let stdout = io::stdout();
228 let mut reader = BufReader::new(stdin).lines();
229 let mut writer = BufWriter::new(stdout);
230 let mut state = ServerState::default();
231 let mut requests_handled: u64 = 0;
232
233 while let Some(line) = reader.next_line().await? {
234 if line.trim().is_empty() {
235 continue;
236 }
237
238 let request = match serde_json::from_str::<JsonRpcRequest>(&line) {
239 Ok(req) => req,
240 Err(e) => {
241 tracing::warn!(error = %e, "json parse error");
242 let response = JsonRpcResponse {
244 jsonrpc: "2.0".to_string(),
245 result: None,
246 error: Some(JsonRpcError {
247 code: -32700,
248 message: "Parse error".to_string(),
249 }),
250 id: Some(serde_json::Value::Null),
251 };
252 let response_str = serde_json::to_string(&response)?;
253 writer.write_all(response_str.as_bytes()).await?;
254 writer.write_all(b"\n").await?;
255 writer.flush().await?;
256 continue;
257 }
258 };
259
260 if request.id.is_none() {
262 tracing::debug!(method = %request.method, "notification");
263 if request.method == "notifications/initialized" {
264 state.initialized = true;
265 }
266 continue;
267 }
268
269 let response = handle_request(&mut state, request).await;
270 let response_str = serde_json::to_string(&response)?;
271 writer.write_all(response_str.as_bytes()).await?;
272 writer.write_all(b"\n").await?;
273 writer.flush().await?;
274 requests_handled += 1;
275 }
276
277 tracing::info!(requests_handled, "netscli MCP server shutting down");
278 Ok(())
279}
280
281async fn handle_request(state: &mut ServerState, req: JsonRpcRequest) -> JsonRpcResponse {
282 let id = req.id.clone();
283 let method = req.method.clone();
284 let tool_name: Option<String> = if method == "tools/call" {
287 req.params
288 .as_ref()
289 .and_then(|p| p.get("name"))
290 .and_then(|n| n.as_str())
291 .map(|s| s.to_string())
292 } else {
293 None
294 };
295
296 let start = std::time::Instant::now();
297 let mut response = JsonRpcResponse {
298 jsonrpc: "2.0".to_string(),
299 result: None,
300 error: None,
301 id,
302 };
303
304 match handle_request_inner(state, &req).await {
305 Ok(val) => {
306 response.result = Some(val);
307 }
308 Err(e) => {
309 response.error = Some(JsonRpcError {
310 code: e.code(),
311 message: e.to_string(),
312 });
313 }
314 }
315
316 let duration_ms = start.elapsed().as_millis() as u64;
317 match response.error.as_ref() {
318 None => tracing::info!(
319 method = %method,
320 tool = tool_name.as_deref(),
321 duration_ms,
322 "ok"
323 ),
324 Some(err) => tracing::warn!(
325 method = %method,
326 tool = tool_name.as_deref(),
327 duration_ms,
328 code = err.code,
329 message = %err.message,
330 "error"
331 ),
332 }
333
334 response
335}
336
337pub fn tools_list() -> serde_json::Value {
338 let tools = vec![
339 json!({
340 "name": "discover_network",
341 "description": "Discover live hosts on a network subnet",
342 "inputSchema": {
343 "type": "object",
344 "properties": {
345 "subnet": { "type": "string", "default": "192.168.1.0/24" },
346 "resolveHostnames": { "type": "boolean", "default": false },
347 "timeout": { "type": "number", "default": 1000 },
348 "maxConcurrent": { "type": "number", "default": 256 }
349 }
350 }
351 }),
352 json!({
353 "name": "scan_ports",
354 "description": "Scan TCP ports on a host",
355 "inputSchema": {
356 "type": "object",
357 "properties": {
358 "host": { "type": "string" },
359 "ports": { "type": "array", "items": { "type": "number" } },
360 "timeout": { "type": "number", "default": 500 },
361 "maxConcurrent": { "type": "number", "default": 256 }
362 },
363 "required": ["host"]
364 }
365 }),
366 json!({
367 "name": "ping_host",
368 "description": "Ping a host (ICMP with TCP-connect fallback). Returns a PingSummary with aggregate loss and min/avg/max RTT when count > 1.",
369 "inputSchema": {
370 "type": "object",
371 "properties": {
372 "host": { "type": "string" },
373 "count": { "type": "number", "default": 1, "minimum": 1, "maximum": 256 },
374 "timeout": { "type": "number", "default": 1000 },
375 "maxConcurrent": { "type": "number", "default": 64 }
376 },
377 "required": ["host"]
378 }
379 }),
380 json!({
381 "name": "dns_lookup",
382 "description": "DNS lookup (A, AAAA, CNAME, MX, NS, TXT, SRV, PTR, SOA, CAA, or ALL/ANY for every record type)",
383 "inputSchema": {
384 "type": "object",
385 "properties": {
386 "host": { "type": "string" },
387 "type": {
388 "type": "string",
389 "default": "A",
390 "enum": ["A", "AAAA", "CNAME", "MX", "NS", "TXT", "SRV", "PTR", "SOA", "CAA", "ALL", "ANY"]
391 }
392 },
393 "required": ["host"]
394 }
395 }),
396 json!({
397 "name": "get_arp_table",
398 "description": "Get ARP/neighbor table with vendor information",
399 "inputSchema": {
400 "type": "object",
401 "properties": {}
402 }
403 }),
404 json!({
405 "name": "inspect_host",
406 "description": "Inspect a host (ping + port scan + optional DNS resolution)",
407 "inputSchema": {
408 "type": "object",
409 "properties": {
410 "host": { "type": "string" },
411 "ports": { "type": "array", "items": { "type": "number" } }
412 },
413 "required": ["host"]
414 }
415 }),
416 json!({
417 "name": "sweep_network",
418 "description": "Sweep a network (discover hosts then scan ports)",
419 "inputSchema": {
420 "type": "object",
421 "properties": {
422 "subnet": { "type": "string", "default": "192.168.1.0/24" },
423 "ports": { "type": "array", "items": { "type": "number" } },
424 "resolveHostnames": { "type": "boolean", "default": false },
425 "timeout": { "type": "number", "default": 500 },
426 "maxConcurrent": { "type": "number", "default": 256 }
427 }
428 }
429 }),
430 json!({
431 "name": "list_network_interfaces",
432 "description": "List network interfaces with details",
433 "inputSchema": {
434 "type": "object",
435 "properties": {}
436 }
437 }),
438 ];
439
440 #[cfg(feature = "pcap")]
441 let tools = {
442 let mut tools = tools;
443 tools.push(json!({
444 "name": "capture_pcap",
445 "description": "Capture network packets to a PCAP file (may require root/admin)",
446 "inputSchema": {
447 "type": "object",
448 "properties": {
449 "interface": { "type": "string" },
450 "filter": { "type": "string" },
451 "duration": { "type": "number", "default": 10 },
452 "outputFile": { "type": "string", "default": "capture.pcap" },
453 "maxPackets": { "type": "number" }
454 },
455 "required": ["interface"]
456 }
457 }));
458 tools
459 };
460
461 json!({ "tools": tools })
462}
463
464fn mcp_tool_result_text(val: serde_json::Value) -> serde_json::Value {
465 json!({
466 "content": [
467 {
468 "type": "text",
469 "text": serde_json::to_string_pretty(&val).unwrap_or_else(|_| "<serialization error>".to_string())
470 }
471 ]
472 })
473}
474
475async fn op_discover(p: DiscoverParams) -> Result<Vec<netscli_core::Host>, RpcError> {
478 if let Some(ref subnet) = p.subnet {
479 validate_subnet(subnet)?;
480 }
481 let concurrency = clamp_concurrency(p.max_concurrent, netscli_core::DEFAULT_CONCURRENCY);
482 let timeout_ms = clamp_timeout_ms(p.timeout, netscli_core::DEFAULT_PING_TIMEOUT_MS);
483 let cfg = netscli_core::OpsConfig {
484 concurrency,
485 ping_timeout_ms: timeout_ms,
486 dns_timeout_ms: timeout_ms,
487 ..Default::default()
488 };
489 let ops = netscli_core::Ops::new(cfg);
490 let (_subnet, hosts) = ops
491 .discover_ipv4(p.subnet, p.resolve_hostnames.unwrap_or(false))
492 .await
493 .map_err(|e| RpcError::ToolError(e.to_string()))?;
494 Ok(hosts)
495}
496
497async fn op_scan_ports(p: ScanParams) -> Result<Vec<netscli_core::PortResult>, RpcError> {
498 let ports = normalize_ports(p.ports)?;
499 let concurrency = clamp_concurrency(p.max_concurrent, netscli_core::DEFAULT_CONCURRENCY);
500 let timeout_ms = clamp_timeout_ms(p.timeout, netscli_core::DEFAULT_SCAN_TIMEOUT_MS);
501 let cfg = netscli_core::OpsConfig {
502 concurrency,
503 scan_timeout_ms: timeout_ms,
504 ..Default::default()
505 };
506 let ops = netscli_core::Ops::new(cfg);
507 let (_ip, res) = ops
508 .scan_ports(&p.host, ports)
509 .await
510 .map_err(|e| RpcError::ToolError(e.to_string()))?;
511 Ok(res)
512}
513
514async fn op_inspect_host(p: ScanParams) -> Result<netscli_core::InspectResult, RpcError> {
515 let ports = normalize_ports(p.ports)?;
516 let concurrency = clamp_concurrency(p.max_concurrent, netscli_core::DEFAULT_CONCURRENCY);
517 let timeout_ms = clamp_timeout_ms(p.timeout, netscli_core::DEFAULT_SCAN_TIMEOUT_MS);
518 let cfg = netscli_core::OpsConfig {
519 concurrency,
520 scan_timeout_ms: timeout_ms,
521 ping_timeout_ms: timeout_ms,
522 dns_timeout_ms: timeout_ms,
523 };
524 let ops = netscli_core::Ops::new(cfg);
525 ops.inspect_host(p.host, ports)
526 .await
527 .map_err(|e| RpcError::ToolError(e.to_string()))
528}
529
530async fn op_sweep(p: SweepParams) -> Result<Vec<netscli_core::SweepEntry>, RpcError> {
531 if let Some(ref subnet) = p.subnet {
532 validate_subnet(subnet)?;
533 }
534 let ports = normalize_ports(p.ports)?;
535 let concurrency = clamp_concurrency(p.max_concurrent, netscli_core::DEFAULT_CONCURRENCY);
536 let timeout_ms = clamp_timeout_ms(p.timeout, netscli_core::DEFAULT_SCAN_TIMEOUT_MS);
537 let cfg = netscli_core::OpsConfig {
538 concurrency,
539 scan_timeout_ms: timeout_ms,
540 ping_timeout_ms: timeout_ms,
541 dns_timeout_ms: timeout_ms,
542 };
543 let ops = netscli_core::Ops::new(cfg);
544 let (_subnet, res) = ops
545 .sweep_ipv4(p.subnet, ports, p.resolve_hostnames.unwrap_or(false))
546 .await
547 .map_err(|e| RpcError::ToolError(e.to_string()))?;
548 Ok(res)
549}
550
551async fn op_ping_host(p: PingHostParams) -> Result<netscli_core::PingSummary, RpcError> {
552 if p.host.trim().is_empty() {
553 return Err(RpcError::InvalidParams("host is required".to_string()));
554 }
555 let _ = p.max_concurrent; let count = p.count.unwrap_or(1).clamp(1, 256);
557 let timeout_ms = clamp_timeout_ms(p.timeout, netscli_core::DEFAULT_PING_TIMEOUT_MS);
558
559 let cfg = netscli_core::OpsConfig {
562 ping_timeout_ms: timeout_ms,
563 dns_timeout_ms: timeout_ms,
564 ..Default::default()
565 };
566 let ops = netscli_core::Ops::new(cfg);
567 ops.ping_host_summary(&p.host, count)
568 .await
569 .map_err(|e| RpcError::ToolError(e.to_string()))
570}
571
572async fn op_dns_lookup(p: DnsParams) -> Result<Vec<netscli_core::dns::DnsRecord>, RpcError> {
573 if let Some(ref t) = p.record_type {
574 let upper = t.to_uppercase();
575 if upper != "ALL"
576 && upper != "ANY"
577 && netscli_core::dns::parse_record_type(&upper).is_none()
578 {
579 return Err(RpcError::InvalidParams(format!(
580 "unsupported record type: {t}"
581 )));
582 }
583 }
584 let ops = netscli_core::Ops::default();
585 ops.dns_lookup(&p.host, p.record_type)
586 .await
587 .map_err(|e| RpcError::ToolError(e.to_string()))
588}
589
590fn op_get_arp_table() -> Result<Vec<netscli_core::ArpEntry>, RpcError> {
591 let ops = netscli_core::Ops::default();
592 ops.get_arp_table()
593 .map_err(|e| RpcError::ToolError(e.to_string()))
594}
595
596fn op_list_interfaces() -> Vec<netscli_core::InterfaceInfo> {
597 let ops = netscli_core::Ops::default();
598 ops.list_interfaces()
599}
600
601#[cfg(feature = "pcap")]
602async fn op_capture_pcap(p: PcapParams) -> Result<netscli_core::PcapResult, RpcError> {
603 if p.interface.trim().is_empty() {
604 return Err(RpcError::InvalidParams("interface is required".to_string()));
605 }
606 let duration = p.duration.map(|s| s.clamp(1, 60 * 60));
607 let max_packets = match p.max_packets {
608 Some(0) => None,
609 Some(n) => {
610 if n > (usize::MAX as u64) {
611 return Err(RpcError::InvalidParams("maxPackets too large".to_string()));
612 }
613 Some(n as usize)
614 }
615 None => None,
616 };
617 let ops = netscli_core::Ops::default();
618 ops.capture_pcap_async(p.interface, p.filter, duration, p.output_file, max_packets)
619 .await
620 .map_err(|e| RpcError::ToolError(e.to_string()))
621}
622
623#[cfg(not(feature = "pcap"))]
624async fn op_capture_pcap(_p: PcapParams) -> Result<netscli_core::PcapResult, RpcError> {
625 Err(RpcError::ToolError(
626 "pcap support disabled at compile time".to_string(),
627 ))
628}
629
630async fn handle_request_inner(
631 state: &mut ServerState,
632 req: &JsonRpcRequest,
633) -> Result<serde_json::Value, RpcError> {
634 if req.jsonrpc != "2.0" {
635 return Err(RpcError::InvalidRequest(format!(
636 "expected jsonrpc='2.0', got '{}'",
637 req.jsonrpc
638 )));
639 }
640
641 if !state.initialized && req.method != "initialize" && req.method != "tools/list" {
643 return Err(RpcError::NotInitialized);
644 }
645
646 let params = req.params.clone().unwrap_or(Value::Null);
647
648 match req.method.as_str() {
649 "initialize" => {
651 let p: InitializeParams = parse_params(params)?;
652 let protocol = p
653 .protocol_version
654 .unwrap_or_else(|| "2024-11-05".to_string());
655 state.initialized = true;
656 Ok(json!({
657 "protocolVersion": protocol,
658 "capabilities": { "tools": {} },
659 "serverInfo": { "name": "netscli", "version": env!("CARGO_PKG_VERSION") }
660 }))
661 }
662 "tools/list" => Ok(tools_list()),
663 "tools/call" => {
664 #[derive(Deserialize)]
665 struct ToolCallParams {
666 name: String,
667 #[serde(default, rename = "arguments")]
668 args: serde_json::Value,
669 }
670
671 let p: ToolCallParams = parse_params(params)?;
672 let args = if p.args.is_null() {
673 Value::Object(serde_json::Map::new())
674 } else {
675 p.args
676 };
677
678 let output = match p.name.as_str() {
679 "discover_network" => {
680 let p: DiscoverParams = parse_params(args)?;
681 let hosts = op_discover(p).await?;
682 json!(hosts)
683 }
684 "scan_ports" => {
685 let p: ScanParams = parse_params(args)?;
686 let res = op_scan_ports(p).await?;
687 let open: Vec<_> = res.into_iter().filter(|r| r.open).collect();
688 json!(open)
689 }
690 "ping_host" => {
691 let p: PingHostParams = parse_params(args)?;
692 let res = op_ping_host(p).await?;
693 json!(res)
694 }
695 "dns_lookup" => {
696 let p: DnsParams = parse_params(args)?;
697 let res = op_dns_lookup(p).await?;
698 json!(res)
699 }
700 "get_arp_table" => {
701 json!(op_get_arp_table()?)
702 }
703 "inspect_host" => {
704 let p: ScanParams = parse_params(args)?;
705 let res = op_inspect_host(p).await?;
706 json!(res)
707 }
708 "sweep_network" => {
709 let p: SweepParams = parse_params(args)?;
710 let res = op_sweep(p).await?;
711 json!(res)
712 }
713 "list_network_interfaces" => {
714 json!(op_list_interfaces())
715 }
716 "capture_pcap" => {
717 let p: PcapParams = parse_params(args)?;
718 let res = op_capture_pcap(p).await?;
719 json!(res)
720 }
721 other => {
722 return Err(RpcError::InvalidParams(format!("Unknown tool: {other}")));
723 }
724 };
725
726 Ok(mcp_tool_result_text(output))
727 }
728
729 "discover_network" => {
731 let p: DiscoverParams = parse_params(params)?;
732 let hosts = op_discover(p).await?;
733 serde_json::to_value(hosts).map_err(|e| RpcError::Internal(e.to_string()))
734 }
735 "scan_ports" => {
736 let p: ScanParams = parse_params(params)?;
737 let res = op_scan_ports(p).await?;
738 serde_json::to_value(res).map_err(|e| RpcError::Internal(e.to_string()))
739 }
740 "ping_host" => {
741 let p: PingHostParams = parse_params(params)?;
742 let res = op_ping_host(p).await?;
743 serde_json::to_value(res).map_err(|e| RpcError::Internal(e.to_string()))
744 }
745 "dns_lookup" => {
746 let p: DnsParams = parse_params(params)?;
747 let res = op_dns_lookup(p).await?;
748 serde_json::to_value(res).map_err(|e| RpcError::Internal(e.to_string()))
749 }
750 "get_arp_table" => {
751 serde_json::to_value(op_get_arp_table()?).map_err(|e| RpcError::Internal(e.to_string()))
752 }
753 "inspect_host" => {
754 let p: ScanParams = parse_params(params)?;
755 let res = op_inspect_host(p).await?;
756 serde_json::to_value(res).map_err(|e| RpcError::Internal(e.to_string()))
757 }
758 "sweep_network" => {
759 let p: SweepParams = parse_params(params)?;
760 let res = op_sweep(p).await?;
761 serde_json::to_value(res).map_err(|e| RpcError::Internal(e.to_string()))
762 }
763 "list_network_interfaces" => serde_json::to_value(op_list_interfaces())
764 .map_err(|e| RpcError::Internal(e.to_string())),
765 "capture_pcap" => {
766 let p: PcapParams = parse_params(params)?;
767 let res = op_capture_pcap(p).await?;
768 serde_json::to_value(res).map_err(|e| RpcError::Internal(e.to_string()))
769 }
770 _ => Err(RpcError::MethodNotFound),
771 }
772}