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
283pub(super) fn mcp_tool_error_text(message: &str) -> serde_json::Value {
289 json!({
290 "content": [{ "type": "text", "text": message }],
291 "isError": true,
292 })
293}