Remove duplicate MCP server files - consolidation complete
- Deleted mcp_server_manager.ex, enhanced_mcp_server.ex, unified_mcp_server.ex - All functionality successfully consolidated into single mcp_server.ex - Server starts correctly with all external servers (context7, filesystem, memory, sequentialthinking) - HTTP server support working for mcp_figma - All 15+ agent coordination tools properly registered - Codebase is now clean with no duplicate files
This commit is contained in:
@@ -1,266 +0,0 @@
|
||||
defmodule AgentCoordinator.EnhancedMCPServer do
|
||||
@moduledoc """
|
||||
Enhanced MCP server with automatic heartbeat management and collision detection.
|
||||
|
||||
This module extends the base MCP server with:
|
||||
1. Automatic heartbeats on every operation
|
||||
2. Agent session tracking
|
||||
3. Enhanced collision detection
|
||||
4. Automatic agent cleanup on disconnect
|
||||
"""
|
||||
|
||||
use GenServer
|
||||
alias AgentCoordinator.{MCPServer, AutoHeartbeat, TaskRegistry}
|
||||
|
||||
# Track active agent sessions
|
||||
defstruct [
|
||||
:agent_sessions,
|
||||
:session_monitors
|
||||
]
|
||||
|
||||
# Client API
|
||||
|
||||
def start_link(opts \\ []) do
|
||||
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Enhanced MCP request handler with automatic heartbeat management
|
||||
"""
|
||||
def handle_enhanced_mcp_request(request, session_info \\ %{}) do
|
||||
GenServer.call(__MODULE__, {:enhanced_mcp_request, request, session_info})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Register an agent with enhanced session tracking
|
||||
"""
|
||||
def register_agent_with_session(name, capabilities, session_pid \\ self()) do
|
||||
GenServer.call(__MODULE__, {:register_agent_with_session, name, capabilities, session_pid})
|
||||
end
|
||||
|
||||
# Server callbacks
|
||||
|
||||
def init(_opts) do
|
||||
state = %__MODULE__{
|
||||
agent_sessions: %{},
|
||||
session_monitors: %{}
|
||||
}
|
||||
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
def handle_call({:enhanced_mcp_request, request, session_info}, {from_pid, _}, state) do
|
||||
# Extract agent_id from session or request
|
||||
agent_id = extract_agent_id(request, session_info, state)
|
||||
|
||||
# If we have an agent_id, send heartbeat before and after operation
|
||||
enhanced_result =
|
||||
case agent_id do
|
||||
nil ->
|
||||
# No agent context, use normal MCP processing
|
||||
MCPServer.handle_mcp_request(request)
|
||||
|
||||
id ->
|
||||
# Send pre-operation heartbeat
|
||||
pre_heartbeat = TaskRegistry.heartbeat_agent(id)
|
||||
|
||||
# Process the request
|
||||
result = MCPServer.handle_mcp_request(request)
|
||||
|
||||
# Send post-operation heartbeat and update session activity
|
||||
post_heartbeat = TaskRegistry.heartbeat_agent(id)
|
||||
update_session_activity(state, id, from_pid)
|
||||
|
||||
# Add heartbeat metadata to successful responses
|
||||
case result do
|
||||
%{"result" => _} = success ->
|
||||
Map.put(success, "_heartbeat_metadata", %{
|
||||
agent_id: id,
|
||||
pre_heartbeat: pre_heartbeat,
|
||||
post_heartbeat: post_heartbeat,
|
||||
timestamp: DateTime.utc_now()
|
||||
})
|
||||
|
||||
error_result ->
|
||||
error_result
|
||||
end
|
||||
end
|
||||
|
||||
{:reply, enhanced_result, state}
|
||||
end
|
||||
|
||||
def handle_call({:register_agent_with_session, name, capabilities, session_pid}, _from, state) 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)
|
||||
|
||||
# Register the agent normally first
|
||||
case MCPServer.handle_mcp_request(%{
|
||||
"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}} ->
|
||||
# Track the session
|
||||
monitor_ref = Process.monitor(session_pid)
|
||||
|
||||
new_state = %{
|
||||
state
|
||||
| agent_sessions:
|
||||
Map.put(state.agent_sessions, agent_id, %{
|
||||
pid: session_pid,
|
||||
name: name,
|
||||
capabilities: capabilities,
|
||||
registered_at: DateTime.utc_now(),
|
||||
last_activity: DateTime.utc_now()
|
||||
}),
|
||||
session_monitors: Map.put(state.session_monitors, monitor_ref, agent_id)
|
||||
}
|
||||
|
||||
# Start automatic heartbeat management
|
||||
AutoHeartbeat.start_link([])
|
||||
|
||||
AutoHeartbeat.register_agent_with_heartbeat(name, capabilities, %{
|
||||
session_pid: session_pid,
|
||||
enhanced_server: true
|
||||
})
|
||||
|
||||
{:reply, {:ok, agent_id}, new_state}
|
||||
|
||||
{:error, reason} ->
|
||||
{:reply, {:error, reason}, state}
|
||||
end
|
||||
|
||||
%{"error" => %{"message" => message}} ->
|
||||
{:reply, {:error, message}, state}
|
||||
|
||||
_ ->
|
||||
{:reply, {:error, "Unexpected response format"}, state}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_call(:get_enhanced_task_board, _from, state) do
|
||||
# Get the regular task board
|
||||
case MCPServer.handle_mcp_request(%{
|
||||
"method" => "tools/call",
|
||||
"params" => %{"name" => "get_task_board", "arguments" => %{}}
|
||||
}) do
|
||||
%{"result" => %{"content" => [%{"text" => response_json}]}} ->
|
||||
case Jason.decode(response_json) do
|
||||
{:ok, %{"agents" => agents}} ->
|
||||
# Enhance with session information
|
||||
enhanced_agents =
|
||||
Enum.map(agents, fn agent ->
|
||||
agent_id = agent["agent_id"]
|
||||
session_info = Map.get(state.agent_sessions, agent_id, %{})
|
||||
|
||||
Map.merge(agent, %{
|
||||
"session_active" => Map.has_key?(state.agent_sessions, agent_id),
|
||||
"last_activity" => Map.get(session_info, :last_activity),
|
||||
"session_duration" => calculate_session_duration(session_info)
|
||||
})
|
||||
end)
|
||||
|
||||
result = %{
|
||||
"agents" => enhanced_agents,
|
||||
"active_sessions" => map_size(state.agent_sessions)
|
||||
}
|
||||
|
||||
{:reply, {:ok, result}, state}
|
||||
|
||||
{:error, reason} ->
|
||||
{:reply, {:error, reason}, state}
|
||||
end
|
||||
|
||||
%{"error" => %{"message" => message}} ->
|
||||
{:reply, {:error, message}, state}
|
||||
end
|
||||
end
|
||||
|
||||
# Handle process monitoring - cleanup when agent session dies
|
||||
def handle_info({:DOWN, monitor_ref, :process, _pid, _reason}, state) do
|
||||
case Map.get(state.session_monitors, monitor_ref) do
|
||||
nil ->
|
||||
{:noreply, state}
|
||||
|
||||
agent_id ->
|
||||
# Clean up the agent session
|
||||
new_state = %{
|
||||
state
|
||||
| agent_sessions: Map.delete(state.agent_sessions, agent_id),
|
||||
session_monitors: Map.delete(state.session_monitors, monitor_ref)
|
||||
}
|
||||
|
||||
# Stop heartbeat management
|
||||
AutoHeartbeat.stop_heartbeat(agent_id)
|
||||
|
||||
# Mark agent as offline in registry
|
||||
# (This could be enhanced to gracefully handle ongoing tasks)
|
||||
|
||||
{:noreply, new_state}
|
||||
end
|
||||
end
|
||||
|
||||
# Private helpers
|
||||
|
||||
defp extract_agent_id(request, session_info, state) do
|
||||
# Try to get agent_id from various sources
|
||||
cond do
|
||||
# From request arguments
|
||||
Map.get(request, "params", %{})
|
||||
|> Map.get("arguments", %{})
|
||||
|> Map.get("agent_id") ->
|
||||
request["params"]["arguments"]["agent_id"]
|
||||
|
||||
# From session info
|
||||
Map.get(session_info, :agent_id) ->
|
||||
session_info.agent_id
|
||||
|
||||
# From session lookup by PID
|
||||
session_pid = Map.get(session_info, :session_pid, self()) ->
|
||||
find_agent_by_session_pid(state, session_pid)
|
||||
|
||||
true ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defp find_agent_by_session_pid(state, session_pid) do
|
||||
Enum.find_value(state.agent_sessions, fn {agent_id, session_data} ->
|
||||
if session_data.pid == session_pid, do: agent_id, else: nil
|
||||
end)
|
||||
end
|
||||
|
||||
defp update_session_activity(state, agent_id, _session_pid) do
|
||||
case Map.get(state.agent_sessions, agent_id) do
|
||||
nil ->
|
||||
:ok
|
||||
|
||||
session_data ->
|
||||
_updated_session = %{session_data | last_activity: DateTime.utc_now()}
|
||||
# Note: This doesn't update the state since we're in a call handler
|
||||
# In a real implementation, you might want to use cast for this
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get enhanced task board with session information
|
||||
"""
|
||||
def get_enhanced_task_board do
|
||||
GenServer.call(__MODULE__, :get_enhanced_task_board)
|
||||
end
|
||||
|
||||
defp calculate_session_duration(%{registered_at: start_time}) do
|
||||
DateTime.diff(DateTime.utc_now(), start_time, :second)
|
||||
end
|
||||
|
||||
defp calculate_session_duration(_), do: nil
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,251 +0,0 @@
|
||||
defmodule AgentCoordinator.UnifiedMCPServer do
|
||||
@moduledoc """
|
||||
Unified MCP Server that aggregates all external MCP servers and Agent Coordinator tools.
|
||||
|
||||
This is the single MCP server that GitHub Copilot sees, which internally manages
|
||||
all other MCP servers and provides automatic task tracking for any tool usage.
|
||||
"""
|
||||
|
||||
use GenServer
|
||||
require Logger
|
||||
|
||||
alias AgentCoordinator.{MCPServerManager, TaskRegistry}
|
||||
|
||||
defstruct [
|
||||
:agent_sessions,
|
||||
:request_id_counter
|
||||
]
|
||||
|
||||
# Client API
|
||||
|
||||
def start_link(opts \\ []) do
|
||||
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Handle MCP request from GitHub Copilot
|
||||
"""
|
||||
def handle_mcp_request(request) do
|
||||
GenServer.call(__MODULE__, {:handle_request, request}, 60_000)
|
||||
end
|
||||
|
||||
# Server callbacks
|
||||
|
||||
def init(_opts) do
|
||||
state = %__MODULE__{
|
||||
agent_sessions: %{},
|
||||
request_id_counter: 0
|
||||
}
|
||||
|
||||
IO.puts(:stderr, "Unified MCP Server starting...")
|
||||
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
def handle_call({:handle_request, request}, _from, state) do
|
||||
response = process_mcp_request(request, state)
|
||||
{:reply, response, state}
|
||||
end
|
||||
|
||||
def handle_call({:register_agent_session, agent_id, session_info}, _from, state) do
|
||||
new_state = %{state | agent_sessions: Map.put(state.agent_sessions, agent_id, session_info)}
|
||||
{:reply, :ok, new_state}
|
||||
end
|
||||
|
||||
def handle_info(_msg, state) do
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
# Private functions
|
||||
|
||||
defp process_mcp_request(request, state) do
|
||||
method = Map.get(request, "method")
|
||||
id = Map.get(request, "id")
|
||||
|
||||
case method do
|
||||
"initialize" ->
|
||||
handle_initialize(request, id)
|
||||
|
||||
"tools/list" ->
|
||||
handle_tools_list(request, id)
|
||||
|
||||
"tools/call" ->
|
||||
handle_tools_call(request, id, state)
|
||||
|
||||
_ ->
|
||||
error_response(id, -32601, "Method not found: #{method}")
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_initialize(_request, id) do
|
||||
%{
|
||||
"jsonrpc" => "2.0",
|
||||
"id" => id,
|
||||
"result" => %{
|
||||
"protocolVersion" => "2024-11-05",
|
||||
"capabilities" => %{
|
||||
"tools" => %{},
|
||||
"coordination" => %{
|
||||
"automatic_task_tracking" => true,
|
||||
"agent_management" => true,
|
||||
"multi_server_proxy" => true,
|
||||
"heartbeat_coverage" => true
|
||||
}
|
||||
},
|
||||
"serverInfo" => %{
|
||||
"name" => "agent-coordinator-unified",
|
||||
"version" => "0.1.0",
|
||||
"description" =>
|
||||
"Unified MCP server with automatic task tracking and agent coordination"
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp handle_tools_list(_request, id) do
|
||||
case MCPServerManager.get_unified_tools() do
|
||||
tools when is_list(tools) ->
|
||||
%{
|
||||
"jsonrpc" => "2.0",
|
||||
"id" => id,
|
||||
"result" => %{
|
||||
"tools" => tools
|
||||
}
|
||||
}
|
||||
|
||||
{:error, reason} ->
|
||||
error_response(id, -32603, "Failed to get tools: #{reason}")
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_tools_call(request, id, state) do
|
||||
params = Map.get(request, "params", %{})
|
||||
tool_name = Map.get(params, "name")
|
||||
arguments = Map.get(params, "arguments", %{})
|
||||
|
||||
# Determine agent context from the request or session
|
||||
agent_context = determine_agent_context(request, arguments, state)
|
||||
|
||||
case MCPServerManager.route_tool_call(tool_name, arguments, agent_context) do
|
||||
%{"error" => _} = error_result ->
|
||||
Map.put(error_result, "id", id)
|
||||
|
||||
result ->
|
||||
# Wrap successful results in MCP format
|
||||
success_response = %{
|
||||
"jsonrpc" => "2.0",
|
||||
"id" => id,
|
||||
"result" => format_tool_result(result, tool_name, agent_context)
|
||||
}
|
||||
|
||||
success_response
|
||||
end
|
||||
end
|
||||
|
||||
defp determine_agent_context(request, arguments, state) do
|
||||
# Try to determine agent from various sources:
|
||||
|
||||
# 1. Explicit agent_id in arguments
|
||||
case Map.get(arguments, "agent_id") do
|
||||
agent_id when is_binary(agent_id) ->
|
||||
%{agent_id: agent_id}
|
||||
|
||||
_ ->
|
||||
# 2. Try to extract from request metadata
|
||||
case extract_agent_from_request(request) do
|
||||
agent_id when is_binary(agent_id) ->
|
||||
%{agent_id: agent_id}
|
||||
|
||||
_ ->
|
||||
# 3. Use a default session for GitHub Copilot
|
||||
default_agent_context(state)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp extract_agent_from_request(_request) do
|
||||
# Look for agent info in request headers, params, etc.
|
||||
# This could be extended to support various ways of identifying the agent
|
||||
nil
|
||||
end
|
||||
|
||||
defp default_agent_context(state) do
|
||||
# Create or use a default agent session for GitHub Copilot
|
||||
default_agent_id = "github_copilot_session"
|
||||
|
||||
case Map.get(state.agent_sessions, default_agent_id) do
|
||||
nil ->
|
||||
# Auto-register GitHub Copilot as an agent
|
||||
case TaskRegistry.register_agent("GitHub Copilot", [
|
||||
"coding",
|
||||
"analysis",
|
||||
"review",
|
||||
"documentation"
|
||||
]) do
|
||||
{:ok, %{agent_id: agent_id}} ->
|
||||
session_info = %{
|
||||
agent_id: agent_id,
|
||||
name: "GitHub Copilot",
|
||||
auto_registered: true,
|
||||
created_at: DateTime.utc_now()
|
||||
}
|
||||
|
||||
GenServer.call(self(), {:register_agent_session, agent_id, session_info})
|
||||
%{agent_id: agent_id}
|
||||
|
||||
_ ->
|
||||
%{agent_id: default_agent_id}
|
||||
end
|
||||
|
||||
session_info ->
|
||||
%{agent_id: session_info.agent_id}
|
||||
end
|
||||
end
|
||||
|
||||
defp format_tool_result(result, tool_name, agent_context) do
|
||||
# Format the result according to MCP tool call response format
|
||||
base_result =
|
||||
case result do
|
||||
%{"result" => content} when is_map(content) ->
|
||||
# Already properly formatted
|
||||
content
|
||||
|
||||
{:ok, content} ->
|
||||
# Convert tuple response to content
|
||||
%{"content" => [%{"type" => "text", "text" => inspect(content)}]}
|
||||
|
||||
%{} = map_result ->
|
||||
# Convert map to text content
|
||||
%{"content" => [%{"type" => "text", "text" => Jason.encode!(map_result)}]}
|
||||
|
||||
binary when is_binary(binary) ->
|
||||
# Simple text result
|
||||
%{"content" => [%{"type" => "text", "text" => binary}]}
|
||||
|
||||
other ->
|
||||
# Fallback for any other type
|
||||
%{"content" => [%{"type" => "text", "text" => inspect(other)}]}
|
||||
end
|
||||
|
||||
# Add metadata about the operation
|
||||
metadata = %{
|
||||
"tool_name" => tool_name,
|
||||
"agent_id" => agent_context.agent_id,
|
||||
"timestamp" => DateTime.utc_now() |> DateTime.to_iso8601(),
|
||||
"auto_tracked" => true
|
||||
}
|
||||
|
||||
Map.put(base_result, "_metadata", metadata)
|
||||
end
|
||||
|
||||
defp error_response(id, code, message) do
|
||||
%{
|
||||
"jsonrpc" => "2.0",
|
||||
"id" => id,
|
||||
"error" => %{
|
||||
"code" => code,
|
||||
"message" => message
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user