Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
"""

__project__ = "oracle.oci-cloud-guard-mcp-server"
__version__ = "1.1.4"
__version__ = "1.1.5"
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion src/oci-cloud-guard-mcp-server/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/oci-cloud-guard-mcp-server/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
"""

__project__ = "oracle.oci-cloud-mcp-server"
__version__ = "1.1.3"
__version__ = "1.1.4"
33 changes: 32 additions & 1 deletion src/oci-cloud-mcp-server/oracle/oci_cloud_mcp_server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/oci-cloud-mcp-server/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/oci-cloud-mcp-server/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
"""

__project__ = "oracle.oci-compute-instance-agent-mcp-server"
__version__ = "2.1.4"
__version__ = "2.1.5"
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion src/oci-compute-instance-agent-mcp-server/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/oci-compute-instance-agent-mcp-server/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
"""

__project__ = "oracle.oci-compute-mcp-server"
__version__ = "1.2.4"
__version__ = "1.2.5"
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion src/oci-compute-mcp-server/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/oci-compute-mcp-server/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
"""

__project__ = "oracle.oci-database-mcp-server"
__version__ = "1.0.5"
__version__ = "1.0.6"
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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):
Expand Down
Original file line number Diff line number Diff line change
@@ -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):
Expand Down
2 changes: 1 addition & 1 deletion src/oci-database-mcp-server/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/oci-database-mcp-server/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
"""

__project__ = "oracle.oci-faaas-mcp-server"
__version__ = "1.0.3"
__version__ = "1.0.4"
Loading
Loading