From 692e73471d4cb7d2cda24049c770fca97154896d Mon Sep 17 00:00:00 2001 From: Richard Gebhardt Date: Fri, 27 Mar 2026 12:12:29 -0400 Subject: [PATCH] feat: allow circuit breaker configuration and update locks Signed-off-by: Richard Gebhardt --- .../oci_cloud_guard_mcp_server/__init__.py | 2 +- .../oci_cloud_guard_mcp_server/server.py | 17 ++++- src/oci-cloud-guard-mcp-server/pyproject.toml | 2 +- src/oci-cloud-guard-mcp-server/uv.lock | 2 +- .../oracle/oci_cloud_mcp_server/__init__.py | 2 +- .../oracle/oci_cloud_mcp_server/server.py | 33 ++++++++- .../tests/test_server_extras.py | 22 ++++++ src/oci-cloud-mcp-server/pyproject.toml | 2 +- src/oci-cloud-mcp-server/uv.lock | 2 +- .../__init__.py | 2 +- .../server.py | 19 ++++- .../pyproject.toml | 2 +- .../uv.lock | 2 +- .../oracle/oci_compute_mcp_server/__init__.py | 2 +- .../oracle/oci_compute_mcp_server/server.py | 17 ++++- src/oci-compute-mcp-server/pyproject.toml | 2 +- src/oci-compute-mcp-server/uv.lock | 2 +- .../oci_database_mcp_server/__init__.py | 2 +- .../oracle/oci_database_mcp_server/server.py | 19 ++++- .../tests/test_database.py | 41 +++++++++- src/oci-database-mcp-server/pyproject.toml | 2 +- src/oci-database-mcp-server/uv.lock | 2 +- .../oracle/oci_faaas_mcp_server/__init__.py | 2 +- .../oracle/oci_faaas_mcp_server/server.py | 17 ++++- src/oci-faaas-mcp-server/pyproject.toml | 2 +- src/oci-faaas-mcp-server/uv.lock | 2 +- .../oci_identity_mcp_server/__init__.py | 2 +- .../oracle/oci_identity_mcp_server/server.py | 17 ++++- src/oci-identity-mcp-server/pyproject.toml | 2 +- src/oci-identity-mcp-server/uv.lock | 2 +- .../oracle/oci_limits_mcp_server/__init__.py | 2 +- .../oracle/oci_limits_mcp_server/server.py | 17 ++++- .../tests/test_limit_tool.py | 43 ++++++++++- src/oci-limits-mcp-server/pyproject.toml | 2 +- src/oci-limits-mcp-server/uv.lock | 2 +- .../oci_load_balancer_mcp_server/__init__.py | 2 +- .../oci_load_balancer_mcp_server/server.py | 17 ++++- .../tests/test_load_balancer_tools.py | 47 +++++++++++- .../pyproject.toml | 2 +- src/oci-load-balancer-mcp-server/uv.lock | 2 +- .../oracle/oci_logging_mcp_server/__init__.py | 2 +- .../oracle/oci_logging_mcp_server/server.py | 19 ++++- .../tests/test_logging_tools.py | 74 +++++++++++++++++++ src/oci-logging-mcp-server/pyproject.toml | 2 +- src/oci-logging-mcp-server/uv.lock | 2 +- .../oci_migration_mcp_server/__init__.py | 2 +- .../oracle/oci_migration_mcp_server/server.py | 17 ++++- src/oci-migration-mcp-server/pyproject.toml | 2 +- src/oci-migration-mcp-server/uv.lock | 2 +- .../oci_monitoring_mcp_server/__init__.py | 2 +- .../oci_monitoring_mcp_server/server.py | 17 ++++- src/oci-monitoring-mcp-server/pyproject.toml | 2 +- src/oci-monitoring-mcp-server/uv.lock | 2 +- .../__init__.py | 2 +- .../server.py | 19 ++++- .../pyproject.toml | 2 +- .../uv.lock | 2 +- .../oci_networking_mcp_server/__init__.py | 2 +- .../oci_networking_mcp_server/server.py | 17 ++++- src/oci-networking-mcp-server/pyproject.toml | 4 +- src/oci-networking-mcp-server/uv.lock | 2 +- .../oci_object_storage_mcp_server/__init__.py | 2 +- .../oci_object_storage_mcp_server/server.py | 17 ++++- .../pyproject.toml | 2 +- src/oci-object-storage-mcp-server/uv.lock | 2 +- .../oci_recovery_mcp_server/__init__.py | 2 +- .../oracle/oci_recovery_mcp_server/server.py | 36 ++++++--- .../tests/test_recovery_tools.py | 51 +++++++++++++ src/oci-recovery-mcp-server/pyproject.toml | 2 +- src/oci-recovery-mcp-server/uv.lock | 4 +- .../oci_registry_mcp_server/__init__.py | 2 +- .../oracle/oci_registry_mcp_server/server.py | 17 ++++- src/oci-registry-mcp-server/pyproject.toml | 2 +- src/oci-registry-mcp-server/uv.lock | 2 +- .../__init__.py | 2 +- .../oci_resource_search_mcp_server/server.py | 17 ++++- .../pyproject.toml | 2 +- src/oci-resource-search-mcp-server/uv.lock | 2 +- .../oracle/oci_usage_mcp_server/__init__.py | 2 +- .../oracle/oci_usage_mcp_server/server.py | 17 ++++- src/oci-usage-mcp-server/pyproject.toml | 2 +- src/oci-usage-mcp-server/uv.lock | 2 +- 82 files changed, 669 insertions(+), 93 deletions(-) diff --git a/src/oci-cloud-guard-mcp-server/oracle/oci_cloud_guard_mcp_server/__init__.py b/src/oci-cloud-guard-mcp-server/oracle/oci_cloud_guard_mcp_server/__init__.py index a6fa8160..ee6bc89f 100644 --- a/src/oci-cloud-guard-mcp-server/oracle/oci_cloud_guard_mcp_server/__init__.py +++ b/src/oci-cloud-guard-mcp-server/oracle/oci_cloud_guard_mcp_server/__init__.py @@ -5,4 +5,4 @@ """ __project__ = "oracle.oci-cloud-guard-mcp-server" -__version__ = "1.1.4" +__version__ = "1.1.5" diff --git a/src/oci-cloud-guard-mcp-server/oracle/oci_cloud_guard_mcp_server/server.py b/src/oci-cloud-guard-mcp-server/oracle/oci_cloud_guard_mcp_server/server.py index dc2065a3..5f9004be 100644 --- a/src/oci-cloud-guard-mcp-server/oracle/oci_cloud_guard_mcp_server/server.py +++ b/src/oci-cloud-guard-mcp-server/oracle/oci_cloud_guard_mcp_server/server.py @@ -25,6 +25,21 @@ mcp = FastMCP(name=__project__) +def _get_oci_client_kwargs(signer=None): + kwargs = { + "circuit_breaker_strategy": oci.circuit_breaker.CircuitBreakerStrategy( + failure_threshold=int(os.getenv("OCI_CIRCUIT_BREAKER_FAILURE_THRESHOLD", "10")), + recovery_timeout=int(os.getenv("OCI_CIRCUIT_BREAKER_RECOVERY_TIMEOUT", "30")), + ), + "circuit_breaker_callback": lambda exc: logger.warning( + "Circuit breaker triggered: %s", exc + ), + } + if signer is not None: + kwargs["signer"] = signer + return kwargs + + def get_cloud_guard_client(): config = oci.config.from_file( file_location=os.getenv("OCI_CONFIG_FILE", oci.config.DEFAULT_LOCATION), @@ -40,7 +55,7 @@ def get_cloud_guard_client(): with open(token_file, "r") as f: token = f.read() signer = oci.auth.signers.SecurityTokenSigner(token, private_key) - return CloudGuardClient(config, signer=signer) + return CloudGuardClient(config, **_get_oci_client_kwargs(signer)) @mcp.tool( diff --git a/src/oci-cloud-guard-mcp-server/pyproject.toml b/src/oci-cloud-guard-mcp-server/pyproject.toml index b6d12791..e46538bc 100644 --- a/src/oci-cloud-guard-mcp-server/pyproject.toml +++ b/src/oci-cloud-guard-mcp-server/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "oracle.oci-cloud-guard-mcp-server" -version = "1.1.4" +version = "1.1.5" description = "OCI Cloud Guard Service MCP server" readme = "README.md" requires-python = ">=3.13" diff --git a/src/oci-cloud-guard-mcp-server/uv.lock b/src/oci-cloud-guard-mcp-server/uv.lock index f494b088..803c82b2 100644 --- a/src/oci-cloud-guard-mcp-server/uv.lock +++ b/src/oci-cloud-guard-mcp-server/uv.lock @@ -783,7 +783,7 @@ wheels = [ [[package]] name = "oracle-oci-cloud-guard-mcp-server" -version = "1.1.4" +version = "1.1.5" source = { editable = "." } dependencies = [ { name = "fastmcp" }, diff --git a/src/oci-cloud-mcp-server/oracle/oci_cloud_mcp_server/__init__.py b/src/oci-cloud-mcp-server/oracle/oci_cloud_mcp_server/__init__.py index 2f83381f..9a42b180 100644 --- a/src/oci-cloud-mcp-server/oracle/oci_cloud_mcp_server/__init__.py +++ b/src/oci-cloud-mcp-server/oracle/oci_cloud_mcp_server/__init__.py @@ -5,4 +5,4 @@ """ __project__ = "oracle.oci-cloud-mcp-server" -__version__ = "1.1.3" +__version__ = "1.1.4" diff --git a/src/oci-cloud-mcp-server/oracle/oci_cloud_mcp_server/server.py b/src/oci-cloud-mcp-server/oracle/oci_cloud_mcp_server/server.py index d8e07575..2b210851 100644 --- a/src/oci-cloud-mcp-server/oracle/oci_cloud_mcp_server/server.py +++ b/src/oci-cloud-mcp-server/oracle/oci_cloud_mcp_server/server.py @@ -35,6 +35,21 @@ _user_agent_name = __project__.split("oracle.", 1)[1].split("-server", 1)[0] _ADDITIONAL_UA = f"{_user_agent_name}/{__version__}" + +def _get_oci_client_kwargs(signer=None): + kwargs = { + "circuit_breaker_strategy": oci.circuit_breaker.CircuitBreakerStrategy( + failure_threshold=int(os.getenv("OCI_CIRCUIT_BREAKER_FAILURE_THRESHOLD", "10")), + recovery_timeout=int(os.getenv("OCI_CIRCUIT_BREAKER_RECOVERY_TIMEOUT", "30")), + ), + "circuit_breaker_callback": lambda exc: logger.warning( + "Circuit breaker triggered: %s", exc + ), + } + if signer is not None: + kwargs["signer"] = signer + return kwargs + # Backwards-compat pagination allowlist placeholder. # Heuristic detection handles most cases; keep an empty set to avoid NameError # in any residual fallback checks. @@ -103,7 +118,23 @@ def _import_client(client_fqn: str) -> Any: if not inspect.isclass(cls): raise ValueError(f"{client_fqn} is not a class") config, signer = _get_config_and_signer() - instance = cls(config, signer=signer) + client_kwargs = _get_oci_client_kwargs(signer) + try: + init_signature = inspect.signature(cls.__init__) + supports_kwargs = any( + param.kind == inspect.Parameter.VAR_KEYWORD + for param in init_signature.parameters.values() + ) + except (TypeError, ValueError): + supports_kwargs = True + + if supports_kwargs: + instance = cls(config, **client_kwargs) + else: + filtered_kwargs = { + key: value for key, value in client_kwargs.items() if key in init_signature.parameters + } + instance = cls(config, **filtered_kwargs) return instance diff --git a/src/oci-cloud-mcp-server/oracle/oci_cloud_mcp_server/tests/test_server_extras.py b/src/oci-cloud-mcp-server/oracle/oci_cloud_mcp_server/tests/test_server_extras.py index 27cc006b..9a76318e 100644 --- a/src/oci-cloud-mcp-server/oracle/oci_cloud_mcp_server/tests/test_server_extras.py +++ b/src/oci-cloud-mcp-server/oracle/oci_cloud_mcp_server/tests/test_server_extras.py @@ -7,6 +7,7 @@ from types import SimpleNamespace from unittest.mock import mock_open, patch +import oci import pytest from fastmcp import Client from fastmcp.exceptions import ToolError @@ -294,6 +295,27 @@ def __init__(self, config, signer): inst = _import_client("x.y.FakeClient") assert isinstance(inst, FakeClient) + def test_import_client_passes_circuit_breaker_to_kwargs_capable_client(self): + class FakeClient: + def __init__(self, config, **kwargs): + self.config = config + self.kwargs = kwargs + + fake_module = SimpleNamespace(FakeClient=FakeClient) + + with ( + patch("oracle.oci_cloud_mcp_server.server.import_module") as m_import, + patch("oracle.oci_cloud_mcp_server.server._get_config_and_signer") as m_cfg, + ): + signer = object() + m_import.return_value = fake_module + m_cfg.return_value = ({"k": "v"}, signer) + inst = _import_client("x.y.FakeClient") + + assert isinstance(inst.kwargs["circuit_breaker_strategy"], oci.circuit_breaker.CircuitBreakerStrategy) + assert callable(inst.kwargs["circuit_breaker_callback"]) + assert inst.kwargs["signer"] is signer + class TestInvokeErrors: @pytest.mark.asyncio diff --git a/src/oci-cloud-mcp-server/pyproject.toml b/src/oci-cloud-mcp-server/pyproject.toml index 856a4496..99ff9f51 100644 --- a/src/oci-cloud-mcp-server/pyproject.toml +++ b/src/oci-cloud-mcp-server/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "oracle.oci-cloud-mcp-server" -version = "1.1.3" +version = "1.1.4" description = "OCI Python SDK MCP server" readme = "README.md" requires-python = ">=3.13" diff --git a/src/oci-cloud-mcp-server/uv.lock b/src/oci-cloud-mcp-server/uv.lock index 3507cf05..06e7a632 100644 --- a/src/oci-cloud-mcp-server/uv.lock +++ b/src/oci-cloud-mcp-server/uv.lock @@ -727,7 +727,7 @@ wheels = [ [[package]] name = "oracle-oci-cloud-mcp-server" -version = "1.1.3" +version = "1.1.4" source = { editable = "." } dependencies = [ { name = "fastmcp" }, diff --git a/src/oci-compute-instance-agent-mcp-server/oracle/oci_compute_instance_agent_mcp_server/__init__.py b/src/oci-compute-instance-agent-mcp-server/oracle/oci_compute_instance_agent_mcp_server/__init__.py index 0e724d74..4b273815 100644 --- a/src/oci-compute-instance-agent-mcp-server/oracle/oci_compute_instance_agent_mcp_server/__init__.py +++ b/src/oci-compute-instance-agent-mcp-server/oracle/oci_compute_instance_agent_mcp_server/__init__.py @@ -5,4 +5,4 @@ """ __project__ = "oracle.oci-compute-instance-agent-mcp-server" -__version__ = "2.1.4" +__version__ = "2.1.5" diff --git a/src/oci-compute-instance-agent-mcp-server/oracle/oci_compute_instance_agent_mcp_server/server.py b/src/oci-compute-instance-agent-mcp-server/oracle/oci_compute_instance_agent_mcp_server/server.py index a127dc1f..53522e31 100644 --- a/src/oci-compute-instance-agent-mcp-server/oracle/oci_compute_instance_agent_mcp_server/server.py +++ b/src/oci-compute-instance-agent-mcp-server/oracle/oci_compute_instance_agent_mcp_server/server.py @@ -33,6 +33,21 @@ mcp = FastMCP(name=__project__) +def _get_oci_client_kwargs(signer=None): + kwargs = { + "circuit_breaker_strategy": oci.circuit_breaker.CircuitBreakerStrategy( + failure_threshold=int(os.getenv("OCI_CIRCUIT_BREAKER_FAILURE_THRESHOLD", "10")), + recovery_timeout=int(os.getenv("OCI_CIRCUIT_BREAKER_RECOVERY_TIMEOUT", "30")), + ), + "circuit_breaker_callback": lambda exc: logger.warning( + "Circuit breaker triggered: %s", exc + ), + } + if signer is not None: + kwargs["signer"] = signer + return kwargs + + def get_compute_instance_agent_client(): logger.info("entering get_compute_instance_agent_client") config = oci.config.from_file( @@ -49,7 +64,9 @@ def get_compute_instance_agent_client(): with open(token_file, "r") as f: token = f.read() signer = oci.auth.signers.SecurityTokenSigner(token, private_key) - return oci.compute_instance_agent.ComputeInstanceAgentClient(config, signer=signer) + return oci.compute_instance_agent.ComputeInstanceAgentClient( + config, **_get_oci_client_kwargs(signer) + ) @mcp.tool( diff --git a/src/oci-compute-instance-agent-mcp-server/pyproject.toml b/src/oci-compute-instance-agent-mcp-server/pyproject.toml index 17db519d..803b2032 100644 --- a/src/oci-compute-instance-agent-mcp-server/pyproject.toml +++ b/src/oci-compute-instance-agent-mcp-server/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "oracle.oci-compute-instance-agent-mcp-server" -version = "2.1.4" +version = "2.1.5" description = "OCI Compute Instance Agent MCP Server" readme = "README.md" requires-python = ">=3.13" diff --git a/src/oci-compute-instance-agent-mcp-server/uv.lock b/src/oci-compute-instance-agent-mcp-server/uv.lock index 0885a14f..c4cf81c9 100644 --- a/src/oci-compute-instance-agent-mcp-server/uv.lock +++ b/src/oci-compute-instance-agent-mcp-server/uv.lock @@ -783,7 +783,7 @@ wheels = [ [[package]] name = "oracle-oci-compute-instance-agent-mcp-server" -version = "2.1.4" +version = "2.1.5" source = { editable = "." } dependencies = [ { name = "fastmcp" }, diff --git a/src/oci-compute-mcp-server/oracle/oci_compute_mcp_server/__init__.py b/src/oci-compute-mcp-server/oracle/oci_compute_mcp_server/__init__.py index 36488fba..603923ba 100644 --- a/src/oci-compute-mcp-server/oracle/oci_compute_mcp_server/__init__.py +++ b/src/oci-compute-mcp-server/oracle/oci_compute_mcp_server/__init__.py @@ -5,4 +5,4 @@ """ __project__ = "oracle.oci-compute-mcp-server" -__version__ = "1.2.4" +__version__ = "1.2.5" diff --git a/src/oci-compute-mcp-server/oracle/oci_compute_mcp_server/server.py b/src/oci-compute-mcp-server/oracle/oci_compute_mcp_server/server.py index 7007f88b..824a2517 100644 --- a/src/oci-compute-mcp-server/oracle/oci_compute_mcp_server/server.py +++ b/src/oci-compute-mcp-server/oracle/oci_compute_mcp_server/server.py @@ -35,6 +35,21 @@ mcp = FastMCP(name=__project__) +def _get_oci_client_kwargs(signer=None): + kwargs = { + "circuit_breaker_strategy": oci.circuit_breaker.CircuitBreakerStrategy( + failure_threshold=int(os.getenv("OCI_CIRCUIT_BREAKER_FAILURE_THRESHOLD", "10")), + recovery_timeout=int(os.getenv("OCI_CIRCUIT_BREAKER_RECOVERY_TIMEOUT", "30")), + ), + "circuit_breaker_callback": lambda exc: logger.warning( + "Circuit breaker triggered: %s", exc + ), + } + if signer is not None: + kwargs["signer"] = signer + return kwargs + + def get_compute_client(): logger.info("entering get_compute_client") config = oci.config.from_file( @@ -50,7 +65,7 @@ def get_compute_client(): with open(token_file, "r") as f: token = f.read() signer = oci.auth.signers.SecurityTokenSigner(token, private_key) - return oci.core.ComputeClient(config, signer=signer) + return oci.core.ComputeClient(config, **_get_oci_client_kwargs(signer)) @mcp.tool(description="List Instances in a given compartment") diff --git a/src/oci-compute-mcp-server/pyproject.toml b/src/oci-compute-mcp-server/pyproject.toml index e5ede404..b2a42234 100644 --- a/src/oci-compute-mcp-server/pyproject.toml +++ b/src/oci-compute-mcp-server/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "oracle.oci-compute-mcp-server" -version = "1.2.4" +version = "1.2.5" description = "OCI Compute Service MCP server" readme = "README.md" requires-python = ">=3.13" diff --git a/src/oci-compute-mcp-server/uv.lock b/src/oci-compute-mcp-server/uv.lock index e2d1e90e..5b463ae6 100644 --- a/src/oci-compute-mcp-server/uv.lock +++ b/src/oci-compute-mcp-server/uv.lock @@ -783,7 +783,7 @@ wheels = [ [[package]] name = "oracle-oci-compute-mcp-server" -version = "1.2.4" +version = "1.2.5" source = { editable = "." } dependencies = [ { name = "fastmcp" }, diff --git a/src/oci-database-mcp-server/oracle/oci_database_mcp_server/__init__.py b/src/oci-database-mcp-server/oracle/oci_database_mcp_server/__init__.py index dfe5111c..2ed48ca0 100644 --- a/src/oci-database-mcp-server/oracle/oci_database_mcp_server/__init__.py +++ b/src/oci-database-mcp-server/oracle/oci_database_mcp_server/__init__.py @@ -5,4 +5,4 @@ """ __project__ = "oracle.oci-database-mcp-server" -__version__ = "1.0.5" +__version__ = "1.0.6" diff --git a/src/oci-database-mcp-server/oracle/oci_database_mcp_server/server.py b/src/oci-database-mcp-server/oracle/oci_database_mcp_server/server.py index c7b747e0..6f8778d3 100644 --- a/src/oci-database-mcp-server/oracle/oci_database_mcp_server/server.py +++ b/src/oci-database-mcp-server/oracle/oci_database_mcp_server/server.py @@ -283,6 +283,21 @@ mcp = FastMCP(name=__project__) +def _get_oci_client_kwargs(signer=None): + kwargs = { + "circuit_breaker_strategy": oci.circuit_breaker.CircuitBreakerStrategy( + failure_threshold=int(os.getenv("OCI_CIRCUIT_BREAKER_FAILURE_THRESHOLD", "10")), + recovery_timeout=int(os.getenv("OCI_CIRCUIT_BREAKER_RECOVERY_TIMEOUT", "30")), + ), + "circuit_breaker_callback": lambda exc: logger.warning( + "Circuit breaker triggered: %s", exc + ), + } + if signer is not None: + kwargs["signer"] = signer + return kwargs + + def get_database_client(region: str = None): config = oci.config.from_file( file_location=os.getenv("OCI_CONFIG_FILE", oci.config.DEFAULT_LOCATION), @@ -296,10 +311,10 @@ def get_database_client(region: str = None): token = f.read() signer = oci.auth.signers.SecurityTokenSigner(token, private_key) if region is None: - return oci.database.DatabaseClient(config, signer=signer) + return oci.database.DatabaseClient(config, **_get_oci_client_kwargs(signer)) regional_config = config.copy() regional_config["region"] = region - return oci.database.DatabaseClient(regional_config, signer=signer) + return oci.database.DatabaseClient(regional_config, **_get_oci_client_kwargs(signer)) def call_create_pdb(client, details, opc_retry_token=None, opc_request_id=None): diff --git a/src/oci-database-mcp-server/oracle/oci_database_mcp_server/tests/test_database.py b/src/oci-database-mcp-server/oracle/oci_database_mcp_server/tests/test_database.py index ee35cc14..c2471f91 100644 --- a/src/oci-database-mcp-server/oracle/oci_database_mcp_server/tests/test_database.py +++ b/src/oci-database-mcp-server/oracle/oci_database_mcp_server/tests/test_database.py @@ -1,11 +1,50 @@ -from unittest.mock import MagicMock, create_autospec, patch +from unittest.mock import MagicMock, create_autospec, mock_open, patch import oci import pytest from fastmcp import Client +import oracle.oci_database_mcp_server.server as server from oracle.oci_database_mcp_server.server import mcp +class TestGetDatabaseClient: + @patch("oracle.oci_database_mcp_server.server.oci.database.DatabaseClient") + @patch("oracle.oci_database_mcp_server.server.oci.auth.signers.SecurityTokenSigner") + @patch("oracle.oci_database_mcp_server.server.oci.signer.load_private_key_from_file") + @patch( + "oracle.oci_database_mcp_server.server.open", + new_callable=mock_open, + read_data="SECURITY_TOKEN", + ) + @patch("oracle.oci_database_mcp_server.server.oci.config.from_file") + @patch("oracle.oci_database_mcp_server.server.os.getenv") + def test_get_database_client_passes_circuit_breaker_and_region( + self, + mock_getenv, + mock_from_file, + mock_open_file, + mock_load_private_key, + mock_security_token_signer, + mock_client, + ): + mock_getenv.side_effect = lambda k, default=None: default + config = {"key_file": "/key.pem", "security_token_file": "/token", "region": "us-ashburn-1"} + mock_from_file.return_value = config + private_key_obj = object() + mock_load_private_key.return_value = private_key_obj + + result = server.get_database_client(region="us-phoenix-1") + + mock_open_file.assert_called_once_with("/token", "r") + mock_security_token_signer.assert_called_once_with("SECURITY_TOKEN", private_key_obj) + args, kwargs = mock_client.call_args + assert args[0]["region"] == "us-phoenix-1" + assert kwargs["signer"] is mock_security_token_signer.return_value + assert isinstance(kwargs["circuit_breaker_strategy"], oci.circuit_breaker.CircuitBreakerStrategy) + assert callable(kwargs["circuit_breaker_callback"]) + assert result is mock_client.return_value + + @pytest.mark.asyncio @patch("oracle.oci_database_mcp_server.server.get_database_client") async def test_list_application_vips(mock_get_client): diff --git a/src/oci-database-mcp-server/pyproject.toml b/src/oci-database-mcp-server/pyproject.toml index 62b38d9b..6b3bf3fa 100644 --- a/src/oci-database-mcp-server/pyproject.toml +++ b/src/oci-database-mcp-server/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "oracle.oci-database-mcp-server" -version = "1.0.5" +version = "1.0.6" description = "OCI Database Service MCP server" readme = "README.md" requires-python = ">=3.13" diff --git a/src/oci-database-mcp-server/uv.lock b/src/oci-database-mcp-server/uv.lock index c3ab72fb..b289aabc 100644 --- a/src/oci-database-mcp-server/uv.lock +++ b/src/oci-database-mcp-server/uv.lock @@ -783,7 +783,7 @@ wheels = [ [[package]] name = "oracle-oci-database-mcp-server" -version = "1.0.5" +version = "1.0.6" source = { editable = "." } dependencies = [ { name = "fastmcp" }, diff --git a/src/oci-faaas-mcp-server/oracle/oci_faaas_mcp_server/__init__.py b/src/oci-faaas-mcp-server/oracle/oci_faaas_mcp_server/__init__.py index 304beee0..34f11617 100644 --- a/src/oci-faaas-mcp-server/oracle/oci_faaas_mcp_server/__init__.py +++ b/src/oci-faaas-mcp-server/oracle/oci_faaas_mcp_server/__init__.py @@ -5,4 +5,4 @@ """ __project__ = "oracle.oci-faaas-mcp-server" -__version__ = "1.0.3" +__version__ = "1.0.4" diff --git a/src/oci-faaas-mcp-server/oracle/oci_faaas_mcp_server/server.py b/src/oci-faaas-mcp-server/oracle/oci_faaas_mcp_server/server.py index d1ffbaf0..6231ce8c 100644 --- a/src/oci-faaas-mcp-server/oracle/oci_faaas_mcp_server/server.py +++ b/src/oci-faaas-mcp-server/oracle/oci_faaas_mcp_server/server.py @@ -27,6 +27,21 @@ mcp = FastMCP(name=__project__) +def _get_oci_client_kwargs(signer=None): + kwargs = { + "circuit_breaker_strategy": oci.circuit_breaker.CircuitBreakerStrategy( + failure_threshold=int(os.getenv("OCI_CIRCUIT_BREAKER_FAILURE_THRESHOLD", "10")), + recovery_timeout=int(os.getenv("OCI_CIRCUIT_BREAKER_RECOVERY_TIMEOUT", "30")), + ), + "circuit_breaker_callback": lambda exc: logger.warning( + "Circuit breaker triggered: %s", exc + ), + } + if signer is not None: + kwargs["signer"] = signer + return kwargs + + def get_faaas_client(): """Initialize and return an OCI Fusion Applications client using security token auth.""" logger.info("entering get_faaas_client") @@ -46,7 +61,7 @@ def get_faaas_client(): token = f.read() signer = oci.auth.signers.SecurityTokenSigner(token, private_key) - return oci.fusion_apps.FusionApplicationsClient(config, signer=signer) + return oci.fusion_apps.FusionApplicationsClient(config, **_get_oci_client_kwargs(signer)) @mcp.tool(description="Returns a list of Fusion Environment Families in the specified compartment.") diff --git a/src/oci-faaas-mcp-server/pyproject.toml b/src/oci-faaas-mcp-server/pyproject.toml index b1b06ad1..7baf09e8 100644 --- a/src/oci-faaas-mcp-server/pyproject.toml +++ b/src/oci-faaas-mcp-server/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "oracle.oci-faaas-mcp-server" -version = "1.0.3" +version = "1.0.4" description = "OCI Fusion Applications (FAaaS) MCP server" readme = "README.md" requires-python = ">=3.13" diff --git a/src/oci-faaas-mcp-server/uv.lock b/src/oci-faaas-mcp-server/uv.lock index 2540ff29..9f09616a 100644 --- a/src/oci-faaas-mcp-server/uv.lock +++ b/src/oci-faaas-mcp-server/uv.lock @@ -784,7 +784,7 @@ wheels = [ [[package]] name = "oracle-oci-faaas-mcp-server" -version = "1.0.3" +version = "1.0.4" source = { editable = "." } dependencies = [ { name = "fastmcp" }, diff --git a/src/oci-identity-mcp-server/oracle/oci_identity_mcp_server/__init__.py b/src/oci-identity-mcp-server/oracle/oci_identity_mcp_server/__init__.py index bbd321de..94009c2a 100644 --- a/src/oci-identity-mcp-server/oracle/oci_identity_mcp_server/__init__.py +++ b/src/oci-identity-mcp-server/oracle/oci_identity_mcp_server/__init__.py @@ -5,4 +5,4 @@ """ __project__ = "oracle.oci-identity-mcp-server" -__version__ = "2.1.5" +__version__ = "2.1.6" diff --git a/src/oci-identity-mcp-server/oracle/oci_identity_mcp_server/server.py b/src/oci-identity-mcp-server/oracle/oci_identity_mcp_server/server.py index 9a229ff2..2533a0ed 100644 --- a/src/oci-identity-mcp-server/oracle/oci_identity_mcp_server/server.py +++ b/src/oci-identity-mcp-server/oracle/oci_identity_mcp_server/server.py @@ -35,6 +35,21 @@ mcp = FastMCP(name=__project__) +def _get_oci_client_kwargs(signer=None): + kwargs = { + "circuit_breaker_strategy": oci.circuit_breaker.CircuitBreakerStrategy( + failure_threshold=int(os.getenv("OCI_CIRCUIT_BREAKER_FAILURE_THRESHOLD", "10")), + recovery_timeout=int(os.getenv("OCI_CIRCUIT_BREAKER_RECOVERY_TIMEOUT", "30")), + ), + "circuit_breaker_callback": lambda exc: logger.warning( + "Circuit breaker triggered: %s", exc + ), + } + if signer is not None: + kwargs["signer"] = signer + return kwargs + + def get_identity_client(): config = oci.config.from_file( file_location=os.getenv("OCI_CONFIG_FILE", oci.config.DEFAULT_LOCATION), @@ -47,7 +62,7 @@ def get_identity_client(): with open(token_file, "r") as f: token = f.read() signer = oci.auth.signers.SecurityTokenSigner(token, private_key) - return oci.identity.IdentityClient(config, signer=signer) + return oci.identity.IdentityClient(config, **_get_oci_client_kwargs(signer)) @mcp.tool(description="List compartments in a given compartment or tenancy.") diff --git a/src/oci-identity-mcp-server/pyproject.toml b/src/oci-identity-mcp-server/pyproject.toml index cf8e1779..0cdb2468 100644 --- a/src/oci-identity-mcp-server/pyproject.toml +++ b/src/oci-identity-mcp-server/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "oracle.oci-identity-mcp-server" -version = "2.1.5" +version = "2.1.6" description = "OCI Identity Service MCP server" readme = "README.md" requires-python = ">=3.13" diff --git a/src/oci-identity-mcp-server/uv.lock b/src/oci-identity-mcp-server/uv.lock index 279db68d..c54d19bd 100644 --- a/src/oci-identity-mcp-server/uv.lock +++ b/src/oci-identity-mcp-server/uv.lock @@ -783,7 +783,7 @@ wheels = [ [[package]] name = "oracle-oci-identity-mcp-server" -version = "2.1.5" +version = "2.1.6" source = { editable = "." } dependencies = [ { name = "fastmcp" }, diff --git a/src/oci-limits-mcp-server/oracle/oci_limits_mcp_server/__init__.py b/src/oci-limits-mcp-server/oracle/oci_limits_mcp_server/__init__.py index fc04a8c4..c234f1f7 100644 --- a/src/oci-limits-mcp-server/oracle/oci_limits_mcp_server/__init__.py +++ b/src/oci-limits-mcp-server/oracle/oci_limits_mcp_server/__init__.py @@ -5,4 +5,4 @@ """ __project__ = "oracle.oci-limits-mcp-server" -__version__ = "1.0.1" +__version__ = "1.0.2" diff --git a/src/oci-limits-mcp-server/oracle/oci_limits_mcp_server/server.py b/src/oci-limits-mcp-server/oracle/oci_limits_mcp_server/server.py index 825b967b..9f4c765a 100644 --- a/src/oci-limits-mcp-server/oracle/oci_limits_mcp_server/server.py +++ b/src/oci-limits-mcp-server/oracle/oci_limits_mcp_server/server.py @@ -30,6 +30,21 @@ mcp = FastMCP(name=__project__) +def _get_oci_client_kwargs(signer=None): + kwargs = { + "circuit_breaker_strategy": oci.circuit_breaker.CircuitBreakerStrategy( + failure_threshold=int(os.getenv("OCI_CIRCUIT_BREAKER_FAILURE_THRESHOLD", "10")), + recovery_timeout=int(os.getenv("OCI_CIRCUIT_BREAKER_RECOVERY_TIMEOUT", "30")), + ), + "circuit_breaker_callback": lambda exc: logger.warning( + "Circuit breaker triggered: %s", exc + ), + } + if signer is not None: + kwargs["signer"] = signer + return kwargs + + def get_limits_client(): """ Build an OCI LimitsClient using Security Token auth (consistent with other servers in this repo). @@ -49,7 +64,7 @@ def get_limits_client(): signer = oci.auth.signers.SecurityTokenSigner(token, private_key) # Limits client - return oci.limits.LimitsClient(config, signer=signer) + return oci.limits.LimitsClient(config, **_get_oci_client_kwargs(signer)) def get_identity_client(): diff --git a/src/oci-limits-mcp-server/oracle/oci_limits_mcp_server/tests/test_limit_tool.py b/src/oci-limits-mcp-server/oracle/oci_limits_mcp_server/tests/test_limit_tool.py index 5eec98ef..6c4e18c8 100644 --- a/src/oci-limits-mcp-server/oracle/oci_limits_mcp_server/tests/test_limit_tool.py +++ b/src/oci-limits-mcp-server/oracle/oci_limits_mcp_server/tests/test_limit_tool.py @@ -4,11 +4,12 @@ https://oss.oracle.com/licenses/upl. """ -from unittest.mock import MagicMock, create_autospec, patch +from unittest.mock import MagicMock, create_autospec, mock_open, patch import oci import pytest from fastmcp import Client +import oracle.oci_limits_mcp_server.server as server from oracle.oci_limits_mcp_server.server import mcp @@ -101,6 +102,46 @@ async def test_list_limit_value(self, mock_get_client): assert len(result) == 1 assert result[0]["name"] == "limit_value1" + +class TestGetClient: + @patch("oracle.oci_limits_mcp_server.server.oci.limits.LimitsClient") + @patch("oracle.oci_limits_mcp_server.server.oci.auth.signers.SecurityTokenSigner") + @patch("oracle.oci_limits_mcp_server.server.oci.signer.load_private_key_from_file") + @patch( + "oracle.oci_limits_mcp_server.server.open", + new_callable=mock_open, + read_data="SECURITY_TOKEN", + ) + @patch("oracle.oci_limits_mcp_server.server.oci.config.from_file") + @patch("oracle.oci_limits_mcp_server.server.os.getenv") + def test_get_limits_client_passes_circuit_breaker( + self, + mock_getenv, + mock_from_file, + mock_open_file, + mock_load_private_key, + mock_security_token_signer, + mock_client, + ): + mock_getenv.side_effect = lambda k, default=None: ( + "MYPROFILE" if k == "OCI_CONFIG_PROFILE" else default + ) + config = {"key_file": "/key.pem", "security_token_file": "/token"} + mock_from_file.return_value = config + private_key_obj = object() + mock_load_private_key.return_value = private_key_obj + + result = server.get_limits_client() + + mock_open_file.assert_called_once_with("/token", "r") + mock_security_token_signer.assert_called_once_with("SECURITY_TOKEN", private_key_obj) + args, kwargs = mock_client.call_args + assert args[0] is config + assert kwargs["signer"] is mock_security_token_signer.return_value + assert isinstance(kwargs["circuit_breaker_strategy"], oci.circuit_breaker.CircuitBreakerStrategy) + assert callable(kwargs["circuit_breaker_callback"]) + assert result is mock_client.return_value + @pytest.mark.asyncio @patch("oracle.oci_limits_mcp_server.server.get_limits_client") async def test_get_resource_availability_ad_scope(self, mock_get_client): diff --git a/src/oci-limits-mcp-server/pyproject.toml b/src/oci-limits-mcp-server/pyproject.toml index 67110c67..b5883352 100644 --- a/src/oci-limits-mcp-server/pyproject.toml +++ b/src/oci-limits-mcp-server/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "oracle.oci-limits-mcp-server" -version = "1.0.1" +version = "1.0.2" description = "OCI Limits MCP server" readme = "README.md" requires-python = ">=3.13" diff --git a/src/oci-limits-mcp-server/uv.lock b/src/oci-limits-mcp-server/uv.lock index 1fcb7e32..8c24e822 100644 --- a/src/oci-limits-mcp-server/uv.lock +++ b/src/oci-limits-mcp-server/uv.lock @@ -686,7 +686,7 @@ wheels = [ [[package]] name = "oracle-oci-limits-mcp-server" -version = "1.0.1" +version = "1.0.2" source = { editable = "." } dependencies = [ { name = "fastmcp" }, diff --git a/src/oci-load-balancer-mcp-server/oracle/oci_load_balancer_mcp_server/__init__.py b/src/oci-load-balancer-mcp-server/oracle/oci_load_balancer_mcp_server/__init__.py index 3ae4e170..c6136f0e 100644 --- a/src/oci-load-balancer-mcp-server/oracle/oci_load_balancer_mcp_server/__init__.py +++ b/src/oci-load-balancer-mcp-server/oracle/oci_load_balancer_mcp_server/__init__.py @@ -5,4 +5,4 @@ """ __project__ = "oracle.oci-load-balancer-mcp-server" -__version__ = "0.0.0" +__version__ = "0.0.2" diff --git a/src/oci-load-balancer-mcp-server/oracle/oci_load_balancer_mcp_server/server.py b/src/oci-load-balancer-mcp-server/oracle/oci_load_balancer_mcp_server/server.py index d4b88d06..936ad0f3 100644 --- a/src/oci-load-balancer-mcp-server/oracle/oci_load_balancer_mcp_server/server.py +++ b/src/oci-load-balancer-mcp-server/oracle/oci_load_balancer_mcp_server/server.py @@ -63,6 +63,21 @@ ) +def _get_oci_client_kwargs(signer=None): + kwargs = { + "circuit_breaker_strategy": oci.circuit_breaker.CircuitBreakerStrategy( + failure_threshold=int(os.getenv("OCI_CIRCUIT_BREAKER_FAILURE_THRESHOLD", "10")), + recovery_timeout=int(os.getenv("OCI_CIRCUIT_BREAKER_RECOVERY_TIMEOUT", "30")), + ), + "circuit_breaker_callback": lambda exc: logger.warning( + "Circuit breaker triggered: %s", exc + ), + } + if signer is not None: + kwargs["signer"] = signer + return kwargs + + def get_load_balancer_client(): logger.info("entering get_load_balancer_client") config = oci.config.from_file( @@ -82,7 +97,7 @@ def get_load_balancer_client(): with open(token_file, "r") as f: token = f.read() signer = oci.auth.signers.SecurityTokenSigner(token, private_key) if token else None - return oci.load_balancer.LoadBalancerClient(config, signer=signer) + return oci.load_balancer.LoadBalancerClient(config, **_get_oci_client_kwargs(signer)) @mcp.tool( diff --git a/src/oci-load-balancer-mcp-server/oracle/oci_load_balancer_mcp_server/tests/test_load_balancer_tools.py b/src/oci-load-balancer-mcp-server/oracle/oci_load_balancer_mcp_server/tests/test_load_balancer_tools.py index 29f643ab..fd571f2f 100644 --- a/src/oci-load-balancer-mcp-server/oracle/oci_load_balancer_mcp_server/tests/test_load_balancer_tools.py +++ b/src/oci-load-balancer-mcp-server/oracle/oci_load_balancer_mcp_server/tests/test_load_balancer_tools.py @@ -7,8 +7,9 @@ """ import types -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, mock_open, patch +import oci import pytest # Import the server module where the tools are defined @@ -29,8 +30,12 @@ def __init__( @pytest.fixture(autouse=True) -def mock_client(monkeypatch): +def mock_client(monkeypatch, request): """Patch ``get_load_balancer_client`` to return a MagicMock client and stub OCI model classes.""" + if request.node.cls is TestGetClient: + yield None + return + # Stub OCI SDK model constructors to accept any kwargs (avoid strict validations during tests) import oci @@ -75,6 +80,44 @@ def __init__(self, **kwargs): yield mock +class TestGetClient: + @patch("oracle.oci_load_balancer_mcp_server.server.oci.load_balancer.LoadBalancerClient") + @patch("oracle.oci_load_balancer_mcp_server.server.oci.auth.signers.SecurityTokenSigner") + @patch("oracle.oci_load_balancer_mcp_server.server.oci.signer.load_private_key_from_file") + @patch( + "oracle.oci_load_balancer_mcp_server.server.open", + new_callable=mock_open, + read_data="SECURITY_TOKEN", + ) + @patch("oracle.oci_load_balancer_mcp_server.server.oci.config.from_file") + @patch("oracle.oci_load_balancer_mcp_server.server.os.getenv") + def test_get_load_balancer_client_passes_circuit_breaker( + self, + mock_getenv, + mock_from_file, + mock_open_file, + mock_load_private_key, + mock_security_token_signer, + mock_client, + ): + mock_getenv.side_effect = lambda k, default=None: default + config = {"key_file": "/key.pem", "security_token_file": "/token"} + mock_from_file.return_value = config + private_key_obj = object() + mock_load_private_key.return_value = private_key_obj + + result = server.get_load_balancer_client() + + mock_open_file.assert_called_once_with("/token", "r") + mock_security_token_signer.assert_called_once_with("SECURITY_TOKEN", private_key_obj) + args, kwargs = mock_client.call_args + assert args[0] is config + assert kwargs["signer"] is mock_security_token_signer.return_value + assert isinstance(kwargs["circuit_breaker_strategy"], oci.circuit_breaker.CircuitBreakerStrategy) + assert callable(kwargs["circuit_breaker_callback"]) + assert result is mock_client.return_value + + # ---------------------------------------------------------------------- # Load Balancer tools # ---------------------------------------------------------------------- diff --git a/src/oci-load-balancer-mcp-server/pyproject.toml b/src/oci-load-balancer-mcp-server/pyproject.toml index 77bd6066..cf5f70c7 100644 --- a/src/oci-load-balancer-mcp-server/pyproject.toml +++ b/src/oci-load-balancer-mcp-server/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "oracle.oci-load-balancer-mcp-server" -version = "0.0.1" +version = "0.0.2" description = "An OCI Model Context Protocol server for load-balancer" readme = "README.md" requires-python = ">=3.13" diff --git a/src/oci-load-balancer-mcp-server/uv.lock b/src/oci-load-balancer-mcp-server/uv.lock index 32dd4024..c3ddc288 100644 --- a/src/oci-load-balancer-mcp-server/uv.lock +++ b/src/oci-load-balancer-mcp-server/uv.lock @@ -678,7 +678,7 @@ wheels = [ [[package]] name = "oracle-oci-load-balancer-mcp-server" -version = "0.0.1" +version = "0.0.2" source = { editable = "." } dependencies = [ { name = "fastmcp" }, diff --git a/src/oci-logging-mcp-server/oracle/oci_logging_mcp_server/__init__.py b/src/oci-logging-mcp-server/oracle/oci_logging_mcp_server/__init__.py index 682dda44..757a8b5b 100644 --- a/src/oci-logging-mcp-server/oracle/oci_logging_mcp_server/__init__.py +++ b/src/oci-logging-mcp-server/oracle/oci_logging_mcp_server/__init__.py @@ -5,4 +5,4 @@ """ __project__ = "oracle.oci-logging-mcp-server" -__version__ = "1.2.4" +__version__ = "1.2.5" diff --git a/src/oci-logging-mcp-server/oracle/oci_logging_mcp_server/server.py b/src/oci-logging-mcp-server/oracle/oci_logging_mcp_server/server.py index 06d414ec..65e37af8 100644 --- a/src/oci-logging-mcp-server/oracle/oci_logging_mcp_server/server.py +++ b/src/oci-logging-mcp-server/oracle/oci_logging_mcp_server/server.py @@ -37,6 +37,21 @@ mcp = FastMCP(name=__project__) +def _get_oci_client_kwargs(signer=None): + kwargs = { + "circuit_breaker_strategy": oci.circuit_breaker.CircuitBreakerStrategy( + failure_threshold=int(os.getenv("OCI_CIRCUIT_BREAKER_FAILURE_THRESHOLD", "10")), + recovery_timeout=int(os.getenv("OCI_CIRCUIT_BREAKER_RECOVERY_TIMEOUT", "30")), + ), + "circuit_breaker_callback": lambda exc: logger.warning( + "Circuit breaker triggered: %s", exc + ), + } + if signer is not None: + kwargs["signer"] = signer + return kwargs + + def get_logging_client(): logger.info("entering get_logging_client") config = oci.config.from_file( @@ -50,7 +65,7 @@ def get_logging_client(): with open(token_file, "r") as f: token = f.read() signer = oci.auth.signers.SecurityTokenSigner(token, private_key) - return oci.logging.LoggingManagementClient(config, signer=signer) + return oci.logging.LoggingManagementClient(config, **_get_oci_client_kwargs(signer)) def get_logging_search_client(): @@ -66,7 +81,7 @@ def get_logging_search_client(): with open(token_file, "r") as f: token = f.read() signer = oci.auth.signers.SecurityTokenSigner(token, private_key) - return oci.loggingsearch.LogSearchClient(config, signer=signer) + return oci.loggingsearch.LogSearchClient(config, **_get_oci_client_kwargs(signer)) @mcp.tool( diff --git a/src/oci-logging-mcp-server/oracle/oci_logging_mcp_server/tests/test_logging_tools.py b/src/oci-logging-mcp-server/oracle/oci_logging_mcp_server/tests/test_logging_tools.py index 0fd73f46..673b59f0 100644 --- a/src/oci-logging-mcp-server/oracle/oci_logging_mcp_server/tests/test_logging_tools.py +++ b/src/oci-logging-mcp-server/oracle/oci_logging_mcp_server/tests/test_logging_tools.py @@ -14,6 +14,80 @@ from oracle.oci_logging_mcp_server.server import mcp +class TestGetClient: + @patch("oracle.oci_logging_mcp_server.server.oci.logging.LoggingManagementClient") + @patch("oracle.oci_logging_mcp_server.server.oci.auth.signers.SecurityTokenSigner") + @patch("oracle.oci_logging_mcp_server.server.oci.signer.load_private_key_from_file") + @patch( + "oracle.oci_logging_mcp_server.server.open", + new_callable=mock_open, + read_data="SECURITY_TOKEN", + ) + @patch("oracle.oci_logging_mcp_server.server.oci.config.from_file") + @patch("oracle.oci_logging_mcp_server.server.os.getenv") + def test_get_logging_client_passes_circuit_breaker( + self, + mock_getenv, + mock_from_file, + mock_open_file, + mock_load_private_key, + mock_security_token_signer, + mock_client, + ): + mock_getenv.side_effect = lambda k, default=None: default + config = {"key_file": "/key.pem", "security_token_file": "/token"} + mock_from_file.return_value = config + private_key_obj = object() + mock_load_private_key.return_value = private_key_obj + + result = server.get_logging_client() + + mock_open_file.assert_called_once_with("/token", "r") + mock_security_token_signer.assert_called_once_with("SECURITY_TOKEN", private_key_obj) + args, kwargs = mock_client.call_args + assert args[0] is config + assert kwargs["signer"] is mock_security_token_signer.return_value + assert isinstance(kwargs["circuit_breaker_strategy"], oci.circuit_breaker.CircuitBreakerStrategy) + assert callable(kwargs["circuit_breaker_callback"]) + assert result is mock_client.return_value + + @patch("oracle.oci_logging_mcp_server.server.oci.loggingsearch.LogSearchClient") + @patch("oracle.oci_logging_mcp_server.server.oci.auth.signers.SecurityTokenSigner") + @patch("oracle.oci_logging_mcp_server.server.oci.signer.load_private_key_from_file") + @patch( + "oracle.oci_logging_mcp_server.server.open", + new_callable=mock_open, + read_data="SECURITY_TOKEN", + ) + @patch("oracle.oci_logging_mcp_server.server.oci.config.from_file") + @patch("oracle.oci_logging_mcp_server.server.os.getenv") + def test_get_logging_search_client_passes_circuit_breaker( + self, + mock_getenv, + mock_from_file, + mock_open_file, + mock_load_private_key, + mock_security_token_signer, + mock_client, + ): + mock_getenv.side_effect = lambda k, default=None: default + config = {"key_file": "/key.pem", "security_token_file": "/token"} + mock_from_file.return_value = config + private_key_obj = object() + mock_load_private_key.return_value = private_key_obj + + result = server.get_logging_search_client() + + mock_open_file.assert_called_once_with("/token", "r") + mock_security_token_signer.assert_called_once_with("SECURITY_TOKEN", private_key_obj) + args, kwargs = mock_client.call_args + assert args[0] is config + assert kwargs["signer"] is mock_security_token_signer.return_value + assert isinstance(kwargs["circuit_breaker_strategy"], oci.circuit_breaker.CircuitBreakerStrategy) + assert callable(kwargs["circuit_breaker_callback"]) + assert result is mock_client.return_value + + class TestLoggingTools: @pytest.mark.asyncio @patch("oracle.oci_logging_mcp_server.server.get_logging_client") diff --git a/src/oci-logging-mcp-server/pyproject.toml b/src/oci-logging-mcp-server/pyproject.toml index 46131b68..c867f082 100644 --- a/src/oci-logging-mcp-server/pyproject.toml +++ b/src/oci-logging-mcp-server/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "oracle.oci-logging-mcp-server" -version = "1.2.4" +version = "1.2.5" description = "OCI Logging Service MCP server" readme = "README.md" authors = [ diff --git a/src/oci-logging-mcp-server/uv.lock b/src/oci-logging-mcp-server/uv.lock index fe439bd2..38e3a2dc 100644 --- a/src/oci-logging-mcp-server/uv.lock +++ b/src/oci-logging-mcp-server/uv.lock @@ -783,7 +783,7 @@ wheels = [ [[package]] name = "oracle-oci-logging-mcp-server" -version = "1.2.4" +version = "1.2.5" source = { editable = "." } dependencies = [ { name = "fastmcp" }, diff --git a/src/oci-migration-mcp-server/oracle/oci_migration_mcp_server/__init__.py b/src/oci-migration-mcp-server/oracle/oci_migration_mcp_server/__init__.py index 47da140a..8477573a 100644 --- a/src/oci-migration-mcp-server/oracle/oci_migration_mcp_server/__init__.py +++ b/src/oci-migration-mcp-server/oracle/oci_migration_mcp_server/__init__.py @@ -5,4 +5,4 @@ """ __project__ = "oracle.oci-migration-mcp-server" -__version__ = "2.1.4" +__version__ = "2.1.5" diff --git a/src/oci-migration-mcp-server/oracle/oci_migration_mcp_server/server.py b/src/oci-migration-mcp-server/oracle/oci_migration_mcp_server/server.py index b9c7f497..5a2236b1 100644 --- a/src/oci-migration-mcp-server/oracle/oci_migration_mcp_server/server.py +++ b/src/oci-migration-mcp-server/oracle/oci_migration_mcp_server/server.py @@ -25,6 +25,21 @@ mcp = FastMCP(name=__project__) +def _get_oci_client_kwargs(signer=None): + kwargs = { + "circuit_breaker_strategy": oci.circuit_breaker.CircuitBreakerStrategy( + failure_threshold=int(os.getenv("OCI_CIRCUIT_BREAKER_FAILURE_THRESHOLD", "10")), + recovery_timeout=int(os.getenv("OCI_CIRCUIT_BREAKER_RECOVERY_TIMEOUT", "30")), + ), + "circuit_breaker_callback": lambda exc: logger.warning( + "Circuit breaker triggered: %s", exc + ), + } + if signer is not None: + kwargs["signer"] = signer + return kwargs + + def get_migration_client(): logger.info("entering get_migration_client") config = oci.config.from_file( @@ -39,7 +54,7 @@ def get_migration_client(): with open(token_file, "r") as f: token = f.read() signer = oci.auth.signers.SecurityTokenSigner(token, private_key) - return oci.cloud_migrations.MigrationClient(config, signer=signer) + return oci.cloud_migrations.MigrationClient(config, **_get_oci_client_kwargs(signer)) @mcp.tool(description="Get details for a specific Migration Project by OCID") diff --git a/src/oci-migration-mcp-server/pyproject.toml b/src/oci-migration-mcp-server/pyproject.toml index 8da33d81..0edcc4c1 100644 --- a/src/oci-migration-mcp-server/pyproject.toml +++ b/src/oci-migration-mcp-server/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "oracle.oci-migration-mcp-server" -version = "2.1.4" +version = "2.1.5" description = "Add your description here" readme = "README.md" requires-python = ">=3.13" diff --git a/src/oci-migration-mcp-server/uv.lock b/src/oci-migration-mcp-server/uv.lock index df2f0d2e..ce8c429a 100644 --- a/src/oci-migration-mcp-server/uv.lock +++ b/src/oci-migration-mcp-server/uv.lock @@ -783,7 +783,7 @@ wheels = [ [[package]] name = "oracle-oci-migration-mcp-server" -version = "2.1.4" +version = "2.1.5" source = { editable = "." } dependencies = [ { name = "fastmcp" }, diff --git a/src/oci-monitoring-mcp-server/oracle/oci_monitoring_mcp_server/__init__.py b/src/oci-monitoring-mcp-server/oracle/oci_monitoring_mcp_server/__init__.py index 98bd1796..fd690143 100644 --- a/src/oci-monitoring-mcp-server/oracle/oci_monitoring_mcp_server/__init__.py +++ b/src/oci-monitoring-mcp-server/oracle/oci_monitoring_mcp_server/__init__.py @@ -5,4 +5,4 @@ """ __project__ = "oracle.oci-monitoring-mcp-server" -__version__ = "1.1.4" +__version__ = "1.1.5" diff --git a/src/oci-monitoring-mcp-server/oracle/oci_monitoring_mcp_server/server.py b/src/oci-monitoring-mcp-server/oracle/oci_monitoring_mcp_server/server.py index 84d56425..a99cde46 100644 --- a/src/oci-monitoring-mcp-server/oracle/oci_monitoring_mcp_server/server.py +++ b/src/oci-monitoring-mcp-server/oracle/oci_monitoring_mcp_server/server.py @@ -41,6 +41,21 @@ ) +def _get_oci_client_kwargs(signer=None): + kwargs = { + "circuit_breaker_strategy": oci.circuit_breaker.CircuitBreakerStrategy( + failure_threshold=int(os.getenv("OCI_CIRCUIT_BREAKER_FAILURE_THRESHOLD", "10")), + recovery_timeout=int(os.getenv("OCI_CIRCUIT_BREAKER_RECOVERY_TIMEOUT", "30")), + ), + "circuit_breaker_callback": lambda exc: logger.warning( + "Circuit breaker triggered: %s", exc + ), + } + if signer is not None: + kwargs["signer"] = signer + return kwargs + + def get_monitoring_client(): logger.info("entering get_monitoring_client") config = oci.config.from_file( @@ -56,7 +71,7 @@ def get_monitoring_client(): with open(token_file, "r") as f: token = f.read() signer = oci.auth.signers.SecurityTokenSigner(token, private_key) - return oci.monitoring.MonitoringClient(config, signer=signer) + return oci.monitoring.MonitoringClient(config, **_get_oci_client_kwargs(signer)) @mcp.tool(name="list_alarms", description="Lists all alarms in a given compartment") diff --git a/src/oci-monitoring-mcp-server/pyproject.toml b/src/oci-monitoring-mcp-server/pyproject.toml index 9ab0a81e..8b997b53 100644 --- a/src/oci-monitoring-mcp-server/pyproject.toml +++ b/src/oci-monitoring-mcp-server/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "oracle.oci-monitoring-mcp-server" -version = "1.1.4" +version = "1.1.5" description = "OCI Monitoring Service MCP server" readme = "README.md" requires-python = ">=3.13" diff --git a/src/oci-monitoring-mcp-server/uv.lock b/src/oci-monitoring-mcp-server/uv.lock index 9e280921..599bb1c3 100644 --- a/src/oci-monitoring-mcp-server/uv.lock +++ b/src/oci-monitoring-mcp-server/uv.lock @@ -783,7 +783,7 @@ wheels = [ [[package]] name = "oracle-oci-monitoring-mcp-server" -version = "1.1.4" +version = "1.1.5" source = { editable = "." } dependencies = [ { name = "fastmcp" }, diff --git a/src/oci-network-load-balancer-mcp-server/oracle/oci_network_load_balancer_mcp_server/__init__.py b/src/oci-network-load-balancer-mcp-server/oracle/oci_network_load_balancer_mcp_server/__init__.py index a4dcfeda..0540354f 100644 --- a/src/oci-network-load-balancer-mcp-server/oracle/oci_network_load_balancer_mcp_server/__init__.py +++ b/src/oci-network-load-balancer-mcp-server/oracle/oci_network_load_balancer_mcp_server/__init__.py @@ -5,4 +5,4 @@ """ __project__ = "oracle.oci-network-load-balancer-mcp-server" -__version__ = "2.1.4" +__version__ = "2.1.5" diff --git a/src/oci-network-load-balancer-mcp-server/oracle/oci_network_load_balancer_mcp_server/server.py b/src/oci-network-load-balancer-mcp-server/oracle/oci_network_load_balancer_mcp_server/server.py index cb140e66..b07cd7ff 100644 --- a/src/oci-network-load-balancer-mcp-server/oracle/oci_network_load_balancer_mcp_server/server.py +++ b/src/oci-network-load-balancer-mcp-server/oracle/oci_network_load_balancer_mcp_server/server.py @@ -29,6 +29,21 @@ mcp = FastMCP(name=__project__) +def _get_oci_client_kwargs(signer=None): + kwargs = { + "circuit_breaker_strategy": oci.circuit_breaker.CircuitBreakerStrategy( + failure_threshold=int(os.getenv("OCI_CIRCUIT_BREAKER_FAILURE_THRESHOLD", "10")), + recovery_timeout=int(os.getenv("OCI_CIRCUIT_BREAKER_RECOVERY_TIMEOUT", "30")), + ), + "circuit_breaker_callback": lambda exc: logger.warning( + "Circuit breaker triggered: %s", exc + ), + } + if signer is not None: + kwargs["signer"] = signer + return kwargs + + def get_nlb_client(): logger.info("entering get_nlb_client") config = oci.config.from_file( @@ -43,7 +58,9 @@ def get_nlb_client(): with open(token_file, "r") as f: token = f.read() signer = oci.auth.signers.SecurityTokenSigner(token, private_key) - return oci.network_load_balancer.NetworkLoadBalancerClient(config, signer=signer) + return oci.network_load_balancer.NetworkLoadBalancerClient( + config, **_get_oci_client_kwargs(signer) + ) @mcp.tool( diff --git a/src/oci-network-load-balancer-mcp-server/pyproject.toml b/src/oci-network-load-balancer-mcp-server/pyproject.toml index 0e23ebdd..e81d0f51 100644 --- a/src/oci-network-load-balancer-mcp-server/pyproject.toml +++ b/src/oci-network-load-balancer-mcp-server/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "oracle.oci-network-load-balancer-mcp-server" -version = "2.1.4" +version = "2.1.5" description = "OCI Network Load Balancer MCP server" readme = "README.md" requires-python = ">=3.13" diff --git a/src/oci-network-load-balancer-mcp-server/uv.lock b/src/oci-network-load-balancer-mcp-server/uv.lock index b2768a4b..e9553457 100644 --- a/src/oci-network-load-balancer-mcp-server/uv.lock +++ b/src/oci-network-load-balancer-mcp-server/uv.lock @@ -783,7 +783,7 @@ wheels = [ [[package]] name = "oracle-oci-network-load-balancer-mcp-server" -version = "2.1.4" +version = "2.1.5" source = { editable = "." } dependencies = [ { name = "fastmcp" }, diff --git a/src/oci-networking-mcp-server/oracle/oci_networking_mcp_server/__init__.py b/src/oci-networking-mcp-server/oracle/oci_networking_mcp_server/__init__.py index d77b4d89..42f533f8 100644 --- a/src/oci-networking-mcp-server/oracle/oci_networking_mcp_server/__init__.py +++ b/src/oci-networking-mcp-server/oracle/oci_networking_mcp_server/__init__.py @@ -5,4 +5,4 @@ """ __project__ = "oracle.oci-networking-mcp-server" -__version__ = "1.2.4" +__version__ = "1.2.5" diff --git a/src/oci-networking-mcp-server/oracle/oci_networking_mcp_server/server.py b/src/oci-networking-mcp-server/oracle/oci_networking_mcp_server/server.py index 548460bb..645a8d34 100644 --- a/src/oci-networking-mcp-server/oracle/oci_networking_mcp_server/server.py +++ b/src/oci-networking-mcp-server/oracle/oci_networking_mcp_server/server.py @@ -33,6 +33,21 @@ mcp = FastMCP(name=__project__) +def _get_oci_client_kwargs(signer=None): + kwargs = { + "circuit_breaker_strategy": oci.circuit_breaker.CircuitBreakerStrategy( + failure_threshold=int(os.getenv("OCI_CIRCUIT_BREAKER_FAILURE_THRESHOLD", "10")), + recovery_timeout=int(os.getenv("OCI_CIRCUIT_BREAKER_RECOVERY_TIMEOUT", "30")), + ), + "circuit_breaker_callback": lambda exc: logger.warning( + "Circuit breaker triggered: %s", exc + ), + } + if signer is not None: + kwargs["signer"] = signer + return kwargs + + def get_networking_client(): config = oci.config.from_file( file_location=os.getenv("OCI_CONFIG_FILE", oci.config.DEFAULT_LOCATION), @@ -46,7 +61,7 @@ def get_networking_client(): with open(token_file, "r") as f: token = f.read() signer = oci.auth.signers.SecurityTokenSigner(token, private_key) - return oci.core.VirtualNetworkClient(config, signer=signer) + return oci.core.VirtualNetworkClient(config, **_get_oci_client_kwargs(signer)) @mcp.tool(description="Lists the VCNs in the specified compartment.") diff --git a/src/oci-networking-mcp-server/pyproject.toml b/src/oci-networking-mcp-server/pyproject.toml index 10527e95..b4c2a4cf 100644 --- a/src/oci-networking-mcp-server/pyproject.toml +++ b/src/oci-networking-mcp-server/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "oracle.oci-networking-mcp-server" -version = "1.2.4" +version = "1.2.5" description = "OCI Networking Service MCP server" readme = "README.md" requires-python = ">=3.13" @@ -49,5 +49,3 @@ omit = [ [tool.coverage.report] precision = 2 fail_under = 90 - - diff --git a/src/oci-networking-mcp-server/uv.lock b/src/oci-networking-mcp-server/uv.lock index bf6d0bef..65884513 100644 --- a/src/oci-networking-mcp-server/uv.lock +++ b/src/oci-networking-mcp-server/uv.lock @@ -783,7 +783,7 @@ wheels = [ [[package]] name = "oracle-oci-networking-mcp-server" -version = "1.2.4" +version = "1.2.5" source = { editable = "." } dependencies = [ { name = "fastmcp" }, diff --git a/src/oci-object-storage-mcp-server/oracle/oci_object_storage_mcp_server/__init__.py b/src/oci-object-storage-mcp-server/oracle/oci_object_storage_mcp_server/__init__.py index ea31b041..a04c31a9 100644 --- a/src/oci-object-storage-mcp-server/oracle/oci_object_storage_mcp_server/__init__.py +++ b/src/oci-object-storage-mcp-server/oracle/oci_object_storage_mcp_server/__init__.py @@ -5,4 +5,4 @@ """ __project__ = "oracle.oci-object-storage-mcp-server" -__version__ = "1.1.4" +__version__ = "1.1.5" diff --git a/src/oci-object-storage-mcp-server/oracle/oci_object_storage_mcp_server/server.py b/src/oci-object-storage-mcp-server/oracle/oci_object_storage_mcp_server/server.py index c9b7d1b9..fe6367c7 100644 --- a/src/oci-object-storage-mcp-server/oracle/oci_object_storage_mcp_server/server.py +++ b/src/oci-object-storage-mcp-server/oracle/oci_object_storage_mcp_server/server.py @@ -29,6 +29,21 @@ mcp = FastMCP(name=__project__) +def _get_oci_client_kwargs(signer=None): + kwargs = { + "circuit_breaker_strategy": oci.circuit_breaker.CircuitBreakerStrategy( + failure_threshold=int(os.getenv("OCI_CIRCUIT_BREAKER_FAILURE_THRESHOLD", "10")), + recovery_timeout=int(os.getenv("OCI_CIRCUIT_BREAKER_RECOVERY_TIMEOUT", "30")), + ), + "circuit_breaker_callback": lambda exc: logger.warning( + "Circuit breaker triggered: %s", exc + ), + } + if signer is not None: + kwargs["signer"] = signer + return kwargs + + def get_object_storage_client(): config = oci.config.from_file( file_location=os.getenv("OCI_CONFIG_FILE", oci.config.DEFAULT_LOCATION), @@ -43,7 +58,7 @@ def get_object_storage_client(): with open(token_file, "r") as f: token = f.read() signer = oci.auth.signers.SecurityTokenSigner(token, private_key) - return oci.object_storage.ObjectStorageClient(config, signer=signer) + return oci.object_storage.ObjectStorageClient(config, **_get_oci_client_kwargs(signer)) # Object storage namespace diff --git a/src/oci-object-storage-mcp-server/pyproject.toml b/src/oci-object-storage-mcp-server/pyproject.toml index 9285baab..45c9e6c3 100644 --- a/src/oci-object-storage-mcp-server/pyproject.toml +++ b/src/oci-object-storage-mcp-server/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "oracle.oci-object-storage-mcp-server" -version = "1.1.4" +version = "1.1.5" description = "OCI Object Storage Service MCP server" readme = "README.md" authors = [ diff --git a/src/oci-object-storage-mcp-server/uv.lock b/src/oci-object-storage-mcp-server/uv.lock index d71f09f6..2d918218 100644 --- a/src/oci-object-storage-mcp-server/uv.lock +++ b/src/oci-object-storage-mcp-server/uv.lock @@ -783,7 +783,7 @@ wheels = [ [[package]] name = "oracle-oci-object-storage-mcp-server" -version = "1.1.4" +version = "1.1.5" source = { editable = "." } dependencies = [ { name = "fastmcp" }, diff --git a/src/oci-recovery-mcp-server/oracle/oci_recovery_mcp_server/__init__.py b/src/oci-recovery-mcp-server/oracle/oci_recovery_mcp_server/__init__.py index d07bb5fb..1c755a0f 100644 --- a/src/oci-recovery-mcp-server/oracle/oci_recovery_mcp_server/__init__.py +++ b/src/oci-recovery-mcp-server/oracle/oci_recovery_mcp_server/__init__.py @@ -5,4 +5,4 @@ """ __project__ = "oracle.oci-recovery-mcp-server" -__version__ = "1.0.0" +__version__ = "1.0.1" diff --git a/src/oci-recovery-mcp-server/oracle/oci_recovery_mcp_server/server.py b/src/oci-recovery-mcp-server/oracle/oci_recovery_mcp_server/server.py index 1119a52e..c57347f2 100644 --- a/src/oci-recovery-mcp-server/oracle/oci_recovery_mcp_server/server.py +++ b/src/oci-recovery-mcp-server/oracle/oci_recovery_mcp_server/server.py @@ -613,6 +613,21 @@ def _build_signer_for_session(config: dict): return oci.auth.signers.SecurityTokenSigner(token, private_key) +def _get_oci_client_kwargs(signer=None): + kwargs = { + "circuit_breaker_strategy": oci.circuit_breaker.CircuitBreakerStrategy( + failure_threshold=int(os.getenv("OCI_CIRCUIT_BREAKER_FAILURE_THRESHOLD", "10")), + recovery_timeout=int(os.getenv("OCI_CIRCUIT_BREAKER_RECOVERY_TIMEOUT", "30")), + ), + "circuit_breaker_callback": lambda exc: logger.warning( + "Circuit breaker triggered: %s", exc + ), + } + if signer is not None: + kwargs["signer"] = signer + return kwargs + + # Create the FastMCP app that exposes the functions decorated with @mcp.tool mcp = FastMCP(name=__project__) @@ -630,10 +645,12 @@ def get_recovery_client( method = _effective_auth_method() if method == "apikey": - client = oci.recovery.DatabaseRecoveryClient(regional_config) + client = oci.recovery.DatabaseRecoveryClient(regional_config, **_get_oci_client_kwargs()) else: signer = _build_signer_for_session(regional_config) - client = oci.recovery.DatabaseRecoveryClient(regional_config, signer=signer) + client = oci.recovery.DatabaseRecoveryClient( + regional_config, **_get_oci_client_kwargs(signer) + ) rid = request_id or uuid.uuid4().hex return _wrap_oci_client(client, request_id=rid, client_name="recovery") @@ -643,10 +660,10 @@ def get_identity_client(*, request_id: Optional[str] = None): config = _load_oci_config_for_server() method = _effective_auth_method() if method == "apikey": - client = oci.identity.IdentityClient(config) + client = oci.identity.IdentityClient(config, **_get_oci_client_kwargs()) else: signer = _build_signer_for_session(config) - client = oci.identity.IdentityClient(config, signer=signer) + client = oci.identity.IdentityClient(config, **_get_oci_client_kwargs(signer)) rid = request_id or uuid.uuid4().hex return _wrap_oci_client(client, request_id=rid, client_name="identity") @@ -657,10 +674,10 @@ def get_database_client(region: str = None, *, request_id: Optional[str] = None) regional_config = config if region is None else {**config, "region": region} method = _effective_auth_method() if method == "apikey": - client = oci.database.DatabaseClient(regional_config) + client = oci.database.DatabaseClient(regional_config, **_get_oci_client_kwargs()) else: signer = _build_signer_for_session(regional_config) - client = oci.database.DatabaseClient(regional_config, signer=signer) + client = oci.database.DatabaseClient(regional_config, **_get_oci_client_kwargs(signer)) rid = request_id or uuid.uuid4().hex return _wrap_oci_client(client, request_id=rid, client_name="database") @@ -672,10 +689,12 @@ def get_monitoring_client(region: str | None = None, *, request_id: Optional[str regional_config = config if region is None else {**config, "region": region} method = _effective_auth_method() if method == "apikey": - client = oci.monitoring.MonitoringClient(regional_config) + client = oci.monitoring.MonitoringClient(regional_config, **_get_oci_client_kwargs()) else: signer = _build_signer_for_session(regional_config) - client = oci.monitoring.MonitoringClient(regional_config, signer=signer) + client = oci.monitoring.MonitoringClient( + regional_config, **_get_oci_client_kwargs(signer) + ) rid = request_id or uuid.uuid4().hex return _wrap_oci_client(client, request_id=rid, client_name="monitoring") @@ -3078,4 +3097,3 @@ def main(): if __name__ == "__main__": main() - diff --git a/src/oci-recovery-mcp-server/oracle/oci_recovery_mcp_server/tests/test_recovery_tools.py b/src/oci-recovery-mcp-server/oracle/oci_recovery_mcp_server/tests/test_recovery_tools.py index ebabd0ad..c9c649ba 100644 --- a/src/oci-recovery-mcp-server/oracle/oci_recovery_mcp_server/tests/test_recovery_tools.py +++ b/src/oci-recovery-mcp-server/oracle/oci_recovery_mcp_server/tests/test_recovery_tools.py @@ -10,9 +10,60 @@ import oci import pytest from fastmcp import Client +import oracle.oci_recovery_mcp_server.server as server from oracle.oci_recovery_mcp_server.server import mcp +class TestGetClientFactories: + @patch("oracle.oci_recovery_mcp_server.server._wrap_oci_client", side_effect=lambda client, **_: client) + @patch("oracle.oci_recovery_mcp_server.server.oci.recovery.DatabaseRecoveryClient") + @patch("oracle.oci_recovery_mcp_server.server._effective_auth_method", return_value="apikey") + @patch("oracle.oci_recovery_mcp_server.server._load_oci_config_for_server") + def test_get_recovery_client_apikey_passes_circuit_breaker( + self, + mock_load_config, + _mock_auth_method, + mock_client, + _mock_wrap, + ): + mock_load_config.return_value = {"region": "us-ashburn-1"} + + result = server.get_recovery_client(region="us-phoenix-1", request_id="rid") + + args, kwargs = mock_client.call_args + assert args[0]["region"] == "us-phoenix-1" + assert "signer" not in kwargs + assert isinstance(kwargs["circuit_breaker_strategy"], oci.circuit_breaker.CircuitBreakerStrategy) + assert callable(kwargs["circuit_breaker_callback"]) + assert result is mock_client.return_value + + @patch("oracle.oci_recovery_mcp_server.server._wrap_oci_client", side_effect=lambda client, **_: client) + @patch("oracle.oci_recovery_mcp_server.server._build_signer_for_session") + @patch("oracle.oci_recovery_mcp_server.server.oci.monitoring.MonitoringClient") + @patch("oracle.oci_recovery_mcp_server.server._effective_auth_method", return_value="session") + @patch("oracle.oci_recovery_mcp_server.server._load_oci_config_for_server") + def test_get_monitoring_client_session_passes_circuit_breaker( + self, + mock_load_config, + _mock_auth_method, + mock_client, + mock_build_signer, + _mock_wrap, + ): + mock_load_config.return_value = {"region": "us-ashburn-1"} + signer = object() + mock_build_signer.return_value = signer + + result = server.get_monitoring_client(region="us-phoenix-1", request_id="rid") + + args, kwargs = mock_client.call_args + assert args[0]["region"] == "us-phoenix-1" + assert kwargs["signer"] is signer + assert isinstance(kwargs["circuit_breaker_strategy"], oci.circuit_breaker.CircuitBreakerStrategy) + assert callable(kwargs["circuit_breaker_callback"]) + assert result is mock_client.return_value + + class TestRecoveryTools: @pytest.mark.asyncio @patch("oracle.oci_recovery_mcp_server.server.get_recovery_client") diff --git a/src/oci-recovery-mcp-server/pyproject.toml b/src/oci-recovery-mcp-server/pyproject.toml index a4285b52..32934355 100644 --- a/src/oci-recovery-mcp-server/pyproject.toml +++ b/src/oci-recovery-mcp-server/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "oracle.oci-recovery-mcp-server" -version = "1.0.0" +version = "1.0.1" description = "OCI Recovery Service MCP server" readme = "README.md" requires-python = ">=3.13" diff --git a/src/oci-recovery-mcp-server/uv.lock b/src/oci-recovery-mcp-server/uv.lock index a480a1f0..056af634 100644 --- a/src/oci-recovery-mcp-server/uv.lock +++ b/src/oci-recovery-mcp-server/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.13" [[package]] @@ -803,7 +803,7 @@ wheels = [ [[package]] name = "oracle-oci-recovery-mcp-server" -version = "1.0.0" +version = "1.0.1" source = { editable = "." } dependencies = [ { name = "fastmcp" }, diff --git a/src/oci-registry-mcp-server/oracle/oci_registry_mcp_server/__init__.py b/src/oci-registry-mcp-server/oracle/oci_registry_mcp_server/__init__.py index 4591e094..214f07e2 100644 --- a/src/oci-registry-mcp-server/oracle/oci_registry_mcp_server/__init__.py +++ b/src/oci-registry-mcp-server/oracle/oci_registry_mcp_server/__init__.py @@ -5,4 +5,4 @@ """ __project__ = "oracle.oci-registry-mcp-server" -__version__ = "2.1.4" +__version__ = "2.1.5" diff --git a/src/oci-registry-mcp-server/oracle/oci_registry_mcp_server/server.py b/src/oci-registry-mcp-server/oracle/oci_registry_mcp_server/server.py index 689a3049..c3eee770 100644 --- a/src/oci-registry-mcp-server/oracle/oci_registry_mcp_server/server.py +++ b/src/oci-registry-mcp-server/oracle/oci_registry_mcp_server/server.py @@ -25,6 +25,21 @@ mcp = FastMCP(name=__project__) +def _get_oci_client_kwargs(signer=None): + kwargs = { + "circuit_breaker_strategy": oci.circuit_breaker.CircuitBreakerStrategy( + failure_threshold=int(os.getenv("OCI_CIRCUIT_BREAKER_FAILURE_THRESHOLD", "10")), + recovery_timeout=int(os.getenv("OCI_CIRCUIT_BREAKER_RECOVERY_TIMEOUT", "30")), + ), + "circuit_breaker_callback": lambda exc: logger.warning( + "Circuit breaker triggered: %s", exc + ), + } + if signer is not None: + kwargs["signer"] = signer + return kwargs + + def get_ocir_client(): config = oci.config.from_file( file_location=os.getenv("OCI_CONFIG_FILE", oci.config.DEFAULT_LOCATION), @@ -40,7 +55,7 @@ def get_ocir_client(): with open(token_file, "r") as f: token = f.read() signer = oci.auth.signers.SecurityTokenSigner(token, private_key) - return oci.artifacts.ArtifactsClient(config, signer=signer) + return oci.artifacts.ArtifactsClient(config, **_get_oci_client_kwargs(signer)) @mcp.tool(description="List container repositories in the given compartment") diff --git a/src/oci-registry-mcp-server/pyproject.toml b/src/oci-registry-mcp-server/pyproject.toml index 1395112d..234c7732 100644 --- a/src/oci-registry-mcp-server/pyproject.toml +++ b/src/oci-registry-mcp-server/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "oracle.oci-registry-mcp-server" -version = "2.1.4" +version = "2.1.5" description = "OCI Registry Service MCP server" readme = "README.md" requires-python = ">=3.13" diff --git a/src/oci-registry-mcp-server/uv.lock b/src/oci-registry-mcp-server/uv.lock index 2e1623be..7d94a498 100644 --- a/src/oci-registry-mcp-server/uv.lock +++ b/src/oci-registry-mcp-server/uv.lock @@ -783,7 +783,7 @@ wheels = [ [[package]] name = "oracle-oci-registry-mcp-server" -version = "2.1.4" +version = "2.1.5" source = { editable = "." } dependencies = [ { name = "fastmcp" }, diff --git a/src/oci-resource-search-mcp-server/oracle/oci_resource_search_mcp_server/__init__.py b/src/oci-resource-search-mcp-server/oracle/oci_resource_search_mcp_server/__init__.py index e3e5213b..51208138 100644 --- a/src/oci-resource-search-mcp-server/oracle/oci_resource_search_mcp_server/__init__.py +++ b/src/oci-resource-search-mcp-server/oracle/oci_resource_search_mcp_server/__init__.py @@ -5,4 +5,4 @@ """ __project__ = "oracle.oci-resource-search-mcp-server" -__version__ = "2.1.4" +__version__ = "2.1.5" diff --git a/src/oci-resource-search-mcp-server/oracle/oci_resource_search_mcp_server/server.py b/src/oci-resource-search-mcp-server/oracle/oci_resource_search_mcp_server/server.py index a7d5175a..a92728a2 100644 --- a/src/oci-resource-search-mcp-server/oracle/oci_resource_search_mcp_server/server.py +++ b/src/oci-resource-search-mcp-server/oracle/oci_resource_search_mcp_server/server.py @@ -24,6 +24,21 @@ mcp = FastMCP(name=__project__) +def _get_oci_client_kwargs(signer=None): + kwargs = { + "circuit_breaker_strategy": oci.circuit_breaker.CircuitBreakerStrategy( + failure_threshold=int(os.getenv("OCI_CIRCUIT_BREAKER_FAILURE_THRESHOLD", "10")), + recovery_timeout=int(os.getenv("OCI_CIRCUIT_BREAKER_RECOVERY_TIMEOUT", "30")), + ), + "circuit_breaker_callback": lambda exc: logger.warning( + "Circuit breaker triggered: %s", exc + ), + } + if signer is not None: + kwargs["signer"] = signer + return kwargs + + def get_search_client(): logger.info("entering get_search_client") config = oci.config.from_file( @@ -40,7 +55,7 @@ def get_search_client(): with open(token_file, "r") as f: token = f.read() signer = oci.auth.signers.SecurityTokenSigner(token, private_key) - return oci.resource_search.ResourceSearchClient(config, signer=signer) + return oci.resource_search.ResourceSearchClient(config, **_get_oci_client_kwargs(signer)) @mcp.tool(description="Returns all resources") diff --git a/src/oci-resource-search-mcp-server/pyproject.toml b/src/oci-resource-search-mcp-server/pyproject.toml index fa3ba9d3..3fb5a54a 100644 --- a/src/oci-resource-search-mcp-server/pyproject.toml +++ b/src/oci-resource-search-mcp-server/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "oracle.oci-resource-search-mcp-server" -version = "2.1.4" +version = "2.1.5" description = "OCI Resource Search Service MCP server" readme = "README.md" requires-python = ">=3.13" diff --git a/src/oci-resource-search-mcp-server/uv.lock b/src/oci-resource-search-mcp-server/uv.lock index b30b57f2..107c02cb 100644 --- a/src/oci-resource-search-mcp-server/uv.lock +++ b/src/oci-resource-search-mcp-server/uv.lock @@ -783,7 +783,7 @@ wheels = [ [[package]] name = "oracle-oci-resource-search-mcp-server" -version = "2.1.4" +version = "2.1.5" source = { editable = "." } dependencies = [ { name = "fastmcp" }, diff --git a/src/oci-usage-mcp-server/oracle/oci_usage_mcp_server/__init__.py b/src/oci-usage-mcp-server/oracle/oci_usage_mcp_server/__init__.py index 22a2fe4e..2ea193e4 100644 --- a/src/oci-usage-mcp-server/oracle/oci_usage_mcp_server/__init__.py +++ b/src/oci-usage-mcp-server/oracle/oci_usage_mcp_server/__init__.py @@ -5,4 +5,4 @@ """ __project__ = "oracle.oci-usage-mcp-server" -__version__ = "1.1.4" +__version__ = "1.1.5" diff --git a/src/oci-usage-mcp-server/oracle/oci_usage_mcp_server/server.py b/src/oci-usage-mcp-server/oracle/oci_usage_mcp_server/server.py index 1087ea83..511548fd 100644 --- a/src/oci-usage-mcp-server/oracle/oci_usage_mcp_server/server.py +++ b/src/oci-usage-mcp-server/oracle/oci_usage_mcp_server/server.py @@ -19,6 +19,21 @@ mcp = FastMCP(name=__project__) +def _get_oci_client_kwargs(signer=None): + kwargs = { + "circuit_breaker_strategy": oci.circuit_breaker.CircuitBreakerStrategy( + failure_threshold=int(os.getenv("OCI_CIRCUIT_BREAKER_FAILURE_THRESHOLD", "10")), + recovery_timeout=int(os.getenv("OCI_CIRCUIT_BREAKER_RECOVERY_TIMEOUT", "30")), + ), + "circuit_breaker_callback": lambda exc: logger.warning( + "Circuit breaker triggered: %s", exc + ), + } + if signer is not None: + kwargs["signer"] = signer + return kwargs + + def get_usage_client(): logger.info("entering get_monitoring_client") config = oci.config.from_file( @@ -34,7 +49,7 @@ def get_usage_client(): with open(token_file, "r") as f: token = f.read() signer = oci.auth.signers.SecurityTokenSigner(token, private_key) - return oci.usage_api.UsageapiClient(config, signer=signer) + return oci.usage_api.UsageapiClient(config, **_get_oci_client_kwargs(signer)) @mcp.tool diff --git a/src/oci-usage-mcp-server/pyproject.toml b/src/oci-usage-mcp-server/pyproject.toml index 94c08761..85b74e33 100644 --- a/src/oci-usage-mcp-server/pyproject.toml +++ b/src/oci-usage-mcp-server/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "oracle.oci-usage-mcp-server" -version = "1.1.4" +version = "1.1.5" description = "OCI Usage MCP server" readme = "README.md" license = "UPL-1.0" diff --git a/src/oci-usage-mcp-server/uv.lock b/src/oci-usage-mcp-server/uv.lock index 84d4560e..85d5adcf 100644 --- a/src/oci-usage-mcp-server/uv.lock +++ b/src/oci-usage-mcp-server/uv.lock @@ -783,7 +783,7 @@ wheels = [ [[package]] name = "oracle-oci-usage-mcp-server" -version = "1.1.4" +version = "1.1.5" source = { editable = "." } dependencies = [ { name = "fastmcp" },