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
31 changes: 27 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,17 @@ time_tools = client.agent_tools(servers=['time'])
# Get tools from multiple servers, only if healthy
subset_tools = client.agent_tools(servers=['time', 'fetch'])

# Get tools from all servers regardless of health (not recommended)
all_tools_unfiltered = client.agent_tools(check_health=False)
# Filter by tool names (cross-cutting)
math_tools = client.agent_tools(tools=['add', 'multiply'])

# Filter by qualified tool names
specific = client.agent_tools(tools=['time__get_current_time'])

# Combine server and tool filtering
filtered = client.agent_tools(
servers=['time', 'math'],
tools=['add', 'get_current_time']
)

agent_config = AgentConfig(
tools=client.agent_tools(),
Expand All @@ -113,6 +122,20 @@ response = agent.run("What is the current time in Tokyo?")
print(response)
```

> [!IMPORTANT]
> Generated functions are cached for performance. Once cached, subsequent calls to `agent_tools()` return
> the cached functions immediately without refetching schemas, regardless of filter parameters.
> Use `refresh_cache=True` or call `client.clear_agent_tools_cache()` to force regeneration when tool schemas have changed.

```python
# Force refresh cache to get latest schemas
fresh_tools = client.agent_tools(refresh_cache=True)

# Or clear cache manually and call again
client.clear_agent_tools_cache()
fresh_tools = client.agent_tools()
```

## Examples

A working SDK examples are available in the `examples/` folder,
Expand Down Expand Up @@ -145,9 +168,9 @@ client = McpdClient(api_endpoint="http://localhost:8090", api_key="optional-key"

* `client.tools(server_name: str) -> list[dict]` - Returns the tool schema definitions for only the specified server.

* `client.agent_tools(servers: list[str] | None = None, *, check_health: bool = True) -> list[Callable]` - Returns a list of self-contained, callable functions suitable for agentic frameworks. By default, filters to healthy servers only. Use `servers` to filter by server names, or `check_health=False` to include all servers regardless of health.
* `client.agent_tools(servers: list[str] | None = None, tools: list[str] | None = None, *, refresh_cache: bool = False) -> list[Callable]` - Returns a list of self-contained, callable functions suitable for agentic frameworks. By default, filters to healthy servers only. Use `servers` to filter by server names, `tools` to filter by tool names (supports both raw names like `'add'` and prefixed names like `'time__get_current_time'`), or `refresh_cache=True` to force regeneration of cached functions. Functions are cached - subsequent calls return cached functions immediately without refetching schemas.

* `client.clear_agent_tools_cache()` - Clears cached generated callable functions that are created when calling agent_tools().
* `client.clear_agent_tools_cache()` - Clears cached generated callable functions. Call this to force regeneration when tool schemas have changed.

* `client.has_tool(server_name: str, tool_name: str) -> bool` - Checks if a specific tool exists on a given server.

Expand Down
6 changes: 4 additions & 2 deletions src/mcpd/dynamic_caller.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@

from __future__ import annotations

from collections.abc import Callable
from typing import TYPE_CHECKING

from .exceptions import ToolNotFoundError
from .function_builder import TOOL_SEPARATOR

if TYPE_CHECKING:
from .mcpd_client import McpdClient
Expand Down Expand Up @@ -109,7 +111,7 @@ def __init__(self, client: McpdClient, server_name: str):
self._client = client
self._server_name = server_name

def __getattr__(self, tool_name: str) -> callable:
def __getattr__(self, tool_name: str) -> Callable:
"""Create a callable function for the specified tool.

When you access an attribute on a ServerProxy (e.g., time_server.get_current_time),
Expand Down Expand Up @@ -161,7 +163,7 @@ def tool_function(**kwargs):
return self._client._perform_call(self._server_name, tool_name, kwargs)

# Add metadata to help with debugging and introspection
tool_function.__name__ = f"{self._server_name}__{tool_name}"
tool_function.__name__ = f"{self._server_name}{TOOL_SEPARATOR}{tool_name}"
tool_function.__qualname__ = f"ServerProxy.{tool_name}"

return tool_function
48 changes: 36 additions & 12 deletions src/mcpd/function_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from __future__ import annotations

import re
from types import FunctionType
from collections.abc import Callable
from typing import TYPE_CHECKING, Any

from .exceptions import McpdError, ValidationError
Expand All @@ -20,6 +20,13 @@
if TYPE_CHECKING:
from .mcpd_client import McpdClient

TOOL_SEPARATOR = "__"
"""Separator used between server name and tool name in qualified tool names.

Format: `{serverName}{TOOL_SEPARATOR}{toolName}`
Example: "time__get_current_time" where "time" is server and "get_current_time" is tool.
"""


class FunctionBuilder:
"""Builds callable Python functions from MCP tool JSON schemas.
Expand Down Expand Up @@ -88,18 +95,18 @@ def _function_name(self, server_name: str, schema_name: str) -> str:
"""Generate a unique function name from server and tool names.

This method creates a qualified function name by combining the server name
and tool name with a double underscore separator. Both names are sanitized
using _safe_name() to ensure the result is a valid Python identifier.
and tool name with TOOL_SEPARATOR. Both names are sanitized using _safe_name()
to ensure the result is a valid Python identifier.

The double underscore convention helps distinguish the server and tool
components while maintaining uniqueness across the entire function namespace.
The separator helps distinguish the server and tool components while maintaining
uniqueness across the entire function namespace.

Args:
server_name: The name of the MCP server hosting the tool.
schema_name: The name of the tool from the schema definition.

Returns:
A qualified function name in the format "{safe_server}__{safe_tool}".
A qualified function name in the format "{safe_server}{TOOL_SEPARATOR}{safe_tool}".
The result is guaranteed to be a valid Python identifier.

Example:
Expand All @@ -112,11 +119,11 @@ def _function_name(self, server_name: str, schema_name: str) -> str:

Note:
This naming convention allows the generated function to be introspected
to determine its originating server and tool names by splitting on '__'.
to determine its originating server and tool names by splitting on TOOL_SEPARATOR.
"""
return f"{self._safe_name(server_name)}__{self._safe_name(schema_name)}"
return f"{self._safe_name(server_name)}{TOOL_SEPARATOR}{self._safe_name(schema_name)}"

def create_function_from_schema(self, schema: dict[str, Any], server_name: str) -> FunctionType:
def create_function_from_schema(self, schema: dict[str, Any], server_name: str) -> Callable[..., Any]:
"""Create a callable Python function from an MCP tool's JSON Schema definition.

This method generates a self-contained, callable function that validates parameters
Expand All @@ -143,6 +150,8 @@ def create_function_from_schema(self, schema: dict[str, Any], server_name: str)
- Comprehensive docstring with parameter descriptions
- Raises ValidationError for missing required parameters
- Returns the tool's execution result via client._perform_call()
- Has _server_name attribute containing the originating server name
- Has _tool_name attribute containing the original tool name

Raises:
McpdError: If function compilation fails due to invalid schema,
Expand All @@ -167,9 +176,9 @@ def create_function_from_schema(self, schema: dict[str, Any], server_name: str)
Note:
The generated function includes validation logic that checks for required
parameters at runtime and builds a parameters dictionary for the API call.
The function is cached using a key of "{server_name}__{tool_name}".
The function is cached using a key of "{server_name}{TOOL_SEPARATOR}{tool_name}".
"""
cache_key = f"{server_name}__{schema.get('name', '')}"
cache_key = f"{server_name}{TOOL_SEPARATOR}{schema.get('name', '')}"

if cache_key in self._function_cache:
cached_func = self._function_cache[cache_key]
Expand All @@ -187,12 +196,19 @@ def create_function_from_schema(self, schema: dict[str, Any], server_name: str)
created_function = namespace[function_name]
created_function.__annotations__ = annotations

# Add metadata attributes.
created_function._server_name = server_name
created_function._tool_name = schema["name"]

# Cache the function creation details
def create_function_instance(annotations: dict[str, Any]) -> FunctionType:
def create_function_instance(annotations: dict[str, Any]) -> Callable[..., Any]:
temp_namespace = namespace.copy()
exec(compiled_code, temp_namespace)
new_func = temp_namespace[function_name]
new_func.__annotations__ = annotations.copy()
# Add metadata attributes to cached instances as well.
new_func._server_name = server_name
new_func._tool_name = schema["name"]
return new_func

self._function_cache[cache_key] = {
Expand Down Expand Up @@ -601,3 +617,11 @@ def clear_cache(self) -> None:
>>> func3 = builder.create_function_from_schema(schema, "server1") # Recompiles
"""
self._function_cache.clear()

def get_cached_functions(self) -> list[Callable[..., Any]]:
"""Get all cached functions.

Returns:
List of all cached agent functions, or empty list if cache is empty.
"""
return [cached["create_function"](cached["annotations"]) for cached in self._function_cache.values()]
Loading