From d9118295423530d17ddfb17bd9b148371f0826c8 Mon Sep 17 00:00:00 2001 From: Louis Knight-Webb Date: Sat, 1 Nov 2025 10:06:11 +0000 Subject: [PATCH] Refactor into single binary, instead use CLI flags to distinguish (vibe-kanban e574aa95) --- Cargo.lock | 121 +++++++++++++++++++++++++++++ Cargo.toml | 5 +- README.md | 34 ++++++-- src/{bin/stdio_proxy.rs => lib.rs} | 34 ++++++-- src/main.rs | 60 +++++++------- 5 files changed, 210 insertions(+), 44 deletions(-) rename src/{bin/stdio_proxy.rs => lib.rs} (81%) diff --git a/Cargo.lock b/Cargo.lock index 545b366..f2b3e4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + [[package]] name = "anyhow" version = "1.0.100" @@ -152,6 +202,52 @@ dependencies = [ "windows-link", ] +[[package]] +name = "clap" +version = "4.5.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -347,6 +443,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "http" version = "1.3.1" @@ -604,6 +706,12 @@ dependencies = [ "serde", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itoa" version = "1.0.15" @@ -665,6 +773,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "clap", "libc", "rand 0.8.5", "rmcp", @@ -712,6 +821,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "parking_lot" version = "0.12.5" @@ -1526,6 +1641,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.18.1" diff --git a/Cargo.toml b/Cargo.toml index 7e2b9ad..6fb850e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,11 +7,8 @@ edition = "2021" name = "mcp-dev-manager" path = "src/main.rs" -[[bin]] -name = "mcp-dev-manager-stdio" -path = "src/bin/stdio_proxy.rs" - [dependencies] +clap = { version = "4", features = ["derive", "env"] } rmcp = { version = "0.8", features = ["server", "client", "transport-sse-server", "transport-sse-client-reqwest", "transport-io"] } tokio = { version = "1", features = ["full"] } serde = { version = "1", features = ["derive"] } diff --git a/README.md b/README.md index 7161999..890c122 100644 --- a/README.md +++ b/README.md @@ -24,14 +24,21 @@ The binary will be at `target/release/mcp-dev-manager`. ### Start the Daemon ```bash -# Foreground +# Foreground (default mode) ./target/release/mcp-dev-manager +# or explicitly: +./target/release/mcp-dev-manager daemon # Background -./target/release/mcp-dev-manager & +./target/release/mcp-dev-manager daemon & + +# Custom port +./target/release/mcp-dev-manager daemon --port 3010 +# or via environment variable: +PORT=3010 ./target/release/mcp-dev-manager daemon ``` -The daemon will listen on `http://127.0.0.1:3009`. +The daemon will listen on `http://127.0.0.1:3009` by default. ### Client Configuration @@ -43,19 +50,34 @@ For Claude Desktop (`claude_desktop_config.json`): { "mcpServers": { "dev-manager": { - "command": "mcp-dev-manager-stdio" + "command": "mcp-dev-manager", + "args": ["stdio"] } } } ``` -The STDIO proxy connects to the daemon at `http://127.0.0.1:3009/sse` by default. To use a different URL, set the `MCP_DAEMON_URL` environment variable: +The STDIO proxy connects to the daemon at `http://127.0.0.1:3009/sse` by default. To use a different URL: ```json { "mcpServers": { "dev-manager": { - "command": "mcp-dev-manager-stdio", + "command": "mcp-dev-manager", + "args": ["stdio", "--daemon-url", "http://127.0.0.1:3010/sse"] + } + } +} +``` + +Or use the environment variable: + +```json +{ + "mcpServers": { + "dev-manager": { + "command": "mcp-dev-manager", + "args": ["stdio"], "env": { "MCP_DAEMON_URL": "http://127.0.0.1:3009/sse" } diff --git a/src/bin/stdio_proxy.rs b/src/lib.rs similarity index 81% rename from src/bin/stdio_proxy.rs rename to src/lib.rs index 69acb14..f6c0472 100644 --- a/src/bin/stdio_proxy.rs +++ b/src/lib.rs @@ -1,10 +1,21 @@ +mod log_buffer; +mod manager; +mod port_allocator; +mod server_entry; +mod service; + use anyhow::Result; +use manager::Manager; use rmcp::service::RxJsonRpcMessage; use rmcp::transport::async_rw::AsyncRwTransport; use rmcp::transport::sse_client::SseClientTransport; +use rmcp::transport::sse_server::SseServer; use rmcp::transport::{io::stdio, Transport}; use rmcp::RoleServer; use serde_json::Value; +use service::DevManagerService; +use std::net::{Ipv4Addr, SocketAddr}; +use std::sync::Arc; use tokio::sync::mpsc; fn inject_cwd_if_start_tool( @@ -37,11 +48,24 @@ fn inject_cwd_if_start_tool( msg } -#[tokio::main] -async fn main() -> Result<()> { - let daemon_url = std::env::var("MCP_DAEMON_URL") - .unwrap_or_else(|_| "http://127.0.0.1:3009/sse".to_string()); +pub async fn run_daemon(port: u16) -> Result<()> { + let manager = Arc::new(Manager::new()); + let bind = SocketAddr::from((Ipv4Addr::LOCALHOST, port)); + println!("MCP daemon listening on {}", bind); + let server = SseServer::serve(bind).await?; + + let cancel = server.with_service({ + let manager = Arc::clone(&manager); + move || DevManagerService::new(Arc::clone(&manager)) + }); + + tokio::signal::ctrl_c().await?; + cancel.cancel(); + Ok(()) +} + +pub async fn run_stdio_proxy(daemon_url: &str) -> Result<()> { let client_cwd = std::env::current_dir() .ok() .and_then(|p| p.to_str().map(String::from)); @@ -53,7 +77,7 @@ async fn main() -> Result<()> { let (stdin, stdout) = stdio(); let mut stdio_transport = AsyncRwTransport::::new_server(stdin, stdout); - let mut sse_transport = SseClientTransport::start(daemon_url.as_str()).await?; + let mut sse_transport = SseClientTransport::start(daemon_url).await?; let (req_tx, mut req_rx) = mpsc::unbounded_channel(); let (resp_tx, mut resp_rx) = mpsc::unbounded_channel(); diff --git a/src/main.rs b/src/main.rs index 0ec0cba..1ff8ac8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,35 +1,37 @@ -mod log_buffer; -mod manager; -mod port_allocator; -mod server_entry; -mod service; +use clap::{Parser, Subcommand}; -use manager::Manager; -use rmcp::transport::sse_server::SseServer; -use service::DevManagerService; -use std::net::{Ipv4Addr, SocketAddr}; -use std::sync::Arc; +#[derive(Parser)] +#[command(name = "mcp-dev-manager")] +#[command(about = "MCP development server manager with shared session state")] +struct Cli { + #[command(subcommand)] + command: Option, +} + +#[derive(Subcommand)] +enum Command { + #[command(about = "Run as HTTP/SSE daemon server (default)")] + Daemon { + #[arg(long, env = "PORT", default_value_t = 3009)] + port: u16, + }, + #[command(about = "Run as STDIO proxy that connects to daemon")] + Stdio { + #[arg( + long, + env = "MCP_DAEMON_URL", + default_value = "http://127.0.0.1:3009/sse" + )] + daemon_url: String, + }, +} #[tokio::main] async fn main() -> anyhow::Result<()> { - let manager = Arc::new(Manager::new()); - - let port = std::env::var("PORT") - .ok() - .and_then(|p| p.parse().ok()) - .unwrap_or(3009); - - let bind = SocketAddr::from((Ipv4Addr::LOCALHOST, port)); + let cli = Cli::parse(); - println!("MCP daemon listening on {}", bind); - let server = SseServer::serve(bind).await?; - - let cancel = server.with_service({ - let manager = Arc::clone(&manager); - move || DevManagerService::new(Arc::clone(&manager)) - }); - - tokio::signal::ctrl_c().await?; - cancel.cancel(); - Ok(()) + match cli.command.unwrap_or(Command::Daemon { port: 3009 }) { + Command::Daemon { port } => mcp_dev_manager::run_daemon(port).await, + Command::Stdio { daemon_url } => mcp_dev_manager::run_stdio_proxy(&daemon_url).await, + } }