1use std::fmt::Write as _;
24
25use crate::llm::actions::{ActionDefinition, Parameter, ParameterDefinition};
26use crate::protocol::EventType;
27use crate::state::app_state::AppState;
28
29pub async fn render_protocol_docs(protocol: &str, state: &AppState) -> Option<String> {
34 let server_registry = crate::protocol::server_registry::registry();
35 let client_registry = &crate::protocol::CLIENT_REGISTRY;
36
37 let server = server_registry
40 .parse_from_str(protocol)
41 .and_then(|name| server_registry.get(&name))
42 .or_else(|| server_registry.get(protocol))
43 .or_else(|| server_registry.get(&protocol.to_uppercase()))
44 .or_else(|| server_registry.get(&protocol.to_lowercase()));
45
46 let client = client_registry
47 .parse_from_str(protocol)
48 .and_then(|name| client_registry.get(&name))
49 .or_else(|| client_registry.get(protocol))
50 .or_else(|| client_registry.get(&protocol.to_lowercase()))
51 .or_else(|| client_registry.get(&protocol.to_uppercase()));
52
53 if server.is_none() && client.is_none() {
54 return None;
55 }
56
57 let mut out = String::new();
58 let _ = writeln!(out, "# `{}` — MCP reference\n", protocol.to_lowercase());
59
60 match (server.is_some(), client.is_some()) {
61 (true, true) => out.push_str(
62 "Available as a **server** (`start_server`) and as a **client** (`start_client`).\n\n",
63 ),
64 (true, false) => out.push_str(
65 "Available as a **server** (`start_server`). No client implementation is compiled in.\n\n",
66 ),
67 (false, true) => out.push_str(
68 "Available as a **client** (`start_client`). No server implementation is compiled in.\n\n",
69 ),
70 (false, false) => unreachable!(),
71 }
72
73 if let Some(server) = server {
74 let metadata = server.metadata();
75 let binding = server.default_binding();
76
77 let _ = writeln!(out, "## Server — `start_server`\n");
78 let _ = writeln!(out, "{}\n", server.description());
79 let _ = writeln!(out, "- **maturity**: {}", metadata.state.as_str());
80 let _ = writeln!(
81 out,
82 "- **privilege required**: {}",
83 metadata.privilege_requirement.description()
84 );
85 let _ = writeln!(out, "- **network stack**: {}", server.stack_name());
86 let _ = writeln!(out, "- **implementation**: {}", metadata.implementation);
87 let _ = writeln!(out, "- **LLM controls**: {}", metadata.llm_control);
88 let _ = writeln!(
89 out,
90 "- **when the model cannot answer**: {}",
91 failure_mode_text(metadata.failure_mode)
92 );
93 let _ = writeln!(
94 out,
95 "- **connections**: {}",
96 if metadata.connectionless {
97 "connectionless — each datagram stands alone; a peer idle for ~10s is \
98 forgotten, and there is no per-connection state to rely on"
99 } else {
100 "connection-oriented — each peer has a connection id (see `server_status`) \
101 that lives until either side closes it"
102 }
103 );
104 if let Some(notes) = metadata.notes {
105 let _ = writeln!(out, "- **notes**: {}", notes);
106 }
107 out.push('\n');
108
109 out.push_str("### `start_server` arguments\n\n");
111 let _ = writeln!(
112 out,
113 "- `protocol` (string, **required**) — `\"{}\"`",
114 protocol.to_lowercase()
115 );
116
117 let interface_based = binding
118 .as_ref()
119 .map(|b| b.interface.is_some())
120 .unwrap_or(false);
121
122 if interface_based {
123 let default_iface = binding
124 .as_ref()
125 .and_then(|b| b.interface.clone())
126 .unwrap_or_else(|| "lo".to_string());
127 let _ = writeln!(
128 out,
129 "- `interface` (string) — NIC to bind. This protocol is interface-bound, \
130 not port-bound. Default `\"{}\"`.",
131 default_iface
132 );
133 let _ = writeln!(
134 out,
135 "- `mac_address` (string) — source MAC for layer-2 frames, e.g. \
136 `\"02:00:00:00:00:01\"`. Optional."
137 );
138 out.push_str("- `port` / `host` — ignored by this protocol.\n");
139 } else {
140 let default_host = binding
141 .as_ref()
142 .and_then(|b| b.host.clone())
143 .unwrap_or_else(|| crate::protocol::default_port::DEFAULT_HOST.to_string());
144 let caps = state.get_system_capabilities().await;
147 let default = crate::protocol::default_port::default_port_for_server(
148 server.as_ref(),
149 None,
150 &caps,
151 );
152 match default.as_ref().map(|d| (d.well_known, d.fallback, d.port)) {
153 Some((Some(p), None, _)) => {
154 let _ = writeln!(
155 out,
156 "- `port` (number) — listen port. Omit it for the well-known port \
157 `{p}`. Pass `0` for an OS-assigned port; an explicit port, `0` \
158 included, is always used as given."
159 );
160 }
161 Some((Some(_), Some(_), _)) => {
162 let _ = writeln!(
163 out,
164 "- `port` (number) — listen port. Omitted, this server starts on an \
165 OS-assigned port: {}. The chosen port is reported in the tool result \
166 and by `list_servers`. An explicit port is always used as given.",
167 default.as_ref().map(|d| d.describe()).unwrap_or_default()
168 );
169 }
170 _ => out.push_str(
171 "- `port` (number) — listen port. This protocol has no well-known port, so \
172 omitting it (or passing `0`) asks the OS for a free one; the chosen port is \
173 reported in the tool result and by `list_servers`.\n",
174 ),
175 }
176 let _ = writeln!(
177 out,
178 "- `host` (string) — bind address. Default `\"{}\"`.",
179 default_host
180 );
181 }
182
183 out.push_str(
184 "- `event_handlers` (array) — **preferred**. Deterministic, in-process, no LLM \
185 call. Each entry is `{\"event_pattern\": \"<event id>\" | \"*\", \"handler\": {…}}`, \
186 matched in order, first match wins. `handler` is one of `{\"type\":\"script\",\
187 \"language\":\"python\"|\"javascript\",\"code\":\"…\"}`, \
188 `{\"type\":\"static\",\"actions\":[…]}`, or `{\"type\":\"llm\",\"instruction\":\"…\"}`. \
189 A script receives the event as JSON on stdin (fields under `data['event']`) and \
190 must print `{\"actions\":[…]}` to stdout. Use the event ids and field names \
191 listed below.\n",
192 );
193 out.push_str(
194 "- `instruction` (string) — natural-language fallback. Costs one model call per \
195 event; use only when the response genuinely needs reasoning.\n",
196 );
197
198 let startup_params = server.get_startup_parameters();
199 if startup_params.is_empty() {
200 out.push_str("- `startup_params` (object) — this protocol declares none.\n");
201 } else {
202 let names: Vec<String> = startup_params
203 .iter()
204 .map(|p| format!("`{}`", p.name))
205 .collect();
206 let _ = writeln!(
207 out,
208 "- `startup_params` (object) — this protocol accepts {}. Schema below. \
209 Passing a key this protocol does not declare is an error.",
210 names.join(", ")
211 );
212 }
213
214 if startup_params.iter().any(|p| p.name == "send_first") {
215 out.push_str(
216 "- `send_first` (boolean) — when true the server speaks first on connect \
217 (FTP/SMTP-style greeting banner) instead of waiting for the client. \
218 Equivalent to `startup_params.send_first`.\n",
219 );
220 }
221
222 out.push_str(
223 "- `initial_memory` (string) — seeds the server's LLM memory before the first \
224 event.\n",
225 );
226 out.push_str(
227 "- `feedback_instructions` (string) — natural-language instructions for the \
228 automatic feedback loop that adjusts the server as it runs.\n",
229 );
230 out.push_str(
231 "- `scheduled_tasks` (array) — server-scoped recurring/delayed LLM tasks. Each \
232 entry: `{\"task_id\":\"…\",\"recurring\":true,\"interval_secs\":60,\
233 \"instruction\":\"…\"}` (or `\"recurring\":false` with `\"delay_secs\"`). \
234 Removed when the server stops.\n",
235 );
236 out.push('\n');
237
238 let event_types = server.get_event_types();
240 if event_types.is_empty() {
241 out.push_str(
242 "### Events\n\nThis protocol declares no event types, so `event_handlers` \
243 cannot be targeted at a specific event id — use `\"event_pattern\": \"*\"` \
244 or the `instruction` fallback.\n\n",
245 );
246 } else {
247 let ids: Vec<String> = event_types.iter().map(|e| format!("`{}`", e.id)).collect();
248 let _ = writeln!(
249 out,
250 "### Events ({}) — valid values for `event_pattern`\n",
251 ids.join(", ")
252 );
253 for event in &event_types {
254 render_event(&mut out, event);
255 }
256 }
257
258 let sync_actions = server.get_sync_actions();
260 if !sync_actions.is_empty() {
261 out.push_str(
262 "### Response actions\n\nThese are the action objects a handler may return \
263 (inside `{\"actions\":[…]}` for a script, or as `handler.actions` for a \
264 static handler).\n\n",
265 );
266 for action in &sync_actions {
267 render_action(&mut out, action);
268 }
269 }
270
271 let async_actions: Vec<ActionDefinition> = server
272 .get_async_actions(state)
273 .into_iter()
274 .filter(|a| !sync_actions.iter().any(|s| s.name == a.name))
275 .collect();
276 if !async_actions.is_empty() {
277 out.push_str(
278 "### Server-level actions\n\nThese are not tied to one event. The \
279 `instruction` LLM can use them, and on a live connection whose protocol \
280 registers a peer handle (`server_status` marks those) `send_to_peer` \
281 accepts them alongside the response actions above.\n\n",
282 );
283 for action in &async_actions {
284 render_action(&mut out, action);
285 }
286 }
287
288 if !startup_params.is_empty() {
290 out.push_str("### `startup_params` schema\n\n");
291 for param in &startup_params {
292 render_startup_param(&mut out, param);
293 }
294 out.push('\n');
295 }
296
297 out.push_str("---\n\n");
298 }
299
300 if let Some(client) = client {
301 let metadata = client.metadata();
302
303 let _ = writeln!(out, "## Client — `start_client`\n");
304 let _ = writeln!(out, "{}\n", client.description());
305 let _ = writeln!(out, "- **maturity**: {}", metadata.state.as_str());
306 let _ = writeln!(
307 out,
308 "- **privilege required**: {}",
309 metadata.privilege_requirement.description()
310 );
311 let _ = writeln!(out, "- **network stack**: {}", client.stack_name());
312 let _ = writeln!(out, "- **implementation**: {}", metadata.implementation);
313 let _ = writeln!(out, "- **LLM controls**: {}", metadata.llm_control);
314 if let Some(notes) = metadata.notes {
315 let _ = writeln!(out, "- **notes**: {}", notes);
316 }
317 out.push('\n');
318
319 out.push_str("### `start_client` arguments\n\n");
320 let _ = writeln!(
321 out,
322 "- `protocol` (string, **required**) — `\"{}\"`",
323 protocol.to_lowercase()
324 );
325 out.push_str(
326 "- `remote_addr` (string, **required**) — the server to connect to, \
327 `host:port` (e.g. `\"127.0.0.1:6379\"`).\n",
328 );
329 out.push_str(
330 "- `event_handlers` (array) — same shape as `start_server`; deterministic, no \
331 LLM call. Target the client event ids listed below.\n",
332 );
333 out.push_str(
334 "- `instruction` (string) — natural-language fallback, one model call per \
335 event.\n",
336 );
337
338 let startup_params = client.get_startup_parameters();
339 if startup_params.is_empty() {
340 out.push_str("- `startup_params` (object) — this protocol declares none.\n");
341 } else {
342 let names: Vec<String> = startup_params
343 .iter()
344 .map(|p| format!("`{}`", p.name))
345 .collect();
346 let _ = writeln!(
347 out,
348 "- `startup_params` (object) — this protocol accepts {}. Schema below.",
349 names.join(", ")
350 );
351 }
352 out.push_str("- `initial_memory` (string) — seeds the client's LLM memory.\n");
353 out.push_str(
354 "- `feedback_instructions` (string) — instructions for the automatic feedback \
355 loop.\n",
356 );
357 out.push_str(
358 "- `scheduled_tasks` (array) — client-scoped recurring/delayed LLM tasks, same \
359 shape as for `start_server`.\n",
360 );
361 out.push('\n');
362
363 let event_types = client.get_event_types();
364 if event_types.is_empty() {
365 out.push_str(
366 "### Events\n\nThis client declares no event types; use \
367 `\"event_pattern\": \"*\"` or the `instruction` fallback.\n\n",
368 );
369 } else {
370 let ids: Vec<String> = event_types.iter().map(|e| format!("`{}`", e.id)).collect();
371 let _ = writeln!(
372 out,
373 "### Events ({}) — valid values for `event_pattern`\n",
374 ids.join(", ")
375 );
376 for event in &event_types {
377 render_event(&mut out, event);
378 }
379 }
380
381 let sync_actions = client.get_sync_actions();
382 if !sync_actions.is_empty() {
383 out.push_str("### Response actions\n\nAction objects a client handler may return.\n\n");
384 for action in &sync_actions {
385 render_action(&mut out, action);
386 }
387 }
388
389 let async_actions: Vec<ActionDefinition> = client
390 .get_async_actions(state)
391 .into_iter()
392 .filter(|a| !sync_actions.iter().any(|s| s.name == a.name))
393 .collect();
394 if !async_actions.is_empty() {
395 out.push_str(
396 "### Client actions\n\nWhat the client can do on its own initiative. The \
397 `instruction` LLM and handlers can use them, and `send_to_client` puts any \
398 of them — or of the response actions above — on the wire now.\n\n",
399 );
400 for action in &async_actions {
401 render_action(&mut out, action);
402 }
403 } else if !sync_actions.is_empty() {
404 out.push_str(
405 "`send_to_client` accepts any of the response actions above to put one on \
406 the wire now.\n\n",
407 );
408 }
409
410 if !startup_params.is_empty() {
411 out.push_str("### `startup_params` schema\n\n");
412 for param in &startup_params {
413 render_startup_param(&mut out, param);
414 }
415 out.push('\n');
416 }
417 }
418
419 Some(out)
420}
421
422fn failure_mode_text(mode: crate::protocol::metadata::FailureMode) -> &'static str {
424 match mode {
425 crate::protocol::metadata::FailureMode::Answers => {
426 "answers — the peer gets the protocol's own error reply carrying a category \
427 (overloaded or unavailable), never an invented success and never the error text"
428 }
429 crate::protocol::metadata::FailureMode::DeliberatelySilent => {
430 "deliberately silent — nothing is written, because every reply this protocol \
431 defines would assert something the server does not know; the log records \
432 `decision=…` instead"
433 }
434 }
435}
436
437fn render_event(out: &mut String, event: &EventType) {
438 let _ = writeln!(out, "#### `{}`\n", event.id);
439 let _ = writeln!(out, "{}\n", event.description);
440
441 if event.parameters.is_empty() {
442 out.push_str("Event data: (no documented fields)\n\n");
443 } else {
444 out.push_str("Event data fields (a script reads these as `data['event'][…]`):\n\n");
445 for param in &event.parameters {
446 render_parameter(out, param);
447 }
448 out.push('\n');
449 }
450
451 let action_names: Vec<String> = event
452 .actions
453 .iter()
454 .map(|a| format!("`{}`", a.name))
455 .collect();
456 if !action_names.is_empty() {
457 let _ = writeln!(out, "Respond with: {}\n", action_names.join(", "));
458 }
459
460 out.push_str("Example handler response:\n\n```json\n");
461 let example = serde_json::json!({ "actions": [event.effective_response_example()] });
462 out.push_str(&serde_json::to_string_pretty(&example).unwrap_or_default());
463 out.push_str("\n```\n\n");
464
465 for alt in &event.alternative_examples {
466 out.push_str("Alternative:\n\n```json\n");
467 let example = serde_json::json!({ "actions": [alt.clone()] });
468 out.push_str(&serde_json::to_string_pretty(&example).unwrap_or_default());
469 out.push_str("\n```\n\n");
470 }
471}
472
473fn render_action(out: &mut String, action: &ActionDefinition) {
474 let _ = writeln!(out, "#### `{}`\n", action.name);
475 let _ = writeln!(out, "{}\n", action.description);
476 if action.parameters.is_empty() {
477 out.push_str("Parameters: none.\n\n");
478 } else {
479 out.push_str("Parameters:\n\n");
480 for param in &action.parameters {
481 render_parameter(out, param);
482 }
483 out.push('\n');
484 }
485 out.push_str("```json\n");
486 out.push_str(&serde_json::to_string_pretty(&action.example).unwrap_or_default());
487 out.push_str("\n```\n\n");
488}
489
490fn render_parameter(out: &mut String, param: &Parameter) {
491 let _ = writeln!(
492 out,
493 "- `{}` ({}, {}) — {}",
494 param.name,
495 param.type_hint,
496 if param.required {
497 "**required**"
498 } else {
499 "optional"
500 },
501 param.description
502 );
503}
504
505fn render_startup_param(out: &mut String, param: &ParameterDefinition) {
506 let example = serde_json::to_string(¶m.example).unwrap_or_default();
507 let default = param
509 .default
510 .as_ref()
511 .map(|d| {
512 format!(
513 ", default: `{}`",
514 serde_json::to_string(d).unwrap_or_default()
515 )
516 })
517 .unwrap_or_default();
518 let _ = writeln!(
519 out,
520 "- `{}` ({}, {}{}) — {} Example: `{}`",
521 param.name,
522 param.type_hint,
523 if param.required {
524 "**required**"
525 } else {
526 "optional"
527 },
528 default,
529 param.description,
530 example
531 );
532}