Louis Knight-Webb c6c68b5916 MCP Dev Server Manager (vibe-kanban 2d397bea)
## 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.
2025-10-30 11:26:09 +00:00

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

cargo build --release

The binary will be at target/release/mcp-dev-manager.

Running

Start the Daemon

# 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):

{
  "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:

{
  "status": "started",
  "port": 3010,
  "session_key": "A3X9"
}

stop

Stop a running development server session.

Parameters:

  • session_key (string): Session identifier

Returns:

{
  "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:

{
  "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:

{
  "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<Manager> 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

Description
Make it easy to launch multiple dev servers in parallel
Readme 91 KiB
Languages
Rust 80.6%
JavaScript 10.8%
Shell 8.6%