Skip to main content

objectiveai_mcp_filesystem/
run.rs

1//! ObjectiveAI MCP filesystem server.
2//!
3//! Mirrors the `objectiveai-mcp-proxy` `run.rs` shape so other crates can
4//! `use objectiveai_mcp_filesystem::{ConfigBuilder, run}` and spawn the
5//! server in-process without going through the binary.
6
7use envconfig::Envconfig;
8use rmcp::transport::streamable_http_server::{
9    StreamableHttpServerConfig, StreamableHttpService,
10    session::local::LocalSessionManager,
11};
12use tokio_util::sync::CancellationToken;
13
14use crate::tools::FilesystemMcp;
15
16#[derive(Envconfig)]
17struct EnvConfigBuilder {
18    #[envconfig(from = "ADDRESS")]
19    address: Option<String>,
20    #[envconfig(from = "PORT")]
21    port: Option<u16>,
22    #[envconfig(from = "SUPPRESS_OUTPUT")]
23    suppress_output: Option<String>,
24}
25
26impl EnvConfigBuilder {
27    fn build(self) -> ConfigBuilder {
28        ConfigBuilder {
29            address: self.address,
30            port: self.port,
31            suppress_output: self.suppress_output.map(|v| {
32                matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on")
33            }),
34        }
35    }
36}
37
38#[derive(Default)]
39pub struct ConfigBuilder {
40    pub address: Option<String>,
41    pub port: Option<u16>,
42    pub suppress_output: Option<bool>,
43}
44
45impl Envconfig for ConfigBuilder {
46    #[allow(deprecated)]
47    fn init() -> Result<Self, envconfig::Error> {
48        EnvConfigBuilder::init().map(|e| e.build())
49    }
50
51    fn init_from_env() -> Result<Self, envconfig::Error> {
52        EnvConfigBuilder::init_from_env().map(|e| e.build())
53    }
54
55    fn init_from_hashmap(
56        hashmap: &std::collections::HashMap<String, String>,
57    ) -> Result<Self, envconfig::Error> {
58        EnvConfigBuilder::init_from_hashmap(hashmap).map(|e| e.build())
59    }
60}
61
62impl ConfigBuilder {
63    pub fn build(self) -> Config {
64        Config {
65            address: self.address.unwrap_or_else(|| "0.0.0.0".to_string()),
66            port: self.port.unwrap_or(3000),
67            suppress_output: self.suppress_output.unwrap_or(false),
68        }
69    }
70}
71
72pub struct Config {
73    pub address: String,
74    pub port: u16,
75    pub suppress_output: bool,
76}
77
78pub async fn setup(config: Config) -> std::io::Result<(tokio::net::TcpListener, axum::Router)> {
79    let Config {
80        address,
81        port,
82        suppress_output: _,
83    } = config;
84
85    let server = FilesystemMcp::new();
86    server.init().await;
87
88    let ct = CancellationToken::new();
89
90    let service: StreamableHttpService<FilesystemMcp, LocalSessionManager> =
91        StreamableHttpService::new(
92            move || Ok(server.clone()),
93            Default::default(),
94            StreamableHttpServerConfig {
95                stateful_mode: true,
96                sse_keep_alive: None,
97                cancellation_token: ct.child_token(),
98                ..Default::default()
99            },
100        );
101
102    // axum 0.8 removed nest_service at "/"; fallback_service mounts the
103    // service at the root catch-all without the path-prefix-stripping
104    // semantics nest_service had (which we never needed since the rmcp
105    // service handles every path it cares about itself).
106    let router = axum::Router::new().fallback_service(service);
107    let listener = tokio::net::TcpListener::bind(format!("{address}:{port}")).await?;
108
109    Ok((listener, router))
110}
111
112pub async fn serve(listener: tokio::net::TcpListener, app: axum::Router) -> std::io::Result<()> {
113    axum::serve(listener, app).await
114}
115
116pub async fn run(config: Config) -> std::io::Result<()> {
117    let suppress_output = config.suppress_output;
118    let (listener, app) = setup(config).await?;
119    if !suppress_output {
120        let addr = listener.local_addr()?;
121        eprintln!("listening on {addr}");
122    }
123    serve(listener, app).await
124}