Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,15 @@ from mcpd import McpdClient
# Assumes the mcpd daemon is running
client = McpdClient(api_endpoint="http://localhost:8090")

# Get all tools from all servers
all_tools = client.agent_tools()

# Get tools from specific servers only
time_tools = client.agent_tools(servers=['time'])

# Get tools from multiple servers
subset_tools = client.agent_tools(servers=['time', 'fetch'])

agent_config = AgentConfig(
tools=client.agent_tools(),
model_id="gpt-4.1-nano", # Requires OPENAI_API_KEY to be set
Expand Down
23 changes: 21 additions & 2 deletions src/mcpd/mcpd_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ def _get_tool_definitions(self, server_name: str) -> list[dict[str, Any]]:
except requests.exceptions.RequestException as e:
raise McpdError(f"Error listing tool definitions for server '{server_name}': {e}") from e

def agent_tools(self) -> list[Callable[..., Any]]:
def agent_tools(self, servers: list[str] | None = None) -> list[Callable[..., Any]]:
"""Generate callable Python functions for all available tools, suitable for AI agents.

This method queries all servers via `tools()` and creates self-contained,
Expand All @@ -378,6 +378,12 @@ def agent_tools(self) -> list[Callable[..., Any]]:
The generated functions are cached for performance. Use clear_agent_tools_cache()
to force regeneration if servers or tools have changed.

Args:
servers: Optional list of server names to filter by.
If None, returns tools from all servers.
If specified, only tools from the listed servers are included.
Non-existent server names are silently ignored.

Returns:
A list of callable functions, one for each tool across all servers.
Each function has the following attributes:
Expand All @@ -404,6 +410,10 @@ def agent_tools(self) -> list[Callable[..., Any]]:
>>> tools = client.agent_tools()
>>> print(f"Generated {len(tools)} callable tools")
>>>
>>> # Get tools from specific servers only
>>> time_tools = client.agent_tools(servers=['time'])
>>> subset_tools = client.agent_tools(servers=['time', 'fetch'])
>>>
>>> # Use with an AI agent framework
>>> agent_config = AgentConfig(
... tools=tools,
Expand All @@ -423,7 +433,16 @@ def agent_tools(self) -> list[Callable[..., Any]]:
agent_tools = []
all_tools = self.tools()

for server_name, tool_schemas in all_tools.items():
# Determine which servers to use.
servers_to_use = all_tools.keys() if servers is None else servers

# Fetch tools from selected servers.
for server_name in servers_to_use:
if server_name not in all_tools:
# Server doesn't exist or has no tools - skip silently.
continue

tool_schemas = all_tools[server_name]
for tool_schema in tool_schemas:
func = self._function_builder.create_function_from_schema(tool_schema, server_name)
agent_tools.append(func)
Expand Down
81 changes: 81 additions & 0 deletions tests/unit/test_mcpd_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,87 @@ def test_agent_tools(self, mock_tools, client):
assert result == [mock_func1, mock_func2]
assert mock_create.call_count == 2

@patch.object(McpdClient, "tools")
def test_agent_tools_filter_by_single_server(self, mock_tools, client):
"""Test filtering tools by a single server name."""
mock_tools.return_value = {
"server1": [{"name": "tool1", "description": "Test tool"}],
"server2": [{"name": "tool2", "description": "Another tool"}],
}

with patch.object(client._function_builder, "create_function_from_schema") as mock_create:
mock_func1 = Mock()
mock_create.return_value = mock_func1

result = client.agent_tools(servers=["server1"])

assert result == [mock_func1]
assert mock_create.call_count == 1
mock_create.assert_called_once_with({"name": "tool1", "description": "Test tool"}, "server1")

@patch.object(McpdClient, "tools")
def test_agent_tools_filter_by_multiple_servers(self, mock_tools, client):
"""Test filtering tools by multiple server names."""
mock_tools.return_value = {
"server1": [{"name": "tool1", "description": "Test tool"}],
"server2": [{"name": "tool2", "description": "Another tool"}],
"server3": [{"name": "tool3", "description": "Third tool"}],
}

with patch.object(client._function_builder, "create_function_from_schema") as mock_create:
mock_func1 = Mock()
mock_func2 = Mock()
mock_create.side_effect = [mock_func1, mock_func2]

result = client.agent_tools(servers=["server1", "server2"])

assert result == [mock_func1, mock_func2]
assert mock_create.call_count == 2

@patch.object(McpdClient, "tools")
def test_agent_tools_with_nonexistent_server(self, mock_tools, client):
"""Test filtering with server that doesn't exist."""
mock_tools.return_value = {
"server1": [{"name": "tool1", "description": "Test tool"}],
}

with patch.object(client._function_builder, "create_function_from_schema") as mock_create:
result = client.agent_tools(servers=["nonexistent"])

assert result == []
assert mock_create.call_count == 0

@patch.object(McpdClient, "tools")
def test_agent_tools_with_empty_servers_list(self, mock_tools, client):
"""Test filtering with empty server list."""
mock_tools.return_value = {
"server1": [{"name": "tool1", "description": "Test tool"}],
}

with patch.object(client._function_builder, "create_function_from_schema") as mock_create:
result = client.agent_tools(servers=[])

assert result == []
assert mock_create.call_count == 0

@patch.object(McpdClient, "tools")
def test_agent_tools_without_servers_parameter(self, mock_tools, client):
"""Test existing behavior - returns all tools when servers parameter not provided."""
mock_tools.return_value = {
"server1": [{"name": "tool1", "description": "Test tool"}],
"server2": [{"name": "tool2", "description": "Another tool"}],
}

with patch.object(client._function_builder, "create_function_from_schema") as mock_create:
mock_func1 = Mock()
mock_func2 = Mock()
mock_create.side_effect = [mock_func1, mock_func2]

result = client.agent_tools()

assert result == [mock_func1, mock_func2]
assert mock_create.call_count == 2

@patch.object(McpdClient, "tools")
def test_has_tool_exists(self, mock_tools, client):
mock_tools.return_value = [{"name": "existing_tool"}, {"name": "another_tool"}]
Expand Down