7 Commits

Author SHA1 Message Date
Louis Knight-Webb
54909d4215 Increase the time before auto shutdown of dev server (vibe-kanban 8b5175a7)
Increase from 60s to 120s, allow to be overriden with CLI
2025-11-05 11:48:41 +00:00
Louis Knight-Webb
a4efc7988e Implement CI and build releases (vibe-kanban 2ef3e91a)
Implement CI and build releases

Please copy the pre-release and logic that allows us to use JS + NPX to distribute the rust binary, with code signing on mac.

You can view some examples in a different repo at /Users/lkw/Documents/repos/vibe-kanban
- .github/workflows/pre-release.yml
- .github/workflows/publish.yml
- npx-cli/bin/cli.js
- npx-cli/package.json
2025-11-01 10:45:13 +00:00
Louis Knight-Webb
d911829542 Refactor into single binary, instead use CLI flags to distinguish (vibe-kanban e574aa95) 2025-11-01 10:06:11 +00:00
Louis Knight-Webb
afd78b614f The stop command doesn't kill the dev server (vibe-kanban 3b4e5e08)
I notice when the client tries to use the stop command, it doesn't always stop the dev server that's running
2025-10-31 17:18:42 +00:00
Louis Knight-Webb
3ca5218538 PORT env 2025-10-30 17:34:00 +00:00
Louis Knight-Webb
b93283b0f0 Clients should use STDIO (vibe-kanban cfddf3b2)
Instead of connecting via SSE, we should allow clients to connect via STDIO. This is a necessary change to solve some issues
2025-10-30 14:18:43 +00:00
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