feat: implement dynamic MCP tool discovery with shared server architecture
- Replace hardcoded tool lists with dynamic discovery via MCP tools/list - Move MCPServerManager to application supervision tree for resource sharing - Eliminate duplicate MCP server instances (one shared instance per server type) - Add automatic tool refresh when servers restart - Implement conditional VS Code tool loading based on module availability - Add comprehensive test suite for dynamic discovery - Update documentation with architecture improvements Benefits: - Full MCP protocol compliance - Massive resource savings (shared servers vs per-agent instances) - Zero maintenance overhead for tool list synchronization - Automatic adaptation to server changes - Improved reliability and performance Closes: Dynamic tool discovery implementation Fixes: Multiple MCP server instance resource waste
This commit is contained in:
85
test/agent_metadata_test.exs
Normal file
85
test/agent_metadata_test.exs
Normal file
@@ -0,0 +1,85 @@
|
||||
defmodule AgentCoordinator.MetadataTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
describe "Agent registration with metadata" do
|
||||
test "register agent with metadata through TaskRegistry" do
|
||||
# Use the existing TaskRegistry (started by application)
|
||||
|
||||
# Test metadata structure
|
||||
metadata = %{
|
||||
client_type: "github_copilot",
|
||||
session_id: "test_session_123",
|
||||
vs_code_version: "1.85.0",
|
||||
auto_registered: true
|
||||
}
|
||||
|
||||
agent_name = "MetadataTestAgent_#{:rand.uniform(1000)}"
|
||||
|
||||
# Register agent with metadata
|
||||
result = AgentCoordinator.TaskRegistry.register_agent(
|
||||
agent_name,
|
||||
["coding", "testing", "vscode_integration"],
|
||||
[metadata: metadata]
|
||||
)
|
||||
|
||||
assert :ok = result
|
||||
|
||||
# Retrieve agent and verify metadata
|
||||
{:ok, agent} = AgentCoordinator.TaskRegistry.get_agent_by_name(agent_name)
|
||||
|
||||
assert agent.metadata[:client_type] == "github_copilot"
|
||||
assert agent.metadata[:session_id] == "test_session_123"
|
||||
assert agent.metadata[:vs_code_version] == "1.85.0"
|
||||
assert agent.metadata[:auto_registered] == true
|
||||
|
||||
# Verify capabilities are preserved
|
||||
assert "coding" in agent.capabilities
|
||||
assert "testing" in agent.capabilities
|
||||
assert "vscode_integration" in agent.capabilities
|
||||
end
|
||||
|
||||
test "register agent without metadata (legacy compatibility)" do
|
||||
# Use the existing TaskRegistry (started by application)
|
||||
|
||||
agent_name = "LegacyTestAgent_#{:rand.uniform(1000)}"
|
||||
|
||||
# Register agent without metadata (old way)
|
||||
result = AgentCoordinator.TaskRegistry.register_agent(
|
||||
agent_name,
|
||||
["coding", "testing"]
|
||||
)
|
||||
|
||||
assert :ok = result
|
||||
|
||||
# Retrieve agent and verify empty metadata
|
||||
{:ok, agent} = AgentCoordinator.TaskRegistry.get_agent_by_name(agent_name)
|
||||
|
||||
assert agent.metadata == %{}
|
||||
assert "coding" in agent.capabilities
|
||||
assert "testing" in agent.capabilities
|
||||
end
|
||||
|
||||
test "Agent.new creates proper metadata structure" do
|
||||
# Test metadata handling in Agent.new
|
||||
metadata = %{
|
||||
test_key: "test_value",
|
||||
number: 42,
|
||||
boolean: true
|
||||
}
|
||||
|
||||
agent = AgentCoordinator.Agent.new(
|
||||
"TestAgent",
|
||||
["capability1"],
|
||||
[metadata: metadata]
|
||||
)
|
||||
|
||||
assert agent.metadata[:test_key] == "test_value"
|
||||
assert agent.metadata[:number] == 42
|
||||
assert agent.metadata[:boolean] == true
|
||||
|
||||
# Test default empty metadata
|
||||
agent_no_metadata = AgentCoordinator.Agent.new("NoMetadataAgent", ["capability1"])
|
||||
assert agent_no_metadata.metadata == %{}
|
||||
end
|
||||
end
|
||||
end
|
||||
87
test/dynamic_tool_discovery_test.exs
Normal file
87
test/dynamic_tool_discovery_test.exs
Normal file
@@ -0,0 +1,87 @@
|
||||
defmodule AgentCoordinator.DynamicToolDiscoveryTest do
|
||||
use ExUnit.Case, async: false # Changed to false since we're using shared resources
|
||||
|
||||
describe "Dynamic tool discovery" do
|
||||
test "tools are discovered from external MCP servers via tools/list" do
|
||||
# Use the shared MCP server manager (started by application supervision tree)
|
||||
|
||||
# Get initial tools - should include coordinator tools and external servers
|
||||
initial_tools = AgentCoordinator.MCPServerManager.get_unified_tools()
|
||||
|
||||
# Should have at least the coordinator native tools
|
||||
coordinator_tool_names = ["register_agent", "create_task", "get_next_task", "complete_task", "get_task_board", "heartbeat"]
|
||||
|
||||
Enum.each(coordinator_tool_names, fn tool_name ->
|
||||
assert Enum.any?(initial_tools, fn tool -> tool["name"] == tool_name end),
|
||||
"Coordinator tool #{tool_name} should be available"
|
||||
end)
|
||||
|
||||
# Verify VS Code tools are conditionally included
|
||||
vscode_tools = Enum.filter(initial_tools, fn tool ->
|
||||
String.starts_with?(tool["name"], "vscode_")
|
||||
end)
|
||||
|
||||
# Should have VS Code tools if the module is available
|
||||
if Code.ensure_loaded?(AgentCoordinator.VSCodeToolProvider) do
|
||||
assert length(vscode_tools) > 0, "VS Code tools should be available when module is loaded"
|
||||
else
|
||||
assert length(vscode_tools) == 0, "VS Code tools should not be available when module is not loaded"
|
||||
end
|
||||
|
||||
# Test tool refresh functionality
|
||||
{:ok, tool_count} = AgentCoordinator.MCPServerManager.refresh_tools()
|
||||
assert is_integer(tool_count) and tool_count >= length(coordinator_tool_names)
|
||||
|
||||
# No cleanup needed - using shared instance
|
||||
end
|
||||
|
||||
test "tool routing works with dynamic discovery" do
|
||||
# Use the shared MCP server manager
|
||||
|
||||
# Test routing for coordinator tools
|
||||
result = AgentCoordinator.MCPServerManager.route_tool_call(
|
||||
"register_agent",
|
||||
%{"name" => "TestAgent", "capabilities" => ["testing"]},
|
||||
%{agent_id: "test_#{:rand.uniform(1000)}"}
|
||||
)
|
||||
|
||||
# Should succeed (returns :ok for register_agent)
|
||||
assert result == :ok or (is_map(result) and not Map.has_key?(result, "error"))
|
||||
|
||||
# Test routing for non-existent tool
|
||||
error_result = AgentCoordinator.MCPServerManager.route_tool_call(
|
||||
"nonexistent_tool",
|
||||
%{},
|
||||
%{agent_id: "test"}
|
||||
)
|
||||
|
||||
assert error_result["error"]["code"] == -32601
|
||||
assert String.contains?(error_result["error"]["message"], "Tool not found")
|
||||
|
||||
# No cleanup needed - using shared instance
|
||||
end
|
||||
|
||||
test "external server tools are discovered via MCP protocol" do
|
||||
# Use the shared MCP server manager
|
||||
|
||||
# Verify the rediscovery function exists and can be called
|
||||
tools = AgentCoordinator.MCPServerManager.get_unified_tools()
|
||||
{:ok, tool_count} = AgentCoordinator.MCPServerManager.refresh_tools()
|
||||
|
||||
assert is_integer(tool_count)
|
||||
assert tool_count >= 0
|
||||
|
||||
# Verify we have external tools (context7, filesystem, etc.)
|
||||
external_tools = Enum.filter(tools, fn tool ->
|
||||
name = tool["name"]
|
||||
not String.starts_with?(name, "vscode_") and
|
||||
name not in ["register_agent", "create_task", "get_next_task", "complete_task", "get_task_board", "heartbeat"]
|
||||
end)
|
||||
|
||||
# Should have some external tools from the configured MCP servers
|
||||
assert length(external_tools) > 0, "Should have external MCP server tools available"
|
||||
|
||||
# No cleanup needed - using shared instance
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user