diff --git a/mcpgateway/services/gateway_service.py b/mcpgateway/services/gateway_service.py index c3312fb4d6..7f8e58b290 100644 --- a/mcpgateway/services/gateway_service.py +++ b/mcpgateway/services/gateway_service.py @@ -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, @@ -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" diff --git a/tests/unit/mcpgateway/services/test_gateway_auto_refresh.py b/tests/unit/mcpgateway/services/test_gateway_auto_refresh.py index 9672fb1d25..fa9b2e4ead 100644 --- a/tests/unit/mcpgateway/services/test_gateway_auto_refresh.py +++ b/tests/unit/mcpgateway/services/test_gateway_auto_refresh.py @@ -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.""" diff --git a/tests/unit/mcpgateway/services/test_gateway_service.py b/tests/unit/mcpgateway/services/test_gateway_service.py index ee2e13d379..55aa5df8a0 100644 --- a/tests/unit/mcpgateway/services/test_gateway_service.py +++ b/tests/unit/mcpgateway/services/test_gateway_service.py @@ -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( diff --git a/tests/unit/mcpgateway/test_main_extended.py b/tests/unit/mcpgateway/test_main_extended.py index 56dffb8280..a00ecca7d9 100644 --- a/tests/unit/mcpgateway/test_main_extended.py +++ b/tests/unit/mcpgateway/test_main_extended.py @@ -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) @@ -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: