Skip to content

Commit bc611e1

Browse files
authored
Docstrings and exceptions (#25)
* Add exception hierarchy * Add detailed docstrings (and improve type hinting on function signatures) * Make as much private as possible
1 parent 768a1d1 commit bc611e1

File tree

9 files changed

+1480
-81
lines changed

9 files changed

+1480
-81
lines changed

src/mcpd/__init__.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,23 @@
1-
from .mcpd_client import McpdClient, McpdError
1+
from .exceptions import (
2+
AuthenticationError,
3+
ConnectionError,
4+
McpdError,
5+
ServerNotFoundError,
6+
TimeoutError,
7+
ToolExecutionError,
8+
ToolNotFoundError,
9+
ValidationError,
10+
)
11+
from .mcpd_client import McpdClient
212

313
__all__ = [
414
"McpdClient",
515
"McpdError",
16+
"AuthenticationError",
17+
"ConnectionError",
18+
"ServerNotFoundError",
19+
"TimeoutError",
20+
"ToolExecutionError",
21+
"ToolNotFoundError",
22+
"ValidationError",
623
]

src/mcpd/dynamic_caller.py

Lines changed: 139 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,157 @@
1-
from .exceptions import McpdError
1+
from .exceptions import McpdError, ToolNotFoundError
22

33

44
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.
67
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+
"""
843
self._client = client
944

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+
"""
1263
return ServerProxy(self._client, server_name)
1364

1465

1566
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.
1777
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+
"""
1997
self._client = client
2098
self._server_name = server_name
2199

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+
"""
24129
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.
26144
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+
"""
28151
return self._client._perform_call(self._server_name, tool_name, kwargs)
29152

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

Comments
 (0)