mirror of
https://github.com/BloopAI/dev-manager-mcp.git
synced 2026-08-23 11:58:33 +00:00
## 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<Mutex<HashMap<SessionKey, ServerEntry>>>`
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<Manager>` containing `Arc<Mutex<HashMap>>` 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<String> 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<Mutex> 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<Manager>` as parameter
- Each connection gets fresh service instance with shared manager
- Derive Clone on service struct
- Include field: `tool_router: ToolRouter<Self>`
- 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<ReqType>) -> Result<CallToolResult, McpError>`
- 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<RoleServer>) -> Result<InitializeResult, McpError>`
## 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<T> 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.
8 B
8 B