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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion mcpgateway/services/gateway_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -5796,7 +5796,7 @@ async def _refresh_gateway_tools_resources_prompts(
_refresh_key = _enc.decrypt_secret_or_plaintext(_refresh_key)
except Exception:
logger.debug("client_key decryption skipped during gateway refresh")
_capabilities, tools, resources, prompts, _ = await self._initialize_gateway(
_capabilities, tools, resources, prompts, validation_errors = await self._initialize_gateway(
url=gateway_url,
authentication=gateway_auth_value,
transport=gateway_transport,
Expand All @@ -5816,6 +5816,8 @@ async def _refresh_gateway_tools_resources_prompts(
result["error"] = str(e)
return result

result["validation_errors"] = validation_errors

# For authorization_code OAuth gateways, empty responses may indicate incomplete auth flow
# Skip only if it's an auth_code gateway with no data (user may not have completed authorization)
is_auth_code_gateway = gateway_oauth_config and isinstance(gateway_oauth_config, dict) and gateway_oauth_config.get("grant_type") == "authorization_code"
Expand Down
23 changes: 23 additions & 0 deletions tests/unit/mcpgateway/services/test_gateway_auto_refresh.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,29 @@ async def test_refresh_skips_auth_code_gateway_with_empty_response(self, gateway
assert result["tools_added"] == 0
assert result["tools_removed"] == 0

@pytest.mark.asyncio
async def test_refresh_propagates_validation_errors(self, gateway_service):
"""Validation errors from _initialize_gateway should propagate through auto-refresh."""
mock_gateway = _make_mock_gateway()
mock_session = MagicMock()
mock_session.execute.return_value.scalar_one_or_none.return_value = mock_gateway

validation_errors = [
"bad_tool: Tool name exceeds MCP spec limit of 128 characters (got 129)",
]

with (
patch("mcpgateway.services.gateway_service.fresh_db_session") as mock_fresh,
patch.object(gateway_service, "_initialize_gateway", new_callable=AsyncMock) as mock_init,
):
mock_fresh.return_value.__enter__.return_value = mock_session
mock_init.return_value = ({}, [], [], [], validation_errors)

result = await gateway_service._refresh_gateway_tools_resources_prompts("gw-123")

assert result["validation_errors"] == validation_errors
assert result["success"] is True

@pytest.mark.asyncio
async def test_refresh_processes_empty_non_auth_code_gateway(self, gateway_service):
"""Test that refresh processes empty responses from non-auth_code gateways."""
Expand Down
23 changes: 23 additions & 0 deletions tests/unit/mcpgateway/services/test_gateway_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -6471,6 +6471,29 @@ async def test_init_failure_returns_error(self, gateway_service):
assert result["success"] is False
assert "connection refused" in result["error"]

@pytest.mark.asyncio
async def test_validation_errors_propagated(self, gateway_service):
"""Validation errors from _initialize_gateway populate result['validation_errors'] before early return."""
gw = SimpleNamespace(
enabled=True,
reachable=True,
name="val-gw",
url="http://example.com",
transport="sse",
auth_type="oauth",
auth_value=None,
oauth_config={"grant_type": "authorization_code"},
ca_certificate=None,
auth_query_params=None,
)
validation_errors = [
"bad_tool: Tool name exceeds MCP spec limit of 128 characters (got 129)",
]
gateway_service._initialize_gateway = AsyncMock(return_value=({}, [], [], [], validation_errors))
result = await gateway_service._refresh_gateway_tools_resources_prompts("gw-1", gateway=gw)
assert result["validation_errors"] == validation_errors
assert result["success"] is True

@pytest.mark.asyncio
async def test_auth_code_empty_response_returns_early(self, gateway_service):
gw = SimpleNamespace(
Expand Down
20 changes: 20 additions & 0 deletions tests/unit/mcpgateway/test_main_extended.py
Original file line number Diff line number Diff line change
Expand Up @@ -4941,6 +4941,7 @@ async def test_refresh_gateway_tools_success_and_errors(self, monkeypatch):
"duration_ms": 1.0,
"refreshed_at": datetime.now(timezone.utc),
"tools_added": 1,
"validation_errors": [],
}
monkeypatch.setattr(main_mod.gateway_service, "get_gateway", AsyncMock(return_value=SimpleNamespace(id="gw-1")))
monkeypatch.setattr(main_mod, "_enforce_scoped_resource_access", lambda *_args, **_kwargs: None)
Expand All @@ -4955,6 +4956,25 @@ async def test_refresh_gateway_tools_success_and_errors(self, monkeypatch):
)
assert response.gateway_id == "gw-1"
assert response.tools_added == 1
assert response.validation_errors == []

# Test with non-empty validation errors
result_payload_with_errors = {
"duration_ms": 1.0,
"refreshed_at": datetime.now(timezone.utc),
"tools_added": 0,
"validation_errors": ["bad_tool: Tool name exceeds MCP spec limit of 128 characters (got 129)"],
}
monkeypatch.setattr(main_mod.gateway_service, "refresh_gateway_manually", AsyncMock(return_value=result_payload_with_errors))
response_with_errors = await main_mod.refresh_gateway_tools(
"gw-1",
request,
include_resources=True,
include_prompts=False,
db=db,
user={"email": "user@example.com"},
)
assert response_with_errors.validation_errors == result_payload_with_errors["validation_errors"]

monkeypatch.setattr(main_mod.gateway_service, "refresh_gateway_manually", AsyncMock(side_effect=GatewayNotFoundError("missing")))
with pytest.raises(HTTPException) as excinfo:
Expand Down
Loading