Save current state before cleaning up duplicate MCP server files

This commit is contained in:
Ra
2025-09-03 00:01:02 -07:00
parent 37f76e060e
commit 074c4473ca
35 changed files with 2369 additions and 1187 deletions

View File

@@ -1,18 +1,271 @@
defmodule AgentCoordinator do
@moduledoc """
Documentation for `AgentCoordinator`.
Agent Coordinator - A Model Context Protocol (MCP) server for multi-agent coordination.
Agent Coordinator enables multiple AI agents to work together seamlessly across codebases
without conflicts. It provides intelligent task distribution, real-time communication,
and cross-codebase coordination through a unified MCP interface.
## Key Features
- **Multi-Agent Coordination**: Register multiple AI agents with different capabilities
- **Intelligent Task Distribution**: Automatically assigns tasks based on agent capabilities
- **Cross-Codebase Support**: Coordinate work across multiple repositories
- **Unified MCP Interface**: Single server providing access to multiple external MCP servers
- **Automatic Task Tracking**: Every tool usage becomes a tracked task
- **Real-Time Communication**: Heartbeat system for agent liveness and coordination
## Quick Start
To start the Agent Coordinator:
# Start the MCP server
./scripts/mcp_launcher.sh
# Or in development mode
iex -S mix
## Main Components
- `AgentCoordinator.MCPServer` - Core MCP protocol implementation
- `AgentCoordinator.TaskRegistry` - Task management and agent coordination
- `AgentCoordinator.UnifiedMCPServer` - Unified interface to external MCP servers
- `AgentCoordinator.CodebaseRegistry` - Multi-repository support
- `AgentCoordinator.VSCodeToolProvider` - VS Code integration tools
## MCP Tools Available
### Agent Coordination
- `register_agent` - Register an agent with capabilities
- `create_task` - Create tasks with requirements
- `get_next_task` - Get assigned tasks
- `complete_task` - Mark tasks complete
- `get_task_board` - View all agent status
- `heartbeat` - Maintain agent liveness
### Codebase Management
- `register_codebase` - Register repositories
- `create_cross_codebase_task` - Tasks spanning multiple repos
- `add_codebase_dependency` - Define repository relationships
### External Tool Access
All tools from external MCP servers are automatically available through
the unified interface, including filesystem, context7, memory, and other servers.
## Usage Example
# Register an agent
AgentCoordinator.MCPServer.handle_mcp_request(%{
"method" => "tools/call",
"params" => %{
"name" => "register_agent",
"arguments" => %{
"name" => "MyAgent",
"capabilities" => ["coding", "testing"]
}
}
})
See the documentation in `docs/` for detailed implementation guides.
"""
alias AgentCoordinator.MCPServer
@doc """
Hello world.
Get the version of Agent Coordinator.
## Examples
iex> AgentCoordinator.hello()
:world
iex> AgentCoordinator.version()
"0.1.0"
"""
def hello do
:world
def version do
Application.spec(:agent_coordinator, :vsn) |> to_string()
end
@doc """
Get the current status of the Agent Coordinator system.
Returns information about active agents, tasks, and external MCP servers.
## Examples
iex> AgentCoordinator.status()
%{
agents: 2,
active_tasks: 1,
external_servers: 3,
uptime: 12345
}
"""
def status do
with {:ok, board} <- get_task_board(),
{:ok, server_status} <- get_server_status() do
%{
agents: length(board[:agents] || []),
active_tasks: count_active_tasks(board),
external_servers: count_active_servers(server_status),
uptime: get_uptime()
}
else
_ -> %{status: :error, message: "Unable to retrieve system status"}
end
end
@doc """
Get the current task board showing all agents and their status.
Returns information about all registered agents, their current tasks,
and overall system status.
## Examples
iex> {:ok, board} = AgentCoordinator.get_task_board()
iex> is_map(board)
true
"""
def get_task_board do
request = %{
"method" => "tools/call",
"params" => %{"name" => "get_task_board", "arguments" => %{}},
"jsonrpc" => "2.0",
"id" => System.unique_integer()
}
case MCPServer.handle_mcp_request(request) do
%{"result" => %{"content" => [%{"text" => text}]}} ->
{:ok, Jason.decode!(text)}
%{"error" => error} ->
{:error, error}
_ ->
{:error, "Unexpected response format"}
end
end
@doc """
Register a new agent with the coordination system.
## Parameters
- `name` - Agent name (string)
- `capabilities` - List of capabilities (["coding", "testing", ...])
- `opts` - Optional parameters (codebase_id, workspace_path, etc.)
## Examples
iex> {:ok, result} = AgentCoordinator.register_agent("TestAgent", ["coding"])
iex> is_map(result)
true
"""
def register_agent(name, capabilities, opts \\ []) do
args =
%{
"name" => name,
"capabilities" => capabilities
}
|> add_optional_arg("codebase_id", opts[:codebase_id])
|> add_optional_arg("workspace_path", opts[:workspace_path])
|> add_optional_arg("cross_codebase_capable", opts[:cross_codebase_capable])
request = %{
"method" => "tools/call",
"params" => %{"name" => "register_agent", "arguments" => args},
"jsonrpc" => "2.0",
"id" => System.unique_integer()
}
case MCPServer.handle_mcp_request(request) do
%{"result" => %{"content" => [%{"text" => text}]}} ->
{:ok, Jason.decode!(text)}
%{"error" => error} ->
{:error, error}
_ ->
{:error, "Unexpected response format"}
end
end
@doc """
Create a new task in the coordination system.
## Parameters
- `title` - Task title (string)
- `description` - Task description (string)
- `opts` - Optional parameters (priority, codebase_id, file_paths, etc.)
## Examples
iex> {:ok, result} = AgentCoordinator.create_task("Test Task", "Test description")
iex> is_map(result)
true
"""
def create_task(title, description, opts \\ []) do
args =
%{
"title" => title,
"description" => description
}
|> add_optional_arg("priority", opts[:priority])
|> add_optional_arg("codebase_id", opts[:codebase_id])
|> add_optional_arg("file_paths", opts[:file_paths])
|> add_optional_arg("required_capabilities", opts[:required_capabilities])
request = %{
"method" => "tools/call",
"params" => %{"name" => "create_task", "arguments" => args},
"jsonrpc" => "2.0",
"id" => System.unique_integer()
}
case MCPServer.handle_mcp_request(request) do
%{"result" => %{"content" => [%{"text" => text}]}} ->
{:ok, Jason.decode!(text)}
%{"error" => error} ->
{:error, error}
_ ->
{:error, "Unexpected response format"}
end
end
# Private helpers
defp add_optional_arg(args, _key, nil), do: args
defp add_optional_arg(args, key, value), do: Map.put(args, key, value)
defp count_active_tasks(%{agents: agents}) do
Enum.count(agents, fn agent ->
Map.get(agent, "current_task") != nil
end)
end
defp count_active_tasks(_), do: 0
defp count_active_servers(server_status) when is_map(server_status) do
Map.get(server_status, :active_servers, 0)
end
defp count_active_servers(_), do: 0
defp get_server_status do
# This would call UnifiedMCPServer to get external server status
# For now, return a placeholder
{:ok, %{active_servers: 3}}
end
defp get_uptime do
# Get system uptime in seconds
{uptime_ms, _} = :erlang.statistics(:wall_clock)
div(uptime_ms, 1000)
end
end

View File

@@ -74,16 +74,18 @@ defmodule AgentCoordinator.Agent do
def can_handle?(agent, task) do
# Check if agent is in the same codebase or can handle cross-codebase tasks
codebase_compatible = agent.codebase_id == task.codebase_id or
Map.get(agent.metadata, :cross_codebase_capable, false)
codebase_compatible =
agent.codebase_id == task.codebase_id or
Map.get(agent.metadata, :cross_codebase_capable, false)
# Simple capability matching - can be enhanced
required_capabilities = Map.get(task.metadata, :required_capabilities, [])
capability_match = case required_capabilities do
[] -> true
caps -> Enum.any?(caps, fn cap -> cap in agent.capabilities end)
end
capability_match =
case required_capabilities do
[] -> true
caps -> Enum.any?(caps, fn cap -> cap in agent.capabilities end)
end
codebase_compatible and capability_match
end

View File

@@ -18,13 +18,15 @@ defmodule AgentCoordinator.Application do
{Phoenix.PubSub, name: AgentCoordinator.PubSub},
# Codebase registry for multi-codebase coordination
{AgentCoordinator.CodebaseRegistry, nats: if(enable_persistence, do: nats_config(), else: nil)},
{AgentCoordinator.CodebaseRegistry,
nats: if(enable_persistence, do: nats_config(), else: nil)},
# Task registry with NATS integration (conditionally add persistence)
{AgentCoordinator.TaskRegistry, nats: if(enable_persistence, do: nats_config(), else: nil)},
# MCP Server Manager (manages external MCP servers)
{AgentCoordinator.MCPServerManager, config_file: Application.get_env(:agent_coordinator, :mcp_config_file, "mcp_servers.json")},
{AgentCoordinator.MCPServerManager,
config_file: System.get_env("MCP_CONFIG_FILE", "mcp_servers.json")},
# MCP server
AgentCoordinator.MCPServer,

View File

@@ -31,19 +31,20 @@ defmodule AgentCoordinator.AutoHeartbeat do
"""
def register_agent_with_heartbeat(name, capabilities, agent_context \\ %{}) do
# Convert capabilities to strings if they're atoms
string_capabilities = Enum.map(capabilities, fn
cap when is_atom(cap) -> Atom.to_string(cap)
cap when is_binary(cap) -> cap
end)
string_capabilities =
Enum.map(capabilities, fn
cap when is_atom(cap) -> Atom.to_string(cap)
cap when is_binary(cap) -> cap
end)
# First register the agent normally
case MCPServer.handle_mcp_request(%{
"method" => "tools/call",
"params" => %{
"name" => "register_agent",
"arguments" => %{"name" => name, "capabilities" => string_capabilities}
}
}) do
"method" => "tools/call",
"params" => %{
"name" => "register_agent",
"arguments" => %{"name" => name, "capabilities" => string_capabilities}
}
}) do
%{"result" => %{"content" => [%{"text" => response_json}]}} ->
case Jason.decode(response_json) do
{:ok, %{"agent_id" => agent_id}} ->
@@ -100,10 +101,14 @@ defmodule AgentCoordinator.AutoHeartbeat do
"method" => "tools/call",
"params" => %{
"name" => "create_task",
"arguments" => Map.merge(%{
"title" => title,
"description" => description
}, opts)
"arguments" =>
Map.merge(
%{
"title" => title,
"description" => description
},
opts
)
}
}
@@ -173,9 +178,10 @@ defmodule AgentCoordinator.AutoHeartbeat do
# Start new timer
timer_ref = Process.send_after(self(), {:heartbeat_timer, agent_id}, @heartbeat_interval)
new_state = %{state |
timers: Map.put(state.timers, agent_id, timer_ref),
agent_contexts: Map.put(state.agent_contexts, agent_id, context)
new_state = %{
state
| timers: Map.put(state.timers, agent_id, timer_ref),
agent_contexts: Map.put(state.agent_contexts, agent_id, context)
}
{:reply, :ok, new_state}
@@ -187,9 +193,10 @@ defmodule AgentCoordinator.AutoHeartbeat do
Process.cancel_timer(state.timers[agent_id])
end
new_state = %{state |
timers: Map.delete(state.timers, agent_id),
agent_contexts: Map.delete(state.agent_contexts, agent_id)
new_state = %{
state
| timers: Map.delete(state.timers, agent_id),
agent_contexts: Map.delete(state.agent_contexts, agent_id)
}
{:reply, :ok, new_state}

View File

@@ -110,10 +110,10 @@ defmodule AgentCoordinator.Client do
def init(config) do
# Register with enhanced MCP server
case EnhancedMCPServer.register_agent_with_session(
config.agent_name,
config.capabilities,
self()
) do
config.agent_name,
config.capabilities,
self()
) do
{:ok, agent_id} ->
state = %__MODULE__{
agent_id: agent_id,
@@ -151,10 +151,14 @@ defmodule AgentCoordinator.Client do
end
def handle_call({:create_task, title, description, opts}, _from, state) do
arguments = Map.merge(%{
"title" => title,
"description" => description
}, opts)
arguments =
Map.merge(
%{
"title" => title,
"description" => description
},
opts
)
request = %{
"method" => "tools/call",

View File

@@ -29,27 +29,27 @@ defmodule AgentCoordinator.Inbox do
end
def add_task(agent_id, task) do
GenServer.call(via_tuple(agent_id), {:add_task, task})
GenServer.call(via_tuple(agent_id), {:add_task, task}, 30_000)
end
def get_next_task(agent_id) do
GenServer.call(via_tuple(agent_id), :get_next_task)
GenServer.call(via_tuple(agent_id), :get_next_task, 15_000)
end
def complete_current_task(agent_id) do
GenServer.call(via_tuple(agent_id), :complete_current_task)
GenServer.call(via_tuple(agent_id), :complete_current_task, 30_000)
end
def get_status(agent_id) do
GenServer.call(via_tuple(agent_id), :get_status)
GenServer.call(via_tuple(agent_id), :get_status, 15_000)
end
def list_tasks(agent_id) do
GenServer.call(via_tuple(agent_id), :list_tasks)
GenServer.call(via_tuple(agent_id), :list_tasks, 15_000)
end
def get_current_task(agent_id) do
GenServer.call(via_tuple(agent_id), :get_current_task)
GenServer.call(via_tuple(agent_id), :get_current_task, 15_000)
end
def stop(agent_id) do

View File

@@ -172,7 +172,8 @@ defmodule AgentCoordinator.MCPServer do
},
%{
"name" => "unregister_agent",
"description" => "Unregister an agent from the coordination system (e.g., when waiting for user input)",
"description" =>
"Unregister an agent from the coordination system (e.g., when waiting for user input)",
"inputSchema" => %{
"type" => "object",
"properties" => %{
@@ -181,6 +182,127 @@ defmodule AgentCoordinator.MCPServer do
},
"required" => ["agent_id"]
}
},
%{
"name" => "register_task_set",
"description" =>
"Register a planned set of tasks for an agent to enable workflow coordination",
"inputSchema" => %{
"type" => "object",
"properties" => %{
"agent_id" => %{
"type" => "string",
"description" => "ID of the agent registering the task set"
},
"task_set" => %{
"type" => "array",
"description" => "Array of tasks to register for this agent",
"items" => %{
"type" => "object",
"properties" => %{
"title" => %{"type" => "string", "description" => "Task title"},
"description" => %{"type" => "string", "description" => "Task description"},
"priority" => %{
"type" => "string",
"enum" => ["low", "normal", "high", "urgent"],
"default" => "normal"
},
"estimated_time" => %{
"type" => "string",
"description" => "Estimated completion time"
},
"file_paths" => %{
"type" => "array",
"items" => %{"type" => "string"},
"description" => "Files this task will work on"
},
"required_capabilities" => %{
"type" => "array",
"items" => %{"type" => "string"},
"description" => "Capabilities required for this task"
}
},
"required" => ["title", "description"]
}
}
},
"required" => ["agent_id", "task_set"]
}
},
%{
"name" => "create_agent_task",
"description" =>
"Create a task specifically for a particular agent (not globally assigned)",
"inputSchema" => %{
"type" => "object",
"properties" => %{
"agent_id" => %{"type" => "string", "description" => "ID of the agent this task is for"},
"title" => %{"type" => "string", "description" => "Task title"},
"description" => %{"type" => "string", "description" => "Detailed task description"},
"priority" => %{
"type" => "string",
"enum" => ["low", "normal", "high", "urgent"],
"default" => "normal"
},
"estimated_time" => %{"type" => "string", "description" => "Estimated completion time"},
"file_paths" => %{
"type" => "array",
"items" => %{"type" => "string"},
"description" => "Files this task will work on"
},
"required_capabilities" => %{
"type" => "array",
"items" => %{"type" => "string"},
"description" => "Capabilities required for this task"
}
},
"required" => ["agent_id", "title", "description"]
}
},
%{
"name" => "get_detailed_task_board",
"description" =>
"Get detailed task information for all agents including completed, current, and planned tasks",
"inputSchema" => %{
"type" => "object",
"properties" => %{
"codebase_id" => %{
"type" => "string",
"description" => "Optional: filter by codebase ID"
},
"include_task_details" => %{
"type" => "boolean",
"default" => true,
"description" => "Include full task details"
}
}
}
},
%{
"name" => "get_agent_task_history",
"description" => "Get detailed task history for a specific agent",
"inputSchema" => %{
"type" => "object",
"properties" => %{
"agent_id" => %{"type" => "string", "description" => "ID of the agent"},
"include_planned" => %{
"type" => "boolean",
"default" => true,
"description" => "Include planned/pending tasks"
},
"include_completed" => %{
"type" => "boolean",
"default" => true,
"description" => "Include completed tasks"
},
"limit" => %{
"type" => "number",
"default" => 50,
"description" => "Maximum number of tasks to return"
}
},
"required" => ["agent_id"]
}
}
]
@@ -213,6 +335,7 @@ defmodule AgentCoordinator.MCPServer do
defp process_mcp_request(%{"method" => "initialize"} = request) do
id = Map.get(request, "id", nil)
%{
"jsonrpc" => "2.0",
"id" => id,
@@ -231,6 +354,7 @@ defmodule AgentCoordinator.MCPServer do
defp process_mcp_request(%{"method" => "tools/list"} = request) do
id = Map.get(request, "id", nil)
%{
"jsonrpc" => "2.0",
"id" => id,
@@ -260,6 +384,10 @@ defmodule AgentCoordinator.MCPServer do
"add_codebase_dependency" -> add_codebase_dependency(args)
"heartbeat" -> heartbeat(args)
"unregister_agent" -> unregister_agent(args)
"register_task_set" -> register_task_set(args)
"create_agent_task" -> create_agent_task(args)
"get_detailed_task_board" -> get_detailed_task_board(args)
"get_agent_task_history" -> get_agent_task_history(args)
_ -> {:error, "Unknown tool: #{tool_name}"}
end
@@ -282,6 +410,7 @@ defmodule AgentCoordinator.MCPServer do
defp process_mcp_request(request) do
id = Map.get(request, "id", nil)
%{
"jsonrpc" => "2.0",
"id" => id,
@@ -343,7 +472,13 @@ defmodule AgentCoordinator.MCPServer do
case TaskRegistry.assign_task(task) do
{:ok, agent_id} ->
{:ok, %{task_id: task.id, assigned_to: agent_id, codebase_id: task.codebase_id, status: "assigned"}}
{:ok,
%{
task_id: task.id,
assigned_to: agent_id,
codebase_id: task.codebase_id,
status: "assigned"
}}
{:error, :no_available_agents} ->
# Add to global pending queue
@@ -383,7 +518,11 @@ defmodule AgentCoordinator.MCPServer do
}
]
Task.new("#{title} (#{codebase_id})", "Cross-codebase task: #{description}", dependent_opts)
Task.new(
"#{title} (#{codebase_id})",
"Cross-codebase task: #{description}",
dependent_opts
)
end
end)
|> Enum.filter(&(&1 != nil))
@@ -394,20 +533,28 @@ defmodule AgentCoordinator.MCPServer do
results =
Enum.map(all_tasks, fn task ->
case TaskRegistry.assign_task(task) do
{:ok, agent_id} -> %{task_id: task.id, codebase_id: task.codebase_id, agent_id: agent_id, status: "assigned"}
{:ok, agent_id} ->
%{
task_id: task.id,
codebase_id: task.codebase_id,
agent_id: agent_id,
status: "assigned"
}
{:error, :no_available_agents} ->
TaskRegistry.add_to_pending(task)
%{task_id: task.id, codebase_id: task.codebase_id, status: "queued"}
end
end)
{:ok, %{
main_task_id: main_task.id,
primary_codebase: primary_codebase,
coordination_strategy: strategy,
tasks: results,
status: "created"
}}
{:ok,
%{
main_task_id: main_task.id,
primary_codebase: primary_codebase,
coordination_strategy: strategy,
tasks: results,
status: "created"
}}
end
defp get_next_task(%{"agent_id" => agent_id}) do
@@ -511,17 +658,24 @@ defmodule AgentCoordinator.MCPServer do
{:ok, %{codebases: codebase_summaries}}
end
defp add_codebase_dependency(%{"source_codebase_id" => source, "target_codebase_id" => target, "dependency_type" => dep_type} = args) do
defp add_codebase_dependency(
%{
"source_codebase_id" => source,
"target_codebase_id" => target,
"dependency_type" => dep_type
} = args
) do
metadata = Map.get(args, "metadata", %{})
case CodebaseRegistry.add_cross_codebase_dependency(source, target, dep_type, metadata) do
:ok ->
{:ok, %{
source_codebase: source,
target_codebase: target,
dependency_type: dep_type,
status: "added"
}}
{:ok,
%{
source_codebase: source,
target_codebase: target,
dependency_type: dep_type,
status: "added"
}}
{:error, reason} ->
{:error, "Failed to add dependency: #{reason}"}
@@ -549,4 +703,212 @@ defmodule AgentCoordinator.MCPServer do
{:error, "Unregister failed: #{reason}"}
end
end
# NEW: Agent-specific task management functions
defp register_task_set(%{"agent_id" => agent_id, "task_set" => task_set}) do
case TaskRegistry.get_agent(agent_id) do
{:error, :not_found} ->
{:error, "Agent not found: #{agent_id}"}
{:ok, _agent} ->
# Create tasks specifically for this agent
created_tasks =
Enum.map(task_set, fn task_data ->
opts = %{
priority: String.to_atom(Map.get(task_data, "priority", "normal")),
# Use agent's codebase
codebase_id: "default",
file_paths: Map.get(task_data, "file_paths", []),
metadata: %{
agent_created: true,
estimated_time: Map.get(task_data, "estimated_time"),
required_capabilities: Map.get(task_data, "required_capabilities", [])
}
}
task = Task.new(task_data["title"], task_data["description"], opts)
# Add directly to agent's inbox (not global pool)
case Inbox.add_task(agent_id, task) do
:ok -> task
{:error, reason} -> {:error, reason}
end
end)
# Check for any errors
case Enum.find(created_tasks, fn result -> match?({:error, _}, result) end) do
nil ->
task_summaries =
Enum.map(created_tasks, fn task ->
%{
task_id: task.id,
title: task.title,
priority: task.priority,
estimated_time: task.metadata[:estimated_time]
}
end)
{:ok,
%{
agent_id: agent_id,
registered_tasks: length(created_tasks),
task_set: task_summaries,
status: "registered"
}}
{:error, reason} ->
{:error, "Failed to register task set: #{reason}"}
end
end
end
defp create_agent_task(
%{"agent_id" => agent_id, "title" => title, "description" => description} = args
) do
case TaskRegistry.get_agent(agent_id) do
{:error, :not_found} ->
{:error, "Agent not found: #{agent_id}"}
{:ok, _agent} ->
opts = %{
priority: String.to_atom(Map.get(args, "priority", "normal")),
# Use agent's codebase
codebase_id: "default",
file_paths: Map.get(args, "file_paths", []),
metadata: %{
agent_created: true,
estimated_time: Map.get(args, "estimated_time"),
required_capabilities: Map.get(args, "required_capabilities", [])
}
}
task = Task.new(title, description, opts)
# Add directly to agent's inbox
case Inbox.add_task(agent_id, task) do
:ok ->
{:ok,
%{
task_id: task.id,
agent_id: agent_id,
title: task.title,
priority: task.priority,
status: "created_for_agent"
}}
{:error, reason} ->
{:error, "Failed to create agent task: #{reason}"}
end
end
end
defp get_detailed_task_board(args) do
codebase_id = Map.get(args, "codebase_id")
include_details = Map.get(args, "include_task_details", true)
agents = TaskRegistry.list_agents()
# Filter agents by codebase if specified
filtered_agents =
case codebase_id do
nil -> agents
id -> Enum.filter(agents, fn agent -> agent.codebase_id == id end)
end
detailed_board =
Enum.map(filtered_agents, fn agent ->
# Get detailed task information
task_info =
case Inbox.list_tasks(agent.id) do
{:error, _} ->
%{pending: [], in_progress: nil, completed: []}
tasks ->
if include_details do
tasks
else
# Just counts like before
%{
pending_count: length(tasks.pending),
in_progress: if(tasks.in_progress, do: 1, else: 0),
completed_count: length(tasks.completed)
}
end
end
%{
agent_id: agent.id,
name: agent.name,
capabilities: agent.capabilities,
status: agent.status,
codebase_id: agent.codebase_id,
workspace_path: agent.workspace_path,
online: Agent.is_online?(agent),
cross_codebase_capable: Agent.can_work_cross_codebase?(agent),
last_heartbeat: agent.last_heartbeat,
tasks: task_info
}
end)
{:ok,
%{
agents: detailed_board,
codebase_filter: codebase_id,
timestamp: DateTime.utc_now()
}}
end
defp get_agent_task_history(%{"agent_id" => agent_id} = args) do
include_planned = Map.get(args, "include_planned", true)
include_completed = Map.get(args, "include_completed", true)
limit = Map.get(args, "limit", 50)
case TaskRegistry.get_agent(agent_id) do
{:error, :not_found} ->
{:error, "Agent not found: #{agent_id}"}
{:ok, agent} ->
case Inbox.list_tasks(agent_id) do
{:error, reason} ->
{:error, "Failed to get task history: #{reason}"}
task_data ->
history = %{}
# Add planned tasks if requested
history =
if include_planned do
Map.put(history, :planned_tasks, Enum.take(task_data.pending, limit))
else
history
end
# Add current task
history =
if task_data.in_progress do
Map.put(history, :current_task, task_data.in_progress)
else
history
end
# Add completed tasks if requested
history =
if include_completed do
Map.put(history, :completed_tasks, Enum.take(task_data.completed, limit))
else
history
end
{:ok,
%{
agent_id: agent_id,
agent_name: agent.name,
history: history,
total_planned: length(task_data.pending),
total_completed: length(task_data.completed),
timestamp: DateTime.utc_now()
}}
end
end
end
end

View File

@@ -75,13 +75,13 @@ defmodule AgentCoordinator.MCPServerManager do
end
def handle_continue(:start_servers, state) do
Logger.info("Starting external MCP servers...")
IO.puts(:stderr, "Starting external MCP servers...")
new_state =
Enum.reduce(state.config.servers, state, fn {name, config}, acc ->
case start_server(name, config) do
{:ok, server_info} ->
Logger.info("Started MCP server: #{name}")
IO.puts(:stderr, "Started MCP server: #{name}")
%{
acc
@@ -90,7 +90,7 @@ defmodule AgentCoordinator.MCPServerManager do
}
{:error, reason} ->
Logger.error("Failed to start MCP server #{name}: #{reason}")
IO.puts(:stderr, "Failed to start MCP server #{name}: #{reason}")
acc
end
end)
@@ -187,9 +187,10 @@ defmodule AgentCoordinator.MCPServerManager do
# Re-discover tools from all running servers
updated_state = rediscover_all_tools(state)
all_tools = get_coordinator_tools() ++ (Map.values(updated_state.tool_registry) |> List.flatten())
all_tools =
get_coordinator_tools() ++ (Map.values(updated_state.tool_registry) |> List.flatten())
Logger.info("Refreshed tool registry: found #{length(all_tools)} total tools")
IO.puts(:stderr, "Refreshed tool registry: found #{length(all_tools)} total tools")
{:reply, {:ok, length(all_tools)}, updated_state}
end
@@ -198,12 +199,13 @@ defmodule AgentCoordinator.MCPServerManager do
# Handle server port death
case find_server_by_port(port, state.servers) do
{server_name, server_info} ->
Logger.warning("MCP server #{server_name} port died: #{reason}")
IO.puts(:stderr, "MCP server #{server_name} port died: #{reason}")
# Cleanup PID file and kill external process
if server_info.pid_file_path do
cleanup_pid_file(server_info.pid_file_path)
end
if server_info.os_pid do
kill_external_process(server_info.os_pid)
end
@@ -218,7 +220,7 @@ defmodule AgentCoordinator.MCPServerManager do
# Attempt restart if configured
if should_auto_restart?(server_name, state.config) do
Logger.info("Auto-restarting MCP server: #{server_name}")
IO.puts(:stderr, "Auto-restarting MCP server: #{server_name}")
Process.send_after(self(), {:restart_server, server_name}, 1000)
end
@@ -234,7 +236,7 @@ defmodule AgentCoordinator.MCPServerManager do
case start_server(server_name, server_config) do
{:ok, server_info} ->
Logger.info("Auto-restarted MCP server: #{server_name}")
IO.puts(:stderr, "Auto-restarted MCP server: #{server_name}")
new_state = %{
state
@@ -246,7 +248,10 @@ defmodule AgentCoordinator.MCPServerManager do
{:noreply, updated_state}
{:error, reason} ->
Logger.error("Failed to auto-restart MCP server #{server_name}: #{reason}")
IO.puts(:stderr,
"Failed to auto-restart MCP server #{server_name}: #{reason}"
)
{:noreply, state}
end
end
@@ -257,11 +262,12 @@ defmodule AgentCoordinator.MCPServerManager do
# Private functions
defp load_server_config(opts) do
defp load_server_config(_opts) do # We should probably use opts, but idk how to fix it, so we're using env var for a single var
# Allow override from opts or config file
config_file = Keyword.get(opts, :config_file, "mcp_servers.json")
config_file = System.get_env("MCP_CONFIG_FILE", "mcp_servers.json")
if File.exists?(config_file) do
IO.puts(:stderr, "Loading MCP server config from #{config_file}")
try do
case Jason.decode!(File.read!(config_file)) do
%{"servers" => servers} = full_config ->
@@ -288,22 +294,30 @@ defmodule AgentCoordinator.MCPServerManager do
# Add any additional config from the JSON file
case Map.get(full_config, "config") do
nil -> base_config
nil ->
base_config
additional_config ->
Map.merge(base_config, %{config: additional_config})
end
_ ->
Logger.warning("Invalid config file format in #{config_file}, using defaults")
IO.puts(:stderr,
"Invalid config file format in #{config_file}, using defaults"
)
get_default_config()
end
rescue
e ->
Logger.warning("Failed to load config file #{config_file}: #{Exception.message(e)}, using defaults")
IO.puts(:stderr,
"Failed to load config file #{config_file}: #{Exception.message(e)}, using defaults"
)
get_default_config()
end
else
Logger.warning("Config file #{config_file} not found, using defaults")
IO.puts(:stderr, "Config file #{config_file} not found, using defaults")
get_default_config()
end
end
@@ -320,35 +334,35 @@ defmodule AgentCoordinator.MCPServerManager do
},
"mcp_figma" => %{
type: :stdio,
command: "npx",
command: "bunx",
args: ["-y", "@figma/mcp-server-figma"],
auto_restart: true,
description: "Figma design integration server"
},
"mcp_filesystem" => %{
type: :stdio,
command: "npx",
command: "bunx",
args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/ra"],
auto_restart: true,
description: "Filesystem operations server with heartbeat coverage"
},
"mcp_firebase" => %{
type: :stdio,
command: "npx",
command: "bunx",
args: ["-y", "@firebase/mcp-server"],
auto_restart: true,
description: "Firebase integration server"
},
"mcp_memory" => %{
type: :stdio,
command: "npx",
command: "bunx",
args: ["-y", "@modelcontextprotocol/server-memory"],
auto_restart: true,
description: "Memory and knowledge graph server"
},
"mcp_sequentialthi" => %{
type: :stdio,
command: "npx",
command: "bunx",
args: ["-y", "@modelcontextprotocol/server-sequential-thinking"],
auto_restart: true,
description: "Sequential thinking and reasoning server"
@@ -366,7 +380,8 @@ defmodule AgentCoordinator.MCPServerManager do
server_info = %{
name: name,
type: :stdio,
pid: port, # Use port as the "pid" for process tracking
# Use port as the "pid" for process tracking
pid: port,
os_pid: os_pid,
port: port,
pid_file_path: pid_file_path,
@@ -388,6 +403,7 @@ defmodule AgentCoordinator.MCPServerManager do
if Port.info(port) do
Port.close(port)
end
{:error, reason}
end
@@ -402,7 +418,8 @@ defmodule AgentCoordinator.MCPServerManager do
name: name,
type: :http,
url: Map.get(config, :url),
pid: nil, # No process to track for HTTP
# No process to track for HTTP
pid: nil,
os_pid: nil,
port: nil,
pid_file_path: nil,
@@ -427,7 +444,8 @@ defmodule AgentCoordinator.MCPServerManager do
env = Map.get(config, :env, %{})
# Convert env map to list format expected by Port.open
env_list = Enum.map(env, fn {key, value} -> {String.to_charlist(key), String.to_charlist(value)} end)
env_list =
Enum.map(env, fn {key, value} -> {String.to_charlist(key), String.to_charlist(value)} end)
port_options = [
:binary,
@@ -438,8 +456,11 @@ defmodule AgentCoordinator.MCPServerManager do
]
try do
port = Port.open({:spawn_executable, System.find_executable(command)},
[{:args, args} | port_options])
port =
Port.open(
{:spawn_executable, System.find_executable(command)},
[{:args, args} | port_options]
)
# Get the OS PID of the spawned process
{:os_pid, os_pid} = Port.info(port, :os_pid)
@@ -447,12 +468,15 @@ defmodule AgentCoordinator.MCPServerManager do
# Create PID file for cleanup
pid_file_path = create_pid_file(name, os_pid)
Logger.info("Started MCP server #{name} with OS PID #{os_pid}")
IO.puts(:stderr, "Started MCP server #{name} with OS PID #{os_pid}")
{:ok, os_pid, port, pid_file_path}
rescue
e ->
Logger.error("Failed to start stdio server #{name}: #{Exception.message(e)}")
IO.puts(:stderr,
"Failed to start stdio server #{name}: #{Exception.message(e)}"
)
{:error, Exception.message(e)}
end
end
@@ -477,16 +501,18 @@ defmodule AgentCoordinator.MCPServerManager do
try do
case System.cmd("kill", ["-TERM", to_string(os_pid)]) do
{_, 0} ->
Logger.info("Successfully terminated process #{os_pid}")
IO.puts(:stderr, "Successfully terminated process #{os_pid}")
:ok
{_, _} ->
# Try force kill
case System.cmd("kill", ["-KILL", to_string(os_pid)]) do
{_, 0} ->
Logger.info("Force killed process #{os_pid}")
IO.puts(:stderr, "Force killed process #{os_pid}")
:ok
{_, _} ->
Logger.warning("Failed to kill process #{os_pid}")
IO.puts(:stderr, "Failed to kill process #{os_pid}")
:error
end
end
@@ -528,7 +554,10 @@ defmodule AgentCoordinator.MCPServerManager do
defp initialize_http_server(server_info) do
# For HTTP servers, we would make HTTP requests instead of using ports
# For now, return empty tools list as we need to implement HTTP client logic
Logger.warning("HTTP server support not fully implemented yet for #{server_info.name}")
IO.puts(:stderr,
"HTTP server support not fully implemented yet for #{server_info.name}"
)
{:ok, []}
rescue
e ->
@@ -547,7 +576,7 @@ defmodule AgentCoordinator.MCPServerManager do
{:ok, tools}
{:ok, unexpected} ->
Logger.warning(
IO.puts(:stderr,
"Unexpected tools response from #{server_info.name}: #{inspect(unexpected)}"
)
@@ -570,17 +599,20 @@ defmodule AgentCoordinator.MCPServerManager do
# Check if we got any response data
response_data == "" ->
{:error, "No response received from server #{server_info.name}"}
# Try to decode JSON response
true ->
case Jason.decode(response_data) do
{:ok, response} -> {:ok, response}
{:ok, response} ->
{:ok, response}
{:error, %Jason.DecodeError{} = error} ->
Logger.error("JSON decode error for server #{server_info.name}: #{Exception.message(error)}")
Logger.debug("Raw response data: #{inspect(response_data)}")
IO.puts(:stderr,
"JSON decode error for server #{server_info.name}: #{Exception.message(error)}"
)
IO.puts(:stderr, "Raw response data: #{inspect(response_data)}")
{:error, "JSON decode error: #{Exception.message(error)}"}
{:error, reason} ->
{:error, "JSON decode error: #{inspect(reason)}"}
end
end
end
@@ -590,25 +622,24 @@ defmodule AgentCoordinator.MCPServerManager do
{^port, {:data, data}} ->
# Accumulate binary data
new_acc = acc <> data
# Try to extract complete JSON messages from the accumulated data
case extract_json_messages(new_acc) do
{json_message, _remaining} when json_message != nil ->
# We found a complete JSON message, return it
json_message
{nil, remaining} ->
# No complete JSON message yet, continue collecting
collect_response(port, remaining, timeout)
end
{^port, {:exit_status, status}} ->
Logger.error("Server exited with status: #{status}")
IO.puts(:stderr, "Server exited with status: #{status}")
acc
after
timeout ->
Logger.error("Server request timeout after #{timeout}ms")
IO.puts(:stderr, "Server request timeout after #{timeout}ms")
acc
end
end
@@ -616,29 +647,30 @@ defmodule AgentCoordinator.MCPServerManager do
# Extract complete JSON messages from accumulated binary data
defp extract_json_messages(data) do
lines = String.split(data, "\n", trim: false)
# Process each line to find JSON messages and skip log messages
{json_lines, _remaining_data} = extract_json_from_lines(lines, [])
case json_lines do
[] ->
# No complete JSON found, return the last partial line if any
last_line = List.last(lines) || ""
if String.trim(last_line) != "" and not String.ends_with?(data, "\n") do
{nil, last_line}
else
{nil, ""}
end
_ ->
# Join all JSON lines and try to parse
json_data = Enum.join(json_lines, "\n")
case Jason.decode(json_data) do
{:ok, _} ->
# Valid JSON found
{json_data, ""}
{:error, _} ->
# Invalid JSON, might be incomplete
{nil, data}
@@ -647,55 +679,55 @@ defmodule AgentCoordinator.MCPServerManager do
end
defp extract_json_from_lines([], acc), do: {Enum.reverse(acc), ""}
defp extract_json_from_lines([line], acc) do
# This is the last line, it might be incomplete
trimmed = String.trim(line)
cond do
trimmed == "" ->
{Enum.reverse(acc), ""}
# Skip log messages
Regex.match?(~r/^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}/, trimmed) ->
{Enum.reverse(acc), ""}
Regex.match?(~r/^\d{2}:\d{2}:\d{2}\.\d+\s+\[(info|warning|error|debug)\]/, trimmed) ->
{Enum.reverse(acc), ""}
# Check if this looks like JSON
String.starts_with?(trimmed, ["{"]) ->
{Enum.reverse([line | acc]), ""}
true ->
# Non-JSON line, might be incomplete
{Enum.reverse(acc), line}
end
end
defp extract_json_from_lines([line | rest], acc) do
trimmed = String.trim(line)
cond do
trimmed == "" ->
extract_json_from_lines(rest, acc)
# Skip log messages
Regex.match?(~r/^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}/, trimmed) ->
Logger.debug("Skipping log message from MCP server: #{trimmed}")
IO.puts(:stderr, "Skipping log message from MCP server: #{trimmed}")
extract_json_from_lines(rest, acc)
Regex.match?(~r/^\d{2}:\d{2}:\d{2}\.\d+\s+\[(info|warning|error|debug)\]/, trimmed) ->
Logger.debug("Skipping log message from MCP server: #{trimmed}")
IO.puts(:stderr, "Skipping log message from MCP server: #{trimmed}")
extract_json_from_lines(rest, acc)
# Check if this looks like JSON
String.starts_with?(trimmed, ["{"]) ->
extract_json_from_lines(rest, [line | acc])
true ->
# Skip non-JSON lines
Logger.debug("Skipping non-JSON line from MCP server: #{trimmed}")
IO.puts(:stderr, "Skipping non-JSON line from MCP server: #{trimmed}")
extract_json_from_lines(rest, acc)
end
end
@@ -715,25 +747,29 @@ defmodule AgentCoordinator.MCPServerManager do
updated_servers =
Enum.reduce(state.servers, state.servers, fn {name, server_info}, acc ->
# Check if server is alive (handle both PID and Port)
server_alive = case server_info.pid do
nil -> false
pid when is_pid(pid) -> Process.alive?(pid)
port when is_port(port) -> Port.info(port) != nil
_ -> false
end
server_alive =
case server_info.pid do
nil -> false
pid when is_pid(pid) -> Process.alive?(pid)
port when is_port(port) -> Port.info(port) != nil
_ -> false
end
if server_alive do
case get_server_tools(server_info) do
{:ok, new_tools} ->
Logger.debug("Rediscovered #{length(new_tools)} tools from #{name}")
IO.puts(:stderr, "Rediscovered #{length(new_tools)} tools from #{name}")
Map.put(acc, name, %{server_info | tools: new_tools})
{:error, reason} ->
Logger.warning("Failed to rediscover tools from #{name}: #{inspect(reason)}")
IO.puts(:stderr,
"Failed to rediscover tools from #{name}: #{inspect(reason)}"
)
acc
end
else
Logger.warning("Server #{name} is not alive, skipping tool rediscovery")
IO.puts(:stderr, "Server #{name} is not alive, skipping tool rediscovery")
acc
end
end)
@@ -747,6 +783,7 @@ defmodule AgentCoordinator.MCPServerManager do
# Check all tool registries (both coordinator and external servers)
# Start with coordinator tools
coordinator_tools = get_coordinator_tools()
if Enum.any?(coordinator_tools, fn tool -> tool["name"] == tool_name end) do
{:coordinator, tool_name}
else
@@ -855,18 +892,19 @@ defmodule AgentCoordinator.MCPServerManager do
]
# Get VS Code tools only if VS Code functionality is available
vscode_tools = try do
if Code.ensure_loaded?(AgentCoordinator.VSCodeToolProvider) do
AgentCoordinator.VSCodeToolProvider.get_tools()
else
Logger.debug("VS Code tools not available - module not loaded")
[]
vscode_tools =
try do
if Code.ensure_loaded?(AgentCoordinator.VSCodeToolProvider) do
AgentCoordinator.VSCodeToolProvider.get_tools()
else
IO.puts(:stderr, "VS Code tools not available - module not loaded")
[]
end
rescue
_ ->
IO.puts(:stderr, "VS Code tools not available - error loading")
[]
end
rescue
_ ->
Logger.debug("VS Code tools not available - error loading")
[]
end
# Combine all coordinator tools
coordinator_native_tools ++ vscode_tools
@@ -878,10 +916,11 @@ defmodule AgentCoordinator.MCPServerManager do
# Route to existing Agent Coordinator functionality or VS Code tools
case tool_name do
"register_agent" ->
opts = case arguments["metadata"] do
nil -> []
metadata -> [metadata: metadata]
end
opts =
case arguments["metadata"] do
nil -> []
metadata -> [metadata: metadata]
end
AgentCoordinator.TaskRegistry.register_agent(
arguments["name"],
@@ -996,6 +1035,8 @@ defmodule AgentCoordinator.MCPServerManager do
end
end
# TODO: Perhaps... copilot should supply what it thinks it is doing?
# Or, we need to fill these out with every possible tool
defp generate_task_title(tool_name, arguments) do
case tool_name do
"read_file" ->
@@ -1027,6 +1068,7 @@ defmodule AgentCoordinator.MCPServerManager do
end
end
# TODO: See Line [1042](#L1042)
defp generate_task_description(tool_name, arguments) do
case tool_name do
"read_file" ->

View File

@@ -95,9 +95,6 @@ defmodule AgentCoordinator.Persistence do
case Gnat.pub(state.nats_conn, subject, message, headers: event_headers()) do
:ok ->
:ok
{:error, reason} ->
IO.puts("Failed to store event: #{inspect(reason)}")
end
end

View File

@@ -24,11 +24,11 @@ defmodule AgentCoordinator.TaskRegistry do
end
def register_agent(agent) do
GenServer.call(__MODULE__, {:register_agent, agent})
GenServer.call(__MODULE__, {:register_agent, agent}, 30_000)
end
def assign_task(task) do
GenServer.call(__MODULE__, {:assign_task, task})
GenServer.call(__MODULE__, {:assign_task, task}, 30_000)
end
def add_to_pending(task) do
@@ -40,7 +40,7 @@ defmodule AgentCoordinator.TaskRegistry do
end
def heartbeat_agent(agent_id) do
GenServer.call(__MODULE__, {:heartbeat_agent, agent_id})
GenServer.call(__MODULE__, {:heartbeat_agent, agent_id}, 30_000)
end
def unregister_agent(agent_id, reason \\ "Agent requested unregistration") do
@@ -52,7 +52,7 @@ defmodule AgentCoordinator.TaskRegistry do
end
def get_agent_current_task(agent_id) do
GenServer.call(__MODULE__, {:get_agent_current_task, agent_id})
GenServer.call(__MODULE__, {:get_agent_current_task, agent_id}, 15_000)
end
def get_agent(agent_id) do
@@ -64,11 +64,11 @@ defmodule AgentCoordinator.TaskRegistry do
end
def update_task_activity(task_id, tool_name, arguments) do
GenServer.call(__MODULE__, {:update_task_activity, task_id, tool_name, arguments})
GenServer.call(__MODULE__, {:update_task_activity, task_id, tool_name, arguments}, 30_000)
end
def create_task(title, description, opts \\ %{}) do
GenServer.call(__MODULE__, {:create_task, title, description, opts})
GenServer.call(__MODULE__, {:create_task, title, description, opts}, 30_000)
end
def get_next_task(agent_id) do
@@ -76,7 +76,7 @@ defmodule AgentCoordinator.TaskRegistry do
end
def complete_task(agent_id) do
GenServer.call(__MODULE__, {:complete_task, agent_id})
GenServer.call(__MODULE__, {:complete_task, agent_id}, 30_000)
end
def get_task_board do
@@ -284,6 +284,7 @@ defmodule AgentCoordinator.TaskRegistry do
case Map.get(state.agents, agent_id) do
nil ->
{:reply, {:error, :not_found}, state}
agent ->
{:reply, {:ok, agent}, state}
end
@@ -293,6 +294,7 @@ defmodule AgentCoordinator.TaskRegistry do
case Enum.find(state.agents, fn {_id, agent} -> agent.name == agent_name end) do
nil ->
{:reply, {:error, :not_found}, state}
{_id, agent} ->
{:reply, {:ok, agent}, state}
end
@@ -559,6 +561,7 @@ defmodule AgentCoordinator.TaskRegistry do
catch
:exit, _ -> 0
end
[] ->
# No inbox process exists, treat as 0 pending tasks
0

View File

@@ -37,7 +37,7 @@ defmodule AgentCoordinator.UnifiedMCPServer do
request_id_counter: 0
}
Logger.info("Unified MCP Server starting...")
IO.puts(:stderr, "Unified MCP Server starting...")
{:ok, state}
end

View File

@@ -33,7 +33,8 @@ defmodule AgentCoordinator.VSCodePermissions do
"vscode_set_selection" => :editor,
# Command Operations (varies by command)
"vscode_run_command" => :admin, # Default to admin, will check specific commands
# Default to admin, will check specific commands
"vscode_run_command" => :admin,
# User Communication
"vscode_show_message" => :read_only
@@ -88,6 +89,7 @@ defmodule AgentCoordinator.VSCodePermissions do
case additional_checks(tool_name, args, context) do
:ok ->
{:ok, required_level}
{:error, reason} ->
{:error, reason}
end
@@ -109,15 +111,18 @@ defmodule AgentCoordinator.VSCodePermissions do
case agent_id do
"github_copilot_session" -> :filesystem
id when is_binary(id) and byte_size(id) > 0 -> :editor # Other registered agents
_ -> :read_only # Unknown agents
# Other registered agents
id when is_binary(id) and byte_size(id) > 0 -> :editor
# Unknown agents
_ -> :read_only
end
end
@doc """
Update an agent's permission level (for administrative purposes).
"""
def set_agent_permission_level(agent_id, level) when level in [:read_only, :editor, :filesystem, :terminal, :git, :admin] do
def set_agent_permission_level(agent_id, level)
when level in [:read_only, :editor, :filesystem, :terminal, :git, :admin] do
# This would persist to a database or configuration store
Logger.info("Setting permission level for agent #{agent_id} to #{level}")
:ok
@@ -127,16 +132,24 @@ defmodule AgentCoordinator.VSCodePermissions do
defp get_required_permission(tool_name, args) do
case Map.get(@tool_permissions, tool_name) do
nil -> :admin # Unknown tools require admin by default
# Unknown tools require admin by default
nil ->
:admin
:admin when tool_name == "vscode_run_command" ->
# Special handling for run_command - check specific command
command = args["command"]
if command in @whitelisted_commands do
:editor # Whitelisted commands only need editor level
# Whitelisted commands only need editor level
:editor
else
:admin # Unknown commands need admin
# Unknown commands need admin
:admin
end
level -> level
level ->
level
end
end
@@ -165,11 +178,19 @@ defmodule AgentCoordinator.VSCodePermissions do
forbidden_patterns = [
# System directories
"/etc/", "/bin/", "/usr/", "/var/", "/tmp/",
"/etc/",
"/bin/",
"/usr/",
"/var/",
"/tmp/",
# User sensitive areas
"/.ssh/", "/.config/", "/home/", "~",
"/.ssh/",
"/.config/",
"/home/",
"~",
# Relative path traversal
"../", "..\\"
"../",
"..\\"
]
if Enum.any?(forbidden_patterns, fn pattern -> String.contains?(path, pattern) end) do
@@ -181,7 +202,7 @@ defmodule AgentCoordinator.VSCodePermissions do
defp check_workspace_bounds(_path, _context), do: {:error, "Invalid path format"}
defp check_command_safety(command, args) when is_binary(command) do
defp check_command_safety(command, _args) when is_binary(command) do
cond do
command in @whitelisted_commands ->
:ok
@@ -219,4 +240,4 @@ defmodule AgentCoordinator.VSCodePermissions do
whitelisted_commands: @whitelisted_commands
}
end
end
end

View File

@@ -18,7 +18,8 @@ defmodule AgentCoordinator.VSCodeToolProvider do
# File Operations
%{
"name" => "vscode_read_file",
"description" => "Read file contents using VS Code's file system API. Only works within workspace folders.",
"description" =>
"Read file contents using VS Code's file system API. Only works within workspace folders.",
"inputSchema" => %{
"type" => "object",
"properties" => %{
@@ -37,7 +38,8 @@ defmodule AgentCoordinator.VSCodeToolProvider do
},
%{
"name" => "vscode_write_file",
"description" => "Write content to a file using VS Code's file system API. Creates directories if needed.",
"description" =>
"Write content to a file using VS Code's file system API. Creates directories if needed.",
"inputSchema" => %{
"type" => "object",
"properties" => %{
@@ -93,7 +95,8 @@ defmodule AgentCoordinator.VSCodeToolProvider do
"properties" => %{
"path" => %{
"type" => "string",
"description" => "Relative or absolute path to the file/directory within the workspace"
"description" =>
"Relative or absolute path to the file/directory within the workspace"
},
"recursive" => %{
"type" => "boolean",
@@ -101,7 +104,8 @@ defmodule AgentCoordinator.VSCodeToolProvider do
},
"use_trash" => %{
"type" => "boolean",
"description" => "Whether to move to trash instead of permanent deletion (default: true)"
"description" =>
"Whether to move to trash instead of permanent deletion (default: true)"
}
},
"required" => ["path"]
@@ -227,7 +231,8 @@ defmodule AgentCoordinator.VSCodeToolProvider do
# Command Operations
%{
"name" => "vscode_run_command",
"description" => "Execute a VS Code command. Only whitelisted commands are allowed for security.",
"description" =>
"Execute a VS Code command. Only whitelisted commands are allowed for security.",
"inputSchema" => %{
"type" => "object",
"properties" => %{
@@ -282,21 +287,26 @@ defmodule AgentCoordinator.VSCodeToolProvider do
required = Map.get(input_schema, "required", [])
# Add agent_id to properties
updated_properties = Map.put(properties, "agent_id", %{
"type" => "string",
"description" => "Unique identifier for the agent making this request. Each agent session must use a consistent, unique ID throughout their interaction. Generate a UUID or use a descriptive identifier like 'agent_main_task_001'."
})
updated_properties =
Map.put(properties, "agent_id", %{
"type" => "string",
"description" =>
"Unique identifier for the agent making this request. Each agent session must use a consistent, unique ID throughout their interaction. Generate a UUID or use a descriptive identifier like 'agent_main_task_001'."
})
# Add agent_id to required fields
updated_required = if "agent_id" in required, do: required, else: ["agent_id" | required]
# Update the tool schema
updated_input_schema = input_schema
|> Map.put("properties", updated_properties)
|> Map.put("required", updated_required)
updated_input_schema =
input_schema
|> Map.put("properties", updated_properties)
|> Map.put("required", updated_required)
# Update tool description to mention agent_id requirement
updated_description = tool["description"] <> " IMPORTANT: Include a unique agent_id parameter to identify your agent session."
updated_description =
tool["description"] <>
" IMPORTANT: Include a unique agent_id parameter to identify your agent session."
tool
|> Map.put("inputSchema", updated_input_schema)
@@ -314,10 +324,13 @@ defmodule AgentCoordinator.VSCodeToolProvider do
if is_nil(agent_id) or agent_id == "" do
Logger.warning("Missing agent_id in VS Code tool call: #{tool_name}")
{:error, %{
"error" => "Missing agent_id",
"message" => "All VS Code tools require a unique agent_id parameter. Please include your agent session identifier."
}}
{:error,
%{
"error" => "Missing agent_id",
"message" =>
"All VS Code tools require a unique agent_id parameter. Please include your agent session identifier."
}}
else
# Ensure agent is registered and create enhanced context
enhanced_context = ensure_agent_registered(agent_id, context)
@@ -364,7 +377,11 @@ defmodule AgentCoordinator.VSCodeToolProvider do
case AgentCoordinator.TaskRegistry.register_agent(
"GitHub Copilot (#{agent_id})",
capabilities,
[metadata: %{agent_id: agent_id, auto_registered: true, session_start: DateTime.utc_now()}]
metadata: %{
agent_id: agent_id,
auto_registered: true,
session_start: DateTime.utc_now()
}
) do
{:ok, _result} ->
Logger.info("Successfully auto-registered agent: #{agent_id}")
@@ -372,10 +389,13 @@ defmodule AgentCoordinator.VSCodeToolProvider do
{:error, reason} ->
Logger.error("Failed to auto-register agent #{agent_id}: #{inspect(reason)}")
Map.put(context, :agent_id, agent_id) # Continue anyway
# Continue anyway
Map.put(context, :agent_id, agent_id)
end
end
end # Private function to execute individual tools
end
# Private function to execute individual tools
defp execute_tool(tool_name, args, context) do
case tool_name do
"vscode_read_file" -> read_file(args, context)
@@ -398,117 +418,129 @@ defmodule AgentCoordinator.VSCodeToolProvider do
defp read_file(args, _context) do
# For now, return a placeholder - we'll implement the actual VS Code API bridge
{:ok, %{
"content" => "// VS Code file content would be here",
"path" => args["path"],
"encoding" => args["encoding"] || "utf8",
"size" => 42,
"timestamp" => DateTime.utc_now() |> DateTime.to_iso8601()
}}
{:ok,
%{
"content" => "// VS Code file content would be here",
"path" => args["path"],
"encoding" => args["encoding"] || "utf8",
"size" => 42,
"timestamp" => DateTime.utc_now() |> DateTime.to_iso8601()
}}
end
defp write_file(args, _context) do
{:ok, %{
"path" => args["path"],
"bytes_written" => String.length(args["content"]),
"timestamp" => DateTime.utc_now() |> DateTime.to_iso8601()
}}
{:ok,
%{
"path" => args["path"],
"bytes_written" => String.length(args["content"]),
"timestamp" => DateTime.utc_now() |> DateTime.to_iso8601()
}}
end
defp create_file(args, _context) do
{:ok, %{
"path" => args["path"],
"created" => true,
"timestamp" => DateTime.utc_now() |> DateTime.to_iso8601()
}}
{:ok,
%{
"path" => args["path"],
"created" => true,
"timestamp" => DateTime.utc_now() |> DateTime.to_iso8601()
}}
end
defp delete_file(args, _context) do
{:ok, %{
"path" => args["path"],
"deleted" => true,
"timestamp" => DateTime.utc_now() |> DateTime.to_iso8601()
}}
{:ok,
%{
"path" => args["path"],
"deleted" => true,
"timestamp" => DateTime.utc_now() |> DateTime.to_iso8601()
}}
end
defp list_directory(args, _context) do
{:ok, %{
"path" => args["path"],
"entries" => [
%{"name" => "file1.txt", "type" => "file", "size" => 123},
%{"name" => "subdir", "type" => "directory", "size" => nil}
]
}}
{:ok,
%{
"path" => args["path"],
"entries" => [
%{"name" => "file1.txt", "type" => "file", "size" => 123},
%{"name" => "subdir", "type" => "directory", "size" => nil}
]
}}
end
defp get_workspace_folders(_args, _context) do
{:ok, %{
"folders" => [
%{"name" => "agent_coordinator", "uri" => "file:///home/ra/agent_coordinator"}
]
}}
{:ok,
%{
"folders" => [
%{"name" => "agent_coordinator", "uri" => "file:///home/ra/agent_coordinator"}
]
}}
end
defp get_active_editor(args, _context) do
{:ok, %{
"file_path" => "/home/ra/agent_coordinator/lib/agent_coordinator.ex",
"language" => "elixir",
"line_count" => 150,
"content" => if(args["include_content"], do: "// Editor content here", else: nil),
"selection" => %{
"start" => %{"line" => 10, "character" => 5},
"end" => %{"line" => 10, "character" => 15}
},
"cursor_position" => %{"line" => 10, "character" => 15}
}}
{:ok,
%{
"file_path" => "/home/ra/agent_coordinator/lib/agent_coordinator.ex",
"language" => "elixir",
"line_count" => 150,
"content" => if(args["include_content"], do: "// Editor content here", else: nil),
"selection" => %{
"start" => %{"line" => 10, "character" => 5},
"end" => %{"line" => 10, "character" => 15}
},
"cursor_position" => %{"line" => 10, "character" => 15}
}}
end
defp set_editor_content(args, _context) do
{:ok, %{
"file_path" => args["file_path"],
"content_length" => String.length(args["content"]),
"timestamp" => DateTime.utc_now() |> DateTime.to_iso8601()
}}
{:ok,
%{
"file_path" => args["file_path"],
"content_length" => String.length(args["content"]),
"timestamp" => DateTime.utc_now() |> DateTime.to_iso8601()
}}
end
defp get_selection(args, _context) do
{:ok, %{
"selection" => %{
"start" => %{"line" => 5, "character" => 0},
"end" => %{"line" => 8, "character" => 20}
},
"content" => if(args["include_content"], do: "Selected text here", else: nil),
"is_empty" => false
}}
{:ok,
%{
"selection" => %{
"start" => %{"line" => 5, "character" => 0},
"end" => %{"line" => 8, "character" => 20}
},
"content" => if(args["include_content"], do: "Selected text here", else: nil),
"is_empty" => false
}}
end
defp set_selection(args, _context) do
{:ok, %{
"selection" => %{
"start" => %{"line" => args["start_line"], "character" => args["start_character"]},
"end" => %{"line" => args["end_line"], "character" => args["end_character"]}
},
"revealed" => args["reveal"] != false
}}
{:ok,
%{
"selection" => %{
"start" => %{"line" => args["start_line"], "character" => args["start_character"]},
"end" => %{"line" => args["end_line"], "character" => args["end_character"]}
},
"revealed" => args["reveal"] != false
}}
end
defp run_command(args, _context) do
# This would execute actual VS Code commands
{:ok, %{
"command" => args["command"],
"args" => args["args"] || [],
"result" => "Command executed successfully",
"timestamp" => DateTime.utc_now() |> DateTime.to_iso8601()
}}
{:ok,
%{
"command" => args["command"],
"args" => args["args"] || [],
"result" => "Command executed successfully",
"timestamp" => DateTime.utc_now() |> DateTime.to_iso8601()
}}
end
defp show_message(args, _context) do
{:ok, %{
"message" => args["message"],
"type" => args["type"] || "info",
"displayed" => true,
"timestamp" => DateTime.utc_now() |> DateTime.to_iso8601()
}}
{:ok,
%{
"message" => args["message"],
"type" => args["type"] || "info",
"displayed" => true,
"timestamp" => DateTime.utc_now() |> DateTime.to_iso8601()
}}
end
# Logging function
@@ -521,4 +553,4 @@ defmodule AgentCoordinator.VSCodeToolProvider do
timestamp: DateTime.utc_now()
})
end
end
end