|
1 |
| -from .exceptions import McpdError |
| 1 | +from .exceptions import McpdError, ToolNotFoundError |
2 | 2 |
|
3 | 3 |
|
4 | 4 | class DynamicCaller:
|
5 |
| - """Helper class to enable client.call.<server_name>.<tool_name>(**kwargs) syntax.""" |
| 5 | + """ |
| 6 | + Enables dynamic, attribute-based tool invocation using natural Python syntax. |
6 | 7 |
|
7 |
| - def __init__(self, client): |
| 8 | + This class provides the magic behind the client.call.<server>.<tool>(**kwargs) syntax, |
| 9 | + allowing you to call MCP tools as if they were native Python methods. It uses Python's |
| 10 | + __getattr__ to dynamically resolve server and tool names at runtime. |
| 11 | +
|
| 12 | + The DynamicCaller is automatically instantiated as the 'call' attribute on McpdClient |
| 13 | + and should not be created directly. |
| 14 | +
|
| 15 | + Attributes: |
| 16 | + _client: Reference to the parent McpdClient instance. |
| 17 | +
|
| 18 | + Example: |
| 19 | + >>> client = McpdClient(api_endpoint="http://localhost:8090") |
| 20 | + >>> |
| 21 | + >>> # Access tools through natural attribute syntax |
| 22 | + >>> # Instead of: client._perform_call("time", "get_current_time", {"timezone": "UTC"}) |
| 23 | + >>> # You can write: |
| 24 | + >>> result = client.call.time.get_current_time(timezone="UTC") |
| 25 | + >>> |
| 26 | + >>> # Works with any server and tool name |
| 27 | + >>> weather = client.call.duckduckgo_mcp.searsch(query="Tokyo", max_results=3) |
| 28 | + >>> commits = client.call.mcp_discord.discord_read_messages(channelId="9223372036854775806", limit=10) |
| 29 | +
|
| 30 | + Note: |
| 31 | + Tool and server names are resolved at runtime. If a server or tool doesn't exist, |
| 32 | + an McpdError will be raised when you attempt to call it. Use client.has_tool() |
| 33 | + to check availability before calling if needed. |
| 34 | + """ |
| 35 | + |
| 36 | + def __init__(self, client: "McpdClient"): |
| 37 | + """ |
| 38 | + Initialize the DynamicCaller with a reference to the client. |
| 39 | +
|
| 40 | + Args: |
| 41 | + client: The McpdClient instance that owns this DynamicCaller. |
| 42 | + """ |
8 | 43 | self._client = client
|
9 | 44 |
|
10 |
| - def __getattr__(self, server_name: str): |
11 |
| - """Get a server proxy for dynamic tool calling.""" |
| 45 | + def __getattr__(self, server_name: str) -> "ServerProxy": |
| 46 | + """ |
| 47 | + Create a ServerProxy for the specified server name. |
| 48 | +
|
| 49 | + This method is called when accessing an attribute on the DynamicCaller, |
| 50 | + e.g., client.call.time returns a ServerProxy for the "time" server. |
| 51 | +
|
| 52 | + Args: |
| 53 | + server_name: The name of the MCP server to create a proxy for. |
| 54 | +
|
| 55 | + Returns: |
| 56 | + A ServerProxy instance that can be used to call tools on that server. |
| 57 | +
|
| 58 | + Example: |
| 59 | + >>> # When you write: client.call.time |
| 60 | + >>> # Python calls: client.call.__getattr__("time") |
| 61 | + >>> # Which returns: ServerProxy(client, "time") |
| 62 | + """ |
12 | 63 | return ServerProxy(self._client, server_name)
|
13 | 64 |
|
14 | 65 |
|
15 | 66 | class ServerProxy:
|
16 |
| - """Proxy class for server-specific tool calling.""" |
| 67 | + """ |
| 68 | + Proxy for a specific MCP server, enabling tool invocation via attributes. |
| 69 | +
|
| 70 | + This class represents a specific MCP server and allows calling its tools |
| 71 | + as if they were methods. It's created automatically by DynamicCaller and |
| 72 | + should not be instantiated directly. |
| 73 | +
|
| 74 | + Attributes: |
| 75 | + _client: Reference to the McpdClient instance. |
| 76 | + _server_name: Name of the MCP server this proxy represents. |
17 | 77 |
|
18 |
| - def __init__(self, client, server_name: str): |
| 78 | + Example: |
| 79 | + >>> # ServerProxy is created when you access a server: |
| 80 | + >>> time_server = client.call.time # Returns ServerProxy(client, "time") |
| 81 | + >>> |
| 82 | + >>> # You can then call tools on it: |
| 83 | + >>> current_time = time_server.get_current_time(timezone="UTC") |
| 84 | + >>> |
| 85 | + >>> # Or chain it directly: |
| 86 | + >>> current_time = client.call.time.get_current_time(timezone="UTC") |
| 87 | + """ |
| 88 | + |
| 89 | + def __init__(self, client: "McpdClient", server_name: str): |
| 90 | + """ |
| 91 | + Initialize a ServerProxy for a specific server. |
| 92 | +
|
| 93 | + Args: |
| 94 | + client: The McpdClient instance to use for API calls. |
| 95 | + server_name: The name of the MCP server this proxy represents. |
| 96 | + """ |
19 | 97 | self._client = client
|
20 | 98 | self._server_name = server_name
|
21 | 99 |
|
22 |
| - def __getattr__(self, tool_name: str): |
23 |
| - """Get a tool callable.""" |
| 100 | + def __getattr__(self, tool_name: str) -> callable: |
| 101 | + """ |
| 102 | + Create a callable function for the specified tool. |
| 103 | +
|
| 104 | + When you access an attribute on a ServerProxy (e.g., time_server.get_current_time), |
| 105 | + this method creates and returns a function that will call that tool when invoked. |
| 106 | +
|
| 107 | + Args: |
| 108 | + tool_name: The name of the tool to create a callable for. |
| 109 | +
|
| 110 | + Returns: |
| 111 | + A callable function that accepts keyword arguments and invokes the tool. |
| 112 | +
|
| 113 | + Raises: |
| 114 | + McpdError: If the tool doesn't exist on this server. |
| 115 | +
|
| 116 | + Example: |
| 117 | + >>> # When you write: client.call.time.get_current_time |
| 118 | + >>> # Python calls: ServerProxy.__getattr__("get_current_time") |
| 119 | + >>> # Which returns a function that calls the tool |
| 120 | + >>> |
| 121 | + >>> # The returned function can then be called: |
| 122 | + >>> result = client.call.time.get_current_time(timezone="UTC") |
| 123 | + >>> |
| 124 | + >>> # You can also store the function reference: |
| 125 | + >>> get_time = client.call.time.get_current_time |
| 126 | + >>> tokyo_time = get_time(timezone="Asia/Tokyo") |
| 127 | + >>> london_time = get_time(timezone="Europe/London") |
| 128 | + """ |
24 | 129 | if not self._client.has_tool(self._server_name, tool_name):
|
25 |
| - raise McpdError(f"Tool '{tool_name}' not found on server '{self._server_name}'") |
| 130 | + raise ToolNotFoundError( |
| 131 | + f"Tool '{tool_name}' not found on server '{self._server_name}'. " |
| 132 | + f"Use client.tools('{self._server_name}') to see available tools.", |
| 133 | + server_name=self._server_name, |
| 134 | + tool_name=tool_name, |
| 135 | + ) |
| 136 | + |
| 137 | + def tool_function(**kwargs): |
| 138 | + """ |
| 139 | + Execute the MCP tool with the provided parameters. |
| 140 | +
|
| 141 | + Args: |
| 142 | + **kwargs: Tool parameters as keyword arguments. |
| 143 | + These should match the tool's inputSchema. |
26 | 144 |
|
27 |
| - def tool_callable(**kwargs): |
| 145 | + Returns: |
| 146 | + The tool's response, typically a dictionary with the results. |
| 147 | +
|
| 148 | + Raises: |
| 149 | + McpdError: If the tool execution fails for any reason. |
| 150 | + """ |
28 | 151 | return self._client._perform_call(self._server_name, tool_name, kwargs)
|
29 | 152 |
|
30 |
| - return tool_callable |
| 153 | + # Add metadata to help with debugging and introspection |
| 154 | + tool_function.__name__ = f"{self._server_name}__{tool_name}" |
| 155 | + tool_function.__qualname__ = f"ServerProxy.{tool_name}" |
| 156 | + |
| 157 | + return tool_function |
0 commit comments