Files
dev-manager-mcp/README.md
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

3.6 KiB

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