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