Skip to main content

mcp_methods/server/
runtime.rs

1//! Boot-time helpers shared by the framework binary and downstream
2//! domain binaries (e.g. `kglite-mcp-server`).
3//!
4//! Each helper is small enough to inline; collecting them here keeps
5//! the duplication out of every shim's `main.rs` and gives a single
6//! place to change boot-time behaviour.
7
8use std::path::{Path, PathBuf};
9
10use anyhow::{Context, Result};
11use tracing_subscriber::EnvFilter;
12
13use crate::server::env;
14use crate::server::manifest::{Manifest, ManifestError};
15use crate::server::watch;
16
17/// Initialise stderr-only `tracing` with `RUST_LOG=info` default.
18///
19/// Safe to call multiple times — `try_init()` is a no-op if a global
20/// subscriber is already installed.
21pub fn init_tracing() {
22    let _ = tracing_subscriber::fmt()
23        .with_env_filter(
24            EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")),
25        )
26        .with_writer(std::io::stderr)
27        .with_ansi(false)
28        .try_init();
29}
30
31/// Load environment variables from a `.env` file before any tool that
32/// reads `GITHUB_TOKEN` / API credentials runs.
33///
34/// Resolution order:
35/// 1. If the manifest sets `env_file:`, load that path (error if missing).
36/// 2. Otherwise walk upward from `start_dir` looking for a `.env`.
37///
38/// Returns the path actually loaded (for boot-summary logging), or
39/// `None` if nothing was found. Existing env vars are never overwritten.
40pub fn load_env_for_mode(manifest: Option<&Manifest>, start_dir: &Path) -> Result<Option<PathBuf>> {
41    if let Some(m) = manifest {
42        if let Some(rel) = m.env_file.as_ref() {
43            let base = m
44                .yaml_path
45                .parent()
46                .map(|p| p.to_path_buf())
47                .unwrap_or_else(|| PathBuf::from("."));
48            let resolved = base.join(rel);
49            env::load_env_explicit(&resolved).map_err(anyhow::Error::msg)?;
50            return Ok(Some(resolved));
51        }
52    }
53    Ok(env::load_env_walk(start_dir))
54}
55
56/// Resolve a manifest's `source_root(s)` declarations to canonical
57/// absolute path strings. Each entry must canonicalise to an existing
58/// directory; failures bubble as a [`ManifestError`].
59pub fn resolve_source_roots(manifest: &Manifest) -> Result<Vec<String>, ManifestError> {
60    let base = manifest
61        .yaml_path
62        .parent()
63        .map(|p| p.to_path_buf())
64        .unwrap_or_else(|| PathBuf::from("."));
65    let mut resolved: Vec<String> = Vec::new();
66    for raw in &manifest.source_roots {
67        let candidate = base.join(raw);
68        let canon = candidate.canonicalize().map_err(|_| {
69            ManifestError::at(
70                &manifest.yaml_path,
71                format!(
72                    "source root {raw:?} resolves to {:?} which is not an existing directory",
73                    candidate.display()
74                ),
75            )
76        })?;
77        if !canon.is_dir() {
78            return Err(ManifestError::at(
79                &manifest.yaml_path,
80                format!(
81                    "source root {raw:?} resolves to {:?} which is not a directory",
82                    canon.display()
83                ),
84            ));
85        }
86        resolved.push(canon.to_string_lossy().into_owned());
87    }
88    Ok(resolved)
89}
90
91/// Spawn the framework's debounced filesystem watcher when the mode
92/// requires one. Returns the handle (drop to stop watching) or `None`
93/// if `dir` is `None` — useful for `let _watch = …;` bindings in
94/// downstream main fns.
95pub fn maybe_watch(
96    dir: Option<&Path>,
97    on_change: Option<watch::ChangeHandler>,
98) -> Result<Option<watch::WatchHandle>> {
99    let Some(d) = dir else { return Ok(None) };
100    let handle = watch::watch(d, on_change, None).context("failed to start file watcher")?;
101    Ok(Some(handle))
102}