From c6c68b591624ba28521e3efc1938e2d90a844d36 Mon Sep 17 00:00:00 2001 From: Louis Knight-Webb Date: Thu, 30 Oct 2025 11:26:09 +0000 Subject: [PATCH] MCP Dev Server Manager (vibe-kanban 2d397bea) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Goal Build a **daemon process** that runs continuously and accepts multiple concurrent MCP client connections, sharing dev server session state across all clients. ## Prerequisites - Rust toolchain - rmcp SDK from github.com/modelcontextprotocol/rust-sdk - Version pin: Use git reference with features: `["server", "transport-sse", "macros"]` - Additional deps: tokio (full), serde, anyhow, async-trait, axum (for HTTP/SSE transport) ## Architecture Overview **Multi-client daemon with shared state:** 1. **HTTP/SSE server** listening on configurable port (default 3009) 2. **Multiple concurrent MCP clients** connecting via HTTP 3. **Shared global state** `Arc>>` 4. Dev server process manager 5. Port allocator starting at 3010 6. Log ring buffers (512KB per server) 7. Background sweeper (5s interval, 60s timeout) **State:** `Arc` containing `Arc>` shared across all client connections ## Implementation Steps (Bottom-Up) ### 1. Port Allocator Module - Sequential allocation from 3010 - Maintain free list (VecDeque) for reused ports - Probe availability with TcpListener::bind before assignment - Track in-use set to prevent collisions - Handle overflow at u16::MAX boundary ### 2. Log Buffer Module - Bounded VecDeque with byte tracking - 512KB total capacity (track bytes not line count) - Evict oldest when full - **Must derive Clone** for async reader tasks - tail() method returns tuple: (logs_string, truncated_bool) ### 3. Server Entry Module - Wraps tokio::process::Child - Captures stdout/stderr via piped stdio - Spawns two async tasks to read pipes into cloned LogBuffer - Tracks last_activity as Instant for sweeper - stop() method: child.kill() then child.wait() with timeout ### 4. Manager Module - **Must derive Clone** for sharing across connections - Holds Arc to server HashMap and port allocator - Methods: start, stop, status, tail (all return serde_json::Value) - start_sweeper() spawns tokio task **once** in constructor: - Runs every 5 seconds - Finds entries idle >60s - Kills process, frees port, removes from map - **Critical:** All tool methods (start/stop/status/tail) update last_activity timestamp ### 5. HTTP/SSE Transport Setup #### Daemon Main Function ```rust #[tokio::main] async fn main() -> anyhow::Result<()> { // Create shared manager (single instance) let manager = Arc::new(Manager::new()); // Create service factory (clones manager for each connection) let service_factory = move || { let manager = manager.clone(); async move { Ok(DevManagerService::new(manager)) } }; // Start HTTP/SSE server let addr = "127.0.0.1:3009".parse()?; println!("MCP daemon listening on {}", addr); rmcp::transport::sse::serve(addr, service_factory).await?; Ok(()) } ``` #### Service Struct Pattern - Constructor takes `Arc` as parameter - Each connection gets fresh service instance with shared manager - Derive Clone on service struct - Include field: `tool_router: ToolRouter` - Constructor calls `Self::tool_router()` (macro-generated) #### Tool Registration - Use `#[tool_router]` attribute on impl block containing tools - Each tool method gets `#[tool(description = "...")]` attribute - Tool signature: `async fn name(&self, Parameters(req): Parameters) -> Result` - Return `CallToolResult::success(vec![Content::text(...)])` or `::error(...)` - **Never return String directly** - always wrap in CallToolResult #### ServerHandler Implementation - Add `#[tool_handler]` attribute to link tools - Implement get_info() returning ServerInfo: - protocol_version: ProtocolVersion::V_2024_11_05 - capabilities: ServerCapabilities::builder().enable_tools().build() - server_info: Implementation::from_build_env() - instructions: Some(String) - Implement initialize() with signature: - `async fn initialize(&self, _params: InitializeRequestParam, _context: RequestContext) -> Result` ## Critical Details to Avoid Errors ### Compile-Time Requirements 1. Use `transport-sse` feature (not `transport-io`) 2. Service struct and Manager must derive Clone 3. All param structs need Deserialize + JsonSchema + Clone 4. LogBuffer needs Clone for async task cloning 5. Use Parameters wrapper in tool signatures 6. Import ErrorData as McpError (not error::Error - private module) 7. tool_router field must exist in service struct 8. Use rmcp::schemars::JsonSchema (not direct schemars dependency) ### Runtime Requirements 1. Call start_sweeper() in Manager::new() **once** 2. Spawn stdout/stderr reader tasks asynchronously (don't block) 3. Update last_activity in ALL tool methods (not just start) 4. Always child.wait() after child.kill() to prevent zombies 5. Free port back to allocator when stopping server 6. **Don't hold MutexGuard across await points** (use scoped blocks) ### Common Gotchas - Don't start sweeper per connection (start it once in Manager::new()) - Drop mutex guards before awaiting futures (not Send) - Port 3009 for daemon, 3010+ for managed dev servers - SSE endpoint typically at `/sse` path - Implementation::from_build_env() not ::new() - Port overflow check: `port == u16::MAX` not `port > 65535` - CallToolResult not String - use builder methods - tool_router() is macro-generated, not manual ## Testing Checklist - [ ] Build succeeds: `cargo build --release` - [ ] Daemon starts and listens on port 3009 - [ ] **Multiple clients can connect simultaneously** - [ ] **All clients see same session state** - [ ] Client A starts server → Client B sees it in status - [ ] Port allocation starts at 3010 - [ ] Same session_key reuses same port after restart - [ ] Stdout/stderr captured in logs - [ ] Server auto-stops after 60s without calls from ANY client - [ ] Multiple sessions get different ports - [ ] Stopped servers free their ports - [ ] Daemon survives client disconnects ## File Structure ``` src/ main.rs - Daemon, HTTP/SSE server, service factory service.rs - DevManagerService with tools, ServerHandler manager.rs - Session manager, sweeper (shared state) port_allocator.rs - Port assignment logic server_entry.rs - Process wrapper, log capture log_buffer.rs - Ring buffer implementation ``` ## Implementation Order 1. Create port allocator (simplest, no dependencies) 2. Create log buffer (simple, needs Clone) 3. Create server entry (uses log buffer) 4. Create manager (uses allocator + server entry, starts sweeper) 5. Build minimal HTTP/SSE daemon skeleton with service factory 6. Create service module with one dummy tool (verify rmcp setup) 7. Wire manager into MCP tools 8. Test with **two concurrent clients** connecting 9. Verify state sharing works correctly 10. Polish error handling and edge cases ## Usage After Build ### Starting the Daemon ```bash # Start daemon in foreground ./target/release/mcp-dev-manager # Or background ./target/release/mcp-dev-manager & ``` ### Client Configuration For Claude Desktop (`claude_desktop_config.json`): ```json { "mcpServers": { "dev-manager": { "url": "http://127.0.0.1:3009/sse" } } } ``` Multiple clients can use the same URL and will share session state. --- .gitignore | 1 + Cargo.lock | 1318 +++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 14 + README.md | 164 +++++ src/log_buffer.rs | 44 ++ src/main.rs | 35 ++ src/manager.rs | 279 +++++++++ src/port_allocator.rs | 53 ++ src/server_entry.rs | 117 ++++ src/service.rs | 96 +++ 10 files changed, 2121 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 src/log_buffer.rs create mode 100644 src/main.rs create mode 100644 src/manager.rs create mode 100644 src/port_allocator.rs create mode 100644 src/server_entry.rs create mode 100644 src/service.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..c2ef412 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1318 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "axum" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cc" +version = "1.2.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "739eb0f94557554b3ca9a86d2d37bebd49c5e6d0c1d2bda35ba5bdac830befc2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "mcp-dev-manager" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "rand 0.8.5", + "rmcp", + "schemars", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "rmcp" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fdad1258f7259fdc0f2dfc266939c82c3b5d1fd72bcde274d600cdc27e60243" +dependencies = [ + "axum", + "base64", + "bytes", + "chrono", + "futures", + "http", + "http-body", + "http-body-util", + "paste", + "pin-project-lite", + "rand 0.9.2", + "rmcp-macros", + "schemars", + "serde", + "serde_json", + "sse-stream", + "thiserror", + "tokio", + "tokio-stream", + "tokio-util", + "tower-service", + "tracing", + "uuid", +] + +[[package]] +name = "rmcp-macros" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede0589a208cc7ce81d1be68aa7e74b917fcd03c81528408bab0457e187dcd9b" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "serde_json", + "syn", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "schemars" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +dependencies = [ + "chrono", + "dyn-clone", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d020396d1d138dc19f1165df7545479dcd58d93810dc5d646a16e55abefa80" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "sse-stream" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb4dc4d33c68ec1f27d386b5610a351922656e1fdf5c05bbaad930cd1519479a" +dependencies = [ + "bytes", + "futures-util", + "http-body", + "http-body-util", + "pin-project-lite", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "unicode-ident" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" + +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "getrandom 0.3.4", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..bf8b904 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "mcp-dev-manager" +version = "0.1.0" +edition = "2021" + +[dependencies] +rmcp = { version = "0.8", features = ["server", "transport-sse-server"] } +tokio = { version = "1", features = ["full"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +anyhow = "1" +async-trait = "0.1" +schemars = "1.0.4" +rand = "0.8" diff --git a/README.md b/README.md new file mode 100644 index 0000000..c0809d4 --- /dev/null +++ b/README.md @@ -0,0 +1,164 @@ +# MCP Dev Server Manager + +A daemon process that runs continuously and accepts multiple concurrent MCP client connections, sharing dev server session state across all clients. + +## Features + +- **Multi-client daemon** with HTTP/SSE transport on port 3009 +- **Shared global state** across all concurrent connections +- **Automatic port allocation** starting at 3010 with reuse +- **Log capture** with 512KB ring buffers per server +- **Auto-cleanup** of idle sessions after 60 seconds +- **Process management** with stdout/stderr capture + +## Building + +```bash +cargo build --release +``` + +The binary will be at `target/release/mcp-dev-manager`. + +## Running + +### Start the Daemon + +```bash +# Foreground +./target/release/mcp-dev-manager + +# Background +./target/release/mcp-dev-manager & +``` + +The daemon will listen on `http://127.0.0.1:3009`. + +### Client Configuration + +For Claude Desktop (`claude_desktop_config.json`): + +```json +{ + "mcpServers": { + "dev-manager": { + "url": "http://127.0.0.1:3009/sse" + } + } +} +``` + +Multiple clients can use the same URL and will share session state. + +## MCP Tools + +### `start` +Start a development server. Auto-generates a unique 4-character session key. + +**Parameters:** +- `command` (string): Shell command to execute (e.g., "npm run dev", "python -m http.server 8080") + +**Returns:** +```json +{ + "status": "started", + "port": 3010, + "session_key": "A3X9" +} +``` + +### `stop` +Stop a running development server session. + +**Parameters:** +- `session_key` (string): Session identifier + +**Returns:** +```json +{ + "status": "stopped", + "session_key": "A3X9" +} +``` + +### `status` +Get status of one or all development server sessions. + +**Parameters:** +- `session_key` (optional string): Specific session to query, or omit for all sessions + +**Returns:** +```json +{ + "sessions": [ + { + "session_key": "B7K2", + "port": 3010, + "running": true + } + ] +} +``` + +### `tail` +Get stdout/stderr logs for a development server session. + +**Parameters:** +- `session_key` (string): Session identifier (e.g., "A3X9") + +**Returns:** +```json +{ + "session_key": "A3X9", + "stdout": "Server started on port 3010...", + "stderr": "" +} +``` + +## Architecture + +### Modules + +- **port_allocator.rs** - Sequential port allocation from 3010 with free list +- **log_buffer.rs** - Bounded 512KB ring buffer with Clone support +- **server_entry.rs** - Process wrapper with async log capture +- **manager.rs** - Shared state manager with auto-cleanup sweeper +- **service.rs** - MCP service with tool definitions +- **main.rs** - HTTP/SSE daemon server + +### State Management + +- Single `Arc` shared across all client connections +- Each connection gets a fresh `DevManagerService` instance +- Mutex-protected HashMap for session storage +- Background sweeper runs every 5 seconds to clean up idle sessions (>60s) + +### Session Keys + +- Auto-generated 4-character uppercase alphanumeric codes (e.g., "A3X9", "K7M2") +- Guaranteed unique across active sessions +- 1,679,616 possible combinations (36^4) + +### Port Allocation + +- Starts at 3010 and increments sequentially +- Maintains free list for reused ports +- Probes availability with TcpListener before assignment + +### Log Capture + +- Each server spawns two async tasks for stdout/stderr +- Logs stored in bounded VecDeque with byte tracking +- Oldest entries evicted when 512KB limit reached +- Non-blocking reads with line buffering + +## Testing Multi-Client Behavior + +1. Start daemon: `./target/release/mcp-dev-manager` +2. Connect Client A and start a server session +3. Connect Client B and query status - should see Client A's session +4. Client B can stop Client A's session +5. All clients share the same session state + +## License + +MIT diff --git a/src/log_buffer.rs b/src/log_buffer.rs new file mode 100644 index 0000000..4512cc4 --- /dev/null +++ b/src/log_buffer.rs @@ -0,0 +1,44 @@ +use std::collections::VecDeque; + +const MAX_BYTES: usize = 512 * 1024; +const MAX_TAIL_LINES: usize = 100; + +#[derive(Clone)] +pub struct LogBuffer { + logs: VecDeque, + total_bytes: usize, +} + +impl LogBuffer { + pub fn new() -> Self { + Self { + logs: VecDeque::new(), + total_bytes: 0, + } + } + + pub fn push(&mut self, line: String) { + let line_bytes = line.len(); + self.total_bytes += line_bytes; + self.logs.push_back(line); + + while self.total_bytes > MAX_BYTES && !self.logs.is_empty() { + if let Some(old_line) = self.logs.pop_front() { + self.total_bytes -= old_line.len(); + } + } + } + + pub fn tail(&self) -> (String, bool) { + let len = self.logs.len(); + let start = len.saturating_sub(MAX_TAIL_LINES); + + let mut out = String::new(); + for line in self.logs.iter().skip(start) { + out.push_str(line); + } + + let truncated = len > MAX_TAIL_LINES; + (out, truncated) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..0ec0cba --- /dev/null +++ b/src/main.rs @@ -0,0 +1,35 @@ +mod log_buffer; +mod manager; +mod port_allocator; +mod server_entry; +mod service; + +use manager::Manager; +use rmcp::transport::sse_server::SseServer; +use service::DevManagerService; +use std::net::{Ipv4Addr, SocketAddr}; +use std::sync::Arc; + +#[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)); + + 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(()) +} diff --git a/src/manager.rs b/src/manager.rs new file mode 100644 index 0000000..a2f79fe --- /dev/null +++ b/src/manager.rs @@ -0,0 +1,279 @@ +use crate::port_allocator::PortAllocator; +use crate::server_entry::ServerEntry; +use rand::{thread_rng, Rng}; +use serde_json::json; +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; +use tokio::process::Command; + +type SessionKey = String; + +const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; +const RUNNING_IDLE_SECS: u64 = 60; +const EXITED_RETENTION_SECS: u64 = 600; + +fn generate_session_key() -> String { + let mut rng = thread_rng(); + (0..4) + .map(|_| CHARSET[rng.gen_range(0..CHARSET.len())] as char) + .collect() +} + +struct ManagerInner { + servers: HashMap, + port_allocator: PortAllocator, +} + +#[derive(Clone)] +pub struct Manager { + inner: Arc>, +} + +impl Manager { + pub fn new() -> Self { + let manager = Self { + inner: Arc::new(Mutex::new(ManagerInner { + servers: HashMap::new(), + port_allocator: PortAllocator::new(3010), + })), + }; + + manager.start_sweeper(); + manager + } + + fn start_sweeper(&self) { + let inner = self.inner.clone(); + tokio::spawn(async move { + let mut interval = tokio::time::interval(Duration::from_secs(5)); + loop { + interval.tick().await; + + let (to_stop, to_prune, _ports_to_free) = { + let mut guard = match inner.lock() { + Ok(g) => g, + Err(_) => continue, + }; + + let now = Instant::now(); + let idle_threshold = Duration::from_secs(RUNNING_IDLE_SECS); + let retention_threshold = Duration::from_secs(EXITED_RETENTION_SECS); + + let mut to_stop = Vec::new(); + let mut to_prune = Vec::new(); + let mut ports_to_free = Vec::new(); + + for (key, entry) in guard.servers.iter_mut() { + entry.poll_exit(); + + if let Some(exited_at) = entry.exited_at() { + if entry.port != 0 { + ports_to_free.push((key.clone(), entry.port)); + } + if now.duration_since(exited_at) > retention_threshold { + to_prune.push(key.clone()); + } + } else if now.duration_since(entry.last_activity) > idle_threshold { + to_stop.push(key.clone()); + } + } + + for (key, port) in &ports_to_free { + guard.port_allocator.free(*port); + if let Some(entry) = guard.servers.get_mut(key) { + entry.port = 0; + } + } + + (to_stop, to_prune, ports_to_free) + }; + + for key in to_stop { + let entry_opt = { + let mut guard = match inner.lock() { + Ok(g) => g, + Err(_) => continue, + }; + guard.servers.remove(&key) + }; + + if let Some(mut entry) = entry_opt { + let port = entry.port; + let _ = entry.stop().await; + entry.port = 0; + + let mut guard = match inner.lock() { + Ok(g) => g, + Err(_) => continue, + }; + guard.port_allocator.free(port); + guard.servers.insert(key, entry); + } + } + + for key in to_prune { + let mut guard = match inner.lock() { + Ok(g) => g, + Err(_) => continue, + }; + if let Some(entry) = guard.servers.remove(&key) { + if entry.port != 0 { + guard.port_allocator.free(entry.port); + } + } + } + } + }); + } + + pub async fn start(&self, command: String) -> serde_json::Value { + let (session_key, port) = { + let mut guard = match self.inner.lock() { + Ok(g) => g, + Err(e) => return json!({"error": format!("Lock error: {}", e)}), + }; + + let session_key = loop { + let key = generate_session_key(); + if !guard.servers.contains_key(&key) { + break key; + } + }; + + let port = match guard.port_allocator.allocate() { + Ok(p) => p, + Err(e) => return json!({"error": format!("Port allocation failed: {}", e)}), + }; + + (session_key, port) + }; + + let mut cmd = if cfg!(target_os = "windows") { + let mut c = Command::new("cmd"); + c.arg("/C").arg(&command); + c + } else { + let mut c = Command::new("sh"); + c.arg("-c").arg(&command); + c + }; + cmd.stdout(std::process::Stdio::piped()); + cmd.stderr(std::process::Stdio::piped()); + + let child = match cmd.spawn() { + Ok(c) => c, + Err(e) => { + let mut guard = self.inner.lock().unwrap(); + guard.port_allocator.free(port); + return json!({"error": format!("Failed to spawn process: {}", e)}); + } + }; + + let entry = ServerEntry::new(child, port); + + { + let mut guard = match self.inner.lock() { + Ok(g) => g, + Err(e) => return json!({"error": format!("Lock error: {}", e)}), + }; + guard.servers.insert(session_key.clone(), entry); + } + + json!({ + "status": "started", + "port": port, + "session_key": session_key + }) + } + + pub async fn stop(&self, session_key: String) -> serde_json::Value { + let mut entry = { + let mut guard = match self.inner.lock() { + Ok(g) => g, + Err(e) => return json!({"error": format!("Lock error: {}", e)}), + }; + + match guard.servers.remove(&session_key) { + Some(e) => e, + None => return json!({"error": "Session not found"}), + } + }; + + let port = entry.port; + entry.last_activity = Instant::now(); + + match entry.stop().await { + Ok(_) => { + entry.port = 0; + + let mut guard = match self.inner.lock() { + Ok(g) => g, + Err(e) => return json!({"error": format!("Lock error: {}", e)}), + }; + + guard.port_allocator.free(port); + guard.servers.insert(session_key.clone(), entry); + + json!({"status": "stopped", "session_key": session_key}) + } + Err(e) => json!({"error": format!("Failed to stop server: {}", e)}), + } + } + + pub fn status(&self, session_key: Option) -> serde_json::Value { + let mut guard = match self.inner.lock() { + Ok(g) => g, + Err(e) => return json!({"error": format!("Lock error: {}", e)}), + }; + + if let Some(key) = session_key { + if let Some(entry) = guard.servers.get_mut(&key) { + entry.last_activity = Instant::now(); + let mut result = json!({ + "session_key": key, + "running": entry.is_running() + }); + if entry.port != 0 { + result["port"] = json!(entry.port); + } + return result; + } else { + return json!({"error": "Session not found"}); + } + } + + let sessions: Vec<_> = guard.servers.iter_mut().map(|(key, entry)| { + entry.last_activity = Instant::now(); + let mut result = json!({ + "session_key": key, + "running": entry.is_running() + }); + if entry.port != 0 { + result["port"] = json!(entry.port); + } + result + }).collect(); + + json!({"sessions": sessions}) + } + + pub fn tail(&self, session_key: String) -> serde_json::Value { + let mut guard = match self.inner.lock() { + Ok(g) => g, + Err(e) => return json!({"error": format!("Lock error: {}", e)}), + }; + + if let Some(entry) = guard.servers.get_mut(&session_key) { + entry.last_activity = Instant::now(); + let (stdout, stderr) = entry.get_logs(); + json!({ + "session_key": session_key, + "stdout": stdout, + "stderr": stderr + }) + } else { + json!({"error": "Session not found"}) + } + } +} diff --git a/src/port_allocator.rs b/src/port_allocator.rs new file mode 100644 index 0000000..8ffa042 --- /dev/null +++ b/src/port_allocator.rs @@ -0,0 +1,53 @@ +use std::collections::{HashSet, VecDeque}; +use std::net::TcpListener; + +pub struct PortAllocator { + next_port: u16, + free_list: VecDeque, + in_use: HashSet, +} + +impl PortAllocator { + pub fn new(start_port: u16) -> Self { + Self { + next_port: start_port, + free_list: VecDeque::new(), + in_use: HashSet::new(), + } + } + + pub fn allocate(&mut self) -> anyhow::Result { + // Try reusing from free list first + while let Some(port) = self.free_list.pop_front() { + if self.is_available(port) { + self.in_use.insert(port); + return Ok(port); + } + } + + // Allocate new port sequentially + loop { + if self.next_port == u16::MAX { + anyhow::bail!("Port allocation overflow - no more ports available"); + } + + let port = self.next_port; + self.next_port += 1; + + if !self.in_use.contains(&port) && self.is_available(port) { + self.in_use.insert(port); + return Ok(port); + } + } + } + + pub fn free(&mut self, port: u16) { + if self.in_use.remove(&port) { + self.free_list.push_back(port); + } + } + + fn is_available(&self, port: u16) -> bool { + TcpListener::bind(("127.0.0.1", port)).is_ok() + } +} diff --git a/src/server_entry.rs b/src/server_entry.rs new file mode 100644 index 0000000..e2d4df1 --- /dev/null +++ b/src/server_entry.rs @@ -0,0 +1,117 @@ +use crate::log_buffer::LogBuffer; +use std::sync::{Arc, Mutex}; +use std::time::Instant; +use tokio::io::{AsyncBufReadExt, BufReader}; +use tokio::process::Child; +use tokio::time::{timeout, Duration}; + +enum ProcessState { + Running(Child), + Exited { exited_at: Instant }, +} + +pub struct ServerEntry { + state: ProcessState, + pub port: u16, + pub last_activity: Instant, + stdout_log: Arc>, + stderr_log: Arc>, +} + +impl ServerEntry { + pub fn new(mut child: Child, port: u16) -> Self { + let stdout_log = Arc::new(Mutex::new(LogBuffer::new())); + let stderr_log = Arc::new(Mutex::new(LogBuffer::new())); + + if let Some(stdout) = child.stdout.take() { + let log = stdout_log.clone(); + tokio::spawn(async move { + let reader = BufReader::new(stdout); + let mut lines = reader.lines(); + while let Ok(Some(line)) = lines.next_line().await { + if let Ok(mut buffer) = log.lock() { + buffer.push(line + "\n"); + } + } + }); + } + + if let Some(stderr) = child.stderr.take() { + let log = stderr_log.clone(); + tokio::spawn(async move { + let reader = BufReader::new(stderr); + let mut lines = reader.lines(); + while let Ok(Some(line)) = lines.next_line().await { + if let Ok(mut buffer) = log.lock() { + buffer.push(line + "\n"); + } + } + }); + } + + Self { + state: ProcessState::Running(child), + port, + last_activity: Instant::now(), + stdout_log, + stderr_log, + } + } + + pub fn poll_exit(&mut self) -> bool { + if let ProcessState::Running(child) = &mut self.state { + if let Ok(Some(_)) = child.try_wait() { + self.state = ProcessState::Exited { + exited_at: Instant::now(), + }; + return true; + } + } + false + } + + pub fn is_running(&self) -> bool { + matches!(self.state, ProcessState::Running(_)) + } + + pub fn exited_at(&self) -> Option { + if let ProcessState::Exited { exited_at } = self.state { + Some(exited_at) + } else { + None + } + } + + pub async fn stop(&mut self) -> anyhow::Result<()> { + if let ProcessState::Running(child) = &mut self.state { + child.kill().await?; + + match timeout(Duration::from_secs(5), child.wait()).await { + Ok(Ok(_)) => {}, + Ok(Err(e)) => return Err(e.into()), + Err(_) => anyhow::bail!("Timeout waiting for process to exit"), + } + + self.state = ProcessState::Exited { + exited_at: Instant::now(), + }; + } + Ok(()) + } + + pub fn get_logs(&self) -> (String, String) { + let stdout = if let Ok(buffer) = self.stdout_log.lock() { + buffer.tail().0 + } else { + String::new() + }; + + let stderr = if let Ok(buffer) = self.stderr_log.lock() { + buffer.tail().0 + } else { + String::new() + }; + + (stdout, stderr) + } +} diff --git a/src/service.rs b/src/service.rs new file mode 100644 index 0000000..57f1bf9 --- /dev/null +++ b/src/service.rs @@ -0,0 +1,96 @@ +use crate::manager::Manager; +use rmcp::handler::server::tool::ToolRouter; +use rmcp::handler::server::wrapper::Parameters; +use rmcp::model::{ + CallToolResult, Content, Implementation, InitializeRequestParam, InitializeResult, + ProtocolVersion, ServerCapabilities, ServerInfo, +}; +use rmcp::service::RequestContext; +use rmcp::{tool, tool_handler, tool_router, ErrorData, RoleServer, ServerHandler}; +use schemars::JsonSchema; +use serde::Deserialize; +use std::sync::Arc; + +#[derive(Deserialize, JsonSchema, Clone)] +struct StartRequest { + command: String, +} + +#[derive(Deserialize, JsonSchema, Clone)] +struct StopRequest { + session_key: String, +} + +#[derive(Deserialize, JsonSchema, Clone)] +struct StatusRequest { + session_key: Option, +} + +#[derive(Deserialize, JsonSchema, Clone)] +struct TailRequest { + session_key: String, +} + +#[derive(Clone)] +pub struct DevManagerService { + manager: Arc, + tool_router: ToolRouter, +} + +impl DevManagerService { + pub fn new(manager: Arc) -> Self { + Self { + manager, + tool_router: Self::tool_router(), + } + } +} + +#[tool_router] +impl DevManagerService { + #[tool(description = "Start a development server. Returns auto-generated session key, port number and status.")] + async fn start(&self, Parameters(req): Parameters) -> Result { + let result = self.manager.start(req.command).await; + Ok(CallToolResult::success(vec![Content::text(result.to_string())])) + } + + #[tool(description = "Stop a running development server session.")] + async fn stop(&self, Parameters(req): Parameters) -> Result { + let result = self.manager.stop(req.session_key).await; + Ok(CallToolResult::success(vec![Content::text(result.to_string())])) + } + + #[tool(description = "Get status of one or all development server sessions.")] + async fn status(&self, Parameters(req): Parameters) -> Result { + let result = self.manager.status(req.session_key); + Ok(CallToolResult::success(vec![Content::text(result.to_string())])) + } + + #[tool(description = "Get stdout/stderr logs for a development server session.")] + async fn tail(&self, Parameters(req): Parameters) -> Result { + let result = self.manager.tail(req.session_key); + Ok(CallToolResult::success(vec![Content::text(result.to_string())])) + } +} + +#[tool_handler] +impl ServerHandler for DevManagerService { + fn get_info(&self) -> ServerInfo { + ServerInfo { + protocol_version: ProtocolVersion::V_2024_11_05, + capabilities: ServerCapabilities::builder().enable_tools().build(), + server_info: Implementation::from_build_env(), + instructions: Some( + "MCP Dev Server Manager - manages multiple development server sessions with automatic port allocation and log capture.".to_string() + ), + } + } + + async fn initialize( + &self, + _params: InitializeRequestParam, + _context: RequestContext, + ) -> Result { + Ok(self.get_info()) + } +}