Skip to main content

netscli_mcp/server/
tools.rs

1use serde_json::json;
2
3pub fn tools_list() -> serde_json::Value {
4    let tools = vec![
5        json!({
6            "name": "discover_network",
7            "description": "Discover live hosts on a network subnet",
8            "inputSchema": {
9                "type": "object",
10                "properties": {
11                    "subnet": {
12                        "type": "string",
13                        "description": "IPv4 CIDR, at most a /16. Defaults to the local subnet."
14                    },
15                    "resolveHostnames": { "type": "boolean", "default": false },
16                    "timeout": { "type": "number", "minimum": 10, "maximum": 600000, "description": "Milliseconds, applied to every step. Omit for the defaults: ping 1000, scan 500, DNS 1500." },
17                    "maxConcurrent": { "type": "number", "default": 256, "minimum": 1, "maximum": 1024 }
18                }
19            },
20            "annotations": {
21                "readOnlyHint": true,
22                "destructiveHint": false,
23                "openWorldHint": true
24            }
25        }),
26        json!({
27            "name": "scan_ports",
28            "description": "Scan TCP ports on a host, or UDP ports with udp: true. UDP without ports probes DNS, NTP, NetBIOS, SSDP and mDNS; a UDP port that neither replies nor refuses is open|filtered. Returns only open, open|filtered and errored ports unless include_closed is true, so an empty list means every port scanned was closed or filtered.",
29            "inputSchema": {
30                "type": "object",
31                "properties": {
32                    "host": { "type": "string" },
33                    "ports": {
34                        "type": "array",
35                        "items": { "type": "number", "minimum": 1, "maximum": 65535 },
36                        "maxItems": 4096
37                    },
38                    "udp": { "type": "boolean", "default": false },
39                    "include_closed": { "type": "boolean", "default": false, "description": "Also return closed and filtered ports, one entry each." },
40                    "timeout": { "type": "number", "default": 500, "minimum": 10, "maximum": 600000 },
41                    "maxConcurrent": { "type": "number", "default": 256, "minimum": 1, "maximum": 1024 }
42                },
43                "required": ["host"]
44            },
45            "annotations": {
46                "readOnlyHint": true,
47                "destructiveHint": false,
48                "openWorldHint": true
49            }
50        }),
51        json!({
52            "name": "ping_host",
53            "description": "Ping a host (ICMP with TCP-connect fallback). Returns a PingSummary with aggregate loss and min/avg/max RTT when count > 1.",
54            "inputSchema": {
55                "type": "object",
56                "properties": {
57                    "host": { "type": "string" },
58                    "count": { "type": "number", "default": 1, "minimum": 1, "maximum": 256 },
59                    "timeout": { "type": "number", "default": 1000, "minimum": 10, "maximum": 600000 }
60                },
61                "required": ["host"]
62            },
63            "annotations": {
64                "readOnlyHint": true,
65                "destructiveHint": false,
66                "openWorldHint": true
67            }
68        }),
69        json!({
70            "name": "dns_lookup",
71            "description": "DNS lookup (A, AAAA, CNAME, MX, NS, TXT, SRV, PTR, SOA, CAA, or ALL/ANY for every record type)",
72            "inputSchema": {
73                "type": "object",
74                "properties": {
75                    "host": { "type": "string" },
76                    "type": {
77                        "type": "string",
78                        "description": "Record type. Omit, or pass ALL, for every type.",
79                        "enum": ["A", "AAAA", "CNAME", "MX", "NS", "TXT", "SRV", "PTR", "SOA", "CAA", "ALL", "ANY"]
80                    }
81                },
82                "required": ["host"]
83            },
84            "annotations": {
85                "readOnlyHint": true,
86                "destructiveHint": false,
87                "openWorldHint": true
88            }
89        }),
90        json!({
91            "name": "get_arp_table",
92            "description": "Get ARP/neighbor table with vendor information",
93            "inputSchema": {
94                "type": "object",
95                "properties": {}
96            },
97            "annotations": {
98                "readOnlyHint": true,
99                "destructiveHint": false,
100                "openWorldHint": false
101            }
102        }),
103        json!({
104            "name": "inspect_host",
105            "description": "Inspect a host: ping, port scan, reverse DNS, MAC vendor on the local segment, and an OS hint (family, detail and the evidence behind it, from SMB, SSH/HTTP banners, open ports, MAC vendor and ping TTL). The hint is a guess, not a fingerprint. ports lists only open, open|filtered and errored ports unless include_closed is true.",
106            "inputSchema": {
107                "type": "object",
108                "properties": {
109                    "host": { "type": "string" },
110                    "ports": {
111                        "type": "array",
112                        "items": { "type": "number", "minimum": 1, "maximum": 65535 },
113                        "maxItems": 4096
114                    },
115                    "timeout": { "type": "number", "minimum": 10, "maximum": 600000, "description": "Milliseconds, applied to every step. Omit for the defaults: ping 1000, scan 500, DNS 1500." },
116                    "include_closed": { "type": "boolean", "default": false, "description": "Also list closed and filtered ports in ports, one entry each." },
117                    "maxConcurrent": { "type": "number", "default": 256, "minimum": 1, "maximum": 1024 }
118                },
119                "required": ["host"]
120            },
121            "annotations": {
122                "readOnlyHint": true,
123                "destructiveHint": false,
124                "openWorldHint": true
125            }
126        }),
127        json!({
128            "name": "sweep_network",
129            "description": "Sweep a network (discover hosts then scan ports)",
130            "inputSchema": {
131                "type": "object",
132                "properties": {
133                    "subnet": { "type": "string", "description": "IPv4 CIDR, at most a /16. Defaults to the local subnet." },
134                    "ports": { "type": "array", "items": { "type": "number" } },
135                    "resolveHostnames": { "type": "boolean", "default": false },
136                    "timeout": { "type": "number", "minimum": 10, "maximum": 600000, "description": "Milliseconds, applied to every step. Omit for the defaults: ping 1000, scan 500, DNS 1500." },
137                    "maxConcurrent": { "type": "number", "default": 256 }
138                }
139            },
140            "annotations": {
141                "readOnlyHint": true,
142                "destructiveHint": false,
143                "openWorldHint": true
144            }
145        }),
146        json!({
147            "name": "list_network_interfaces",
148            "description": "List network interfaces with details",
149            "inputSchema": {
150                "type": "object",
151                "properties": {}
152            },
153            "annotations": {
154                "readOnlyHint": true,
155                "destructiveHint": false,
156                "openWorldHint": false
157            }
158        }),
159    ];
160
161    #[cfg(feature = "pcap")]
162    let tools = {
163        let mut tools = tools;
164        tools.push(json!({
165            "name": "capture_pcap",
166            "description": "Capture network packets to a PCAP file in one blocking tool call (may require root/admin). For longer captures, prefer start_pcap_capture then poll status and fetch the result.",
167            "inputSchema": {
168                "type": "object",
169                "properties": {
170                    "interface": { "type": "string" },
171                    "filter": { "type": "string" },
172                    "duration": { "type": "number", "default": 10, "minimum": 1, "maximum": 120 },
173                    "outputFile": { "type": "string", "default": "capture.pcap" },
174                    "maxPackets": { "type": "number" }
175                },
176                "required": ["interface"]
177            },
178            "annotations": {
179                "readOnlyHint": false,
180                "destructiveHint": false,
181                "openWorldHint": true
182            }
183        }));
184        tools.push(json!({
185            "name": "start_pcap_capture",
186            "description": "Start packet capture as a background MCP job. Poll with get_pcap_capture_status, then fetch output with get_pcap_capture_result.",
187            "inputSchema": {
188                "type": "object",
189                "properties": {
190                    "interface": { "type": "string" },
191                    "filter": { "type": "string" },
192                    "duration": { "type": "number", "default": 10 },
193                    "outputFile": { "type": "string", "description": "Where to write the capture. Omit for a file named after the job." },
194                    "maxPackets": { "type": "number" }
195                },
196                "required": ["interface"]
197            },
198            "annotations": {
199                "readOnlyHint": false,
200                "destructiveHint": false,
201                "openWorldHint": true
202            }
203        }));
204        tools.push(json!({
205            "name": "get_pcap_capture_status",
206            "description": "Get the running/completed/failed status for a packet capture job.",
207            "inputSchema": {
208                "type": "object",
209                "properties": {
210                    "jobId": { "type": "string" }
211                },
212                "required": ["jobId"]
213            },
214            "annotations": {
215                "readOnlyHint": true,
216                "destructiveHint": false,
217                "openWorldHint": false
218            }
219        }));
220        tools.push(json!({
221            "name": "get_pcap_capture_result",
222            "description": "Fetch the result for a completed packet capture job, including parsed packet summaries when available.",
223            "inputSchema": {
224                "type": "object",
225                "properties": {
226                    "jobId": { "type": "string" }
227                },
228                "required": ["jobId"]
229            },
230            "annotations": {
231                "readOnlyHint": true,
232                "destructiveHint": false,
233                "openWorldHint": false
234            }
235        }));
236        tools
237    };
238
239    #[cfg(feature = "mdns")]
240    let tools = {
241        let mut tools = tools;
242        tools.push(json!({
243            "name": "discover_mdns",
244            "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.",
245            "inputSchema": {
246                "type": "object",
247                "properties": {
248                    "timeout_ms": {
249                        "type": "number",
250                        "default": 3000,
251                        "description": "How long to browse for responses. 3000-5000ms is typical; many devices re-announce on a multi-second cadence."
252                    },
253                    "service_types": {
254                        "type": "array",
255                        "items": { "type": "string" },
256                        "description": "Explicit service types to browse (e.g. [\"_http._tcp.local.\", \"_airplay._tcp.local.\"]). Omit to use a curated default set."
257                    }
258                }
259            },
260            "annotations": {
261                "readOnlyHint": true,
262                "destructiveHint": false,
263                "openWorldHint": true
264            }
265        }));
266        tools
267    };
268
269    json!({ "tools": tools })
270}
271
272pub(super) fn mcp_tool_result_text(val: serde_json::Value) -> serde_json::Value {
273    json!({
274        "content": [
275            {
276                "type": "text",
277                "text": serde_json::to_string(&val).unwrap_or_else(|_| "<serialization error>".to_string())
278            }
279        ]
280    })
281}
282
283/// A tool that ran and failed, in the shape MCP defines for that.
284///
285/// `isError` is what tells a client the call completed but the work did not,
286/// so the model can read the reason and adapt. A JSON-RPC error in the same
287/// situation reads as a transport or server fault.
288pub(super) fn mcp_tool_error_text(message: &str) -> serde_json::Value {
289    json!({
290        "content": [{ "type": "text", "text": message }],
291        "isError": true,
292    })
293}