Fix docker startup so that it works properly with stdio mode. Probably worthwhile to toss majority of this readme, less confusing

This commit is contained in:
Ra
2025-09-08 19:34:32 -07:00
parent 5d3e04c5f8
commit 74a8574778
13 changed files with 386 additions and 564 deletions

39
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,39 @@
name: build-container
on:
push:
branches:
- main
run-name: build-image-${{ github.run_id }}
permissions:
contents: read
packages: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
rooba/agentcoordinator:latest
rooba/agentcoordinator:${{ github.sha }}

View File

@@ -2,18 +2,16 @@
# Creates a production-ready container for the MCP server without requiring local Elixir/OTP installation # Creates a production-ready container for the MCP server without requiring local Elixir/OTP installation
# Build stage - Use official Elixir image with OTP # Build stage - Use official Elixir image with OTP
FROM elixir:1.16-otp-26-alpine AS builder FROM elixir:1.18 AS builder
# Install build dependencies
RUN apk add --no-cache \ # Set environment variables
build-base \ RUN apt-get update && apt-get install -y \
git \ git \
curl \ curl \
bash bash \
unzip \
# Install Node.js and npm for MCP external servers (bunx dependency) zlib1g
RUN apk add --no-cache nodejs npm
RUN npm install -g bun
# Set build environment # Set build environment
ENV MIX_ENV=prod ENV MIX_ENV=prod
@@ -22,79 +20,30 @@ ENV MIX_ENV=prod
WORKDIR /app WORKDIR /app
# Copy mix files # Copy mix files
COPY mix.exs mix.lock ./ COPY lib lib
COPY mcp_servers.json \
mcp_interfaces_config.json \
mix.exs \
mix.lock \
docker-entrypoint.sh ./
COPY scripts ./scripts/
# Install mix dependencies # Install mix dependencies
RUN mix local.hex --force && \ RUN mix deps.get
mix local.rebar --force && \ RUN mix deps.compile
mix deps.get --only $MIX_ENV && \
mix deps.compile
# Copy source code
COPY lib lib
COPY config config
# Compile the release
RUN mix compile RUN mix compile
# Prepare release
RUN mix release RUN mix release
RUN chmod +x ./docker-entrypoint.sh ./scripts/mcp_launcher.sh
RUN curl -fsSL https://bun.sh/install | bash
RUN ln -s /root/.bun/bin/* /usr/local/bin/
# Runtime stage - Use smaller Alpine image
FROM alpine:3.18 AS runtime
# Install runtime dependencies
RUN apk add --no-cache \
bash \
openssl \
ncurses-libs \
libstdc++ \
nodejs \
npm
# Install Node.js packages for external MCP servers
RUN npm install -g bun
# Create non-root user for security
RUN addgroup -g 1000 appuser && \
adduser -u 1000 -G appuser -s /bin/bash -D appuser
# Create app directory and set permissions
WORKDIR /app
RUN chown -R appuser:appuser /app
# Copy the release from builder stage
COPY --from=builder --chown=appuser:appuser /app/_build/prod/rel/agent_coordinator ./
# Copy configuration files
COPY --chown=appuser:appuser mcp_servers.json ./
COPY --chown=appuser:appuser scripts/mcp_launcher.sh ./scripts/
# Make scripts executable
RUN chmod +x ./scripts/mcp_launcher.sh
# Copy Docker entrypoint script
COPY --chown=appuser:appuser docker-entrypoint.sh ./
RUN chmod +x ./docker-entrypoint.sh
# Switch to non-root user
USER appuser
# Set environment variables
ENV MIX_ENV=prod
ENV NATS_HOST=localhost ENV NATS_HOST=localhost
ENV NATS_PORT=4222 ENV NATS_PORT=4222
ENV SHELL=/bin/bash ENV SHELL=/bin/bash
# Expose the default port (if needed for HTTP endpoints)
EXPOSE 4000 EXPOSE 4000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD /app/bin/agent_coordinator ping || exit 1
# Set the entrypoint
ENTRYPOINT ["/app/docker-entrypoint.sh"] ENTRYPOINT ["/app/docker-entrypoint.sh"]
# Default command CMD ["/app/scripts/mcp_launcher.sh"]
CMD ["/app/scripts/mcp_launcher.sh"]

413
README.md
View File

@@ -1,11 +1,9 @@
# Agent Coordinator # Agent Coordinator
A **Model Context Protocol (MCP) server** that enables multiple AI agents to coordinate their work seamlessly across codebases without conflicts. Built with Elixir for reliability and fault tolerance. Agent Coordinator is a MCP proxy server that enables multiple AI agents to collaborate seamlessly without conflicts. It acts as a single MCP interface that proxies ALL tool calls through itself, ensuring every agent maintains full project awareness while the coordinator tracks real-time agent presence.
## What is Agent Coordinator? ## What is Agent Coordinator?
Agent Coordinator is a **MCP proxy server** that enables multiple AI agents to collaborate seamlessly without conflicts. As shown in the architecture diagram above, it acts as a **single MCP interface** that proxies ALL tool calls through itself, ensuring every agent maintains full project awareness while the coordinator tracks real-time agent presence.
**The coordinator operates as a transparent proxy layer:** **The coordinator operates as a transparent proxy layer:**
- **Single Interface**: All agents connect to one MCP server (the coordinator) - **Single Interface**: All agents connect to one MCP server (the coordinator)
@@ -20,122 +18,23 @@ Agent Coordinator is a **MCP proxy server** that enables multiple AI agents to c
- **Codebase Registry**: Cross-repository coordination, dependency management, and workspace organization - **Codebase Registry**: Cross-repository coordination, dependency management, and workspace organization
- **Unified Tool Registry**: Seamlessly proxies external MCP tools while adding coordination capabilities - **Unified Tool Registry**: Seamlessly proxies external MCP tools while adding coordination capabilities
Instead of agents conflicting over files or duplicating work, they connect through a **single MCP proxy interface** that routes ALL tool calls through the coordinator. This ensures every tool usage updates agent presence, tracks coordinated tasks, and maintains real-time project awareness across all agents via shared task boards and agent inboxes.
**Key Features:**
- **MCP Proxy Architecture**: Single server that proxies ALL external MCP servers for unified agent access
- **Real-Time Activity Tracking**: Live visibility into agent activities: "Reading file.ex", "Editing main.py", "Sequential thinking"
- **Real-Time Presence Tracking**: Every tool call updates agent status and project awareness
- **File-Level Coordination**: Track exactly which files each agent is working on to prevent conflicts
- **Activity History**: Rolling log of recent agent actions with timestamps and file details
- **Multi-Agent Coordination**: Register multiple AI agents (GitHub Copilot, Claude, etc.) with different capabilities
- **Transparent Tool Routing**: Automatically routes tool calls to appropriate external servers while tracking usage
- **Automatic Task Creation**: Every tool usage becomes a tracked task with agent coordination context
- **Full Project Awareness**: All agents see unified project state through the proxy layer
- **External Server Management**: Automatically starts, monitors, and manages MCP servers defined in `mcp_servers.json`
- **Universal Tool Registry**: Proxies tools from all external servers while adding native coordination tools
- **Dynamic Tool Discovery**: Automatically discovers new tools when external servers start/restart
- **Cross-Codebase Support**: Coordinate work across multiple repositories and projects
- **MCP Standard Compliance**: Works with any MCP-compatible AI agent or tool
## Overview ## Overview
<!-- ![Agent Coordinator Architecture](docs/architecture-diagram.svg) Let's not show this it's confusing -->
![Agent Coordinator Architecture](docs/architecture-diagram.svg)
**The Agent Coordinator acts as a transparent MCP proxy server** that routes ALL tool calls through itself to maintain agent presence and provide full project awareness. Every external MCP server is proxied through the coordinator, ensuring unified agent coordination.
### Proxy Architecture Flow
1. **Agent Registration**: Multiple AI agents (Purple Zebra, Yellow Elephant, etc.) register with their capabilities
2. **External Server Discovery**: Coordinator automatically starts and discovers tools from external MCP servers
3. **Unified Proxy Interface**: All tools (native + external) are available through a single MCP interface
4. **Transparent Tool Routing**: ALL tool calls proxy through coordinator → external servers → coordinator → agents
5. **Presence Tracking**: Every proxied tool call updates agent heartbeat and task status
6. **Project Awareness**: All agents maintain unified project state through the proxy layer
## Real-Time Activity Tracking - FANTASTIC Feature!
**See exactly what every agent is doing in real-time!** The coordinator intelligently tracks and displays agent activities as they happen:
### Live Activity Examples
```json
{
"agent_id": "github-copilot-purple-elephant",
"name": "GitHub Copilot Purple Elephant",
"current_activity": "Reading mix.exs",
"current_files": ["/home/ra/agent_coordinator/mix.exs"],
"activity_history": [
{
"activity": "Reading mix.exs",
"files": ["/home/ra/agent_coordinator/mix.exs"],
"timestamp": "2025-09-06T16:41:09.193087Z"
},
{
"activity": "Sequential thinking: Analyzing the current codebase structure...",
"files": [],
"timestamp": "2025-09-06T16:41:05.123456Z"
},
{
"activity": "Editing agent.ex",
"files": ["/home/ra/agent_coordinator/lib/agent_coordinator/agent.ex"],
"timestamp": "2025-09-06T16:40:58.987654Z"
}
]
}
```
### 🚀 Activity Types Tracked
- **📂 File Operations**: "Reading config.ex", "Editing main.py", "Writing README.md", "Creating new_feature.js"
- **🧠 Thinking Activities**: "Sequential thinking: Analyzing the problem...", "Having a sequential thought..."
- **🔍 Search Operations**: "Searching for 'function'", "Semantic search for 'authentication'"
- **⚡ Terminal Commands**: "Running: mix test...", "Checking terminal output"
- **🛠️ VS Code Actions**: "VS Code: set editor content", "Viewing active editor in VS Code"
- **🧪 Testing**: "Running tests in user_test.exs", "Running all tests"
- **📊 Task Management**: "Creating task: Fix bug", "Getting next task", "Completing current task"
- **🌐 Web Operations**: "Fetching 3 webpages", "Getting library docs for React"
### 🎯 Benefits
- **🚫 Prevent File Conflicts**: See which files are being edited by which agents
- **👥 Coordinate Team Work**: Know when agents are working on related tasks
- **🐛 Debug Agent Behavior**: Track what agents did before encountering issues
- **📈 Monitor Progress**: Watch real-time progress across multiple agents
- **🔄 Optimize Workflows**: Identify bottlenecks and coordination opportunities
**Every tool call automatically updates the agent's activity - no configuration needed!** 🫡😸
### 🏗️ Architecture Components ### 🏗️ Architecture Components
**Core Coordinator Components:** **Core Coordinator Components:**
- **Task Registry**: Intelligent task queuing, agent matching, and progress tracking - Task Registry: Intelligent task queuing, agent matching, and progress tracking
- **Agent Manager**: Registration, heartbeat monitoring, and capability-based assignment - Agent Manager: Registration, heartbeat monitoring, and capability-based assignment
- **Codebase Registry**: Cross-repository coordination and workspace management Codebase Registry: Cross-repository coordination and workspace management
- **Unified Tool Registry**: Combines native coordination tools with external MCP tools - Unified Tool Registry: Combines native coordination tools with external MCP tools
- Every tool call automatically updates the agent's activity for other agent's to see
**External Integration:** **External Integration:**
- **MCP Servers**: Filesystem, Memory, Context7, Sequential Thinking, and more - VS Code Integration: Direct editor commands and workspace management
- **VS Code Integration**: Direct editor commands and workspace management
- **Real-Time Dashboard**: Live task board showing agent status and progress
**Example Proxy Tool Call Flow:** ### External Server Management
```text
Agent calls "read_file" → Coordinator proxies to filesystem server →
Updates agent presence + task tracking → Returns file content to agent
Result: All other agents now aware of the file access via task board
```
## 🔧 MCP Server Management & Unified Tool Registry
Agent Coordinator acts as a **unified MCP proxy server** that manages multiple external MCP servers while providing its own coordination capabilities. This creates a single, powerful interface for AI agents to access hundreds of tools seamlessly.
### 📡 External Server Management
The coordinator automatically manages external MCP servers based on configuration in `mcp_servers.json`: The coordinator automatically manages external MCP servers based on configuration in `mcp_servers.json`:
@@ -145,7 +44,7 @@ The coordinator automatically manages external MCP servers based on configuratio
"mcp_filesystem": { "mcp_filesystem": {
"type": "stdio", "type": "stdio",
"command": "bunx", "command": "bunx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/home/ra"], "args": ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"],
"auto_restart": true, "auto_restart": true,
"description": "Filesystem operations server" "description": "Filesystem operations server"
}, },
@@ -155,12 +54,6 @@ The coordinator automatically manages external MCP servers based on configuratio
"args": ["-y", "@modelcontextprotocol/server-memory"], "args": ["-y", "@modelcontextprotocol/server-memory"],
"auto_restart": true, "auto_restart": true,
"description": "Memory and knowledge graph server" "description": "Memory and knowledge graph server"
},
"mcp_figma": {
"type": "http",
"url": "http://127.0.0.1:3845/mcp",
"auto_restart": true,
"description": "Figma design integration server"
} }
}, },
"config": { "config": {
@@ -172,178 +65,202 @@ The coordinator automatically manages external MCP servers based on configuratio
} }
``` ```
**Server Lifecycle Management:**
1. **Startup**: Reads config and spawns each external server process
2. **Discovery**: Sends MCP `initialize` and `tools/list` requests to discover available tools
3. **Registration**: Adds discovered tools to the unified tool registry
4. **Monitoring**: Continuously monitors server health and heartbeat
5. **Auto-Restart**: Automatically restarts failed servers (if configured)
6. **Cleanup**: Properly terminates processes and cleans up resources on shutdown
### 🛠️ Unified Tool Registry
The coordinator combines tools from multiple sources into a single, coherent interface:
**Native Coordination Tools:**
- `register_agent` - Register agents with capabilities
- `create_task` - Create coordination tasks
- `get_next_task` - Get assigned tasks
- `complete_task` - Mark tasks complete
- `get_task_board` - View all agent status
- `heartbeat` - Maintain agent liveness
**External Server Tools (Auto-Discovered):**
- **Filesystem**: `read_file`, `write_file`, `list_directory`, `search_files`
- **Memory**: `search_nodes`, `store_memory`, `recall_information`
- **Context7**: `get-library-docs`, `search-docs`, `get-library-info`
- **Figma**: `get_code`, `get_designs`, `fetch_assets`
- **Sequential Thinking**: `sequentialthinking`, `analyze_problem`
- **VS Code**: `run_command`, `install_extension`, `open_file`, `create_task`
**Dynamic Discovery Process:**
1. **Startup**: Agent Coordinator starts external MCP server process
2. **Initialize**: Sends MCP `initialize` request → Server responds with capabilities
3. **Discovery**: Sends `tools/list` request → Server returns available tools
4. **Registration**: Adds discovered tools to unified tool registry
This process repeats automatically when servers restart or new servers are added.
### Intelligent Tool Routing
When an AI agent calls a tool, the coordinator intelligently routes the request:
**Routing Logic:**
1. **Native Tools**: Handled directly by Agent Coordinator modules
2. **External Tools**: Routed to the appropriate external MCP server
3. **VS Code Tools**: Routed to integrated VS Code Tool Provider
4. **Unknown Tools**: Return helpful error with available alternatives
**Automatic Task Tracking:**
- Every tool call automatically creates or updates agent tasks
- Maintains context of what agents are working on
- Provides visibility into cross-agent coordination
- Enables intelligent task distribution and conflict prevention
**Example Tool Call Flow:**
```bash
Agent calls "read_file" → Coordinator routes to filesystem server →
Updates agent task → Sends heartbeat → Returns file content
```
## Prerequisites ## Prerequisites
Choose one of these installation methods: Choose one of these installation methods:
### Option 1: Docker (Recommended - No Elixir Installation Required) [Docker](#1-start-nats-server)
- **Docker**: 20.10+ and Docker Compose [Manual Installation](#manual-setup)
- **Node.js**: 18+ (for external MCP servers via bun)
### Option 2: Manual Installation
- **Elixir**: 1.16+ with OTP 26+ - **Elixir**: 1.16+ with OTP 26+
- **Mix**: Comes with Elixir installation - **Node.js**: 18+ (for some MCP servers)
- **Node.js**: 18+ (for external MCP servers via bun) - **uv**: If using python MCP servers
## ⚡ Quick Start ### Docker Setup
### Option A: Docker Setup (Easiest) #### 1. Start NATS Server
#### 1. Get the Code First, start a NATS server that the Agent Coordinator can connect to:
```bash ```bash
git clone https://github.com/your-username/agent_coordinator.git # Start NATS server with persistent storage
cd agent_coordinator docker run -d \
--name nats-server \
--network agent-coordinator-net \
-p 4222:4222 \
-p 8222:8222 \
-v nats_data:/data \
nats:2.10-alpine \
--jetstream \
--store_dir=/data \
--max_mem_store=1Gb \
--max_file_store=10Gb
# Create the network first if it doesn't exist
docker network create agent-coordinator-net
``` ```
#### 2. Run with Docker Compose #### 2. Configure Your AI Tools
**For STDIO Mode (Recommended - Direct MCP Integration):**
First, create a Docker network and start the NATS server:
```bash ```bash
# Start the full stack (MCP server + NATS + monitoring) # Create network for secure communication
docker-compose up -d docker network create agent-coordinator-net
# Or start just the MCP server # Start NATS server
docker-compose up agent-coordinator docker run -d \
--name nats-server \
# Check logs --network agent-coordinator-net \
docker-compose logs -f agent-coordinator -p 4222:4222 \
-v nats_data:/data \
nats:2.10-alpine \
--jetstream \
--store_dir=/data \
--max_mem_store=1Gb \
--max_file_store=10Gb
``` ```
#### 3. Configuration Then add this configuration to your VS Code `mcp.json` configuration file via `ctrl + shift + p``MCP: Open User Configuration` or `MCP: Open Remote User Configuration` if running on a remote server:
Edit `mcp_servers.json` to configure external MCP servers, then restart: ```json
{
```bash "servers": {
docker-compose restart agent-coordinator "agent-coordinator": {
"command": "docker",
"args": [
"run",
"--network=agent-coordinator-net",
"-v=./mcp_servers.json:/app/mcp_servers.json:ro",
"-v=/path/to/your/workspace:/workspace:rw",
"-e=NATS_HOST=nats-server",
"-e=NATS_PORT=4222",
"-i",
"--rm",
"ghcr.io/rooba/agentcoordinator:latest"
],
"type": "stdio"
}
}
}
``` ```
### Option B: Manual Setup **Important Notes for File System Access:**
#### 1. Clone the Repository If you're using MCP filesystem servers, mount the directories they need access to:
```json
{
"args": [
"run",
"--network=agent-coordinator-net",
"-v=./mcp_servers.json:/app/mcp_servers.json:ro",
"-v=/home/user/projects:/home/user/projects:rw",
"-v=/path/to/workspace:/workspace:rw",
"-e=NATS_HOST=nats-server",
"-e=NATS_PORT=4222",
"-i",
"--rm",
"ghcr.io/rooba/agentcoordinator:latest"
]
}
```
**For HTTP/WebSocket Mode (Alternative - Web API Access):**
If you prefer to run as a web service instead of stdio:
```bash ```bash
git clone https://github.com/your-username/agent_coordinator.git # Create network first
cd agent_coordinator docker network create agent-coordinator-net
# Start NATS server
docker run -d \
--name nats-server \
--network agent-coordinator-net \
-p 4222:4222 \
-v nats_data:/data \
nats:2.10-alpine \
--jetstream \
--store_dir=/data \
--max_mem_store=1Gb \
--max_file_store=10Gb
# Run Agent Coordinator in HTTP mode
docker run -d \
--name agent-coordinator \
--network agent-coordinator-net \
-p 8080:4000 \
-v ./mcp_servers.json:/app/mcp_servers.json:ro \
-v /path/to/workspace:/workspace:rw \
-e NATS_HOST=nats-server \
-e NATS_PORT=4222 \
-e MCP_INTERFACE_MODE=http \
-e MCP_HTTP_PORT=4000 \
ghcr.io/rooba/agentcoordinator:latest
```
Then access via HTTP API at `http://localhost:8080/mcp` or configure your MCP client to use the HTTP endpoint.
Create or edit `mcp_servers.json` in your project directory to configure external MCP servers:
```json
{
"servers": {
"mcp_filesystem": {
"type": "stdio",
"command": "bunx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"],
"auto_restart": true
}
}
}
```
### Manual Setup
#### Clone the Repository
> It is suggested to install Elixir (and Erlang) via [asdf](https://asdf-vm.com/) for easy version management.
> NATS can be found at [nats.io](https://github.com/nats-io/nats-server/releases/latest), or via Docker
```bash
git clone https://github.com/rooba/agentcoordinator.git
cd agentcoordinator
mix deps.get mix deps.get
mix compile
``` ```
#### 2. Start the MCP Server #### Start the MCP Server directly
```bash ```bash
# Start the MCP server directly # Start the MCP server directly
export MCP_INTERFACE_MODE=stdio # or http / websocket
# export MCP_HTTP_PORT=4000 # if using http mode
./scripts/mcp_launcher.sh ./scripts/mcp_launcher.sh
# Or in development mode # Or in development mode
mix run --no-halt mix run --no-halt
``` ```
### 3. Configure Your AI Tools ### Run via VS Code or similar tools
#### For Docker Setup Add this to your `mcp.json` or `mcp_servers.json` depending on your tool:
If using Docker, the MCP server is available at the container's stdio interface. Add this to your VS Code `settings.json`:
```json ```json
{ {
"github.copilot.advanced": { "servers": {
"mcp": { "agent-coordinator": {
"servers": { "command": "/path/to/agent_coordinator/scripts/mcp_launcher.sh",
"agent-coordinator": { "args": [],
"command": "docker", "env": {
"args": ["exec", "-i", "agent-coordinator", "/app/scripts/mcp_launcher.sh"], "MIX_ENV": "prod",
"env": { "NATS_HOST": "localhost",
"MIX_ENV": "prod" "NATS_PORT": "4222"
}
}
}
}
}
}
```
#### For Manual Setup
Add this to your VS Code `settings.json`:
```json
{
"github.copilot.advanced": {
"mcp": {
"servers": {
"agent-coordinator": {
"command": "/path/to/agent_coordinator/scripts/mcp_launcher.sh",
"args": [],
"env": {
"MIX_ENV": "dev"
}
}
} }
} }
} }

View File

@@ -18,10 +18,9 @@ services:
profiles: profiles:
- dev - dev
# Lightweight development NATS without persistence
nats: nats:
command: command:
- '--jetstream' - '--jetstream'
volumes: [] volumes: []
profiles: profiles:
- dev - dev

View File

@@ -1,51 +1,17 @@
version: '3.8' version: '3.8'
services: services:
# Agent Coordinator MCP Server
agent-coordinator:
build:
context: .
dockerfile: Dockerfile
container_name: agent-coordinator
environment:
- MIX_ENV=prod
- NATS_HOST=nats
- NATS_PORT=4222
volumes:
# Mount local mcp_servers.json for easy configuration
- ./mcp_servers.json:/app/mcp_servers.json:ro
# Mount a directory for persistent data (optional)
- agent_data:/app/data
ports:
# Expose port 4000 if the app serves HTTP endpoints
- "4000:4000"
depends_on:
nats:
condition: service_healthy
restart: unless-stopped
healthcheck:
test: ["/app/bin/agent_coordinator", "ping"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
# NATS Message Broker (optional but recommended for production)
nats: nats:
image: nats:2.10-alpine image: nats:2.10-alpine
container_name: agent-coordinator-nats container_name: agent-coordinator-nats
command: command:
- '--jetstream' - '--jetstream'
- '--store_dir=/data' - '--store_dir=/data'
- '--max_file_store=1G' - '--http_port=8222'
- '--max_mem_store=256M'
ports: ports:
# NATS client port - "4223:4222"
- "4222:4222" - "8223:8222"
# NATS HTTP monitoring port - "6223:6222"
- "8222:8222"
# NATS routing port for clustering
- "6222:6222"
volumes: volumes:
- nats_data:/data - nats_data:/data
restart: unless-stopped restart: unless-stopped
@@ -55,31 +21,32 @@ services:
timeout: 5s timeout: 5s
retries: 3 retries: 3
start_period: 10s start_period: 10s
networks:
- agent-coordinator-network
# Optional: NATS Monitoring Dashboard agent-coordinator:
nats-board: image: ghcr.io/rooba/agentcoordinator:latest
image: devforth/nats-board:latest container_name: agent-coordinator
container_name: agent-coordinator-nats-board
environment: environment:
- NATS_HOSTS=nats:4222 - NATS_HOST=nats
- NATS_PORT=4222
- MIX_ENV=prod
volumes:
- ./mcp_servers.json:/app/mcp_servers.json:ro
- ./workspace:/workspace:rw
ports: ports:
- "8080:8080" - "4000:4000"
depends_on: depends_on:
nats: nats:
condition: service_healthy condition: service_healthy
restart: unless-stopped restart: unless-stopped
profiles: networks:
- monitoring - agent-coordinator-network
volumes: volumes:
# Persistent storage for NATS JetStream
nats_data: nats_data:
driver: local driver: local
# Persistent storage for agent coordinator data
agent_data:
driver: local
networks: networks:
default: agent-coordinator-network:
name: agent-coordinator-network driver: bridge

View File

@@ -9,13 +9,23 @@ set -e
export MIX_ENV="${MIX_ENV:-prod}" export MIX_ENV="${MIX_ENV:-prod}"
export NATS_HOST="${NATS_HOST:-localhost}" export NATS_HOST="${NATS_HOST:-localhost}"
export NATS_PORT="${NATS_PORT:-4222}" export NATS_PORT="${NATS_PORT:-4222}"
export DOCKERIZED="true"
COLORIZED="${COLORIZED:-}"
# Colors for output if [ ! -z "$COLORIZED" ]; then
RED='\033[0;31m' # Colors for output
GREEN='\033[0;32m' RED='\033[0;31m'
YELLOW='\033[1;33m' GREEN='\033[0;32m'
BLUE='\033[0;34m' YELLOW='\033[1;33m'
NC='\033[0m' # No Color BLUE='\033[0;34m'
NC='\033[0m' # No Color
else
RED=''
GREEN=''
YELLOW=''
BLUE=''
NC=''
fi
# Logging functions # Logging functions
log_info() { log_info() {
@@ -30,22 +40,12 @@ log_error() {
echo -e "${RED}[ERROR]${NC} $1" >&2 echo -e "${RED}[ERROR]${NC} $1" >&2
} }
log_success() { log_debug() {
echo -e "${GREEN}[SUCCESS]${NC} $1" >&2 echo -e "${GREEN}[DEBUG]${NC} $1" >&2
} }
# Cleanup function for graceful shutdown
cleanup() { cleanup() {
log_info "Received shutdown signal, cleaning up..." log_info "Received shutdown signal, shutting down..."
# Send termination signals to child processes
if [ ! -z "$MAIN_PID" ]; then
log_info "Stopping main process (PID: $MAIN_PID)..."
kill -TERM "$MAIN_PID" 2>/dev/null || true
wait "$MAIN_PID" 2>/dev/null || true
fi
log_success "Cleanup completed"
exit 0 exit 0
} }
@@ -62,7 +62,7 @@ wait_for_nats() {
while [ $count -lt $timeout ]; do while [ $count -lt $timeout ]; do
if nc -z "$NATS_HOST" "$NATS_PORT" 2>/dev/null; then if nc -z "$NATS_HOST" "$NATS_PORT" 2>/dev/null; then
log_success "NATS is available" log_debug "NATS is available"
return 0 return 0
fi fi
@@ -88,13 +88,7 @@ validate_config() {
exit 1 exit 1
fi fi
# Validate JSON log_debug "Configuration validation passed"
if ! cat /app/mcp_servers.json | bun run -e "JSON.parse(require('fs').readFileSync(0, 'utf8'))" >/dev/null 2>&1; then
log_error "Invalid JSON in mcp_servers.json"
exit 1
fi
log_success "Configuration validation passed"
} }
# Pre-install external MCP server dependencies # Pre-install external MCP server dependencies
@@ -120,7 +114,7 @@ preinstall_dependencies() {
bun add --global --silent "$package" || log_warn "Failed to cache $package" bun add --global --silent "$package" || log_warn "Failed to cache $package"
done done
log_success "Dependencies pre-installed" log_debug "Dependencies pre-installed"
} }
# Main execution # Main execution
@@ -129,6 +123,7 @@ main() {
log_info "Environment: $MIX_ENV" log_info "Environment: $MIX_ENV"
log_info "NATS: $NATS_HOST:$NATS_PORT" log_info "NATS: $NATS_HOST:$NATS_PORT"
# Validate configuration # Validate configuration
validate_config validate_config
@@ -147,8 +142,7 @@ main() {
if [ "$#" -eq 0 ] || [ "$1" = "/app/scripts/mcp_launcher.sh" ]; then if [ "$#" -eq 0 ] || [ "$1" = "/app/scripts/mcp_launcher.sh" ]; then
# Default: start the MCP server # Default: start the MCP server
log_info "Starting MCP server via launcher script..." log_info "Starting MCP server via launcher script..."
exec "/app/scripts/mcp_launcher.sh" & exec "/app/scripts/mcp_launcher.sh"
MAIN_PID=$!
elif [ "$1" = "bash" ] || [ "$1" = "sh" ]; then elif [ "$1" = "bash" ] || [ "$1" = "sh" ]; then
# Interactive shell mode # Interactive shell mode
log_info "Starting interactive shell..." log_info "Starting interactive shell..."
@@ -156,21 +150,10 @@ main() {
elif [ "$1" = "release" ]; then elif [ "$1" = "release" ]; then
# Direct release mode # Direct release mode
log_info "Starting via Elixir release..." log_info "Starting via Elixir release..."
exec "/app/bin/agent_coordinator" "start" & exec "/app/bin/agent_coordinator" "start"
MAIN_PID=$!
else else
# Custom command exit 0
log_info "Starting custom command: $*"
exec "$@" &
MAIN_PID=$!
fi
# Wait for the main process if it's running in background
if [ ! -z "$MAIN_PID" ]; then
log_success "Main process started (PID: $MAIN_PID)"
wait "$MAIN_PID"
fi fi
} }
# Execute main function with all arguments
main "$@" main "$@"

View File

@@ -26,7 +26,7 @@ defmodule AgentCoordinator.HttpInterface do
def start_link(opts \\ []) do def start_link(opts \\ []) do
port = Keyword.get(opts, :port, 8080) port = Keyword.get(opts, :port, 8080)
Logger.info("Starting Agent Coordinator HTTP interface on port #{port}") IO.puts(:stderr, "Starting Agent Coordinator HTTP interface on port #{port}")
Plug.Cowboy.http(__MODULE__, [], Plug.Cowboy.http(__MODULE__, [],
port: port, port: port,
@@ -158,7 +158,7 @@ defmodule AgentCoordinator.HttpInterface do
send_json_response(conn, 400, %{error: error}) send_json_response(conn, 400, %{error: error})
unexpected -> unexpected ->
Logger.error("Unexpected MCP response: #{inspect(unexpected)}") IO.puts(:stderr, "Unexpected MCP response: #{inspect(unexpected)}")
send_json_response(conn, 500, %{ send_json_response(conn, 500, %{
error: %{ error: %{
code: -32603, code: -32603,
@@ -317,7 +317,7 @@ defmodule AgentCoordinator.HttpInterface do
rescue rescue
# Client disconnected # Client disconnected
_ -> _ ->
Logger.info("SSE client disconnected") IO.puts(:stderr, "SSE client disconnected")
conn conn
end end
end end
@@ -411,7 +411,7 @@ defmodule AgentCoordinator.HttpInterface do
origin origin
else else
# For production, be more restrictive # For production, be more restrictive
Logger.warning("Potentially unsafe origin: #{origin}") IO.puts(:stderr, "Potentially unsafe origin: #{origin}")
"*" # Fallback for now, could be more restrictive "*" # Fallback for now, could be more restrictive
end end
_ -> "*" _ -> "*"
@@ -487,7 +487,7 @@ defmodule AgentCoordinator.HttpInterface do
validated: true validated: true
}} }}
{:error, reason} -> {:error, reason} ->
Logger.warning("Invalid MCP session token: #{reason}") IO.puts(:stderr, "Invalid MCP session token: #{reason}")
# Fall back to generating anonymous session # Fall back to generating anonymous session
anonymous_id = "http_anonymous_" <> (:crypto.strong_rand_bytes(8) |> Base.encode16(case: :lower)) anonymous_id = "http_anonymous_" <> (:crypto.strong_rand_bytes(8) |> Base.encode16(case: :lower))
{anonymous_id, %{validated: false, reason: reason}} {anonymous_id, %{validated: false, reason: reason}}
@@ -559,7 +559,7 @@ defmodule AgentCoordinator.HttpInterface do
send_json_response(conn, 400, response) send_json_response(conn, 400, response)
unexpected -> unexpected ->
Logger.error("Unexpected MCP response: #{inspect(unexpected)}") IO.puts(:stderr, "Unexpected MCP response: #{inspect(unexpected)}")
send_json_response(conn, 500, %{ send_json_response(conn, 500, %{
jsonrpc: "2.0", jsonrpc: "2.0",
id: Map.get(mcp_request, "id"), id: Map.get(mcp_request, "id"),

View File

@@ -102,7 +102,7 @@ defmodule AgentCoordinator.InterfaceManager do
metrics: initialize_metrics() metrics: initialize_metrics()
} }
Logger.info("Interface Manager starting with config: #{inspect(config.enabled_interfaces)}") IO.puts(:stderr, "Interface Manager starting with config: #{inspect(config.enabled_interfaces)}")
# Start enabled interfaces # Start enabled interfaces
{:ok, state, {:continue, :start_interfaces}} {:ok, state, {:continue, :start_interfaces}}
@@ -114,11 +114,11 @@ defmodule AgentCoordinator.InterfaceManager do
updated_state = Enum.reduce(state.config.enabled_interfaces, state, fn interface_type, acc -> updated_state = Enum.reduce(state.config.enabled_interfaces, state, fn interface_type, acc ->
case start_interface_server(interface_type, state.config, acc) do case start_interface_server(interface_type, state.config, acc) do
{:ok, interface_info} -> {:ok, interface_info} ->
Logger.info("Started #{interface_type} interface") IO.puts(:stderr, "Started #{interface_type} interface")
%{acc | interfaces: Map.put(acc.interfaces, interface_type, interface_info)} %{acc | interfaces: Map.put(acc.interfaces, interface_type, interface_info)}
{:error, reason} -> {:error, reason} ->
Logger.error("Failed to start #{interface_type} interface: #{reason}") IO.puts(:stderr, "Failed to start #{interface_type} interface: #{reason}")
acc acc
end end
end) end)
@@ -152,11 +152,11 @@ defmodule AgentCoordinator.InterfaceManager do
updated_interfaces = Map.put(state.interfaces, interface_type, interface_info) updated_interfaces = Map.put(state.interfaces, interface_type, interface_info)
updated_state = %{state | interfaces: updated_interfaces} updated_state = %{state | interfaces: updated_interfaces}
Logger.info("Started #{interface_type} interface on demand") IO.puts(:stderr, "Started #{interface_type} interface on demand")
{:reply, {:ok, interface_info}, updated_state} {:reply, {:ok, interface_info}, updated_state}
{:error, reason} -> {:error, reason} ->
Logger.error("Failed to start #{interface_type} interface: #{reason}") IO.puts(:stderr, "Failed to start #{interface_type} interface: #{reason}")
{:reply, {:error, reason}, state} {:reply, {:error, reason}, state}
end end
else else
@@ -176,11 +176,11 @@ defmodule AgentCoordinator.InterfaceManager do
updated_interfaces = Map.delete(state.interfaces, interface_type) updated_interfaces = Map.delete(state.interfaces, interface_type)
updated_state = %{state | interfaces: updated_interfaces} updated_state = %{state | interfaces: updated_interfaces}
Logger.info("Stopped #{interface_type} interface") IO.puts(:stderr, "Stopped #{interface_type} interface")
{:reply, :ok, updated_state} {:reply, :ok, updated_state}
{:error, reason} -> {:error, reason} ->
Logger.error("Failed to stop #{interface_type} interface: #{reason}") IO.puts(:stderr, "Failed to stop #{interface_type} interface: #{reason}")
{:reply, {:error, reason}, state} {:reply, {:error, reason}, state}
end end
end end
@@ -202,7 +202,7 @@ defmodule AgentCoordinator.InterfaceManager do
updated_interfaces = Map.put(state.interfaces, interface_type, new_interface_info) updated_interfaces = Map.put(state.interfaces, interface_type, new_interface_info)
updated_state = %{state | interfaces: updated_interfaces} updated_state = %{state | interfaces: updated_interfaces}
Logger.info("Restarted #{interface_type} interface") IO.puts(:stderr, "Restarted #{interface_type} interface")
{:reply, {:ok, new_interface_info}, updated_state} {:reply, {:ok, new_interface_info}, updated_state}
{:error, reason} -> {:error, reason} ->
@@ -210,12 +210,12 @@ defmodule AgentCoordinator.InterfaceManager do
updated_interfaces = Map.delete(state.interfaces, interface_type) updated_interfaces = Map.delete(state.interfaces, interface_type)
updated_state = %{state | interfaces: updated_interfaces} updated_state = %{state | interfaces: updated_interfaces}
Logger.error("Failed to restart #{interface_type} interface: #{reason}") IO.puts(:stderr, "Failed to restart #{interface_type} interface: #{reason}")
{:reply, {:error, reason}, updated_state} {:reply, {:error, reason}, updated_state}
end end
{:error, reason} -> {:error, reason} ->
Logger.error("Failed to stop #{interface_type} interface for restart: #{reason}") IO.puts(:stderr, "Failed to stop #{interface_type} interface for restart: #{reason}")
{:reply, {:error, reason}, state} {:reply, {:error, reason}, state}
end end
end end
@@ -253,7 +253,7 @@ defmodule AgentCoordinator.InterfaceManager do
updated_registry = Map.put(state.session_registry, session_id, session_data) updated_registry = Map.put(state.session_registry, session_id, session_data)
updated_state = %{state | session_registry: updated_registry} updated_state = %{state | session_registry: updated_registry}
Logger.debug("Registered session #{session_id} for #{interface_type}") IO.puts(:stderr, "Registered session #{session_id} for #{interface_type}")
{:noreply, updated_state} {:noreply, updated_state}
end end
@@ -261,14 +261,14 @@ defmodule AgentCoordinator.InterfaceManager do
def handle_cast({:unregister_session, session_id}, state) do def handle_cast({:unregister_session, session_id}, state) do
case Map.get(state.session_registry, session_id) do case Map.get(state.session_registry, session_id) do
nil -> nil ->
Logger.debug("Attempted to unregister unknown session: #{session_id}") IO.puts(:stderr, "Attempted to unregister unknown session: #{session_id}")
{:noreply, state} {:noreply, state}
_session_data -> _session_data ->
updated_registry = Map.delete(state.session_registry, session_id) updated_registry = Map.delete(state.session_registry, session_id)
updated_state = %{state | session_registry: updated_registry} updated_state = %{state | session_registry: updated_registry}
Logger.debug("Unregistered session #{session_id}") IO.puts(:stderr, "Unregistered session #{session_id}")
{:noreply, updated_state} {:noreply, updated_state}
end end
end end
@@ -278,7 +278,7 @@ defmodule AgentCoordinator.InterfaceManager do
# Handle interface process crashes # Handle interface process crashes
case find_interface_by_pid(pid, state.interfaces) do case find_interface_by_pid(pid, state.interfaces) do
{interface_type, _interface_info} -> {interface_type, _interface_info} ->
Logger.error("#{interface_type} interface crashed: #{inspect(reason)}") IO.puts(:stderr, "#{interface_type} interface crashed: #{inspect(reason)}")
# Remove from running interfaces # Remove from running interfaces
updated_interfaces = Map.delete(state.interfaces, interface_type) updated_interfaces = Map.delete(state.interfaces, interface_type)
@@ -286,14 +286,14 @@ defmodule AgentCoordinator.InterfaceManager do
# Optionally restart if configured # Optionally restart if configured
if should_auto_restart?(interface_type, state.config) do if should_auto_restart?(interface_type, state.config) do
Logger.info("Auto-restarting #{interface_type} interface") IO.puts(:stderr, "Auto-restarting #{interface_type} interface")
Process.send_after(self(), {:restart_interface, interface_type}, 5000) Process.send_after(self(), {:restart_interface, interface_type}, 5000)
end end
{:noreply, updated_state} {:noreply, updated_state}
nil -> nil ->
Logger.debug("Unknown process died: #{inspect(pid)}") IO.puts(:stderr, "Unknown process died: #{inspect(pid)}")
{:noreply, state} {:noreply, state}
end end
end end
@@ -305,18 +305,18 @@ defmodule AgentCoordinator.InterfaceManager do
updated_interfaces = Map.put(state.interfaces, interface_type, interface_info) updated_interfaces = Map.put(state.interfaces, interface_type, interface_info)
updated_state = %{state | interfaces: updated_interfaces} updated_state = %{state | interfaces: updated_interfaces}
Logger.info("Auto-restarted #{interface_type} interface") IO.puts(:stderr, "Auto-restarted #{interface_type} interface")
{:noreply, updated_state} {:noreply, updated_state}
{:error, reason} -> {:error, reason} ->
Logger.error("Failed to auto-restart #{interface_type} interface: #{reason}") IO.puts(:stderr, "Failed to auto-restart #{interface_type} interface: #{reason}")
{:noreply, state} {:noreply, state}
end end
end end
@impl GenServer @impl GenServer
def handle_info(message, state) do def handle_info(message, state) do
Logger.debug("Interface Manager received unexpected message: #{inspect(message)}") IO.puts(:stderr, "Interface Manager received unexpected message: #{inspect(message)}")
{:noreply, state} {:noreply, state}
end end
@@ -516,18 +516,46 @@ defmodule AgentCoordinator.InterfaceManager do
defp handle_stdio_loop(state) do defp handle_stdio_loop(state) do
# Handle MCP JSON-RPC messages from STDIO # Handle MCP JSON-RPC messages from STDIO
# Use different approaches for Docker vs regular environments
if docker_environment?() do
handle_stdio_docker_loop(state)
else
handle_stdio_regular_loop(state)
end
end
defp handle_stdio_regular_loop(state) do
case IO.read(:stdio, :line) do case IO.read(:stdio, :line) do
:eof -> :eof ->
Logger.info("STDIO interface shutting down (EOF)") IO.puts(:stderr, "STDIO interface shutting down (EOF)")
exit(:normal) exit(:normal)
{:error, reason} -> {:error, reason} ->
Logger.error("STDIO error: #{inspect(reason)}") IO.puts(:stderr, "STDIO error: #{inspect(reason)}")
exit({:error, reason}) exit({:error, reason})
line -> line ->
handle_stdio_message(String.trim(line), state) handle_stdio_message(String.trim(line), state)
handle_stdio_loop(state) handle_stdio_regular_loop(state)
end
end
defp handle_stdio_docker_loop(state) do
# In Docker, use regular IO.read instead of Port.open({:fd, 0, 1})
# to avoid "driver_select stealing control of fd=0" conflicts with external MCP servers
# This allows external servers to use pipes while Agent Coordinator reads from stdin
case IO.read(:stdio, :line) do
:eof ->
IO.puts(:stderr, "STDIO interface shutting down (EOF)")
exit(:normal)
{:error, reason} ->
IO.puts(:stderr, "STDIO error: #{inspect(reason)}")
exit({:error, reason})
line ->
handle_stdio_message(String.trim(line), state)
handle_stdio_docker_loop(state)
end end
end end
@@ -646,4 +674,21 @@ defmodule AgentCoordinator.InterfaceManager do
end end
defp deep_merge(_left, right), do: right defp deep_merge(_left, right), do: right
# Check if running in Docker environment
defp docker_environment? do
# Check common Docker environment indicators
System.get_env("DOCKER_CONTAINER") != nil or
System.get_env("container") != nil or
System.get_env("DOCKERIZED") != nil or
File.exists?("/.dockerenv") or
File.exists?("/proc/1/cgroup") and
(File.read!("/proc/1/cgroup") |> String.contains?("docker")) or
String.contains?(to_string(System.get_env("PATH", "")), "/app/") or
# Check if we're running under a container init system
case File.read("/proc/1/comm") do
{:ok, comm} -> String.trim(comm) in ["bash", "sh", "docker-init", "tini"]
_ -> false
end
end
end end

View File

@@ -654,7 +654,7 @@ defmodule AgentCoordinator.MCPServer do
}} }}
{:error, reason} -> {:error, reason} ->
Logger.error("Failed to create session for agent #{agent.id}: #{inspect(reason)}") IO.puts(:stderr, "Failed to create session for agent #{agent.id}: #{inspect(reason)}")
# Still return success but without session token for backward compatibility # Still return success but without session token for backward compatibility
{:ok, %{agent_id: agent.id, codebase_id: agent.codebase_id, status: "registered"}} {:ok, %{agent_id: agent.id, codebase_id: agent.codebase_id, status: "registered"}}
end end
@@ -1226,17 +1226,15 @@ defmodule AgentCoordinator.MCPServer do
tools: [] tools: []
} }
# Initialize the server and get tools # Initialize the server and get tools with shorter timeout
case initialize_external_server(server_info) do case initialize_external_server(server_info) do
{:ok, tools} -> {:ok, tools} ->
{:ok, %{server_info | tools: tools}} {:ok, %{server_info | tools: tools}}
{:error, reason} -> {:error, reason} ->
# Cleanup on initialization failure # Log error but don't fail - continue with empty tools
cleanup_external_pid_file(pid_file_path) IO.puts(:stderr, "Failed to initialize #{name}: #{reason}")
kill_external_process(os_pid) {:ok, %{server_info | tools: []}}
if Port.info(port), do: Port.close(port)
{:error, reason}
end end
{:error, reason} -> {:error, reason} ->
@@ -1276,6 +1274,8 @@ defmodule AgentCoordinator.MCPServer do
env_list = env_list =
Enum.map(env, fn {key, value} -> {String.to_charlist(key), String.to_charlist(value)} end) Enum.map(env, fn {key, value} -> {String.to_charlist(key), String.to_charlist(value)} end)
# Use pipe communication but allow stdin/stdout for MCP protocol
# Remove :nouse_stdio since MCP servers need stdin/stdout to communicate
port_options = [ port_options = [
:binary, :binary,
:stream, :stream,
@@ -1357,7 +1357,7 @@ defmodule AgentCoordinator.MCPServer do
Port.command(server_info.port, request_json) Port.command(server_info.port, request_json)
# Collect full response by reading multiple lines if needed # Collect full response by reading multiple lines if needed
response_data = collect_external_response(server_info.port, "", 30_000) response_data = collect_external_response(server_info.port, "", 5_000)
cond do cond do
response_data == "" -> response_data == "" ->
@@ -1503,35 +1503,6 @@ defmodule AgentCoordinator.MCPServer do
pid_file_path pid_file_path
end end
defp cleanup_external_pid_file(pid_file_path) do
if File.exists?(pid_file_path) do
File.rm(pid_file_path)
end
end
defp kill_external_process(os_pid) when is_integer(os_pid) do
try do
case System.cmd("kill", ["-TERM", to_string(os_pid)]) do
{_, 0} ->
IO.puts(:stderr, "Successfully terminated process #{os_pid}")
:ok
{_, _} ->
case System.cmd("kill", ["-KILL", to_string(os_pid)]) do
{_, 0} ->
IO.puts(:stderr, "Force killed process #{os_pid}")
:ok
{_, _} ->
IO.puts(:stderr, "Failed to kill process #{os_pid}")
:error
end
end
rescue
_ -> :error
end
end
defp get_all_unified_tools_from_state(state) do defp get_all_unified_tools_from_state(state) do
# Combine coordinator tools with external server tools from state # Combine coordinator tools with external server tools from state
coordinator_tools = @mcp_tools coordinator_tools = @mcp_tools
@@ -1560,12 +1531,12 @@ defmodule AgentCoordinator.MCPServer do
defp route_tool_call(tool_name, args, state) do defp route_tool_call(tool_name, args, state) do
# Extract agent_id for activity tracking # Extract agent_id for activity tracking
agent_id = Map.get(args, "agent_id") agent_id = Map.get(args, "agent_id")
# Update agent activity before processing the tool call # Update agent activity before processing the tool call
if agent_id do if agent_id do
ActivityTracker.update_agent_activity(agent_id, tool_name, args) ActivityTracker.update_agent_activity(agent_id, tool_name, args)
end end
# Check if it's a coordinator tool first # Check if it's a coordinator tool first
coordinator_tool_names = Enum.map(@mcp_tools, & &1["name"]) coordinator_tool_names = Enum.map(@mcp_tools, & &1["name"])
@@ -1583,12 +1554,12 @@ defmodule AgentCoordinator.MCPServer do
# Try to route to external server # Try to route to external server
route_to_external_server(tool_name, args, state) route_to_external_server(tool_name, args, state)
end end
# Clear agent activity after tool call completes (optional - could keep until next call) # Clear agent activity after tool call completes (optional - could keep until next call)
# if agent_id do # if agent_id do
# ActivityTracker.clear_agent_activity(agent_id) # ActivityTracker.clear_agent_activity(agent_id)
# end # end
result result
end end

View File

@@ -78,7 +78,7 @@ defmodule AgentCoordinator.SessionManager do
} }
} }
Logger.info("SessionManager started with #{state.config.expiry_minutes}min expiry") IO.puts(:stderr, "SessionManager started with #{state.config.expiry_minutes}min expiry")
{:ok, state} {:ok, state}
end end
@@ -99,7 +99,7 @@ defmodule AgentCoordinator.SessionManager do
new_sessions = Map.put(state.sessions, session_token, session_data) new_sessions = Map.put(state.sessions, session_token, session_data)
new_state = %{state | sessions: new_sessions} new_state = %{state | sessions: new_sessions}
Logger.debug("Created session #{session_token} for agent #{agent_id}") IO.puts(:stderr, "Created session #{session_token} for agent #{agent_id}")
{:reply, {:ok, session_token}, new_state} {:reply, {:ok, session_token}, new_state}
end end
@@ -136,7 +136,7 @@ defmodule AgentCoordinator.SessionManager do
session_data -> session_data ->
new_sessions = Map.delete(state.sessions, session_token) new_sessions = Map.delete(state.sessions, session_token)
new_state = %{state | sessions: new_sessions} new_state = %{state | sessions: new_sessions}
Logger.debug("Invalidated session #{session_token} for agent #{session_data.agent_id}") IO.puts(:stderr, "Invalidated session #{session_token} for agent #{session_data.agent_id}")
{:reply, :ok, new_state} {:reply, :ok, new_state}
end end
end end
@@ -161,7 +161,7 @@ defmodule AgentCoordinator.SessionManager do
end) end)
if length(expired_sessions) > 0 do if length(expired_sessions) > 0 do
Logger.debug("Cleaned up #{length(expired_sessions)} expired sessions") IO.puts(:stderr, "Cleaned up #{length(expired_sessions)} expired sessions")
end end
new_state = %{state | sessions: Map.new(active_sessions)} new_state = %{state | sessions: Map.new(active_sessions)}

View File

@@ -37,7 +37,7 @@ defmodule AgentCoordinator.WebSocketHandler do
# Start heartbeat timer # Start heartbeat timer
Process.send_after(self(), :heartbeat, @heartbeat_interval) Process.send_after(self(), :heartbeat, @heartbeat_interval)
Logger.info("WebSocket connection established: #{session_id}") IO.puts(:stderr, "WebSocket connection established: #{session_id}")
{:ok, state} {:ok, state}
end end
@@ -64,7 +64,7 @@ defmodule AgentCoordinator.WebSocketHandler do
@impl WebSock @impl WebSock
def handle_in({_binary, [opcode: :binary]}, state) do def handle_in({_binary, [opcode: :binary]}, state) do
Logger.warning("Received unexpected binary data on WebSocket") IO.puts(:stderr, "Received unexpected binary data on WebSocket")
{:ok, state} {:ok, state}
end end
@@ -95,20 +95,20 @@ defmodule AgentCoordinator.WebSocketHandler do
@impl WebSock @impl WebSock
def handle_info(message, state) do def handle_info(message, state) do
Logger.debug("Received unexpected message: #{inspect(message)}") IO.puts(:stderr, "Received unexpected message: #{inspect(message)}")
{:ok, state} {:ok, state}
end end
@impl WebSock @impl WebSock
def terminate(:remote, state) do def terminate(:remote, state) do
Logger.info("WebSocket connection closed by client: #{state.session_id}") IO.puts(:stderr, "WebSocket connection closed by client: #{state.session_id}")
cleanup_session(state) cleanup_session(state)
:ok :ok
end end
@impl WebSock @impl WebSock
def terminate(reason, state) do def terminate(reason, state) do
Logger.info("WebSocket connection terminated: #{state.session_id}, reason: #{inspect(reason)}") IO.puts(:stderr, "WebSocket connection terminated: #{state.session_id}, reason: #{inspect(reason)}")
cleanup_session(state) cleanup_session(state)
:ok :ok
end end
@@ -245,7 +245,7 @@ defmodule AgentCoordinator.WebSocketHandler do
{:reply, {:text, Jason.encode!(response)}, updated_state} {:reply, {:text, Jason.encode!(response)}, updated_state}
unexpected -> unexpected ->
Logger.error("Unexpected MCP response: #{inspect(unexpected)}") IO.puts(:stderr, "Unexpected MCP response: #{inspect(unexpected)}")
error_response = %{ error_response = %{
"jsonrpc" => "2.0", "jsonrpc" => "2.0",
"id" => Map.get(message, "id"), "id" => Map.get(message, "id"),
@@ -287,7 +287,7 @@ defmodule AgentCoordinator.WebSocketHandler do
defp handle_initialized_notification(_message, state) do defp handle_initialized_notification(_message, state) do
# Client is ready to receive notifications # Client is ready to receive notifications
Logger.info("WebSocket client initialized: #{state.session_id}") IO.puts(:stderr, "WebSocket client initialized: #{state.session_id}")
{:ok, state} {:ok, state}
end end
@@ -304,7 +304,7 @@ defmodule AgentCoordinator.WebSocketHandler do
{:ok, state} {:ok, state}
unexpected -> unexpected ->
Logger.error("Unexpected MCP response: #{inspect(unexpected)}") IO.puts(:stderr, "Unexpected MCP response: #{inspect(unexpected)}")
{:ok, state} {:ok, state}
end end
else else

12
nats-server.conf Normal file
View File

@@ -0,0 +1,12 @@
port: 4222
jetstream {
store_dir: /var/lib/nats/jetstream
max_memory_store: 1GB
max_file_store: 10GB
}
http_port: 8222
log_file: "/var/log/nats-server.log"
debug: false
trace: false

View File

@@ -37,67 +37,7 @@ end
# Log that we're ready # Log that we're ready
IO.puts(:stderr, \"Unified MCP server ready with automatic task tracking\") IO.puts(:stderr, \"Unified MCP server ready with automatic task tracking\")
# Handle MCP JSON-RPC messages through the unified server # STDIO handling is now managed by InterfaceManager, not here
defmodule UnifiedMCPStdio do # Just keep the process alive
def start do Process.sleep(:infinity)
spawn_link(fn -> message_loop() end) "
Process.sleep(:infinity)
end
defp message_loop do
case IO.read(:stdio, :line) do
:eof ->
IO.puts(:stderr, \"Unified MCP server shutting down\")
System.halt(0)
{:error, reason} ->
IO.puts(:stderr, \"IO Error: #{inspect(reason)}\")
System.halt(1)
line ->
handle_message(String.trim(line))
message_loop()
end
end
defp handle_message(\"\"), do: :ok
defp handle_message(json_line) do
try do
request = Jason.decode!(json_line)
# Route through unified MCP server for automatic task tracking
response = AgentCoordinator.MCPServer.handle_mcp_request(request)
IO.puts(Jason.encode!(response))
rescue
e in Jason.DecodeError ->
error_response = %{
\"jsonrpc\" => \"2.0\",
\"id\" => nil,
\"error\" => %{
\"code\" => -32700,
\"message\" => \"Parse error: #{Exception.message(e)}\"
}
}
IO.puts(Jason.encode!(error_response))
e ->
# Try to get the ID from the malformed request
id = try do
partial = Jason.decode!(json_line)
Map.get(partial, \"id\")
rescue
_ -> nil
end
error_response = %{
\"jsonrpc\" => \"2.0\",
\"id\" => id,
\"error\" => %{
\"code\" => -32603,
\"message\" => \"Internal error: #{Exception.message(e)}\"
}
}
IO.puts(Jason.encode!(error_response))
end
end
end
UnifiedMCPStdio.start()
"