Skip to content

Commit f5fdd48

Browse files
authored
Configure linting (and lint!) (#28)
* Configure linting (and lint!) * Add ruff.toml, remove settings from pyproject.toml * Linting fixes * Bug: Fix JSON Schema Null Type Handling
1 parent bc611e1 commit f5fdd48

13 files changed

+275
-112
lines changed

pyproject.toml

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,3 @@ namespaces = false
4646
"*" = ["py.typed"]
4747

4848
[tool.setuptools_scm]
49-
50-
[tool.ruff]
51-
fix = true
52-
line-length=120
53-
54-
[tool.ruff.lint]
55-
select = ["I"]

ruff.toml

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Ruff configuration for mcpd Python SDK
2+
target-version = "py311"
3+
src = ["src"]
4+
line-length = 120
5+
include = ["*.py", "*.pyi"]
6+
# Enable auto-fixing
7+
fix = true
8+
9+
# Exclude common directories
10+
exclude = [
11+
".bzr",
12+
".direnv",
13+
".eggs",
14+
".git",
15+
".git-rewrite",
16+
".hg",
17+
".mypy_cache",
18+
".nox",
19+
".pants.d",
20+
".pytype",
21+
".ruff_cache",
22+
".svn",
23+
".tox",
24+
".venv",
25+
"__pypackages__",
26+
"_build",
27+
"buck-out",
28+
"build",
29+
"dist",
30+
"node_modules",
31+
"venv",
32+
]
33+
34+
[lint]
35+
36+
# Select rules to enforce
37+
select = [
38+
"E", # pycodestyle errors
39+
"F", # pyflakes
40+
"UP", # pyupgrade
41+
"B", # flake8-bugbear
42+
"SIM", # flake8-simplify
43+
"I", # isort (import sorting)
44+
"N", # pep8-naming
45+
"ISC", # flake8-implicit-str-concat
46+
"PTH", # flake8-use-pathlib
47+
"D", # pydocstyle (docstrings)
48+
]
49+
50+
# Rules to ignore
51+
ignore = [
52+
# No ignored rules - we want strict enforcement for a public SDK
53+
]
54+
55+
# Rules that should not be auto-fixed
56+
unfixable = [
57+
"B", # flake8-bugbear (safety-related, should be manually reviewed)
58+
"F841", # unused variables (might indicate logic issues)
59+
]
60+
61+
# Per-file ignores
62+
[lint.per-file-ignores]
63+
"__init__.py" = ["F401"] # Allow unused imports in __init__.py
64+
"tests/**" = [
65+
"D", # Don't require docstrings in tests
66+
"N999", # Allow invalid module names in tests
67+
]
68+
69+
[lint.pydocstyle]
70+
# Use Google docstring convention
71+
convention = "google"
72+
73+
[format]
74+
# Formatting preferences
75+
quote-style = "double"
76+
indent-style = "space"
77+
line-ending = "auto"

src/mcpd/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
"""mcpd Python SDK.
2+
3+
A Python SDK for interacting with the mcpd daemon, which manages
4+
Model Context Protocol (MCP) servers and enables seamless tool execution
5+
through natural Python syntax.
6+
7+
This package provides:
8+
- McpdClient: Main client for server management and tool execution
9+
- Dynamic calling: Natural syntax like client.call.server.tool(**kwargs)
10+
- Agent-ready functions: Generate callable functions via agent_tools() for AI frameworks
11+
- Type-safe function generation: Create callable functions from tool schemas
12+
- Comprehensive error handling: Detailed exceptions for different failure modes
13+
"""
14+
115
from .exceptions import (
216
AuthenticationError,
317
ConnectionError,

src/mcpd/dynamic_caller.py

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,25 @@
1-
from .exceptions import McpdError, ToolNotFoundError
1+
"""Dynamic tool invocation for mcpd client.
2+
3+
This module provides the DynamicCaller and ServerProxy classes that enable
4+
natural Python syntax for calling MCP tools, such as:
5+
client.call.server.tool(**kwargs)
6+
7+
The dynamic calling system uses Python's __getattr__ magic method to create
8+
a fluent interface that resolves server and tool names at runtime.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
from typing import TYPE_CHECKING
14+
15+
from .exceptions import ToolNotFoundError
16+
17+
if TYPE_CHECKING:
18+
from .mcpd_client import McpdClient
219

320

421
class DynamicCaller:
5-
"""
6-
Enables dynamic, attribute-based tool invocation using natural Python syntax.
22+
"""Enables dynamic, attribute-based tool invocation using natural Python syntax.
723
824
This class provides the magic behind the client.call.<server>.<tool>(**kwargs) syntax,
925
allowing you to call MCP tools as if they were native Python methods. It uses Python's
@@ -33,18 +49,16 @@ class DynamicCaller:
3349
to check availability before calling if needed.
3450
"""
3551

36-
def __init__(self, client: "McpdClient"):
37-
"""
38-
Initialize the DynamicCaller with a reference to the client.
52+
def __init__(self, client: McpdClient):
53+
"""Initialize the DynamicCaller with a reference to the client.
3954
4055
Args:
4156
client: The McpdClient instance that owns this DynamicCaller.
4257
"""
4358
self._client = client
4459

45-
def __getattr__(self, server_name: str) -> "ServerProxy":
46-
"""
47-
Create a ServerProxy for the specified server name.
60+
def __getattr__(self, server_name: str) -> ServerProxy:
61+
"""Create a ServerProxy for the specified server name.
4862
4963
This method is called when accessing an attribute on the DynamicCaller,
5064
e.g., client.call.time returns a ServerProxy for the "time" server.
@@ -64,8 +78,7 @@ def __getattr__(self, server_name: str) -> "ServerProxy":
6478

6579

6680
class ServerProxy:
67-
"""
68-
Proxy for a specific MCP server, enabling tool invocation via attributes.
81+
"""Proxy for a specific MCP server, enabling tool invocation via attributes.
6982
7083
This class represents a specific MCP server and allows calling its tools
7184
as if they were methods. It's created automatically by DynamicCaller and
@@ -86,9 +99,8 @@ class ServerProxy:
8699
>>> current_time = client.call.time.get_current_time(timezone="UTC")
87100
"""
88101

89-
def __init__(self, client: "McpdClient", server_name: str):
90-
"""
91-
Initialize a ServerProxy for a specific server.
102+
def __init__(self, client: McpdClient, server_name: str):
103+
"""Initialize a ServerProxy for a specific server.
92104
93105
Args:
94106
client: The McpdClient instance to use for API calls.
@@ -98,8 +110,7 @@ def __init__(self, client: "McpdClient", server_name: str):
98110
self._server_name = server_name
99111

100112
def __getattr__(self, tool_name: str) -> callable:
101-
"""
102-
Create a callable function for the specified tool.
113+
"""Create a callable function for the specified tool.
103114
104115
When you access an attribute on a ServerProxy (e.g., time_server.get_current_time),
105116
this method creates and returns a function that will call that tool when invoked.
@@ -135,8 +146,7 @@ def __getattr__(self, tool_name: str) -> callable:
135146
)
136147

137148
def tool_function(**kwargs):
138-
"""
139-
Execute the MCP tool with the provided parameters.
149+
"""Execute the MCP tool with the provided parameters.
140150
141151
Args:
142152
**kwargs: Tool parameters as keyword arguments.

src/mcpd/exceptions.py

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
1-
"""
2-
Exception hierarchy for the mcpd SDK.
1+
"""Exception hierarchy for the mcpd SDK.
32
43
This module provides a structured exception hierarchy to help users handle
54
different error scenarios appropriately.
65
"""
76

87

98
class McpdError(Exception):
10-
"""
11-
Base exception for all mcpd SDK errors.
9+
"""Base exception for all mcpd SDK errors.
1210
1311
This exception wraps all errors that occur during interaction with the mcpd daemon,
1412
including network failures, authentication errors, server errors, and tool execution
@@ -62,8 +60,7 @@ class McpdError(Exception):
6260

6361

6462
class ConnectionError(McpdError):
65-
"""
66-
Raised when unable to connect to the mcpd daemon.
63+
"""Raised when unable to connect to the mcpd daemon.
6764
6865
This typically indicates that:
6966
- The mcpd daemon is not running
@@ -84,8 +81,7 @@ class ConnectionError(McpdError):
8481

8582

8683
class AuthenticationError(McpdError):
87-
"""
88-
Raised when authentication with the mcpd daemon fails.
84+
"""Raised when authentication with the mcpd daemon fails.
8985
9086
This indicates that:
9187
- The API key is invalid or expired
@@ -107,8 +103,7 @@ class AuthenticationError(McpdError):
107103

108104

109105
class ServerNotFoundError(McpdError):
110-
"""
111-
Raised when a specified MCP server doesn't exist.
106+
"""Raised when a specified MCP server doesn't exist.
112107
113108
This error occurs when trying to access a server that:
114109
- Is not configured in the mcpd daemon
@@ -127,13 +122,18 @@ class ServerNotFoundError(McpdError):
127122
"""
128123

129124
def __init__(self, message: str, server_name: str = None):
125+
"""Initialize ServerNotFoundError.
126+
127+
Args:
128+
message: The error message.
129+
server_name: The name of the server that was not found.
130+
"""
130131
super().__init__(message)
131132
self.server_name = server_name
132133

133134

134135
class ToolNotFoundError(McpdError):
135-
"""
136-
Raised when a specified tool doesn't exist on a server.
136+
"""Raised when a specified tool doesn't exist on a server.
137137
138138
This error occurs when trying to call a tool that:
139139
- Doesn't exist on the specified server
@@ -154,14 +154,20 @@ class ToolNotFoundError(McpdError):
154154
"""
155155

156156
def __init__(self, message: str, server_name: str = None, tool_name: str = None):
157+
"""Initialize ToolNotFoundError.
158+
159+
Args:
160+
message: The error message.
161+
server_name: The name of the server where the tool was not found.
162+
tool_name: The name of the tool that was not found.
163+
"""
157164
super().__init__(message)
158165
self.server_name = server_name
159166
self.tool_name = tool_name
160167

161168

162169
class ToolExecutionError(McpdError):
163-
"""
164-
Raised when a tool execution fails on the server side.
170+
"""Raised when a tool execution fails on the server side.
165171
166172
This indicates that the tool was found and called, but failed during execution:
167173
- Invalid parameters provided
@@ -185,15 +191,22 @@ class ToolExecutionError(McpdError):
185191
"""
186192

187193
def __init__(self, message: str, server_name: str = None, tool_name: str = None, details: dict = None):
194+
"""Initialize ToolExecutionError.
195+
196+
Args:
197+
message: The error message.
198+
server_name: The name of the server where the tool execution failed.
199+
tool_name: The name of the tool that failed to execute.
200+
details: Additional error details from the server.
201+
"""
188202
super().__init__(message)
189203
self.server_name = server_name
190204
self.tool_name = tool_name
191205
self.details = details
192206

193207

194208
class ValidationError(McpdError):
195-
"""
196-
Raised when input validation fails.
209+
"""Raised when input validation fails.
197210
198211
This occurs when:
199212
- Required parameters are missing
@@ -214,13 +227,18 @@ class ValidationError(McpdError):
214227
"""
215228

216229
def __init__(self, message: str, validation_errors: list = None):
230+
"""Initialize ValidationError.
231+
232+
Args:
233+
message: The error message.
234+
validation_errors: List of specific validation error messages.
235+
"""
217236
super().__init__(message)
218237
self.validation_errors = validation_errors or []
219238

220239

221240
class TimeoutError(McpdError):
222-
"""
223-
Raised when an operation times out.
241+
"""Raised when an operation times out.
224242
225243
This can occur during:
226244
- Long-running tool executions
@@ -240,6 +258,13 @@ class TimeoutError(McpdError):
240258
"""
241259

242260
def __init__(self, message: str, operation: str = None, timeout: float = None):
261+
"""Initialize TimeoutError.
262+
263+
Args:
264+
message: The error message.
265+
operation: The operation that timed out.
266+
timeout: The timeout value in seconds.
267+
"""
243268
super().__init__(message)
244269
self.operation = operation
245270
self.timeout = timeout

0 commit comments

Comments
 (0)