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