diff --git a/pyproject.toml b/pyproject.toml index 27e625b..f21e8be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,10 +46,3 @@ namespaces = false "*" = ["py.typed"] [tool.setuptools_scm] - -[tool.ruff] -fix = true -line-length=120 - -[tool.ruff.lint] -select = ["I"] diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..90d2fb8 --- /dev/null +++ b/ruff.toml @@ -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" diff --git a/src/mcpd/__init__.py b/src/mcpd/__init__.py index 1766d47..234a793 100644 --- a/src/mcpd/__init__.py +++ b/src/mcpd/__init__.py @@ -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, diff --git a/src/mcpd/dynamic_caller.py b/src/mcpd/dynamic_caller.py index 6bf1204..33f49bd 100644 --- a/src/mcpd/dynamic_caller.py +++ b/src/mcpd/dynamic_caller.py @@ -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..(**kwargs) syntax, allowing you to call MCP tools as if they were native Python methods. It uses Python's @@ -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. @@ -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 @@ -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. @@ -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. @@ -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. diff --git a/src/mcpd/exceptions.py b/src/mcpd/exceptions.py index 85622b3..32b61b3 100644 --- a/src/mcpd/exceptions.py +++ b/src/mcpd/exceptions.py @@ -1,5 +1,4 @@ -""" -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. @@ -7,8 +6,7 @@ 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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -185,6 +191,14 @@ 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 @@ -192,8 +206,7 @@ def __init__(self, message: str, server_name: str = None, tool_name: str = None, class ValidationError(McpdError): - """ - Raised when input validation fails. + """Raised when input validation fails. This occurs when: - Required parameters are missing @@ -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 @@ -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 diff --git a/src/mcpd/function_builder.py b/src/mcpd/function_builder.py index 2039879..e87b544 100644 --- a/src/mcpd/function_builder.py +++ b/src/mcpd/function_builder.py @@ -1,14 +1,28 @@ +"""Function generation from MCP tool schemas. + +This module provides the FunctionBuilder class that dynamically generates +callable Python functions from MCP tool JSON Schema definitions. These +functions can be used with AI agent frameworks and include proper parameter +validation, type annotations, and comprehensive docstrings. + +The generated functions are self-contained and cached for performance. +""" + +from __future__ import annotations + import re from types import FunctionType -from typing import Any +from typing import TYPE_CHECKING, Any from .exceptions import McpdError, ValidationError from .type_converter import TypeConverter +if TYPE_CHECKING: + from .mcpd_client import McpdClient + class FunctionBuilder: - """ - Builds callable Python functions from MCP tool JSON schemas. + """Builds callable Python functions from MCP tool JSON schemas. This class generates self-contained functions that can be used with AI agent frameworks. It uses dynamic string compilation to create functions with proper @@ -32,9 +46,8 @@ class FunctionBuilder: >>> result = func(timezone="UTC") # Executes the MCP tool """ - def __init__(self, client: "McpdClient"): - """ - Initialize a FunctionBuilder for the given client. + def __init__(self, client: McpdClient): + """Initialize a FunctionBuilder for the given client. Args: client: The McpdClient instance that will be used to execute @@ -44,8 +57,7 @@ def __init__(self, client: "McpdClient"): self._function_cache = {} def _safe_name(self, name: str) -> str: - """ - Convert a string into a safe Python identifier. + """Convert a string into a safe Python identifier. This method sanitizes arbitrary strings (like server names or tool names) to create valid Python identifiers that can be used as function names or variable names. @@ -73,8 +85,7 @@ def _safe_name(self, name: str) -> str: return re.sub(r"\W|^(?=\d)", "_", name) # replace non‑word chars, leading digit def _function_name(self, server_name: str, schema_name: str) -> str: - """ - Generate a unique function name from server and tool names. + """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 @@ -106,8 +117,7 @@ def _function_name(self, server_name: str, schema_name: str) -> str: return f"{self._safe_name(server_name)}__{self._safe_name(schema_name)}" def create_function_from_schema(self, schema: dict[str, Any], server_name: str) -> FunctionType: - """ - Create a callable Python function from an MCP tool's JSON Schema definition. + """Create a callable Python function from an MCP tool's JSON Schema definition. This method generates a self-contained, callable function that validates parameters and executes the corresponding MCP tool. The function is dynamically compiled from @@ -197,8 +207,7 @@ def create_function_instance(annotations: dict[str, Any]) -> FunctionType: raise McpdError(f"Error creating function {cache_key}: {e}") from e def _build_function_code(self, schema: dict[str, Any], server_name: str) -> str: - """ - Generate Python function source code from an MCP tool's JSON Schema. + """Generate Python function source code from an MCP tool's JSON Schema. This method is the core of the dynamic function generation system. It creates a complete Python function as a string that includes parameter validation, @@ -260,7 +269,10 @@ def server__get_time(timezone): missing_params.append(param) if missing_params: - raise ValidationError(f"Missing required parameters: {missing_params}", validation_errors=missing_params) + raise ValidationError( + f"Missing required parameters: {missing_params}", + validation_errors=missing_params, + ) # Build parameters dictionary params = {} @@ -278,15 +290,15 @@ def server__get_time(timezone): The generated code uses string interpolation and list literals to embed the schema data directly into the function code. This creates a completely self-contained function that doesn't depend on the original schema object. - """ + """ # noqa: D214 function_name = self._function_name(server_name, schema["name"]) input_schema = schema.get("inputSchema", {}) properties = input_schema.get("properties", {}) required_params = set(input_schema.get("required", [])) # Sort parameters: required first, then optional - required_param_names = [p for p in properties.keys() if p in required_params] - optional_param_names = [p for p in properties.keys() if p not in required_params] + required_param_names = [p for p in properties if p in required_params] + optional_param_names = [p for p in properties if p not in required_params] sorted_param_names = required_param_names + optional_param_names param_declarations = [] @@ -314,7 +326,10 @@ def server__get_time(timezone): " missing_params.append(param)", "", " if missing_params:", - ' raise ValidationError(f"Missing required parameters: {missing_params}", validation_errors=missing_params)', + " raise ValidationError(", + ' f"Missing required parameters: {missing_params}",', + " validation_errors=missing_params,", + " )", "", " # Build parameters dictionary", " params = {}", @@ -331,13 +346,12 @@ def server__get_time(timezone): return "\n".join(function_lines) def _create_annotations(self, schema: dict[str, Any]) -> dict[str, Any]: - """ - Generate Python type annotations from a tool's JSON Schema. + """Generate Python type annotations from a tool's JSON Schema. This method converts JSON Schema type definitions into Python type hints that are attached to the generated function. It uses the TypeConverter utility to handle complex schema types and properly marks optional - parameters with Union[type, None] notation. + parameters with modern union syntax (type | None). The method processes each parameter in the schema's properties, determines if it's required, and creates appropriate type annotations. Required @@ -404,8 +418,7 @@ def _create_annotations(self, schema: dict[str, Any]) -> dict[str, Any]: return annotations def _create_docstring(self, schema: dict[str, Any]) -> str: - """ - Generate a comprehensive docstring for the dynamically created function. + """Generate a comprehensive docstring for the dynamically created function. This method builds a properly formatted Python docstring that includes the tool's description, parameter documentation with optional/required status, @@ -448,8 +461,7 @@ def _create_docstring(self, schema: dict[str, Any]) -> str: ``` Generates a docstring like: - ``` - Search for items in database + ```Search for items in database Args: query: Search query string @@ -468,7 +480,7 @@ def _create_docstring(self, schema: dict[str, Any]) -> str: - Optional parameters are marked with "(optional)" suffix - The Raises section accurately documents both validation and execution errors - Empty properties result in a docstring without an Args section - """ + """ # noqa: D214 description = schema.get("description", "No description provided") input_schema = schema.get("inputSchema", {}) properties = input_schema.get("properties", {}) @@ -501,8 +513,7 @@ def _create_docstring(self, schema: dict[str, Any]) -> str: return "\n".join(docstring_parts) def _create_namespace(self) -> dict[str, Any]: - """ - Create the execution namespace for dynamically generated functions. + """Create the execution namespace for dynamically generated functions. This method builds a dictionary containing all the Python built-ins, types, and references that the generated function code needs at runtime. The namespace @@ -563,8 +574,7 @@ def server__tool(param: str = None): } def clear_cache(self) -> None: - """ - Clear the internal function compilation cache. + """Clear the internal function compilation cache. This method removes all cached function templates created by previous calls to create_function_from_schema(). After clearing, subsequent calls to diff --git a/src/mcpd/mcpd_client.py b/src/mcpd/mcpd_client.py index 5e20e6e..acfef38 100644 --- a/src/mcpd/mcpd_client.py +++ b/src/mcpd/mcpd_client.py @@ -1,4 +1,16 @@ -from typing import Any, Callable, Union +"""mcpd client for MCP server management and tool execution. + +This module provides the main McpdClient class that interfaces with the mcpd +daemon to manage interactions with MCP servers and execute tools. It offers +multiple interaction patterns including direct API calls, dynamic calling +syntax, and agent-ready function generation. + +The client handles authentication, error management, and provides a unified +interface for working with multiple MCP servers through the mcpd daemon. +""" + +from collections.abc import Callable +from typing import Any import requests @@ -15,8 +27,7 @@ class McpdClient: - """ - Client for interacting with MCP (Model Context Protocol) servers through an mcpd daemon. + """Client for interacting with MCP (Model Context Protocol) servers through an mcpd daemon. The McpdClient provides a high-level interface to discover, inspect, and invoke tools exposed by MCP servers running behind an mcpd daemon proxy/gateway. @@ -40,8 +51,7 @@ class McpdClient: """ def __init__(self, api_endpoint: str, api_key: str | None = None): - """ - Initialize a new McpdClient instance. + """Initialize a new McpdClient instance. Args: api_endpoint: The base URL of the mcpd daemon (e.g., "http://localhost:8090"). @@ -79,8 +89,7 @@ def __init__(self, api_endpoint: str, api_key: str | None = None): self.call = DynamicCaller(self) def _perform_call(self, server_name: str, tool_name: str, params: dict[str, Any]) -> Any: - """ - Perform the actual API call to execute a tool on an MCP server. + """Perform the actual API call to execute a tool on an MCP server. This method handles the low-level HTTP communication with the mcpd daemon and maps various failure modes to specific exception types. It is used @@ -128,7 +137,7 @@ def _perform_call(self, server_name: str, tool_name: str, params: dict[str, Any] raise ConnectionError(f"Cannot connect to mcpd daemon at {self._endpoint}: {e}") from e except requests.exceptions.Timeout as e: raise TimeoutError( - f"Tool execution timed out after 30 seconds", operation=f"{server_name}.{tool_name}", timeout=30 + "Tool execution timed out after 30 seconds", operation=f"{server_name}.{tool_name}", timeout=30 ) from e except requests.exceptions.HTTPError as e: if e.response.status_code == 401: @@ -153,8 +162,7 @@ def _perform_call(self, server_name: str, tool_name: str, params: dict[str, Any] raise McpdError(f"Error calling tool '{tool_name}' on server '{server_name}': {e}") from e def servers(self) -> list[str]: - """ - Retrieve a list of all available MCP server names. + """Retrieve a list of all available MCP server names. Queries the mcpd daemon to discover all configured and running MCP servers. Server names can be used with other methods to inspect tools or invoke them. @@ -187,7 +195,7 @@ def servers(self) -> list[str]: except requests.exceptions.ConnectionError as e: raise ConnectionError(f"Cannot connect to mcpd daemon at {self._endpoint}: {e}") from e except requests.exceptions.Timeout as e: - raise TimeoutError(f"Request timed out after 5 seconds", operation="list servers", timeout=5) from e + raise TimeoutError("Request timed out after 5 seconds", operation="list servers", timeout=5) from e except requests.exceptions.HTTPError as e: if e.response.status_code == 401: raise AuthenticationError(f"Authentication failed: {e}") from e @@ -203,8 +211,7 @@ def servers(self) -> list[str]: raise McpdError(f"Error listing servers: {e}") from e def tools(self, server_name: str | None = None) -> dict[str, list[dict]] | list[dict]: - """ - Retrieve tool schema definitions from one or all MCP servers. + """Retrieve tool schema definitions from one or all MCP servers. Tool schemas describe the available tools, their parameters, and expected types. These schemas follow the JSON Schema specification and can be used to validate @@ -279,7 +286,7 @@ def _get_tool_definitions(self, server_name: str) -> list[dict[str, Any]]: raise ConnectionError(f"Cannot connect to mcpd daemon at {self._endpoint}: {e}") from e except requests.exceptions.Timeout as e: raise TimeoutError( - f"Request timed out after 5 seconds", operation=f"list tools for {server_name}", timeout=5 + "Request timed out after 5 seconds", operation=f"list tools for {server_name}", timeout=5 ) from e except requests.exceptions.HTTPError as e: if e.response.status_code == 401: @@ -292,8 +299,7 @@ def _get_tool_definitions(self, server_name: str) -> list[dict[str, Any]]: raise McpdError(f"Error listing tool definitions for server '{server_name}': {e}") from e def agent_tools(self) -> list[Callable[..., Any]]: - """ - Generate callable Python functions for all available tools, suitable for AI agents. + """Generate callable Python functions for all available tools, suitable for AI agents. This method queries all servers via `tools()` and creates self-contained, deepcopy-safe functions that can be passed to agentic frameworks like any-agent, @@ -356,8 +362,7 @@ def agent_tools(self) -> list[Callable[..., Any]]: return agent_tools def has_tool(self, server_name: str, tool_name: str) -> bool: - """ - Check if a specific tool exists on a given server. + """Check if a specific tool exists on a given server. This method queries the server's tool definitions via tools(server_name) and searches for the specified tool. It's useful for validation before attempting @@ -395,8 +400,7 @@ def has_tool(self, server_name: str, tool_name: str) -> bool: return False def clear_agent_tools_cache(self) -> None: - """ - Clear the cache of generated callable functions from agent_tools(). + """Clear the cache of generated callable functions from agent_tools(). This method clears the internal FunctionBuilder cache that stores compiled function templates. Call this when server configurations have changed to diff --git a/src/mcpd/type_converter.py b/src/mcpd/type_converter.py index f7a2309..59d58df 100644 --- a/src/mcpd/type_converter.py +++ b/src/mcpd/type_converter.py @@ -1,5 +1,16 @@ +"""JSON Schema to Python type conversion utilities. + +This module provides the TypeConverter class that handles conversion between +JSON Schema type definitions and Python type annotations. It supports all +standard JSON Schema types including complex constructs like unions (anyOf) +and properly maps nullable types to Python's type system. + +Used primarily by the FunctionBuilder to create accurate type annotations +for dynamically generated functions. +""" + from types import NoneType -from typing import Any, Literal, Union +from typing import Any, Literal class TypeConverter: @@ -7,7 +18,18 @@ class TypeConverter: @staticmethod def json_type_to_python_type(json_type: str, schema_def: dict[str, Any]) -> Any: - """Convert JSON schema types to Python type annotations.""" + """Convert JSON schema types to Python type annotations. + + Maps JSON Schema types to their Python equivalents: + - "string" → str (or Literal for enums) + - "integer" → int + - "number" → int | float + - "boolean" → bool + - "array" → list[T] + - "object" → dict[str, Any] + - "null" → NoneType + - unknown types → Any + """ if json_type == "string": if "enum" in schema_def: enum_values = tuple(schema_def["enum"]) @@ -33,6 +55,8 @@ def json_type_to_python_type(json_type: str, schema_def: dict[str, Any]) -> Any: return list[Any] elif json_type == "object": return dict[str, Any] + elif json_type == "null": + return NoneType else: return Any diff --git a/tests/conftest.py b/tests/conftest.py index 89a0e70..2a7a6b9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,7 +2,7 @@ import pytest -from mcpd import McpdClient, McpdError +from mcpd import McpdClient @pytest.fixture(scope="function") diff --git a/tests/unit/test_dynamic_caller.py b/tests/unit/test_dynamic_caller.py index 6411aa7..58e191c 100644 --- a/tests/unit/test_dynamic_caller.py +++ b/tests/unit/test_dynamic_caller.py @@ -52,7 +52,7 @@ def test_getattr_tool_not_exists(self, server_proxy, mock_client): mock_client.has_tool.return_value = False with pytest.raises(McpdError, match="Tool 'nonexistent_tool' not found on server 'test_server'"): - server_proxy.nonexistent_tool + _ = server_proxy.nonexistent_tool def test_tool_function_execution(self, server_proxy, mock_client): mock_client.has_tool.return_value = True @@ -113,8 +113,8 @@ def test_has_tool_called_for_each_access(self, server_proxy, mock_client): mock_client.has_tool.return_value = True # Access the same tool multiple times - tool1 = server_proxy.test_tool - tool2 = server_proxy.test_tool + _ = server_proxy.test_tool + _ = server_proxy.test_tool # has_tool should be called each time assert mock_client.has_tool.call_count == 2 @@ -156,8 +156,6 @@ def test_multiple_servers_and_tools(self, mock_client): assert result3 == {"result": "success"} # Verify all calls were made correctly - expected_calls = [(("server1", "tool1"), {}), (("server2", "tool2"), {}), (("server1", "tool3"), {})] - has_tool_calls = mock_client.has_tool.call_args_list perform_call_calls = mock_client._perform_call.call_args_list diff --git a/tests/unit/test_exceptions.py b/tests/unit/test_exceptions.py index c05ad44..9b36fee 100644 --- a/tests/unit/test_exceptions.py +++ b/tests/unit/test_exceptions.py @@ -48,7 +48,7 @@ def test_mcpd_error_raising(self): def test_mcpd_error_catching_as_exception(self): """Test that McpdError can be caught as Exception.""" - with pytest.raises(Exception): + with pytest.raises(McpdError): raise McpdError("Test error") def test_mcpd_error_chaining(self): diff --git a/tests/unit/test_function_builder.py b/tests/unit/test_function_builder.py index ab19239..b17274b 100644 --- a/tests/unit/test_function_builder.py +++ b/tests/unit/test_function_builder.py @@ -107,11 +107,11 @@ def test_create_function_from_schema_optional_params(self, function_builder): func = function_builder.create_function_from_schema(schema, "test_server") # Test with only required param - result = func(param1="test") + _ = func(param1="test") function_builder._client._perform_call.assert_called_with("test_server", "test_tool", {"param1": "test"}) # Test with optional param - result = func(param1="test", param2="optional") + _ = func(param1="test", param2="optional") function_builder._client._perform_call.assert_called_with( "test_server", "test_tool", {"param1": "test", "param2": "optional"} ) @@ -124,7 +124,7 @@ def test_create_function_from_schema_no_params(self, function_builder): } func = function_builder.create_function_from_schema(schema, "test_server") - result = func() + _ = func() function_builder._client._perform_call.assert_called_once_with("test_server", "test_tool", {}) @@ -157,7 +157,7 @@ def test_create_annotations_basic_types(self, function_builder): annotations = function_builder._create_annotations(schema) - assert annotations["str_param"] == str + assert annotations["str_param"] is str assert annotations["int_param"] == (int | None) assert annotations["bool_param"] == (bool | None) diff --git a/tests/unit/test_type_converter.py b/tests/unit/test_type_converter.py index e2963d2..8d3d8b7 100644 --- a/tests/unit/test_type_converter.py +++ b/tests/unit/test_type_converter.py @@ -6,7 +6,7 @@ class TestTypeConverter: def test_json_type_to_python_type_string(self): result = TypeConverter.json_type_to_python_type("string", {}) - assert result == str + assert result is str def test_json_type_to_python_type_string_with_enum(self): schema = {"enum": ["option1", "option2", "option3"]} @@ -29,11 +29,17 @@ def test_json_type_to_python_type_number(self): def test_json_type_to_python_type_integer(self): result = TypeConverter.json_type_to_python_type("integer", {}) - assert result == int + assert result is int def test_json_type_to_python_type_boolean(self): result = TypeConverter.json_type_to_python_type("boolean", {}) - assert result == bool + assert result is bool + + def test_json_type_to_python_type_null(self): + from types import NoneType + + result = TypeConverter.json_type_to_python_type("null", {}) + assert result is NoneType def test_json_type_to_python_type_array_with_items(self): schema = {"items": {"type": "string"}} @@ -55,7 +61,7 @@ def test_json_type_to_python_type_unknown(self): def test_parse_schema_type_simple_type(self): schema = {"type": "string"} result = TypeConverter.parse_schema_type(schema) - assert result == str + assert result is str def test_parse_schema_type_anyof_simple(self): schema = {"anyOf": [{"type": "string"}, {"type": "integer"}]} @@ -136,4 +142,6 @@ def test_parse_schema_type_with_null_in_anyof(self): schema = {"anyOf": [{"type": "string"}, {"type": "null"}]} result = TypeConverter.parse_schema_type(schema) # Should handle null type properly - assert result == (str | Any) # null maps to Any in this implementation + from types import NoneType + + assert result == (str | NoneType) # null maps to NoneType