Skip to main content

netget/mcp_stdio/
docs.rs

1//! MCP-shaped protocol documentation.
2//!
3//! The internal TUI LLM has its own documentation renderer
4//! (`llm::actions::tools::execute_read_documentation`) which describes the
5//! `open_server` / `open_client` **actions** and the `base_stack` parameter.
6//! None of that exists on the MCP surface: an MCP caller has `start_server` /
7//! `start_client` tools with a fixed argument list.
8//!
9//! This module renders documentation aimed at an MCP caller — the exact tool
10//! arguments that apply, the protocol's event ids with their field names and
11//! types (needed to write an `event_handlers` script), its action names with
12//! parameter schemas and examples, its startup parameters, its privilege
13//! requirement, its maturity, what it writes when the model cannot answer
14//! (`failure_mode`), and whether it keeps per-connection state (`connectionless`).
15//!
16//! Every action is rendered in full — parameters and a JSON example — including
17//! the async ones, because those are invocable from MCP: a client's through
18//! `send_to_client`, a server's through `send_to_peer` on a connection whose
19//! protocol registers a peer handle.
20//!
21//! Nothing here mentions `open_server`, `open_client` or `base_stack`.
22
23use std::fmt::Write as _;
24
25use crate::llm::actions::{ActionDefinition, Parameter, ParameterDefinition};
26use crate::protocol::EventType;
27use crate::state::app_state::AppState;
28
29/// Render MCP-shaped documentation for one protocol.
30///
31/// Returns `None` when the protocol is in neither registry (i.e. not compiled
32/// into this build).
33pub 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    // Registries disagree on case: servers are keyed upper-ish, clients lower-ish.
38    // Try the parse helpers first, then the raw casings.
39    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        // --- start_server arguments -----------------------------------------
110        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            // The same resolution `start_server` applies when `port` is omitted, against this
145            // process's privilege and what is bound right now.
146            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        // --- events ----------------------------------------------------------
239        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        // --- actions ---------------------------------------------------------
259        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        // --- startup params ---------------------------------------------------
289        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
422/// What `failure_mode` means for a peer, in one sentence.
423fn 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(&param.example).unwrap_or_default();
507    // The value the server uses when the parameter is omitted, where the protocol declares one.
508    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}