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:
39
.github/workflows/build.yml
vendored
Normal file
39
.github/workflows/build.yml
vendored
Normal 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 }}
|
||||||
91
Dockerfile
91
Dockerfile
@@ -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"]
|
||||||
403
README.md
403
README.md
@@ -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
|
||||||
|
<!--  Let's not show this it's confusing -->
|
||||||

|
|
||||||
|
|
||||||
**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": {
|
|
||||||
"mcp": {
|
|
||||||
"servers": {
|
|
||||||
"agent-coordinator": {
|
|
||||||
"command": "docker",
|
|
||||||
"args": ["exec", "-i", "agent-coordinator", "/app/scripts/mcp_launcher.sh"],
|
|
||||||
"env": {
|
|
||||||
"MIX_ENV": "prod"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### For Manual Setup
|
|
||||||
|
|
||||||
Add this to your VS Code `settings.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"github.copilot.advanced": {
|
|
||||||
"mcp": {
|
|
||||||
"servers": {
|
"servers": {
|
||||||
"agent-coordinator": {
|
"agent-coordinator": {
|
||||||
"command": "/path/to/agent_coordinator/scripts/mcp_launcher.sh",
|
"command": "/path/to/agent_coordinator/scripts/mcp_launcher.sh",
|
||||||
"args": [],
|
"args": [],
|
||||||
"env": {
|
"env": {
|
||||||
"MIX_ENV": "dev"
|
"MIX_ENV": "prod",
|
||||||
}
|
"NATS_HOST": "localhost",
|
||||||
}
|
"NATS_PORT": "4222"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ services:
|
|||||||
profiles:
|
profiles:
|
||||||
- dev
|
- dev
|
||||||
|
|
||||||
# Lightweight development NATS without persistence
|
|
||||||
nats:
|
nats:
|
||||||
command:
|
command:
|
||||||
- '--jetstream'
|
- '--jetstream'
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:-}"
|
||||||
|
|
||||||
|
if [ ! -z "$COLORIZED" ]; then
|
||||||
# Colors for output
|
# Colors for output
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
GREEN='\033[0;32m'
|
GREEN='\033[0;32m'
|
||||||
YELLOW='\033[1;33m'
|
YELLOW='\033[1;33m'
|
||||||
BLUE='\033[0;34m'
|
BLUE='\033[0;34m'
|
||||||
NC='\033[0m' # No Color
|
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 "$@"
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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
12
nats-server.conf
Normal 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
|
||||||
@@ -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
|
|
||||||
spawn_link(fn -> message_loop() end)
|
|
||||||
Process.sleep(:infinity)
|
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()
|
|
||||||
"
|
"
|
||||||
Reference in New Issue
Block a user