Files
mcp/internal/agent/portalloc.go
Kyle Isom 777ba8a0e1 Add route declarations and automatic port allocation to MCP agent
Service definitions can now declare routes per component instead of
manual port mappings:

  [[components.routes]]
  name = "rest"
  port = 8443
  mode = "l4"

The agent allocates free host ports at deploy time and injects
$PORT/$PORT_<NAME> env vars into containers. Backward compatible:
components with old-style ports= work unchanged.

Changes:
- Proto: RouteSpec message, routes + env fields on ComponentSpec
- Servicedef: RouteDef parsing and validation from TOML
- Registry: component_routes table with host_port tracking
- Runtime: Env field on ContainerSpec, -e flag in BuildRunArgs
- Agent: PortAllocator (random 10000-60000, availability check),
  deploy wiring for route→port mapping and env injection

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 01:04:47 -07:00

70 lines
1.4 KiB
Go

package agent
import (
"fmt"
"math/rand/v2"
"net"
"sync"
)
const (
portRangeMin = 10000
portRangeMax = 60000
maxRetries = 10
)
// PortAllocator manages host port allocation for route-based deployments.
// It tracks allocated ports within the agent session to avoid double-allocation.
type PortAllocator struct {
mu sync.Mutex
allocated map[int]bool
}
// NewPortAllocator creates a new PortAllocator.
func NewPortAllocator() *PortAllocator {
return &PortAllocator{
allocated: make(map[int]bool),
}
}
// Allocate picks a free port in range [10000, 60000).
// It tries random ports, checks availability with net.Listen, and retries up to 10 times.
func (pa *PortAllocator) Allocate() (int, error) {
pa.mu.Lock()
defer pa.mu.Unlock()
for i := range maxRetries {
port := portRangeMin + rand.IntN(portRangeMax-portRangeMin)
if pa.allocated[port] {
continue
}
if !isPortFree(port) {
continue
}
pa.allocated[port] = true
return port, nil
_ = i
}
return 0, fmt.Errorf("failed to allocate port after %d attempts", maxRetries)
}
// Release marks a port as available again.
func (pa *PortAllocator) Release(port int) {
pa.mu.Lock()
defer pa.mu.Unlock()
delete(pa.allocated, port)
}
// isPortFree checks if a TCP port is available by attempting to listen on it.
func isPortFree(port int) bool {
ln, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port))
if err != nil {
return false
}
_ = ln.Close()
return true
}