zagens_runtime/cli/
doctor.rs1use std::path::Path;
4
5use crate::config::Config;
6use crate::mcp::McpServerConfig;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub(crate) struct DoctorApiTarget {
10 pub(crate) provider: &'static str,
11 pub(crate) base_url: String,
12 pub(crate) model: String,
13}
14
15pub(crate) fn doctor_api_target(config: &Config) -> DoctorApiTarget {
16 let provider = config.api_provider();
17 DoctorApiTarget {
18 provider: provider.as_str(),
19 base_url: config.deepseek_base_url(),
20 model: config.default_model(),
21 }
22}
23
24pub(crate) fn doctor_timeout_recovery_lines(config: &Config) -> Vec<String> {
25 let target = doctor_api_target(config);
26 let mut lines = vec![format!(
27 "Connection timed out while reaching {}.",
28 target.base_url
29 )];
30
31 match config.api_provider() {
32 crate::config::ApiProvider::Deepseek
33 if target.base_url.contains("api.deepseek.com")
34 && !target.base_url.contains("api.deepseeki.com") =>
35 {
36 lines.push(
37 "If you are in mainland China, set `provider = \"deepseek-cn\"` or `base_url = \"https://api.deepseek.com\"` in ~/.zagens/config.toml, then rerun `zagens doctor`."
38 .to_string(),
39 );
40 }
41 crate::config::ApiProvider::Deepseek | crate::config::ApiProvider::DeepseekCN => {
42 lines.push(
43 "If this is a custom DeepSeek-compatible endpoint, confirm it serves `/v1/models` and `/v1/chat/completions` over HTTPS."
44 .to_string(),
45 );
46 }
47 _ => {
48 lines.push(
49 "Confirm the configured provider endpoint is reachable and OpenAI-compatible for `/v1/models` and `/v1/chat/completions`."
50 .to_string(),
51 );
52 }
53 }
54
55 lines.push(
56 "Run `deepseek doctor --json` and include `base_url`, `default_text_model`, and `api_connectivity` when filing an issue."
57 .to_string(),
58 );
59 lines
60}
61
62#[derive(Debug)]
63pub(crate) enum McpServerDoctorStatus {
64 Ok(String),
65 Warning(String),
66 Error(String),
67}
68
69pub(crate) fn doctor_check_mcp_server(server: &McpServerConfig) -> McpServerDoctorStatus {
70 if server.command.is_none() && server.url.is_none() {
71 return McpServerDoctorStatus::Error("no command or url configured".to_string());
72 }
73
74 if let Some(ref url) = server.url {
75 return McpServerDoctorStatus::Ok(format!("HTTP/SSE server at {url}"));
76 }
77
78 let cmd = server.command.as_deref().unwrap_or("");
79 if cmd.is_empty() {
80 return McpServerDoctorStatus::Error("empty command".to_string());
81 }
82
83 let cmd_path = Path::new(cmd);
84 let is_absolute = cmd_path.is_absolute() || cmd.starts_with('/');
85
86 if is_absolute && !cmd_path.exists() {
87 return McpServerDoctorStatus::Error(format!("command not found: {cmd}"));
88 }
89
90 let is_self_hosted = server
91 .args
92 .windows(2)
93 .any(|w| w[0] == "serve" && w[1] == "--mcp");
94
95 let args_str = server.args.join(" ");
96 if is_self_hosted {
97 if is_absolute {
98 McpServerDoctorStatus::Ok(format!("self-hosted MCP server ({cmd} {args_str})"))
99 } else {
100 McpServerDoctorStatus::Warning(format!(
101 "self-hosted MCP server uses relative command \"{cmd}\" — consider using an absolute path"
102 ))
103 }
104 } else {
105 McpServerDoctorStatus::Ok(format!(
106 "stdio server ({cmd}{})",
107 if args_str.is_empty() {
108 String::new()
109 } else {
110 format!(" {args_str}")
111 }
112 ))
113 }
114}