1use crate::llm::actions::{
4 client_trait::{Client, ClientActionResult},
5 protocol_trait::Protocol,
6 ActionDefinition, Parameter,
7};
8use crate::protocol::log_template::LogTemplate;
9use crate::protocol::EventType;
10use crate::state::app_state::AppState;
11use anyhow::{Context, Result};
12use serde_json::json;
13use std::sync::LazyLock;
14
15pub static MDNS_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
17 EventType::new(
18 "mdns_connected",
19 "mDNS client initialized and ready for service discovery",
20 json!({"type": "browse_service", "service_type": "_http._tcp.local"}),
21 )
22 .with_parameters(vec![
23 Parameter {
24 name: "status".to_string(),
25 type_hint: "string".to_string(),
26 description: "Connection status".to_string(),
27 required: true,
28 },
29 Parameter {
30 name: "message".to_string(),
31 type_hint: "string".to_string(),
32 description: "Status message".to_string(),
33 required: false,
34 },
35 ])
36});
37
38pub static MDNS_CLIENT_SERVICE_FOUND_EVENT: LazyLock<EventType> = LazyLock::new(|| {
40 EventType::new(
41 "mdns_service_found",
42 "mDNS service instance discovered on the local network",
43 json!({"type": "wait_for_more"}),
44 )
45 .with_parameters(vec![
46 Parameter {
47 name: "service_type".to_string(),
48 type_hint: "string".to_string(),
49 description: "Service type (e.g., '_http._tcp.local')".to_string(),
50 required: true,
51 },
52 Parameter {
53 name: "fullname".to_string(),
54 type_hint: "string".to_string(),
55 description: "Full service instance name".to_string(),
56 required: true,
57 },
58 ])
59});
60
61pub static MDNS_CLIENT_SERVICE_RESOLVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
63 EventType::new(
64 "mdns_service_resolved",
65 "mDNS service fully resolved with IP addresses and port",
66 json!({"type": "wait_for_more"}),
67 )
68 .with_parameters(vec![
69 Parameter {
70 name: "fullname".to_string(),
71 type_hint: "string".to_string(),
72 description: "Full service instance name".to_string(),
73 required: true,
74 },
75 Parameter {
76 name: "hostname".to_string(),
77 type_hint: "string".to_string(),
78 description: "Service hostname".to_string(),
79 required: true,
80 },
81 Parameter {
82 name: "addresses".to_string(),
83 type_hint: "array".to_string(),
84 description: "List of IP addresses".to_string(),
85 required: true,
86 },
87 Parameter {
88 name: "port".to_string(),
89 type_hint: "number".to_string(),
90 description: "Service port number".to_string(),
91 required: true,
92 },
93 Parameter {
94 name: "properties".to_string(),
95 type_hint: "array".to_string(),
96 description: "Service TXT record properties".to_string(),
97 required: false,
98 },
99 ])
100});
101
102pub struct MdnsClientProtocol;
104
105impl MdnsClientProtocol {
106 pub fn new() -> Self {
107 Self
108 }
109}
110
111impl Protocol for MdnsClientProtocol {
113 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
114 vec![
115 ActionDefinition {
116 name: "browse_service".to_string(),
117 description: "Browse for mDNS services of a specific type on the local network".to_string(),
118 parameters: vec![
119 Parameter {
120 name: "service_type".to_string(),
121 type_hint: "string".to_string(),
122 description: "Service type to browse for (e.g., '_http._tcp.local', '_ssh._tcp.local', '_printer._tcp.local')".to_string(),
123 required: true,
124 },
125 ],
126 example: json!({
127 "type": "browse_service",
128 "service_type": "_http._tcp.local"
129 }),
130 log_template: None,
131 },
132 ActionDefinition {
133 name: "resolve_hostname".to_string(),
134 description: "Resolve a .local hostname to IP addresses using mDNS".to_string(),
135 parameters: vec![
136 Parameter {
137 name: "hostname".to_string(),
138 type_hint: "string".to_string(),
139 description: "Hostname to resolve (e.g., 'myserver.local')".to_string(),
140 required: true,
141 },
142 ],
143 example: json!({
144 "type": "resolve_hostname",
145 "hostname": "myserver.local"
146 }),
147 log_template: None,
148 },
149 ActionDefinition {
150 name: "disconnect".to_string(),
151 description: "Stop mDNS service discovery and disconnect".to_string(),
152 parameters: vec![],
153 example: json!({
154 "type": "disconnect"
155 }),
156 log_template: None,
157 },
158 ]
159 }
160 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
161 vec![ActionDefinition {
162 name: "wait_for_more".to_string(),
163 description: "Wait for more service discovery events before responding".to_string(),
164 parameters: vec![],
165 example: json!({
166 "type": "wait_for_more"
167 }),
168 log_template: Some(
169 LogTemplate::new()
170 .with_info("-> mDNS wait for more")
171 .with_debug("mDNS wait_for_more"),
172 ),
173 }]
174 }
175 fn protocol_name(&self) -> &'static str {
176 "mDNS"
177 }
178 fn get_event_types(&self) -> Vec<EventType> {
179 vec![
180 EventType::new(
181 "mdns_connected",
182 "Triggered when mDNS client is initialized",
183 json!({"type": "wait_for_more"}),
184 ),
185 EventType::new(
186 "mdns_service_found",
187 "Triggered when an mDNS service is discovered",
188 json!({"type": "wait_for_more"}),
189 ),
190 EventType::new(
191 "mdns_service_resolved",
192 "Triggered when an mDNS service is fully resolved with IP and port",
193 json!({"type": "wait_for_more"}),
194 ),
195 ]
196 }
197 fn stack_name(&self) -> &'static str {
198 "ETH>IP>UDP>mDNS"
199 }
200 fn keywords(&self) -> Vec<&'static str> {
201 vec![
202 "mdns",
203 "multicast dns",
204 "service discovery",
205 "zeroconf",
206 "bonjour",
207 ".local",
208 ]
209 }
210 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
211 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
212
213 ProtocolMetadataV2::builder()
214 .state(DevelopmentState::Experimental)
215 .implementation("mdns-sd crate for RFC 6762 and RFC 6763 compliance")
216 .llm_control("Browse services, resolve hostnames, analyze service properties")
217 .e2e_testing("Built-in macOS/Linux mDNS responders for testing")
218 .build()
219 }
220 fn description(&self) -> &'static str {
221 "mDNS client for discovering services on the local network"
222 }
223 fn example_prompt(&self) -> &'static str {
224 "Browse for HTTP services on the local network using mDNS"
225 }
226 fn group_name(&self) -> &'static str {
227 "DNS"
228 }
229
230 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
231 use crate::llm::actions::StartupExamples;
232 use serde_json::json;
233
234 StartupExamples::new(
235 json!({
237 "type": "open_client",
238 "remote_addr": "0.0.0.0:5353",
239 "base_stack": "mdns",
240 "instruction": "Browse for HTTP services on the local network and list all discovered web servers"
241 }),
242 json!({
244 "type": "open_client",
245 "remote_addr": "0.0.0.0:5353",
246 "base_stack": "mdns",
247 "event_handlers": [{
248 "event_pattern": "mdns_service_resolved",
249 "handler": {
250 "type": "script",
251 "language": "python",
252 "code": "<mdns_client_handler>"
253 }
254 }]
255 }),
256 json!({
258 "type": "open_client",
259 "remote_addr": "0.0.0.0:5353",
260 "base_stack": "mdns",
261 "event_handlers": [
262 {
263 "event_pattern": "mdns_connected",
264 "handler": {
265 "type": "static",
266 "actions": [{
267 "type": "browse_service",
268 "service_type": "_http._tcp.local"
269 }]
270 }
271 },
272 {
273 "event_pattern": "mdns_service_resolved",
274 "handler": {
275 "type": "static",
276 "actions": [{
277 "type": "wait_for_more"
278 }]
279 }
280 }
281 ]
282 }),
283 )
284 }
285}
286
287impl Client for MdnsClientProtocol {
289 fn connect(
290 &self,
291 ctx: crate::protocol::ConnectContext,
292 ) -> std::pin::Pin<
293 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
294 > {
295 Box::pin(async move {
296 use crate::client::mdns::MdnsClient;
297 MdnsClient::connect_with_llm_actions(
298 ctx.remote_addr,
299 ctx.llm_client,
300 ctx.state,
301 ctx.status_tx,
302 ctx.client_id,
303 )
304 .await
305 })
306 }
307 fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
308 let action_type = action
309 .get("type")
310 .and_then(|v| v.as_str())
311 .context("Missing 'type' field in action")?;
312
313 match action_type {
314 "browse_service" => {
315 let service_type = action
316 .get("service_type")
317 .and_then(|v| v.as_str())
318 .context("Missing 'service_type' field")?;
319
320 Ok(ClientActionResult::Custom {
321 name: "browse_service".to_string(),
322 data: json!({
323 "service_type": service_type
324 }),
325 })
326 }
327 "resolve_hostname" => {
328 let hostname = action
329 .get("hostname")
330 .and_then(|v| v.as_str())
331 .context("Missing 'hostname' field")?;
332
333 Ok(ClientActionResult::Custom {
334 name: "resolve_hostname".to_string(),
335 data: json!({
336 "hostname": hostname
337 }),
338 })
339 }
340 "disconnect" => Ok(ClientActionResult::Disconnect),
341 "wait_for_more" => Ok(ClientActionResult::WaitForMore),
342 _ => Err(anyhow::anyhow!(
343 "Unknown mDNS client action: {}",
344 action_type
345 )),
346 }
347 }
348}