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
7 changes: 0 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,3 @@ namespaces = false
"*" = ["py.typed"]

[tool.setuptools_scm]

[tool.ruff]
fix = true
line-length=120

[tool.ruff.lint]
select = ["I"]
77 changes: 77 additions & 0 deletions ruff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Ruff configuration for mcpd Python SDK
target-version = "py311"
src = ["src"]
line-length = 120
include = ["*.py", "*.pyi"]
# Enable auto-fixing
fix = true

# Exclude common directories
exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".mypy_cache",
".nox",
".pants.d",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"venv",
]

[lint]

# Select rules to enforce
select = [
"E", # pycodestyle errors
"F", # pyflakes
"UP", # pyupgrade
"B", # flake8-bugbear
"SIM", # flake8-simplify
"I", # isort (import sorting)
"N", # pep8-naming
"ISC", # flake8-implicit-str-concat
"PTH", # flake8-use-pathlib
"D", # pydocstyle (docstrings)
]

# Rules to ignore
ignore = [
# No ignored rules - we want strict enforcement for a public SDK
]

# Rules that should not be auto-fixed
unfixable = [
"B", # flake8-bugbear (safety-related, should be manually reviewed)
"F841", # unused variables (might indicate logic issues)
]

# Per-file ignores
[lint.per-file-ignores]
"__init__.py" = ["F401"] # Allow unused imports in __init__.py
"tests/**" = [
"D", # Don't require docstrings in tests
"N999", # Allow invalid module names in tests
]

[lint.pydocstyle]
# Use Google docstring convention
convention = "google"

[format]
# Formatting preferences
quote-style = "double"
indent-style = "space"
line-ending = "auto"
14 changes: 14 additions & 0 deletions src/mcpd/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
"""mcpd Python SDK.

A Python SDK for interacting with the mcpd daemon, which manages
Model Context Protocol (MCP) servers and enables seamless tool execution
through natural Python syntax.

This package provides:
- McpdClient: Main client for server management and tool execution
- Dynamic calling: Natural syntax like client.call.server.tool(**kwargs)
- Agent-ready functions: Generate callable functions via agent_tools() for AI frameworks
- Type-safe function generation: Create callable functions from tool schemas
- Comprehensive error handling: Detailed exceptions for different failure modes
"""

from .exceptions import (
AuthenticationError,
ConnectionError,
Expand Down
46 changes: 28 additions & 18 deletions src/mcpd/dynamic_caller.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
from .exceptions import McpdError, ToolNotFoundError
"""Dynamic tool invocation for mcpd client.

This module provides the DynamicCaller and ServerProxy classes that enable
natural Python syntax for calling MCP tools, such as:
client.call.server.tool(**kwargs)

The dynamic calling system uses Python's __getattr__ magic method to create
a fluent interface that resolves server and tool names at runtime.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from .exceptions import ToolNotFoundError

if TYPE_CHECKING:
from .mcpd_client import McpdClient


class DynamicCaller:
"""
Enables dynamic, attribute-based tool invocation using natural Python syntax.
"""Enables dynamic, attribute-based tool invocation using natural Python syntax.

This class provides the magic behind the client.call.<server>.<tool>(**kwargs) syntax,
allowing you to call MCP tools as if they were native Python methods. It uses Python's
Expand Down Expand Up @@ -33,18 +49,16 @@ class DynamicCaller:
to check availability before calling if needed.
"""

def __init__(self, client: "McpdClient"):
"""
Initialize the DynamicCaller with a reference to the client.
def __init__(self, client: McpdClient):
"""Initialize the DynamicCaller with a reference to the client.

Args:
client: The McpdClient instance that owns this DynamicCaller.
"""
self._client = client

def __getattr__(self, server_name: str) -> "ServerProxy":
"""
Create a ServerProxy for the specified server name.
def __getattr__(self, server_name: str) -> ServerProxy:
"""Create a ServerProxy for the specified server name.

This method is called when accessing an attribute on the DynamicCaller,
e.g., client.call.time returns a ServerProxy for the "time" server.
Expand All @@ -64,8 +78,7 @@ def __getattr__(self, server_name: str) -> "ServerProxy":


class ServerProxy:
"""
Proxy for a specific MCP server, enabling tool invocation via attributes.
"""Proxy for a specific MCP server, enabling tool invocation via attributes.

This class represents a specific MCP server and allows calling its tools
as if they were methods. It's created automatically by DynamicCaller and
Expand All @@ -86,9 +99,8 @@ class ServerProxy:
>>> current_time = client.call.time.get_current_time(timezone="UTC")
"""

def __init__(self, client: "McpdClient", server_name: str):
"""
Initialize a ServerProxy for a specific server.
def __init__(self, client: McpdClient, server_name: str):
"""Initialize a ServerProxy for a specific server.

Args:
client: The McpdClient instance to use for API calls.
Expand All @@ -98,8 +110,7 @@ def __init__(self, client: "McpdClient", server_name: str):
self._server_name = server_name

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

When you access an attribute on a ServerProxy (e.g., time_server.get_current_time),
this method creates and returns a function that will call that tool when invoked.
Expand Down Expand Up @@ -135,8 +146,7 @@ def __getattr__(self, tool_name: str) -> callable:
)

def tool_function(**kwargs):
"""
Execute the MCP tool with the provided parameters.
"""Execute the MCP tool with the provided parameters.

Args:
**kwargs: Tool parameters as keyword arguments.
Expand Down
61 changes: 43 additions & 18 deletions src/mcpd/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
"""
Exception hierarchy for the mcpd SDK.
"""Exception hierarchy for the mcpd SDK.

This module provides a structured exception hierarchy to help users handle
different error scenarios appropriately.
"""


class McpdError(Exception):
"""
Base exception for all mcpd SDK errors.
"""Base exception for all mcpd SDK errors.

This exception wraps all errors that occur during interaction with the mcpd daemon,
including network failures, authentication errors, server errors, and tool execution
Expand Down Expand Up @@ -62,8 +60,7 @@ class McpdError(Exception):


class ConnectionError(McpdError):
"""
Raised when unable to connect to the mcpd daemon.
"""Raised when unable to connect to the mcpd daemon.

This typically indicates that:
- The mcpd daemon is not running
Expand All @@ -84,8 +81,7 @@ class ConnectionError(McpdError):


class AuthenticationError(McpdError):
"""
Raised when authentication with the mcpd daemon fails.
"""Raised when authentication with the mcpd daemon fails.

This indicates that:
- The API key is invalid or expired
Expand All @@ -107,8 +103,7 @@ class AuthenticationError(McpdError):


class ServerNotFoundError(McpdError):
"""
Raised when a specified MCP server doesn't exist.
"""Raised when a specified MCP server doesn't exist.

This error occurs when trying to access a server that:
- Is not configured in the mcpd daemon
Expand All @@ -127,13 +122,18 @@ class ServerNotFoundError(McpdError):
"""

def __init__(self, message: str, server_name: str = None):
"""Initialize ServerNotFoundError.

Args:
message: The error message.
server_name: The name of the server that was not found.
"""
super().__init__(message)
self.server_name = server_name


class ToolNotFoundError(McpdError):
"""
Raised when a specified tool doesn't exist on a server.
"""Raised when a specified tool doesn't exist on a server.

This error occurs when trying to call a tool that:
- Doesn't exist on the specified server
Expand All @@ -154,14 +154,20 @@ class ToolNotFoundError(McpdError):
"""

def __init__(self, message: str, server_name: str = None, tool_name: str = None):
"""Initialize ToolNotFoundError.

Args:
message: The error message.
server_name: The name of the server where the tool was not found.
tool_name: The name of the tool that was not found.
"""
super().__init__(message)
self.server_name = server_name
self.tool_name = tool_name


class ToolExecutionError(McpdError):
"""
Raised when a tool execution fails on the server side.
"""Raised when a tool execution fails on the server side.

This indicates that the tool was found and called, but failed during execution:
- Invalid parameters provided
Expand All @@ -185,15 +191,22 @@ class ToolExecutionError(McpdError):
"""

def __init__(self, message: str, server_name: str = None, tool_name: str = None, details: dict = None):
"""Initialize ToolExecutionError.

Args:
message: The error message.
server_name: The name of the server where the tool execution failed.
tool_name: The name of the tool that failed to execute.
details: Additional error details from the server.
"""
super().__init__(message)
self.server_name = server_name
self.tool_name = tool_name
self.details = details


class ValidationError(McpdError):
"""
Raised when input validation fails.
"""Raised when input validation fails.

This occurs when:
- Required parameters are missing
Expand All @@ -214,13 +227,18 @@ class ValidationError(McpdError):
"""

def __init__(self, message: str, validation_errors: list = None):
"""Initialize ValidationError.

Args:
message: The error message.
validation_errors: List of specific validation error messages.
"""
super().__init__(message)
self.validation_errors = validation_errors or []


class TimeoutError(McpdError):
"""
Raised when an operation times out.
"""Raised when an operation times out.

This can occur during:
- Long-running tool executions
Expand All @@ -240,6 +258,13 @@ class TimeoutError(McpdError):
"""

def __init__(self, message: str, operation: str = None, timeout: float = None):
"""Initialize TimeoutError.

Args:
message: The error message.
operation: The operation that timed out.
timeout: The timeout value in seconds.
"""
super().__init__(message)
self.operation = operation
self.timeout = timeout
Loading
Loading